1use std::{
15 any::Any,
16 borrow::Cow,
17 collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
18 fmt,
19 future::Future,
20 hash::Hash,
21 marker::PhantomData,
22 num::NonZeroU64,
23 path::PathBuf,
24 pin::Pin,
25 sync::{
26 Arc,
27 atomic::{AtomicU64, Ordering},
28 },
29 time::{Duration, Instant},
30};
31
32use alloy_consensus::{BlockHeader as _, Transaction as _};
33use alloy_eips::{BlockId, BlockNumberOrTag};
34use alloy_network::{
35 Ethereum, Network,
36 primitives::{
37 BlockResponse as _, HeaderResponse as HeaderResponseTrait,
38 TransactionResponse as TransactionResponseTrait,
39 },
40};
41use alloy_primitives::{Address, B256, Bytes, FixedBytes, Keccak256, U256};
42use alloy_provider::{Provider, RootProvider};
43use alloy_rpc_client::BatchRequest;
44use alloy_rpc_types_eth::{Filter, FilterSet, Log};
45pub use alloy_transport_balancer::EndpointId;
46use bincode::Options;
47use futures::{StreamExt, stream};
48use futures::{
49 future::{Either, poll_fn, select},
50 stream::{BoxStream, FuturesUnordered},
51};
52
53use crate::{
54 cache::{
55 AccountProof, BlockStateDiff, DurableCheckpointBlock, DurableCheckpointError,
56 DurableCheckpointIdentity, DurableCheckpointMetadata, DurableCheckpointStore, EvmCache,
57 EvmCacheStateSnapshot, LoadedDurableCheckpoint,
58 },
59 errors::{BlockContextError, StorageFetchResult},
60 events::{EventDecoder, StateView},
61 freshness::FreshnessRegistry,
62 state_update::{AccountPatch, PurgeScope, StateDiff, StateUpdate},
63};
64
65#[cfg(feature = "raw-flashblocks-json")]
66mod raw_json_flashblocks;
67#[cfg(feature = "raw-flashblocks-json")]
68pub use raw_json_flashblocks::{
69 BufferedRawJsonFlashblocksAdapter, FlashblockInvalidation, FlashblockInvalidationReason,
70 FlashblockSnapshot, FlashblockUpdate, FlashblockUpdateAcknowledgement,
71 FlashblockUpdateChannelError, FlashblockUpdateSender, RawJsonFlashblocksAdapter,
72 RawJsonFlashblocksError, RawJsonFlashblocksLimits, TimedFlashblockUpdate,
73};
74
75#[derive(Clone, Debug, PartialEq, Eq)]
77pub enum ReactiveInput<N: Network = Ethereum> {
78 Log(Log),
80 BlockHeader(N::HeaderResponse),
82 FullBlock(N::BlockResponse),
84 PendingTxHash(B256),
86 PendingTx(N::TransactionResponse),
88}
89
90#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
92pub struct ReactiveContext {
93 pub chain_id: Option<u64>,
95 pub source: InputSource,
97 pub chain_status: ChainStatus,
99 pub block: Option<BlockRef>,
101 pub transaction_index: Option<u64>,
103 pub log_index: Option<u64>,
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
109pub struct BlockRef {
110 pub number: u64,
112 pub hash: B256,
114 pub parent_hash: Option<B256>,
116 pub timestamp: Option<u64>,
118}
119
120#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
127pub struct ProviderRef {
128 pub endpoint: EndpointId,
130 pub generation: u64,
132}
133
134impl ProviderRef {
135 pub fn new(endpoint: impl Into<EndpointId>, generation: u64) -> Self {
137 Self {
138 endpoint: endpoint.into(),
139 generation,
140 }
141 }
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub struct FlashblockIngressTiming {
151 source_ingress: Instant,
152}
153
154impl FlashblockIngressTiming {
155 pub const fn new(source_ingress: Instant) -> Self {
157 Self { source_ingress }
158 }
159
160 pub const fn source_ingress(self) -> Instant {
162 self.source_ingress
163 }
164
165 pub fn earliest(self, other: Self) -> Self {
167 Self::new(self.source_ingress.min(other.source_ingress))
168 }
169}
170
171#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
173pub struct FlashblockRef {
174 pub provider: ProviderRef,
176 pub payload_id: Option<FixedBytes<8>>,
180 pub index: Option<u64>,
182 pub block_number: u64,
184 pub content_hash: B256,
190 pub partial_block_hash: Option<B256>,
192 pub parent_hash: Option<B256>,
194 pub state_root: Option<B256>,
196 pub transactions_root: Option<B256>,
198 pub transaction_hashes: Vec<B256>,
200 pub timestamp: Option<u64>,
202 pub base_fee_per_gas: Option<u64>,
204 pub beneficiary: Option<Address>,
206 pub prevrandao: Option<B256>,
208 pub gas_limit: Option<u64>,
210}
211
212impl FlashblockRef {
213 pub const fn block_ref(&self) -> BlockRef {
218 BlockRef {
219 number: self.block_number,
220 hash: self.content_hash,
221 parent_hash: self.parent_hash,
222 timestamp: self.timestamp,
223 }
224 }
225
226 pub fn contains_transaction(&self, transaction_hash: &B256) -> bool {
228 self.transaction_hashes.contains(transaction_hash)
229 }
230
231 fn transaction_index(&self, transaction_hash: &B256) -> Option<u64> {
232 self.transaction_hashes
233 .iter()
234 .position(|candidate| candidate == transaction_hash)
235 .and_then(|index| u64::try_from(index).ok())
236 }
237
238 fn same_payload(&self, other: &Self) -> bool {
239 self.provider == other.provider
240 && match (self.payload_id, other.payload_id) {
241 (Some(left), Some(right)) => left == right,
242 _ => {
243 self.block_number == other.block_number && self.parent_hash == other.parent_hash
244 }
245 }
246 }
247
248 #[cfg(feature = "raw-flashblocks-json")]
256 pub fn same_base_identity(&self, other: &Self) -> bool {
257 self.block_number == other.block_number
258 && self.parent_hash == other.parent_hash
259 && self.timestamp == other.timestamp
260 && self.base_fee_per_gas == other.base_fee_per_gas
261 && self.beneficiary == other.beneficiary
262 && self.prevrandao == other.prevrandao
263 && self.gas_limit == other.gas_limit
264 }
265
266 fn is_cumulative_successor_of(&self, previous: &Self) -> bool {
267 self.same_payload(previous)
268 && self.transaction_hashes.len() >= previous.transaction_hashes.len()
269 && self
270 .transaction_hashes
271 .starts_with(&previous.transaction_hashes)
272 && match (previous.index, self.index) {
273 (Some(previous), Some(current)) => current >= previous,
274 _ => true,
275 }
276 }
277}
278
279#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
281pub enum PreconfirmationMode {
282 #[default]
284 Disabled,
285 Preferred,
288 Required,
290}
291
292#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
294pub struct BaseFlashblockPayload {
295 pub payload_id: FixedBytes<8>,
297 pub index: u64,
299 pub base: Option<BaseFlashblockBase>,
301 pub diff: BaseFlashblockDiff,
303 #[serde(default)]
305 pub metadata: Option<BaseFlashblockMetadata>,
306}
307
308#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
310pub struct BaseFlashblockBase {
311 pub parent_hash: B256,
313 #[serde(deserialize_with = "deserialize_rpc_u64")]
315 pub block_number: u64,
316 #[serde(deserialize_with = "deserialize_rpc_u64")]
318 pub timestamp: u64,
319 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
321 pub gas_limit: Option<u64>,
322 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
324 pub base_fee_per_gas: Option<u64>,
325 #[serde(default, alias = "fee_recipient", alias = "feeRecipient")]
327 pub beneficiary: Option<Address>,
328 #[serde(
330 default,
331 alias = "prev_randao",
332 alias = "prevRandao",
333 alias = "mixHash"
334 )]
335 pub prevrandao: Option<B256>,
336}
337
338#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
340pub struct BaseFlashblockDiff {
341 pub state_root: B256,
343 pub block_hash: B256,
345 #[serde(default)]
347 pub transactions: Vec<serde_json::Value>,
348 #[serde(default)]
350 pub transactions_root: Option<B256>,
351}
352
353#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
356pub struct BaseFlashblockMetadata {
357 #[serde(deserialize_with = "deserialize_rpc_u64")]
359 pub block_number: u64,
360}
361
362#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
365#[serde(rename_all = "camelCase")]
366struct BaseFlashblockBlockPayload {
367 hash: B256,
368 #[serde(deserialize_with = "deserialize_rpc_u64")]
369 number: u64,
370 parent_hash: B256,
371 state_root: B256,
372 #[serde(default)]
373 transactions_root: Option<B256>,
374 #[serde(default)]
375 transactions: Vec<serde_json::Value>,
376 #[serde(deserialize_with = "deserialize_rpc_u64")]
377 timestamp: u64,
378 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
379 base_fee_per_gas: Option<u64>,
380 #[serde(default, alias = "beneficiary", alias = "feeRecipient")]
381 miner: Option<Address>,
382 #[serde(default, alias = "prevRandao")]
383 mix_hash: Option<B256>,
384 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
385 gas_limit: Option<u64>,
386}
387
388#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
392#[serde(untagged)]
393enum BaseFlashblockWirePayload {
394 Indexed(BaseFlashblockPayload),
395 Block(BaseFlashblockBlockPayload),
396}
397
398fn deserialize_rpc_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
399where
400 D: serde::Deserializer<'de>,
401{
402 #[derive(serde::Deserialize)]
403 #[serde(untagged)]
404 enum RpcU64 {
405 Number(u64),
406 String(String),
407 }
408
409 match <RpcU64 as serde::Deserialize>::deserialize(deserializer)? {
410 RpcU64::Number(number) => Ok(number),
411 RpcU64::String(value) => {
412 let value = value.strip_prefix("0x").unwrap_or(&value);
413 u64::from_str_radix(value, 16).map_err(serde::de::Error::custom)
414 }
415 }
416}
417
418fn deserialize_optional_rpc_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
419where
420 D: serde::Deserializer<'de>,
421{
422 #[derive(serde::Deserialize)]
423 #[serde(untagged)]
424 enum RpcU64 {
425 Number(u64),
426 String(String),
427 }
428
429 let Some(value) = <Option<RpcU64> as serde::Deserialize>::deserialize(deserializer)? else {
430 return Ok(None);
431 };
432 match value {
433 RpcU64::Number(number) => Ok(Some(number)),
434 RpcU64::String(value) => {
435 let value = value.strip_prefix("0x").unwrap_or(&value);
436 u64::from_str_radix(value, 16)
437 .map(Some)
438 .map_err(serde::de::Error::custom)
439 }
440 }
441}
442
443fn non_placeholder_hash(hash: B256) -> Option<B256> {
444 (!hash.is_zero()).then_some(hash)
445}
446
447fn flashblock_transaction_hashes(
448 transactions: &[serde_json::Value],
449) -> Result<Vec<B256>, SubscriberError> {
450 let hashes: Vec<B256> = transactions
451 .iter()
452 .map(|transaction| {
453 let value = match transaction {
454 serde_json::Value::String(value) => value.as_str(),
455 serde_json::Value::Object(object) => object
456 .get("hash")
457 .or_else(|| object.get("transactionHash"))
458 .and_then(serde_json::Value::as_str)
459 .ok_or_else(|| {
460 SubscriberError::Provider(
461 "Flashblock transaction object is missing its hash".into(),
462 )
463 })?,
464 _ => {
465 return Err(SubscriberError::Provider(
466 "Flashblock transaction must be a hash, raw transaction, or object".into(),
467 ));
468 }
469 };
470 if value.len() == 66 {
471 return value.parse::<B256>().map_err(|error| {
472 SubscriberError::Provider(format!(
473 "Flashblock transaction hash is invalid: {error}"
474 ))
475 });
476 }
477 let encoded = value.strip_prefix("0x").unwrap_or(value);
478 let raw = alloy_primitives::hex::decode(encoded).map_err(|error| {
479 SubscriberError::Provider(format!(
480 "Flashblock raw transaction is invalid hex: {error}"
481 ))
482 })?;
483 Ok(alloy_primitives::keccak256(raw))
484 })
485 .collect::<Result<_, _>>()?;
486 let mut unique = HashSet::with_capacity(hashes.len());
487 if hashes.iter().any(|hash| !unique.insert(*hash)) {
488 return Err(SubscriberError::Provider(
489 "Flashblock cumulative transaction membership contains a duplicate hash".into(),
490 ));
491 }
492 Ok(hashes)
493}
494
495struct FlashblockContentCommitment<'a> {
496 provider: &'a ProviderRef,
497 payload_id: Option<FixedBytes<8>>,
498 index: Option<u64>,
499 block_number: u64,
500 partial_block_hash: Option<B256>,
501 parent_hash: Option<B256>,
502 state_root: Option<B256>,
503 transactions_root: Option<B256>,
504 transaction_hashes: &'a [B256],
505 timestamp: Option<u64>,
506 base_fee_per_gas: Option<u64>,
507 beneficiary: Option<Address>,
508 prevrandao: Option<B256>,
509 gas_limit: Option<u64>,
510}
511
512fn flashblock_content_hash(content: FlashblockContentCommitment<'_>) -> B256 {
513 let mut commitment = Keccak256::new();
514 commitment.update(b"evm-fork-cache/flashblock-content/v1");
515 let endpoint = content.provider.endpoint.as_str().as_bytes();
516 commitment.update((endpoint.len() as u64).to_be_bytes());
517 commitment.update(endpoint);
518 commitment.update(content.provider.generation.to_be_bytes());
519 commitment.update(content.block_number.to_be_bytes());
520 commit_optional_bytes(
521 &mut commitment,
522 content.payload_id.as_ref().map(FixedBytes::as_slice),
523 );
524 commit_optional_u64(&mut commitment, content.index);
525 commit_optional_bytes(
526 &mut commitment,
527 content
528 .partial_block_hash
529 .as_ref()
530 .map(FixedBytes::as_slice),
531 );
532 commit_optional_bytes(
533 &mut commitment,
534 content.parent_hash.as_ref().map(FixedBytes::as_slice),
535 );
536 commit_optional_bytes(
537 &mut commitment,
538 content.state_root.as_ref().map(FixedBytes::as_slice),
539 );
540 commit_optional_bytes(
541 &mut commitment,
542 content.transactions_root.as_ref().map(FixedBytes::as_slice),
543 );
544 commitment.update((content.transaction_hashes.len() as u64).to_be_bytes());
545 for transaction_hash in content.transaction_hashes {
546 commitment.update(transaction_hash);
547 }
548 commit_optional_u64(&mut commitment, content.timestamp);
549 commit_optional_u64(&mut commitment, content.base_fee_per_gas);
550 commit_optional_bytes(
551 &mut commitment,
552 content
553 .beneficiary
554 .as_ref()
555 .map(|address| address.as_slice()),
556 );
557 commit_optional_bytes(
558 &mut commitment,
559 content.prevrandao.as_ref().map(FixedBytes::as_slice),
560 );
561 commit_optional_u64(&mut commitment, content.gas_limit);
562 let hash = commitment.finalize();
563 if hash.is_zero() {
564 B256::with_last_byte(1)
565 } else {
566 hash
567 }
568}
569
570#[cfg(feature = "raw-flashblocks-json")]
571fn validate_standard_flashblock_snapshot(
572 snapshot: &FlashblockSnapshot,
573) -> Result<(), SubscriberError> {
574 let flashblock = &snapshot.flashblock;
575 if flashblock.payload_id.is_none() || flashblock.index.is_none() {
576 return Err(SubscriberError::Provider(
577 "external Flashblock snapshot is missing its indexed payload identity".into(),
578 ));
579 }
580 let expected_content_hash = flashblock_content_hash(FlashblockContentCommitment {
581 provider: &flashblock.provider,
582 payload_id: flashblock.payload_id,
583 index: flashblock.index,
584 block_number: flashblock.block_number,
585 partial_block_hash: flashblock.partial_block_hash,
586 parent_hash: flashblock.parent_hash,
587 state_root: flashblock.state_root,
588 transactions_root: flashblock.transactions_root,
589 transaction_hashes: &flashblock.transaction_hashes,
590 timestamp: flashblock.timestamp,
591 base_fee_per_gas: flashblock.base_fee_per_gas,
592 beneficiary: flashblock.beneficiary,
593 prevrandao: flashblock.prevrandao,
594 gas_limit: flashblock.gas_limit,
595 });
596 if flashblock.content_hash != expected_content_hash {
597 return Err(SubscriberError::Provider(
598 "external Flashblock content commitment is invalid".into(),
599 ));
600 }
601
602 let mut transactions = HashSet::with_capacity(flashblock.transaction_hashes.len());
603 if flashblock
604 .transaction_hashes
605 .iter()
606 .any(|hash| !transactions.insert(*hash))
607 {
608 return Err(SubscriberError::Provider(
609 "external Flashblock cumulative transaction membership contains a duplicate hash"
610 .into(),
611 ));
612 }
613
614 let mut log_ids = HashSet::with_capacity(snapshot.logs.len());
615 for log in &snapshot.logs {
616 if log.removed || log.block_number != Some(flashblock.block_number) {
617 return Err(SubscriberError::Provider(
618 "external pre-confirmed log disagrees with its Flashblock block identity".into(),
619 ));
620 }
621 if log.block_hash != Some(flashblock.content_hash) {
622 return Err(SubscriberError::Provider(
623 "external pre-confirmed log is not bound to its Flashblock content commitment"
624 .into(),
625 ));
626 }
627 let transaction_hash = log.transaction_hash.ok_or_else(|| {
628 SubscriberError::Provider(
629 "external pre-confirmed log is missing its transaction hash".into(),
630 )
631 })?;
632 let expected_transaction_index = flashblock
633 .transaction_index(&transaction_hash)
634 .ok_or_else(|| {
635 SubscriberError::Provider(
636 "external pre-confirmed log transaction is absent from the cumulative Flashblock"
637 .into(),
638 )
639 })?;
640 if log.transaction_index != Some(expected_transaction_index) {
641 return Err(SubscriberError::Provider(
642 "external pre-confirmed log transaction index disagrees with cumulative membership"
643 .into(),
644 ));
645 }
646 let log_index = log.log_index.ok_or_else(|| {
647 SubscriberError::Provider("external pre-confirmed log is missing its log index".into())
648 })?;
649 if !log_ids.insert((transaction_hash, log_index)) {
650 return Err(SubscriberError::Provider(
651 "external Flashblock snapshot contains a duplicate log identity".into(),
652 ));
653 }
654 }
655 Ok(())
656}
657
658fn commit_optional_bytes(commitment: &mut Keccak256, value: Option<&[u8]>) {
659 match value {
660 Some(value) => {
661 commitment.update([1]);
662 commitment.update((value.len() as u64).to_be_bytes());
663 commitment.update(value);
664 }
665 None => commitment.update([0]),
666 }
667}
668
669fn commit_optional_u64(commitment: &mut Keccak256, value: Option<u64>) {
670 match value {
671 Some(value) => {
672 commitment.update([1]);
673 commitment.update(value.to_be_bytes());
674 }
675 None => commitment.update([0]),
676 }
677}
678
679#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
682pub struct ReactiveCanonicalBaseline {
683 pub chain_id: u64,
685 pub block: BlockRef,
687}
688
689impl ReactiveCanonicalBaseline {
690 pub const fn new(chain_id: u64, block: BlockRef) -> Self {
692 Self { chain_id, block }
693 }
694}
695
696#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
705#[non_exhaustive]
706pub enum ChainControl {
707 Reorg {
709 common_ancestor: BlockRef,
711 old_tip: BlockRef,
713 new_tip: BlockRef,
715 },
716 Safe(BlockRef),
718 Finalized(BlockRef),
720 CanonicalProgress(BlockRef),
727 Barrier {
729 id: Vec<u8>,
731 block: Option<BlockRef>,
733 },
734}
735
736#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
755pub struct CanonicalSequenceState {
756 retained_canonical_history: Vec<BlockRef>,
757 coverage_head: Option<BlockRef>,
758 safe_head: Option<BlockRef>,
759 finalized_head: Option<BlockRef>,
760}
761
762impl CanonicalSequenceState {
763 pub fn new(
769 retained_canonical_history: Vec<BlockRef>,
770 coverage_head: Option<BlockRef>,
771 safe_head: Option<BlockRef>,
772 finalized_head: Option<BlockRef>,
773 ) -> Self {
774 Self {
775 retained_canonical_history,
776 coverage_head,
777 safe_head,
778 finalized_head,
779 }
780 }
781
782 pub fn retained_canonical_history(&self) -> &[BlockRef] {
784 &self.retained_canonical_history
785 }
786
787 pub const fn coverage_head(&self) -> Option<&BlockRef> {
789 self.coverage_head.as_ref()
790 }
791
792 pub const fn safe_head(&self) -> Option<&BlockRef> {
794 self.safe_head.as_ref()
795 }
796
797 pub const fn finalized_head(&self) -> Option<&BlockRef> {
799 self.finalized_head.as_ref()
800 }
801
802 pub fn retain_recent_history(&mut self, max_entries: usize) {
811 let remove = self
812 .retained_canonical_history
813 .len()
814 .saturating_sub(max_entries);
815 self.retained_canonical_history.drain(..remove);
816 }
817
818 pub fn validate(&self) -> Result<(), ReactiveError> {
832 validate_canonical_sequence_snapshot(self)
833 }
834}
835
836#[derive(Clone, Debug, PartialEq, Eq)]
838#[non_exhaustive]
839pub enum CanonicalSequenceMutation {
840 Rewind {
842 common_ancestor: Option<BlockRef>,
848 dropped: Vec<BlockRef>,
850 },
851 Canonical(BlockRef),
853 Safe(BlockRef),
855 Finalized(BlockRef),
857}
858
859#[derive(Clone, Debug, PartialEq, Eq)]
861pub struct CanonicalSequenceValidation {
862 pre_record_state: CanonicalSequenceState,
863 next_state: CanonicalSequenceState,
864 mutations: Vec<CanonicalSequenceMutation>,
865 normalized_chain_controls: Vec<ChainControl>,
866}
867
868impl CanonicalSequenceValidation {
869 pub const fn pre_record_state(&self) -> &CanonicalSequenceState {
871 &self.pre_record_state
872 }
873
874 pub const fn next_state(&self) -> &CanonicalSequenceState {
876 &self.next_state
877 }
878
879 pub fn mutations(&self) -> &[CanonicalSequenceMutation] {
881 &self.mutations
882 }
883
884 pub fn normalized_chain_controls(&self) -> &[ChainControl] {
894 &self.normalized_chain_controls
895 }
896}
897
898#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
900#[non_exhaustive]
901pub enum ChainStatus {
902 Pending,
904 Preconfirmed {
909 flashblock: Arc<FlashblockRef>,
913 },
914 Included {
916 block: BlockRef,
918 confirmations: u64,
920 },
921 Safe {
923 block: BlockRef,
925 },
926 Finalized {
928 block: BlockRef,
930 },
931 Reorged {
933 dropped_from: BlockRef,
935 },
936}
937
938#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
940#[non_exhaustive]
941pub enum InputSource {
942 Batch,
944 Subscription,
946 Poll,
948 Backfill,
950 Flashblocks,
952 Synthetic,
954}
955
956#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
958pub enum InputRef {
959 Log {
961 chain_id: Option<u64>,
963 block_hash: B256,
965 transaction_hash: B256,
967 log_index: u64,
969 },
970 PendingTx {
972 chain_id: Option<u64>,
974 hash: B256,
976 },
977 Block {
979 chain_id: Option<u64>,
981 hash: B256,
983 number: u64,
985 },
986}
987
988#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
995#[non_exhaustive]
996pub enum ReactiveInputKind {
997 CanonicalLog,
999 ReorgSignalLog,
1001 BlockHeader,
1003 FullBlock,
1005 PendingTxHash,
1007 PendingTx,
1009}
1010
1011#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1018pub struct ReactiveInputIdentity {
1019 input_ref: InputRef,
1020 kind: ReactiveInputKind,
1021}
1022
1023impl ReactiveInputIdentity {
1024 pub fn try_from_parts(
1038 input_ref: InputRef,
1039 kind: ReactiveInputKind,
1040 ) -> Result<Self, ReactiveInputIdentityError> {
1041 let compatible = matches!(
1042 (input_ref, kind),
1043 (
1044 InputRef::Log { .. },
1045 ReactiveInputKind::CanonicalLog | ReactiveInputKind::ReorgSignalLog
1046 ) | (
1047 InputRef::Block { .. },
1048 ReactiveInputKind::BlockHeader | ReactiveInputKind::FullBlock
1049 ) | (
1050 InputRef::PendingTx { .. },
1051 ReactiveInputKind::PendingTxHash | ReactiveInputKind::PendingTx
1052 )
1053 );
1054 if !compatible {
1055 return Err(ReactiveInputIdentityError { input_ref, kind });
1056 }
1057 Ok(Self { input_ref, kind })
1058 }
1059
1060 pub const fn input_ref(&self) -> InputRef {
1062 self.input_ref
1063 }
1064
1065 pub const fn kind(&self) -> ReactiveInputKind {
1067 self.kind
1068 }
1069}
1070
1071#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
1074#[error("reactive input kind {kind:?} is incompatible with input reference {input_ref:?}")]
1075pub struct ReactiveInputIdentityError {
1076 input_ref: InputRef,
1077 kind: ReactiveInputKind,
1078}
1079
1080impl ReactiveInputIdentityError {
1081 pub const fn input_ref(&self) -> InputRef {
1083 self.input_ref
1084 }
1085
1086 pub const fn kind(&self) -> ReactiveInputKind {
1088 self.kind
1089 }
1090}
1091
1092#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1094pub enum StateEffectQuality {
1095 ExactFromInput,
1097 AppliedWithPendingResync,
1099 ResyncedAuthoritatively,
1101 RequiresRepair,
1103 NoStateEffect,
1105}
1106
1107#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize)]
1109pub struct HandlerId(String);
1110
1111impl HandlerId {
1112 pub fn new(id: impl Into<String>) -> Self {
1119 Self::try_new(id).expect("handler id must not be empty")
1120 }
1121
1122 pub fn try_new(id: impl Into<String>) -> Result<Self, HandlerIdError> {
1129 let id = id.into();
1130 if id.is_empty() {
1131 return Err(HandlerIdError);
1132 }
1133 Ok(Self(id))
1134 }
1135
1136 pub fn as_str(&self) -> &str {
1138 &self.0
1139 }
1140}
1141
1142impl<'de> serde::Deserialize<'de> for HandlerId {
1143 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1144 where
1145 D: serde::Deserializer<'de>,
1146 {
1147 let id = <String as serde::Deserialize>::deserialize(deserializer)?;
1148 Self::try_new(id).map_err(serde::de::Error::custom)
1149 }
1150}
1151
1152#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
1155#[error("handler id must not be empty")]
1156pub struct HandlerIdError;
1157
1158impl fmt::Display for HandlerId {
1159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1160 self.0.fmt(f)
1161 }
1162}
1163
1164#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1166pub struct ReportTag {
1167 pub key: String,
1169 pub value: String,
1171}
1172
1173impl ReportTag {
1174 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1176 Self {
1177 key: key.into(),
1178 value: value.into(),
1179 }
1180 }
1181}
1182
1183#[derive(Clone)]
1185pub struct HookSignal {
1186 pub namespace: Cow<'static, str>,
1188 pub kind: Cow<'static, str>,
1190 pub labels: Vec<ReportTag>,
1192 pub payload: Option<Arc<dyn Any + Send + Sync>>,
1194}
1195
1196impl fmt::Debug for HookSignal {
1197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1198 f.debug_struct("HookSignal")
1199 .field("namespace", &self.namespace)
1200 .field("kind", &self.kind)
1201 .field("labels", &self.labels)
1202 .field("payload", &self.payload.as_ref().map(|_| "<payload>"))
1203 .finish()
1204 }
1205}
1206
1207#[derive(Clone, Debug)]
1209pub enum ReactiveEffect {
1210 StateUpdate(StateUpdate),
1212 Resync(ResyncRequest),
1214 Invalidate(InvalidationRequest),
1216 Hook(HookSignal),
1218 Speculative(SpeculativeRequest),
1220}
1221
1222#[derive(Clone, Debug)]
1224pub struct HandlerOutcome {
1225 pub effects: Vec<ReactiveEffect>,
1227 pub quality: StateEffectQuality,
1229 pub tags: Vec<ReportTag>,
1231}
1232
1233impl HandlerOutcome {
1234 pub fn empty(quality: StateEffectQuality) -> Self {
1236 Self {
1237 effects: Vec::new(),
1238 quality,
1239 tags: Vec::new(),
1240 }
1241 }
1242}
1243
1244#[derive(Clone, Debug)]
1246pub struct ReactiveInputRecord<N: Network = Ethereum> {
1247 pub input: ReactiveInput<N>,
1249 pub context: ReactiveContext,
1251 pub provider: Option<ProviderRef>,
1254}
1255
1256impl<N: Network> ReactiveInputRecord<N> {
1257 pub fn new(input: ReactiveInput<N>, context: ReactiveContext) -> Self {
1259 Self {
1260 input,
1261 context,
1262 provider: None,
1263 }
1264 }
1265
1266 #[must_use]
1268 pub fn with_provider(mut self, provider: ProviderRef) -> Self {
1269 self.provider = Some(provider);
1270 self
1271 }
1272
1273 pub fn input_ref(&self) -> InputRef {
1275 input_ref(&self.input, &self.context)
1276 }
1277
1278 pub fn validated_identity(&self) -> Result<ReactiveInputIdentity, ReactiveError> {
1295 validate_input_record(self)?;
1296 let kind = match &self.input {
1297 ReactiveInput::Log(log)
1298 if log.removed
1299 || matches!(self.context.chain_status, ChainStatus::Reorged { .. }) =>
1300 {
1301 ReactiveInputKind::ReorgSignalLog
1302 }
1303 ReactiveInput::Log(_) => ReactiveInputKind::CanonicalLog,
1304 ReactiveInput::BlockHeader(_) => ReactiveInputKind::BlockHeader,
1305 ReactiveInput::FullBlock(_) => ReactiveInputKind::FullBlock,
1306 ReactiveInput::PendingTxHash(_) => ReactiveInputKind::PendingTxHash,
1307 ReactiveInput::PendingTx(_) => ReactiveInputKind::PendingTx,
1308 };
1309 ReactiveInputIdentity::try_from_parts(self.input_ref(), kind).map_err(|error| {
1310 ReactiveError::InvalidInputRecord {
1311 message: error.to_string(),
1312 }
1313 })
1314 }
1315
1316 pub fn same_deduplicable_payload(&self, other: &Self) -> bool {
1330 match (&self.input, &other.input) {
1331 (ReactiveInput::Log(left), ReactiveInput::Log(right)) => {
1332 left.inner == right.inner
1333 && left.block_hash == right.block_hash
1334 && left.block_number == right.block_number
1335 && optional_metadata_compatible(
1336 left.block_timestamp.as_ref(),
1337 right.block_timestamp.as_ref(),
1338 )
1339 && left.transaction_hash == right.transaction_hash
1340 && left.transaction_index == right.transaction_index
1341 && left.log_index == right.log_index
1342 && left.removed == right.removed
1343 }
1344 (ReactiveInput::BlockHeader(left), ReactiveInput::BlockHeader(right)) => {
1345 left.hash() == right.hash()
1346 }
1347 (ReactiveInput::FullBlock(_), ReactiveInput::FullBlock(_)) => false,
1348 (ReactiveInput::PendingTxHash(left), ReactiveInput::PendingTxHash(right)) => {
1349 left == right
1350 }
1351 (ReactiveInput::PendingTx(_), ReactiveInput::PendingTx(_)) => false,
1352 _ => false,
1353 }
1354 }
1355
1356 pub fn is_payload_deduplicable(&self) -> bool {
1362 matches!(
1363 &self.input,
1364 ReactiveInput::Log(_) | ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_)
1365 )
1366 }
1367
1368 pub fn merge_compatible_duplicate(&mut self, other: &Self) -> Result<bool, ReactiveError> {
1383 let identity = self.validated_identity()?;
1384 let other_identity = other.validated_identity()?;
1385 if identity != other_identity
1386 || !self.is_payload_deduplicable()
1387 || !other.is_payload_deduplicable()
1388 {
1389 return Ok(false);
1390 }
1391 if !self.same_deduplicable_payload(other) || !self.dedupe_context_is_compatible(other) {
1392 return Err(ReactiveError::InvalidInputRecord {
1393 message: format!(
1394 "conflicting payload or semantic context for identity {identity:?}"
1395 ),
1396 });
1397 }
1398 let mut merged = self.clone();
1399 merge_deduplicable_record(&mut merged, other);
1400 merged.validated_identity()?;
1401 *self = merged;
1402 Ok(true)
1403 }
1404
1405 pub fn dedupe_context_is_compatible(&self, other: &Self) -> bool {
1413 let left = &self.context;
1414 let right = &other.context;
1415 left.chain_id == right.chain_id
1416 && optional_block_refs_are_compatible(left.block.as_ref(), right.block.as_ref())
1417 && left.transaction_index == right.transaction_index
1418 && left.log_index == right.log_index
1419 && chain_statuses_are_dedupe_compatible(&left.chain_status, &right.chain_status)
1420 }
1421}
1422
1423fn chain_statuses_are_dedupe_compatible(left: &ChainStatus, right: &ChainStatus) -> bool {
1424 match (left, right) {
1425 (ChainStatus::Pending, ChainStatus::Pending)
1426 | (ChainStatus::Reorged { .. }, ChainStatus::Reorged { .. }) => true,
1427 (
1428 ChainStatus::Preconfirmed { flashblock: left },
1429 ChainStatus::Preconfirmed { flashblock: right },
1430 ) => left == right,
1431 (
1432 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. },
1433 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. },
1434 ) => true,
1435 _ => false,
1436 }
1437}
1438
1439fn optional_metadata_compatible<T: PartialEq>(left: Option<&T>, right: Option<&T>) -> bool {
1440 left.zip(right).is_none_or(|(left, right)| left == right)
1441}
1442
1443fn optional_block_refs_are_compatible(left: Option<&BlockRef>, right: Option<&BlockRef>) -> bool {
1444 match (left, right) {
1445 (None, None) => true,
1446 (Some(left), Some(right)) => {
1447 left.number == right.number
1448 && left.hash == right.hash
1449 && optional_metadata_compatible(
1450 left.parent_hash.as_ref(),
1451 right.parent_hash.as_ref(),
1452 )
1453 && optional_metadata_compatible(left.timestamp.as_ref(), right.timestamp.as_ref())
1454 }
1455 _ => false,
1456 }
1457}
1458
1459fn merge_deduplicable_record<N: Network>(
1460 retained: &mut ReactiveInputRecord<N>,
1461 incoming: &ReactiveInputRecord<N>,
1462) {
1463 if let (ReactiveInput::Log(retained), ReactiveInput::Log(incoming)) =
1464 (&mut retained.input, &incoming.input)
1465 && retained.block_timestamp.is_none()
1466 {
1467 retained.block_timestamp = incoming.block_timestamp;
1468 }
1469 if let (Some(retained), Some(incoming)) =
1470 (&mut retained.context.block, incoming.context.block.as_ref())
1471 {
1472 enrich_block_ref(retained, incoming);
1473 }
1474 retained.context.chain_status = merged_chain_status(
1475 &retained.context.chain_status,
1476 &incoming.context.chain_status,
1477 );
1478 if input_source_rank(incoming.context.source) > input_source_rank(retained.context.source) {
1479 retained.context.source = incoming.context.source;
1480 }
1481 if retained.provider.is_none() {
1482 retained.provider = incoming.provider.clone();
1483 }
1484}
1485
1486fn enrich_block_ref(retained: &mut BlockRef, incoming: &BlockRef) {
1487 if retained.parent_hash.is_none() {
1488 retained.parent_hash = incoming.parent_hash;
1489 }
1490 if retained.timestamp.is_none() {
1491 retained.timestamp = incoming.timestamp;
1492 }
1493}
1494
1495fn merged_chain_status(retained: &ChainStatus, incoming: &ChainStatus) -> ChainStatus {
1496 let merged_block = |left: &BlockRef, right: &BlockRef| {
1497 let mut block = *left;
1498 enrich_block_ref(&mut block, right);
1499 block
1500 };
1501 match (retained, incoming) {
1502 (ChainStatus::Pending, ChainStatus::Pending) => ChainStatus::Pending,
1503 (
1504 ChainStatus::Preconfirmed { flashblock: left },
1505 ChainStatus::Preconfirmed { flashblock: right },
1506 ) => {
1507 debug_assert_eq!(left, right, "compatible pre-confirmed records agree");
1508 ChainStatus::Preconfirmed {
1509 flashblock: left.clone(),
1510 }
1511 }
1512 (
1513 ChainStatus::Reorged { dropped_from: left },
1514 ChainStatus::Reorged {
1515 dropped_from: right,
1516 },
1517 ) => ChainStatus::Reorged {
1518 dropped_from: merged_block(left, right),
1519 },
1520 (left, right) => {
1521 let (left_block, left_rank, left_confirmations) = canonical_status_parts(left)
1522 .expect("compatible duplicate has a canonical lifecycle");
1523 let (right_block, right_rank, right_confirmations) = canonical_status_parts(right)
1524 .expect("compatible duplicate has a canonical lifecycle");
1525 let block = merged_block(left_block, right_block);
1526 let rank = left_rank.max(right_rank);
1527 match rank {
1528 3 => ChainStatus::Finalized { block },
1529 2 => ChainStatus::Safe { block },
1530 _ => ChainStatus::Included {
1531 block,
1532 confirmations: left_confirmations.max(right_confirmations),
1533 },
1534 }
1535 }
1536 }
1537}
1538
1539fn canonical_status_parts(status: &ChainStatus) -> Option<(&BlockRef, u8, u64)> {
1540 match status {
1541 ChainStatus::Included {
1542 block,
1543 confirmations,
1544 } => Some((block, 1, *confirmations)),
1545 ChainStatus::Safe { block } => Some((block, 2, 0)),
1546 ChainStatus::Finalized { block } => Some((block, 3, 0)),
1547 ChainStatus::Pending | ChainStatus::Preconfirmed { .. } | ChainStatus::Reorged { .. } => {
1548 None
1549 }
1550 }
1551}
1552
1553fn input_source_rank(source: InputSource) -> u8 {
1554 match source {
1555 InputSource::Backfill => 0,
1556 InputSource::Poll => 1,
1557 InputSource::Subscription => 2,
1558 InputSource::Flashblocks => 3,
1559 InputSource::Batch => 4,
1560 InputSource::Synthetic => 5,
1561 }
1562}
1563
1564#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1574pub struct SubscriberDeliveryToken(Vec<u8>);
1575
1576impl SubscriberDeliveryToken {
1577 pub fn new(bytes: Vec<u8>) -> Self {
1579 Self(bytes)
1580 }
1581
1582 pub fn as_bytes(&self) -> &[u8] {
1584 &self.0
1585 }
1586
1587 pub fn into_bytes(self) -> Vec<u8> {
1589 self.0
1590 }
1591}
1592
1593#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1599pub struct SubscriberCheckpoint(Vec<u8>);
1600
1601impl SubscriberCheckpoint {
1602 pub fn new(bytes: Vec<u8>) -> Self {
1604 Self(bytes)
1605 }
1606
1607 pub fn as_bytes(&self) -> &[u8] {
1609 &self.0
1610 }
1611
1612 pub fn into_bytes(self) -> Vec<u8> {
1614 self.0
1615 }
1616}
1617
1618#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1628pub struct SubscriberPayloadCommitment(B256);
1629
1630impl SubscriberPayloadCommitment {
1631 pub const fn new(commitment: B256) -> Self {
1633 Self(commitment)
1634 }
1635
1636 pub const fn digest(&self) -> B256 {
1638 self.0
1639 }
1640}
1641
1642#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1648#[non_exhaustive]
1649pub struct SubscriberResumePosition {
1650 pub chain_id: u64,
1652 pub coverage_head: BlockRef,
1654 pub canonical_history: Vec<BlockRef>,
1656 pub delivery_token: Option<SubscriberDeliveryToken>,
1660 pub subscriber_checkpoint: Option<SubscriberCheckpoint>,
1662}
1663
1664impl SubscriberResumePosition {
1665 pub fn new(
1667 chain_id: u64,
1668 coverage_head: BlockRef,
1669 canonical_history: Vec<BlockRef>,
1670 delivery_token: Option<SubscriberDeliveryToken>,
1671 subscriber_checkpoint: Option<SubscriberCheckpoint>,
1672 ) -> Self {
1673 Self {
1674 chain_id,
1675 coverage_head,
1676 canonical_history,
1677 delivery_token,
1678 subscriber_checkpoint,
1679 }
1680 }
1681}
1682
1683#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1691#[non_exhaustive]
1692pub enum DeliveryAudience {
1693 #[default]
1695 All,
1696 Owners(Vec<HandlerId>),
1698 AllExcept(Vec<HandlerId>),
1703}
1704
1705#[derive(
1713 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
1714)]
1715#[non_exhaustive]
1716pub enum DeliveryScope {
1717 #[default]
1719 Canonical,
1720 CanonicalProgress,
1722 OwnerCatchup,
1724 Preconfirmed,
1727}
1728
1729impl DeliveryScope {
1730 const fn advances_canonical_state(self) -> bool {
1731 matches!(self, Self::Canonical | Self::CanonicalProgress)
1732 }
1733}
1734
1735#[derive(Clone, Debug)]
1737pub struct ReactiveInputDelivery<N: Network = Ethereum> {
1738 record: ReactiveInputRecord<N>,
1739 audience: DeliveryAudience,
1740 scope: DeliveryScope,
1741}
1742
1743impl<N: Network> ReactiveInputDelivery<N> {
1744 pub fn new(
1746 record: ReactiveInputRecord<N>,
1747 audience: DeliveryAudience,
1748 scope: DeliveryScope,
1749 ) -> Self {
1750 Self {
1751 record,
1752 audience,
1753 scope,
1754 }
1755 }
1756
1757 pub const fn record(&self) -> &ReactiveInputRecord<N> {
1759 &self.record
1760 }
1761
1762 pub const fn audience(&self) -> &DeliveryAudience {
1764 &self.audience
1765 }
1766
1767 pub const fn scope(&self) -> DeliveryScope {
1769 self.scope
1770 }
1771
1772 pub fn into_parts(self) -> (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope) {
1774 (self.record, self.audience, self.scope)
1775 }
1776}
1777
1778#[derive(Clone, Debug)]
1783#[non_exhaustive]
1784pub struct ReactiveInputBatchParts<N: Network = Ethereum> {
1785 pub chain_id: Option<u64>,
1787 pub deliveries: Vec<ReactiveInputDelivery<N>>,
1789 pub delivery_token: Option<SubscriberDeliveryToken>,
1791 pub subscriber_checkpoint: Option<SubscriberCheckpoint>,
1793 pub payload_commitment: Option<SubscriberPayloadCommitment>,
1795 pub chain_controls: Vec<ChainControl>,
1797 pub preconfirmation_timing: Option<FlashblockIngressTiming>,
1799}
1800
1801#[derive(Clone, Debug)]
1803pub struct ReactiveInputBatch<N: Network = Ethereum> {
1804 records: Vec<ReactiveInputRecord<N>>,
1805 chain_id: Option<u64>,
1806 delivery_token: Option<SubscriberDeliveryToken>,
1807 subscriber_checkpoint: Option<SubscriberCheckpoint>,
1808 payload_commitment: Option<SubscriberPayloadCommitment>,
1809 audience: DeliveryAudience,
1810 record_audiences: Option<Vec<DeliveryAudience>>,
1811 delivery_scope: DeliveryScope,
1812 record_delivery_scopes: Option<Vec<DeliveryScope>>,
1813 chain_controls: Vec<ChainControl>,
1814 preconfirmation_timing: Option<FlashblockIngressTiming>,
1815}
1816
1817type RuntimeInputDelivery<N> = (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope);
1818
1819impl<N: Network> ReactiveInputBatch<N> {
1820 pub fn new(records: Vec<ReactiveInputRecord<N>>) -> Self {
1822 let chain_id = common_record_chain_id(&records);
1823 Self {
1824 records,
1825 chain_id,
1826 delivery_token: None,
1827 subscriber_checkpoint: None,
1828 payload_commitment: None,
1829 audience: DeliveryAudience::All,
1830 record_audiences: None,
1831 delivery_scope: DeliveryScope::Canonical,
1832 record_delivery_scopes: None,
1833 chain_controls: Vec::new(),
1834 preconfirmation_timing: None,
1835 }
1836 }
1837
1838 pub fn with_chain_id(mut self, chain_id: u64) -> Self {
1841 self.chain_id = Some(chain_id);
1842 self
1843 }
1844
1845 pub const fn chain_id(&self) -> Option<u64> {
1848 self.chain_id
1849 }
1850
1851 pub fn with_delivery_token(mut self, token: SubscriberDeliveryToken) -> Self {
1853 self.delivery_token = Some(token);
1854 self
1855 }
1856
1857 pub fn delivery_token(&self) -> Option<&SubscriberDeliveryToken> {
1859 self.delivery_token.as_ref()
1860 }
1861
1862 pub fn with_subscriber_checkpoint(mut self, checkpoint: SubscriberCheckpoint) -> Self {
1864 self.subscriber_checkpoint = Some(checkpoint);
1865 self
1866 }
1867
1868 pub fn subscriber_checkpoint(&self) -> Option<&SubscriberCheckpoint> {
1870 self.subscriber_checkpoint.as_ref()
1871 }
1872
1873 pub fn with_payload_commitment(mut self, commitment: SubscriberPayloadCommitment) -> Self {
1876 self.payload_commitment = Some(commitment);
1877 self
1878 }
1879
1880 pub const fn payload_commitment(&self) -> Option<&SubscriberPayloadCommitment> {
1882 self.payload_commitment.as_ref()
1883 }
1884
1885 pub fn with_audience(mut self, audience: DeliveryAudience) -> Self {
1887 self.audience = audience;
1888 self.record_audiences = None;
1889 self
1890 }
1891
1892 pub const fn audience(&self) -> &DeliveryAudience {
1894 &self.audience
1895 }
1896
1897 pub fn from_scoped_records(
1899 records: impl IntoIterator<Item = (ReactiveInputRecord<N>, DeliveryAudience)>,
1900 ) -> Self {
1901 let (records, record_audiences): (Vec<_>, Vec<_>) = records.into_iter().unzip();
1902 let chain_id = common_record_chain_id(&records);
1903 Self {
1904 records,
1905 chain_id,
1906 delivery_token: None,
1907 subscriber_checkpoint: None,
1908 payload_commitment: None,
1909 audience: DeliveryAudience::All,
1910 record_audiences: Some(record_audiences),
1911 delivery_scope: DeliveryScope::Canonical,
1912 record_delivery_scopes: None,
1913 chain_controls: Vec::new(),
1914 preconfirmation_timing: None,
1915 }
1916 }
1917
1918 pub fn from_deliveries(deliveries: impl IntoIterator<Item = ReactiveInputDelivery<N>>) -> Self {
1920 Self::from_scoped_records_with_delivery_scope(
1921 deliveries
1922 .into_iter()
1923 .map(ReactiveInputDelivery::into_parts),
1924 )
1925 }
1926
1927 pub fn record_audience(&self, index: usize) -> Option<&DeliveryAudience> {
1929 if index >= self.records.len() {
1930 return None;
1931 }
1932 Some(
1933 self.record_audiences
1934 .as_ref()
1935 .and_then(|audiences| audiences.get(index))
1936 .unwrap_or(&self.audience),
1937 )
1938 }
1939
1940 pub fn with_delivery_scope(mut self, scope: DeliveryScope) -> Self {
1942 self.delivery_scope = scope;
1943 self.record_delivery_scopes = None;
1944 self
1945 }
1946
1947 pub fn record_delivery_scope(&self, index: usize) -> Option<DeliveryScope> {
1949 if index >= self.records.len() {
1950 return None;
1951 }
1952 Some(
1953 self.record_delivery_scopes
1954 .as_ref()
1955 .and_then(|scopes| scopes.get(index))
1956 .copied()
1957 .unwrap_or(self.delivery_scope),
1958 )
1959 }
1960
1961 fn from_scoped_records_with_delivery_scope(
1962 records: impl IntoIterator<Item = (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
1963 ) -> Self {
1964 let mut input_records = Vec::new();
1965 let mut audiences = Vec::new();
1966 let mut scopes = Vec::new();
1967 for (record, audience, scope) in records {
1968 input_records.push(record);
1969 audiences.push(audience);
1970 scopes.push(scope);
1971 }
1972 let chain_id = common_record_chain_id(&input_records);
1973 Self {
1974 records: input_records,
1975 chain_id,
1976 delivery_token: None,
1977 subscriber_checkpoint: None,
1978 payload_commitment: None,
1979 audience: DeliveryAudience::All,
1980 record_audiences: Some(audiences),
1981 delivery_scope: DeliveryScope::Canonical,
1982 record_delivery_scopes: Some(scopes),
1983 chain_controls: Vec::new(),
1984 preconfirmation_timing: None,
1985 }
1986 }
1987
1988 pub fn with_preconfirmation_timing(mut self, timing: FlashblockIngressTiming) -> Self {
1990 self.preconfirmation_timing = Some(timing);
1991 self
1992 }
1993
1994 pub const fn preconfirmation_timing(&self) -> Option<FlashblockIngressTiming> {
1996 self.preconfirmation_timing
1997 }
1998
1999 pub fn with_chain_controls(mut self, controls: impl IntoIterator<Item = ChainControl>) -> Self {
2006 self.chain_controls = controls.into_iter().collect();
2007 self
2008 }
2009
2010 pub fn chain_controls(&self) -> &[ChainControl] {
2012 &self.chain_controls
2013 }
2014
2015 pub fn records(&self) -> &[ReactiveInputRecord<N>] {
2017 &self.records
2018 }
2019
2020 pub fn into_records(self) -> Vec<ReactiveInputRecord<N>> {
2027 self.records
2028 }
2029
2030 pub fn into_parts(self) -> ReactiveInputBatchParts<N> {
2032 let chain_id = self.chain_id;
2033 let delivery_token = self.delivery_token;
2034 let subscriber_checkpoint = self.subscriber_checkpoint;
2035 let payload_commitment = self.payload_commitment;
2036 let chain_controls = self.chain_controls;
2037 let preconfirmation_timing = self.preconfirmation_timing;
2038 let audiences = self
2039 .record_audiences
2040 .unwrap_or_else(|| vec![self.audience; self.records.len()]);
2041 let scopes = self
2042 .record_delivery_scopes
2043 .unwrap_or_else(|| vec![self.delivery_scope; self.records.len()]);
2044 let deliveries = self
2045 .records
2046 .into_iter()
2047 .zip(audiences)
2048 .zip(scopes)
2049 .map(|((record, audience), scope)| ReactiveInputDelivery::new(record, audience, scope))
2050 .collect();
2051 ReactiveInputBatchParts {
2052 chain_id,
2053 deliveries,
2054 delivery_token,
2055 subscriber_checkpoint,
2056 payload_commitment,
2057 chain_controls,
2058 preconfirmation_timing,
2059 }
2060 }
2061
2062 fn into_runtime_parts(self) -> (Vec<RuntimeInputDelivery<N>>, Vec<ChainControl>, Option<u64>) {
2063 let audiences = self
2064 .record_audiences
2065 .unwrap_or_else(|| vec![self.audience; self.records.len()]);
2066 let scopes = self
2067 .record_delivery_scopes
2068 .unwrap_or_else(|| vec![self.delivery_scope; self.records.len()]);
2069 let records = self
2070 .records
2071 .into_iter()
2072 .zip(audiences)
2073 .zip(scopes)
2074 .map(|((record, audience), scope)| (record, audience, scope))
2075 .collect();
2076 (records, self.chain_controls, self.chain_id)
2077 }
2078
2079 fn take_delivery_token(&mut self) -> Option<SubscriberDeliveryToken> {
2080 self.delivery_token.take()
2081 }
2082
2083 fn take_subscriber_checkpoint(&mut self) -> Option<SubscriberCheckpoint> {
2084 self.subscriber_checkpoint.take()
2085 }
2086}
2087
2088fn common_record_chain_id<N: Network>(records: &[ReactiveInputRecord<N>]) -> Option<u64> {
2089 let chain_id = records.first()?.context.chain_id?;
2090 records
2091 .iter()
2092 .all(|record| record.context.chain_id == Some(chain_id))
2093 .then_some(chain_id)
2094}
2095
2096pub trait ReactiveHandler<N: Network = Ethereum>: Send + Sync {
2098 fn id(&self) -> HandlerId;
2100
2101 fn interests(&self) -> Vec<ReactiveInterest<N>>;
2103
2104 fn log_route_index(&self) -> Option<LogRouteIndex> {
2111 None
2112 }
2113
2114 fn handle(
2116 &self,
2117 ctx: &ReactiveContext,
2118 input: &ReactiveInput<N>,
2119 state: &dyn StateView,
2120 ) -> Result<HandlerOutcome, HandlerError>;
2121}
2122
2123pub trait ReactiveHook<N: Network = Ethereum>: Send + Sync {
2133 fn on_report(&self, report: Arc<ReactiveReport<N>>);
2135}
2136
2137#[allow(clippy::large_enum_variant)]
2139#[derive(Clone)]
2140pub enum ReactiveInterest<N: Network = Ethereum> {
2141 Logs(LogInterest),
2143 Blocks(BlockInterest),
2145 PendingTransactions(PendingTxInterest<N>),
2147}
2148
2149impl<N: Network> fmt::Debug for ReactiveInterest<N> {
2150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2151 match self {
2152 Self::Logs(interest) => f.debug_tuple("Logs").field(interest).finish(),
2153 Self::Blocks(interest) => f.debug_tuple("Blocks").field(interest).finish(),
2154 Self::PendingTransactions(interest) => f
2155 .debug_tuple("PendingTransactions")
2156 .field(interest)
2157 .finish(),
2158 }
2159 }
2160}
2161
2162#[derive(Clone)]
2164pub struct LogInterest {
2165 pub provider_filter: Filter,
2167 pub local_matcher: Option<Arc<dyn LogMatcher>>,
2169 pub route_key: Option<RouteKeySpec>,
2171}
2172
2173impl LogInterest {
2174 pub fn matches(&self, log: &Log) -> bool {
2176 self.provider_filter.rpc_matches(log)
2177 && self
2178 .local_matcher
2179 .as_ref()
2180 .is_none_or(|matcher| matcher.matches(log))
2181 }
2182
2183 pub fn route_key(&self, log: &Log) -> Option<RouteKey> {
2185 self.route_key.as_ref().and_then(|spec| spec.extract(log))
2186 }
2187}
2188
2189impl fmt::Debug for LogInterest {
2190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2191 f.debug_struct("LogInterest")
2192 .field("provider_filter", &self.provider_filter)
2193 .field(
2194 "local_matcher",
2195 &self.local_matcher.as_ref().map(|_| "<matcher>"),
2196 )
2197 .field("route_key", &self.route_key)
2198 .finish()
2199 }
2200}
2201
2202pub trait LogMatcher: Send + Sync {
2204 fn matches(&self, log: &Log) -> bool;
2206}
2207
2208#[derive(Clone)]
2210pub enum RouteKeySpec {
2211 EmitterAddress,
2213 Topic {
2215 index: usize,
2217 },
2218 DataSlice {
2220 offset: usize,
2222 len: usize,
2224 },
2225 Custom(Arc<dyn RouteKeyExtractor>),
2227}
2228
2229impl RouteKeySpec {
2230 pub fn extract(&self, log: &Log) -> Option<RouteKey> {
2232 match self {
2233 Self::EmitterAddress => Some(RouteKey::Address(log.address())),
2234 Self::Topic { index } => log.topics().get(*index).copied().map(RouteKey::Bytes32),
2235 Self::DataSlice { offset, len } => {
2236 let data = log.inner.data.data.as_ref();
2237 let end = offset.checked_add(*len)?;
2238 data.get(*offset..end)
2239 .map(|bytes| RouteKey::Bytes(bytes.to_vec()))
2240 }
2241 Self::Custom(extractor) => extractor.extract(log),
2242 }
2243 }
2244}
2245
2246impl fmt::Debug for RouteKeySpec {
2247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2248 match self {
2249 Self::EmitterAddress => f.write_str("EmitterAddress"),
2250 Self::Topic { index } => f.debug_struct("Topic").field("index", index).finish(),
2251 Self::DataSlice { offset, len } => f
2252 .debug_struct("DataSlice")
2253 .field("offset", offset)
2254 .field("len", len)
2255 .finish(),
2256 Self::Custom(_) => f.write_str("Custom(<extractor>)"),
2257 }
2258 }
2259}
2260
2261pub trait RouteKeyExtractor: Send + Sync {
2263 fn extract(&self, log: &Log) -> Option<RouteKey>;
2265}
2266
2267#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2269pub enum RouteKey {
2270 Address(Address),
2272 Bytes32(B256),
2274 Bytes(Vec<u8>),
2276}
2277
2278#[non_exhaustive]
2280#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2281pub enum LogRouteKey {
2282 Emitter(Address),
2284 Topic {
2286 index: usize,
2288 value: B256,
2290 },
2291 DataSlice {
2293 offset: usize,
2295 value: Vec<u8>,
2297 },
2298}
2299
2300#[derive(Clone, Debug, PartialEq, Eq)]
2302pub struct LogRouteIndex {
2303 keys: Vec<LogRouteKey>,
2304}
2305
2306impl LogRouteIndex {
2307 pub fn new(primary: LogRouteKey, additional: impl IntoIterator<Item = LogRouteKey>) -> Self {
2309 let mut keys = vec![primary];
2310 for key in additional {
2311 if !keys.contains(&key) {
2312 keys.push(key);
2313 }
2314 }
2315 Self { keys }
2316 }
2317
2318 pub fn single(key: LogRouteKey) -> Self {
2320 Self { keys: vec![key] }
2321 }
2322
2323 pub fn keys(&self) -> &[LogRouteKey] {
2325 &self.keys
2326 }
2327}
2328
2329#[derive(Clone, Debug, PartialEq, Eq)]
2331pub struct ReactiveLogRoute {
2332 pub handler_id: HandlerId,
2334 pub route_key: Option<RouteKey>,
2336}
2337
2338#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2340pub struct BlockInterest {
2341 pub mode: BlockInterestMode,
2343}
2344
2345impl Default for BlockInterest {
2346 fn default() -> Self {
2347 Self {
2348 mode: BlockInterestMode::Header,
2349 }
2350 }
2351}
2352
2353#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2355pub enum BlockInterestMode {
2356 Header,
2358 FullBlock,
2360}
2361
2362#[derive(Clone)]
2364pub struct PendingTxInterest<N: Network = Ethereum> {
2365 pub full_transactions: bool,
2367 pub from: AddressMatcher,
2369 pub to: AddressMatcher,
2371 pub selectors: SelectorMatcher,
2373 pub local_matcher: Option<Arc<dyn PendingTxMatcher<N>>>,
2375}
2376
2377impl<N: Network> Default for PendingTxInterest<N> {
2378 fn default() -> Self {
2379 Self {
2380 full_transactions: false,
2381 from: AddressMatcher::Any,
2382 to: AddressMatcher::Any,
2383 selectors: SelectorMatcher::Any,
2384 local_matcher: None,
2385 }
2386 }
2387}
2388
2389impl<N: Network> PendingTxInterest<N> {
2390 fn matches_hash_only(&self) -> bool {
2391 !self.full_transactions
2392 && self.from.is_any()
2393 && self.to.is_any()
2394 && self.selectors.is_any()
2395 && self.local_matcher.is_none()
2396 }
2397
2398 fn matches_tx(&self, tx: &N::TransactionResponse) -> bool {
2399 self.from.matches(tx.from())
2400 && self.to.matches_option(tx.to())
2401 && self.selectors.matches(tx.input())
2402 && self
2403 .local_matcher
2404 .as_ref()
2405 .is_none_or(|matcher| matcher.matches(tx))
2406 }
2407}
2408
2409impl<N: Network> fmt::Debug for PendingTxInterest<N> {
2410 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2411 f.debug_struct("PendingTxInterest")
2412 .field("full_transactions", &self.full_transactions)
2413 .field("from", &self.from)
2414 .field("to", &self.to)
2415 .field("selectors", &self.selectors)
2416 .field(
2417 "local_matcher",
2418 &self.local_matcher.as_ref().map(|_| "<matcher>"),
2419 )
2420 .finish()
2421 }
2422}
2423
2424#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2426pub enum AddressMatcher {
2427 Any,
2429 Exact(Address),
2431 AnyOf(Vec<Address>),
2433}
2434
2435impl AddressMatcher {
2436 pub fn is_any(&self) -> bool {
2438 matches!(self, Self::Any)
2439 }
2440
2441 pub fn matches(&self, address: Address) -> bool {
2443 match self {
2444 Self::Any => true,
2445 Self::Exact(expected) => *expected == address,
2446 Self::AnyOf(addresses) => addresses.contains(&address),
2447 }
2448 }
2449
2450 pub fn matches_option(&self, address: Option<Address>) -> bool {
2452 match (self, address) {
2453 (Self::Any, _) => true,
2454 (_, Some(address)) => self.matches(address),
2455 _ => false,
2456 }
2457 }
2458}
2459
2460#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2462pub enum SelectorMatcher {
2463 Any,
2465 AnyOf(Vec<[u8; 4]>),
2467}
2468
2469impl SelectorMatcher {
2470 pub fn is_any(&self) -> bool {
2472 matches!(self, Self::Any)
2473 }
2474
2475 pub fn matches(&self, input: &Bytes) -> bool {
2477 match self {
2478 Self::Any => true,
2479 Self::AnyOf(selectors) => input
2480 .get(..4)
2481 .and_then(|bytes| bytes.try_into().ok())
2482 .is_some_and(|selector| selectors.contains(&selector)),
2483 }
2484 }
2485}
2486
2487pub trait PendingTxMatcher<N: Network = Ethereum>: Send + Sync {
2489 fn matches(&self, tx: &N::TransactionResponse) -> bool;
2491}
2492
2493#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
2511#[non_exhaustive]
2512pub enum TrackingPolicy {
2513 Slots {
2518 slots: Vec<U256>,
2520 },
2521 WholeAccount,
2526 Scalars,
2531}
2532
2533#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2546pub enum RootGateCadence {
2547 EveryNBlocks(NonZeroU64),
2551 Disabled,
2553}
2554
2555impl RootGateCadence {
2556 pub fn every_n_blocks(n: u64) -> Self {
2558 Self::EveryNBlocks(NonZeroU64::new(n.max(1)).expect("clamped to at least 1"))
2559 }
2560}
2561
2562impl Default for RootGateCadence {
2563 fn default() -> Self {
2567 Self::every_n_blocks(16)
2568 }
2569}
2570
2571#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
2578struct TrackedRoot {
2579 last_root: B256,
2580 last_block: u64,
2581 balance: U256,
2582 nonce: u64,
2583 code_hash: B256,
2584}
2585
2586#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2588pub struct ResyncRequest {
2589 pub id: ResyncId,
2591 pub reason: ResyncReason,
2593 pub block: ResyncBlock,
2595 pub targets: Vec<ResyncTarget>,
2597 pub priority: ResyncPriority,
2599}
2600
2601#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2603pub struct ResyncId(String);
2604
2605impl ResyncId {
2606 pub fn new(id: impl Into<String>) -> Self {
2608 Self(id.into())
2609 }
2610}
2611
2612#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2614#[non_exhaustive]
2615pub enum ResyncReason {
2616 HandlerRequested,
2618 SkippedStateEffect,
2620 MissedBlockRange,
2627 RootMoved,
2637 Custom(String),
2639}
2640
2641#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2643pub enum ResyncBlock {
2644 Latest,
2646 Pending,
2648 Safe,
2650 Finalized,
2652 Number(u64),
2654 Hash {
2656 number: u64,
2658 hash: B256,
2660 require_canonical: bool,
2662 },
2663}
2664
2665#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2667pub enum ResyncTarget {
2668 StorageSlot {
2670 address: Address,
2672 slot: U256,
2674 },
2675 StorageSlots {
2677 address: Address,
2679 slots: Vec<U256>,
2681 },
2682 Account {
2684 address: Address,
2686 fields: AccountFieldMask,
2688 },
2689}
2690
2691#[derive(
2693 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
2694)]
2695pub struct AccountFieldMask {
2696 pub balance: bool,
2698 pub nonce: bool,
2700 pub code: bool,
2702}
2703
2704#[derive(
2706 Clone,
2707 Copy,
2708 Debug,
2709 Default,
2710 PartialEq,
2711 Eq,
2712 Hash,
2713 PartialOrd,
2714 Ord,
2715 serde::Serialize,
2716 serde::Deserialize,
2717)]
2718pub enum ResyncPriority {
2719 Low,
2721 #[default]
2723 Normal,
2724 High,
2726}
2727
2728#[derive(Clone, Debug, PartialEq, Eq)]
2730pub struct InvalidationRequest {
2731 pub scope: PurgeScope,
2733 pub address: Address,
2735 pub reason: InvalidationReason,
2737}
2738
2739#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2741pub enum InvalidationReason {
2742 HandlerRequested,
2744 Reorg,
2746 Custom(String),
2748}
2749
2750#[derive(Clone, Debug, PartialEq, Eq)]
2752pub struct SpeculativeRequest {
2753 pub id: SpeculativeId,
2755 pub input_ref: InputRef,
2757 pub labels: Vec<ReportTag>,
2759}
2760
2761#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2763pub struct SpeculativeId(String);
2764
2765impl SpeculativeId {
2766 pub fn new(id: impl Into<String>) -> Self {
2768 Self(id.into())
2769 }
2770}
2771
2772#[derive(Clone, Debug, PartialEq, Eq)]
2774pub struct ReactiveConfig {
2775 pub hook_backpressure: HookBackpressure,
2780 pub journal_depth: usize,
2797}
2798
2799impl Default for ReactiveConfig {
2800 fn default() -> Self {
2801 Self {
2802 hook_backpressure: HookBackpressure::Block,
2803 journal_depth: 64,
2804 }
2805 }
2806}
2807
2808#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2810pub enum HookBackpressure {
2811 Block,
2813 DropNewest,
2815 DropOldest,
2817 Error,
2819}
2820
2821#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2829#[non_exhaustive]
2830pub enum CacheHealth {
2831 #[default]
2833 Healthy,
2834 Degraded {
2838 since_block: u64,
2840 },
2841 Unhealthy {
2844 since_block: u64,
2846 },
2847}
2848
2849#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2856#[non_exhaustive]
2857pub struct CacheMetricsSnapshot {
2858 pub deep_reorgs: u64,
2861 pub reorgs_recovered: u64,
2863 pub resync_requests: u64,
2865 pub resync_failures: u64,
2867 pub missed_ranges: u64,
2869 pub coverage_gaps: u64,
2871 pub pending_contamination: u64,
2873 pub stale_verdicts: u64,
2875}
2876
2877#[derive(Debug, Default)]
2883struct CacheMetrics {
2884 deep_reorgs: AtomicU64,
2885 reorgs_recovered: AtomicU64,
2886 resync_requests: AtomicU64,
2887 resync_failures: AtomicU64,
2888 missed_ranges: AtomicU64,
2889 coverage_gaps: AtomicU64,
2890 pending_contamination: AtomicU64,
2891 stale_verdicts: AtomicU64,
2892}
2893
2894impl CacheMetrics {
2895 fn snapshot(&self) -> CacheMetricsSnapshot {
2896 CacheMetricsSnapshot {
2897 deep_reorgs: self.deep_reorgs.load(Ordering::Relaxed),
2898 reorgs_recovered: self.reorgs_recovered.load(Ordering::Relaxed),
2899 resync_requests: self.resync_requests.load(Ordering::Relaxed),
2900 resync_failures: self.resync_failures.load(Ordering::Relaxed),
2901 missed_ranges: self.missed_ranges.load(Ordering::Relaxed),
2902 coverage_gaps: self.coverage_gaps.load(Ordering::Relaxed),
2903 pending_contamination: self.pending_contamination.load(Ordering::Relaxed),
2904 stale_verdicts: self.stale_verdicts.load(Ordering::Relaxed),
2905 }
2906 }
2907
2908 fn restore(&self, snapshot: CacheMetricsSnapshot) {
2909 self.deep_reorgs
2910 .store(snapshot.deep_reorgs, Ordering::Relaxed);
2911 self.reorgs_recovered
2912 .store(snapshot.reorgs_recovered, Ordering::Relaxed);
2913 self.resync_requests
2914 .store(snapshot.resync_requests, Ordering::Relaxed);
2915 self.resync_failures
2916 .store(snapshot.resync_failures, Ordering::Relaxed);
2917 self.missed_ranges
2918 .store(snapshot.missed_ranges, Ordering::Relaxed);
2919 self.coverage_gaps
2920 .store(snapshot.coverage_gaps, Ordering::Relaxed);
2921 self.pending_contamination
2922 .store(snapshot.pending_contamination, Ordering::Relaxed);
2923 self.stale_verdicts
2924 .store(snapshot.stale_verdicts, Ordering::Relaxed);
2925 }
2926}
2927
2928#[derive(Clone, Debug)]
2930#[non_exhaustive]
2931pub enum ReactiveReport<N: Network = Ethereum> {
2932 Input(InputReport<N>),
2934 Decoded(DecodedReport<N>),
2936 Applied(AppliedReport<N>),
2938 Resynced(ResyncReport),
2940 BlockCommitted(BlockReport<N>),
2942 Reorg(ReorgReport<N>),
2944 ChainControl(ChainControlReport),
2946 MissedBlockRange(MissedRangeReport<N>),
2949 Health(HealthReport<N>),
2951 CoverageGap(CoverageGapReport<N>),
2954 Error(ReactiveErrorReport<N>),
2956}
2957
2958#[derive(Clone, Debug, PartialEq, Eq)]
2960pub struct ChainControlReport {
2961 pub control: ChainControl,
2963}
2964
2965#[derive(Clone, Debug)]
2967pub struct InputReport<N: Network = Ethereum> {
2968 pub input_ref: InputRef,
2970 pub context: ReactiveContext,
2972 pub provider: Option<ProviderRef>,
2974 pub _network: PhantomData<N>,
2976}
2977
2978#[derive(Clone, Debug)]
2980pub struct DecodedReport<N: Network = Ethereum> {
2981 pub input_ref: InputRef,
2983 pub handler_ids: Vec<HandlerId>,
2985 pub _network: PhantomData<N>,
2987}
2988
2989#[derive(Clone, Debug)]
2991pub struct AppliedReport<N: Network = Ethereum> {
2992 pub input_ref: InputRef,
2994 pub handler_id: HandlerId,
2996 pub quality: StateEffectQuality,
2998 pub tags: Vec<ReportTag>,
3000 pub diff: StateDiff,
3002 pub state_updates: Vec<StateUpdate>,
3004 pub invalidations: Vec<InvalidationRequest>,
3006 pub resyncs: Vec<ResyncRequest>,
3008 pub speculative: Vec<SpeculativeRequest>,
3010 pub hook_signals: Vec<HookSignal>,
3012 pub _network: PhantomData<N>,
3014}
3015
3016#[derive(Clone, Debug, Default, PartialEq, Eq)]
3020pub struct ResyncReport {
3021 pub requested: Vec<ResyncRequest>,
3023 pub state_updates: Vec<StateUpdate>,
3025 pub diff: StateDiff,
3027 pub failed: Vec<ResyncFailure>,
3029}
3030
3031#[derive(Clone, Debug, PartialEq, Eq)]
3033pub struct ResyncFailure {
3034 pub request_id: ResyncId,
3036 pub block: ResyncBlock,
3038 pub target: ResyncTarget,
3040 pub kind: ResyncFailureKind,
3042 pub message: String,
3044}
3045
3046#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
3048#[non_exhaustive]
3049pub enum ResyncFailureKind {
3050 MissingStorageFetcher,
3052 StorageFetchFailed,
3054 StorageFetchOmitted,
3056 MissingAccountFetcher,
3058 AccountFetchFailed,
3060 AccountFetchOmitted,
3062}
3063
3064#[derive(Clone, Debug)]
3066pub struct BlockReport<N: Network = Ethereum> {
3067 pub block: Option<BlockRef>,
3069 pub inputs: Vec<InputRef>,
3071 pub _network: PhantomData<N>,
3073}
3074
3075#[derive(Clone, Debug)]
3091pub struct ReorgReport<N: Network = Ethereum> {
3092 pub dropped: Option<BlockRef>,
3094 pub dropped_blocks: Vec<BlockRef>,
3096 pub dropped_inputs: Vec<InputRef>,
3098 pub rollback_updates: Vec<StateUpdate>,
3100 pub rollback_diff: StateDiff,
3102 pub purge_updates: Vec<StateUpdate>,
3104 pub purge_diff: StateDiff,
3106 pub canceled_resyncs: Vec<ResyncRequest>,
3108 pub reason: ReorgReason,
3110 pub _network: PhantomData<N>,
3112}
3113
3114#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
3116pub enum ReorgReason {
3117 RemovedLog,
3119 ReorgedInput,
3121 ParentMismatch,
3123 Explicit,
3125}
3126
3127#[derive(Clone, Debug)]
3135pub struct MissedRangeReport<N: Network = Ethereum> {
3136 pub from: u64,
3138 pub to: u64,
3140 pub block: u64,
3142 pub _network: PhantomData<N>,
3144}
3145
3146#[derive(Clone, Debug)]
3149pub struct HealthReport<N: Network = Ethereum> {
3150 pub from: CacheHealth,
3152 pub to: CacheHealth,
3154 pub block: Option<u64>,
3156 pub _network: PhantomData<N>,
3158}
3159
3160#[derive(Clone, Debug)]
3173pub struct CoverageGapReport<N: Network = Ethereum> {
3174 pub address: Address,
3176 pub block: u64,
3178 pub _network: PhantomData<N>,
3180}
3181
3182#[derive(Clone, Debug)]
3185pub struct ReactiveErrorReport<N: Network = Ethereum> {
3186 pub input_ref: Option<InputRef>,
3188 pub message: String,
3190 pub _network: PhantomData<N>,
3192}
3193
3194#[derive(Clone, Debug)]
3197pub struct ReactiveBatchReport<N: Network = Ethereum> {
3198 pub applied: Vec<AppliedReport<N>>,
3200 pub resyncs: Vec<ResyncRequest>,
3202 pub speculative: Vec<SpeculativeRequest>,
3204 pub reports: Vec<Arc<ReactiveReport<N>>>,
3206}
3207
3208impl<N: Network> Default for ReactiveBatchReport<N> {
3209 fn default() -> Self {
3210 Self {
3211 applied: Vec::new(),
3212 resyncs: Vec::new(),
3213 speculative: Vec::new(),
3214 reports: Vec::new(),
3215 }
3216 }
3217}
3218
3219#[derive(Clone, Debug, PartialEq, Eq)]
3221pub struct HandlerError {
3222 message: String,
3223}
3224
3225impl HandlerError {
3226 pub fn new(message: impl Into<String>) -> Self {
3228 Self {
3229 message: message.into(),
3230 }
3231 }
3232}
3233
3234impl fmt::Display for HandlerError {
3235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3236 self.message.fmt(f)
3237 }
3238}
3239
3240impl std::error::Error for HandlerError {}
3241
3242impl From<String> for HandlerError {
3243 fn from(message: String) -> Self {
3244 Self::new(message)
3245 }
3246}
3247
3248impl From<&str> for HandlerError {
3249 fn from(message: &str) -> Self {
3250 Self::new(message)
3251 }
3252}
3253
3254#[derive(Debug, thiserror::Error)]
3256#[non_exhaustive]
3257pub enum ReactiveError {
3258 #[error("handler `{handler_id}` failed: {source}")]
3260 HandlerFailed {
3261 handler_id: HandlerId,
3263 source: HandlerError,
3265 },
3266 #[error(
3268 "conflicting effects for input {input_ref:?} on target {target:?}: `{first}` vs `{second}`"
3269 )]
3270 ConflictingEffects {
3271 input_ref: Box<InputRef>,
3273 target: Box<EffectTarget>,
3275 first: HandlerId,
3277 second: HandlerId,
3279 },
3280 #[error(
3282 "pending input {input_ref:?} emitted invalid canonical effect `{effect_kind}` from `{handler_id}`"
3283 )]
3284 InvalidPendingEffect {
3285 input_ref: Box<InputRef>,
3287 handler_id: HandlerId,
3289 effect_kind: &'static str,
3291 },
3292 #[error("invalid reactive input record: {message}")]
3295 InvalidInputRecord {
3296 message: String,
3298 },
3299 #[error("invalid chain control: {message}")]
3301 InvalidChainControl {
3302 message: String,
3304 },
3305 #[error(
3308 "owner catch-up block {number} {hash} is outside the retained canonical rollback journal"
3309 )]
3310 OwnerCatchupOutsideJournal {
3311 number: u64,
3313 hash: B256,
3315 },
3316 #[error(transparent)]
3318 Register(#[from] RegisterError),
3319}
3320
3321#[derive(Debug, thiserror::Error)]
3323#[non_exhaustive]
3324pub enum RegisterError {
3325 #[error("handler id `{0}` is already registered")]
3327 DuplicateHandler(HandlerId),
3328}
3329
3330#[derive(Debug, thiserror::Error)]
3333#[non_exhaustive]
3334pub enum ReactiveEngineRegisterError {
3335 #[error(transparent)]
3337 Register(#[from] RegisterError),
3338 #[error(transparent)]
3340 Subscriber(#[from] SubscriberError),
3341 #[error(
3344 "owner backfill {start_block}..={end_block:?} must target exactly one hash-certified block in the retained rollback journal"
3345 )]
3346 BackfillOutsideJournal {
3347 start_block: u64,
3349 end_block: Option<u64>,
3351 retained_anchor: Option<BlockRef>,
3353 },
3354}
3355
3356#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
3359#[non_exhaustive]
3360pub enum ReactiveBaselineError {
3361 #[error("cannot adopt a canonical baseline after reactive processing has started")]
3363 ActiveRuntime,
3364 #[error(
3367 "canonical baseline conflicts with existing block {existing_number} {existing_hash} (requested {requested_number} {requested_hash})"
3368 )]
3369 ConflictingBaseline {
3370 existing_number: u64,
3372 existing_hash: B256,
3374 requested_number: u64,
3376 requested_hash: B256,
3378 },
3379 #[error("baseline chain id {baseline_chain_id} does not match cache chain id {cache_chain_id}")]
3381 CacheChainMismatch {
3382 baseline_chain_id: u64,
3384 cache_chain_id: u64,
3386 },
3387 #[error("cache block selector is not canonically hash-pinned to baseline {number} {hash}")]
3389 CacheBlockMismatch {
3390 number: u64,
3392 hash: B256,
3394 },
3395}
3396
3397#[derive(Debug, thiserror::Error)]
3400#[non_exhaustive]
3401pub enum ReactiveEngineError {
3402 #[error(transparent)]
3404 Subscriber(#[from] SubscriberError),
3405 #[error(transparent)]
3407 Runtime(ReactiveError),
3408 #[error(transparent)]
3410 Baseline(#[from] ReactiveBaselineError),
3411 #[error("runtime ingestion succeeded but subscriber acknowledgement failed: {0}")]
3414 Acknowledgement(#[source] SubscriberError),
3415 #[error("runtime ingestion succeeded but durable checkpoint commit failed: {0}")]
3419 Checkpoint(#[source] DurableCheckpointError),
3420 #[error("cannot durably checkpoint reactive state before observing a canonical block")]
3422 MissingCheckpointBlock,
3423 #[error("pre-confirmed Flashblock batches cannot be durably checkpointed")]
3426 PreconfirmationNotCheckpointable,
3427 #[error("failed to encode durable reactive runtime state: {0}")]
3429 RuntimeCheckpoint(String),
3430 #[error("cannot use ordinary ingestion while a durable checkpoint commit is pending")]
3433 PendingCheckpointCommit,
3434 #[error("cannot use checkpointed ingestion while an ordinary acknowledgement is pending")]
3437 PendingAcknowledgementCommit,
3438 #[error(
3442 "raw engine ingestion cannot consume delivery tokens or subscriber checkpoints; use a combined next_ingest helper"
3443 )]
3444 UncommittedDeliveryMetadata,
3445 #[error(
3447 "subscriber chain id {subscriber_chain_id} does not match cache chain id {cache_chain_id}"
3448 )]
3449 SubscriberChainMismatch {
3450 subscriber_chain_id: u64,
3452 cache_chain_id: u64,
3454 },
3455 #[error("subscriber does not advertise durable replay support")]
3457 SubscriberNotDurable,
3458 #[error(
3461 "committed delivery token has no delivery witness; replay cannot be acknowledged safely"
3462 )]
3463 MissingReplayWitness,
3464 #[error("replayed delivery token does not match its committed delivery witness")]
3467 ReplayDeliveryMismatch,
3468 #[error("failed to encode durable delivery witness: {0}")]
3470 DeliveryWitness(String),
3471 #[error(
3474 "tokened block-header, full-block, or hydrated-transaction delivery requires an exact payload commitment"
3475 )]
3476 MissingPayloadCommitment,
3477 #[error(
3480 "cache changed while durable checkpoint commit was pending (staged generation {staged_generation}, current generation {current_generation})"
3481 )]
3482 PendingCheckpointCacheChanged {
3483 staged_generation: u64,
3485 current_generation: u64,
3487 },
3488 #[error(
3491 "reorg after block {common_ancestor} exceeds the retained rollback journal (oldest retained block {oldest_journaled:?}, configured depth {journal_depth})"
3492 )]
3493 CheckpointReorgOutsideJournal {
3494 common_ancestor: u64,
3496 oldest_journaled: Option<u64>,
3498 journal_depth: usize,
3500 },
3501 #[error(
3504 "owner catch-up block {number} {hash} is outside the retained canonical rollback journal"
3505 )]
3506 OwnerCatchupOutsideJournal {
3507 number: u64,
3509 hash: B256,
3511 },
3512}
3513
3514impl From<ReactiveError> for ReactiveEngineError {
3515 fn from(error: ReactiveError) -> Self {
3516 match error {
3517 ReactiveError::OwnerCatchupOutsideJournal { number, hash } => {
3518 Self::OwnerCatchupOutsideJournal { number, hash }
3519 }
3520 error => Self::Runtime(error),
3521 }
3522 }
3523}
3524
3525#[derive(Debug, thiserror::Error)]
3527#[non_exhaustive]
3528pub enum ReactiveCheckpointRestoreError {
3529 #[error("cannot restore a durable checkpoint into a runtime with canonical journal state")]
3531 ActiveRuntime,
3532 #[error("invalid durable reactive runtime state: {0}")]
3534 InvalidRuntimeCheckpoint(String),
3535 #[error(transparent)]
3537 Checkpoint(#[from] DurableCheckpointError),
3538 #[error("subscriber rejected durable resume position: {0}")]
3540 Subscriber(#[source] SubscriberError),
3541 #[error(
3543 "subscriber chain id {subscriber_chain_id} does not match checkpoint chain id {checkpoint_chain_id}"
3544 )]
3545 SubscriberChainMismatch {
3546 subscriber_chain_id: u64,
3548 checkpoint_chain_id: u64,
3550 },
3551 #[error("subscriber does not advertise durable replay support")]
3553 SubscriberNotDurable,
3554}
3555
3556#[derive(Clone, Debug)]
3558#[non_exhaustive]
3559pub enum CheckpointedIngest<N: Network = Ethereum> {
3560 Applied(ReactiveBatchReport<N>),
3562 ReplayAcknowledged,
3565}
3566
3567#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3569pub enum EffectTarget {
3570 StorageSlot {
3572 address: Address,
3574 slot: U256,
3576 },
3577 AccountBalance {
3579 address: Address,
3581 },
3582 AccountNonce {
3584 address: Address,
3586 },
3587 AccountCode {
3589 address: Address,
3591 },
3592 MaskedStorageSlot {
3594 address: Address,
3596 slot: U256,
3598 mask: U256,
3600 },
3601}
3602
3603#[derive(Clone, Debug, PartialEq, Eq)]
3604enum AbsoluteValue {
3605 U256(U256),
3606 U64(u64),
3607 Bytes(Bytes),
3608}
3609
3610pub struct ReactiveRuntime<N: Network = Ethereum> {
3612 registry: ReactiveRegistry<N>,
3613 hooks: Vec<Arc<dyn ReactiveHook<N>>>,
3614 config: ReactiveConfig,
3615 journal: VecDeque<BlockJournal<N>>,
3616 coverage_head: Option<BlockRef>,
3617 pending_resyncs: Vec<ResyncRequest>,
3618 health: CacheHealth,
3619 safe_head: Option<BlockRef>,
3620 finalized_head: Option<BlockRef>,
3621 metrics: CacheMetrics,
3622 freshness: Option<FreshnessRegistry>,
3631 tracking: HashMap<Address, TrackingPolicy>,
3635 tracked_roots: HashMap<Address, TrackedRoot>,
3638 root_gate_cadence: RootGateCadence,
3640 last_gate_block: Option<u64>,
3644 touched_since_gate: HashSet<Address>,
3650 preconfirmed_branch: Option<PreconfirmedBranch>,
3653}
3654
3655#[derive(Clone)]
3656struct PreconfirmedBranch {
3657 flashblock: FlashblockRef,
3658 canonical_cache: EvmCacheStateSnapshot,
3659}
3660
3661#[derive(Clone, Debug)]
3662struct BlockJournal<N: Network = Ethereum> {
3663 block: BlockRef,
3664 inputs: Vec<InputRef>,
3665 applied: Vec<AppliedReport<N>>,
3666 handler_ids: Vec<HandlerId>,
3667 resynced: Vec<ResyncReport>,
3668 rollback_diffs: Vec<StateDiff>,
3669}
3670
3671const DURABLE_RUNTIME_CHECKPOINT_VERSION: u32 = 3;
3672
3673#[derive(serde::Serialize, serde::Deserialize)]
3674struct DurableRuntimeCheckpoint {
3675 version: u32,
3676 safe_head: Option<BlockRef>,
3677 finalized_head: Option<BlockRef>,
3678 health: CacheHealth,
3679 pending_resyncs: Vec<ResyncRequest>,
3680 coverage_head: Option<BlockRef>,
3681 journal: Vec<DurableBlockJournal>,
3682 freshness: Option<FreshnessRegistry>,
3683 tracking: HashMap<Address, TrackingPolicy>,
3684 tracked_roots: HashMap<Address, TrackedRoot>,
3685 root_gate_cadence: RootGateCadence,
3686 last_gate_block: Option<u64>,
3687 touched_since_gate: HashSet<Address>,
3688 metrics: CacheMetricsSnapshot,
3689}
3690
3691#[derive(serde::Serialize, serde::Deserialize)]
3692struct DurableBlockJournal {
3693 block: BlockRef,
3694 handler_ids: Vec<HandlerId>,
3695 rollback_diffs: Vec<StateDiff>,
3696}
3697
3698struct DurableRuntimeRestorePlan {
3699 checkpoint: Option<DurableRuntimeCheckpoint>,
3700 fallback_history: Vec<BlockRef>,
3701}
3702
3703impl DurableRuntimeRestorePlan {
3704 fn canonical_history(&self) -> Vec<BlockRef> {
3705 self.checkpoint.as_ref().map_or_else(
3706 || self.fallback_history.clone(),
3707 |checkpoint| checkpoint.journal.iter().map(|entry| entry.block).collect(),
3708 )
3709 }
3710}
3711
3712#[derive(Clone)]
3713struct ReactiveRuntimeState<N: Network> {
3714 journal: VecDeque<BlockJournal<N>>,
3715 coverage_head: Option<BlockRef>,
3716 pending_resyncs: Vec<ResyncRequest>,
3717 health: CacheHealth,
3718 safe_head: Option<BlockRef>,
3719 finalized_head: Option<BlockRef>,
3720 freshness: Option<FreshnessRegistry>,
3721 tracking: HashMap<Address, TrackingPolicy>,
3722 tracked_roots: HashMap<Address, TrackedRoot>,
3723 root_gate_cadence: RootGateCadence,
3724 last_gate_block: Option<u64>,
3725 touched_since_gate: HashSet<Address>,
3726 metrics: CacheMetricsSnapshot,
3727}
3728
3729#[derive(Clone)]
3730struct ChainControlState {
3731 journal_invalidated_from: Option<u64>,
3732 resolved_canonical_blocks: HashMap<(u64, B256), BlockRef>,
3733}
3734
3735#[derive(Default)]
3744struct BatchDroppedCanonical {
3745 identities: HashSet<(u64, B256)>,
3746 implicit_spans: Vec<(u64, u64)>,
3747}
3748
3749impl BatchDroppedCanonical {
3750 fn covers_implicit_number(&self, number: u64) -> bool {
3751 self.implicit_spans
3752 .iter()
3753 .any(|(from, through)| number >= *from && number <= *through)
3754 }
3755
3756 fn contains(&self, block: &BlockRef) -> bool {
3757 self.identities.contains(&(block.number, block.hash))
3758 }
3759
3760 fn record_identity(&mut self, block: &BlockRef) {
3761 self.identities.insert((block.number, block.hash));
3762 }
3763
3764 fn record_explicit(&mut self, _common_ancestor: &BlockRef, old_tip: &BlockRef) {
3765 self.identities.insert((old_tip.number, old_tip.hash));
3766 }
3767
3768 fn record_drained(&mut self, blocks: &[BlockRef]) {
3769 let Some(from) = blocks.iter().map(|block| block.number).min() else {
3770 return;
3771 };
3772 let through = blocks
3773 .iter()
3774 .map(|block| block.number)
3775 .max()
3776 .expect("a non-empty drained set has a maximum");
3777 self.implicit_spans.push((from, through));
3778 self.identities
3779 .extend(blocks.iter().map(|block| (block.number, block.hash)));
3780 }
3781}
3782
3783pub struct ReactiveRegistry<N: Network = Ethereum> {
3791 handlers: BTreeMap<u128, RegisteredHandler<N>>,
3792 handler_positions: HashMap<HandlerId, u128>,
3793 next_handler_position: u128,
3794 indexed_log_handlers: HashMap<LogRouteKey, BTreeSet<u128>>,
3795 fallback_log_handlers: BTreeSet<u128>,
3796 data_slice_shapes: HashMap<(usize, usize), usize>,
3797}
3798
3799struct RegisteredHandler<N: Network = Ethereum> {
3800 id: HandlerId,
3801 handler: Arc<dyn ReactiveHandler<N>>,
3802 interests: Vec<ReactiveInterest<N>>,
3803 has_log_interests: bool,
3804 log_route_index: Option<LogRouteIndex>,
3805}
3806
3807impl<N: Network> Default for ReactiveRegistry<N> {
3808 fn default() -> Self {
3809 Self::new()
3810 }
3811}
3812
3813impl<N: Network> ReactiveRegistry<N> {
3814 pub fn new() -> Self {
3816 Self {
3817 handlers: BTreeMap::new(),
3818 handler_positions: HashMap::new(),
3819 next_handler_position: 0,
3820 indexed_log_handlers: HashMap::new(),
3821 fallback_log_handlers: BTreeSet::new(),
3822 data_slice_shapes: HashMap::new(),
3823 }
3824 }
3825
3826 pub fn register_handler(
3836 &mut self,
3837 handler: Arc<dyn ReactiveHandler<N>>,
3838 ) -> Result<(), RegisterError> {
3839 let id = handler.id();
3840 if self.handler_positions.contains_key(&id) {
3841 return Err(RegisterError::DuplicateHandler(id));
3842 }
3843 let interests = handler.interests();
3844 self.insert_handler_prepared(id, handler, interests);
3845 Ok(())
3846 }
3847
3848 fn insert_handler_prepared(
3849 &mut self,
3850 id: HandlerId,
3851 handler: Arc<dyn ReactiveHandler<N>>,
3852 interests: Vec<ReactiveInterest<N>>,
3853 ) {
3854 debug_assert!(!self.handler_positions.contains_key(&id));
3855 let has_log_interests = interests
3856 .iter()
3857 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)));
3858 let log_route_index = handler.log_route_index();
3859 if self.next_handler_position == u128::MAX {
3860 self.compact_handler_positions();
3861 }
3862 let position = self.next_handler_position;
3863 self.next_handler_position += 1;
3864 self.handler_positions.insert(id.clone(), position);
3865 if let Some(index) = &log_route_index {
3866 for key in index.keys() {
3867 if let LogRouteKey::DataSlice { offset, value } = key {
3868 *self
3869 .data_slice_shapes
3870 .entry((*offset, value.len()))
3871 .or_default() += 1;
3872 }
3873 self.indexed_log_handlers
3874 .entry(key.clone())
3875 .or_default()
3876 .insert(position);
3877 }
3878 } else if has_log_interests {
3879 self.fallback_log_handlers.insert(position);
3880 }
3881 self.handlers.insert(
3882 position,
3883 RegisteredHandler {
3884 id,
3885 handler,
3886 interests,
3887 has_log_interests,
3888 log_route_index,
3889 },
3890 );
3891 }
3892
3893 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
3899 let position = self.handler_positions.remove(id)?;
3900 let registered = self.handlers.remove(&position)?;
3901 if let Some(index) = ®istered.log_route_index {
3902 for key in index.keys() {
3903 let remove_bucket = self
3904 .indexed_log_handlers
3905 .get_mut(key)
3906 .is_some_and(|owners| {
3907 owners.remove(&position);
3908 owners.is_empty()
3909 });
3910 if remove_bucket {
3911 self.indexed_log_handlers.remove(key);
3912 }
3913 if let LogRouteKey::DataSlice { offset, value } = key {
3914 let shape = (*offset, value.len());
3915 let remove_shape =
3916 self.data_slice_shapes.get_mut(&shape).is_some_and(|count| {
3917 *count -= 1;
3918 *count == 0
3919 });
3920 if remove_shape {
3921 self.data_slice_shapes.remove(&shape);
3922 }
3923 }
3924 }
3925 } else {
3926 self.fallback_log_handlers.remove(&position);
3927 }
3928 Some(registered.handler)
3929 }
3930
3931 pub fn contains_handler(&self, id: &HandlerId) -> bool {
3933 self.handler_positions.contains_key(id)
3934 }
3935
3936 pub fn handler_ids(&self) -> Vec<HandlerId> {
3938 self.handlers
3939 .values()
3940 .map(|handler| handler.id.clone())
3941 .collect()
3942 }
3943
3944 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
3946 self.handler_positions
3947 .get(id)
3948 .and_then(|position| self.handlers.get(position))
3949 .map(|registered| registered.interests.as_slice())
3950 }
3951
3952 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
3954 self.handlers
3955 .values()
3956 .flat_map(|handler| handler.interests.clone())
3957 .collect()
3958 }
3959
3960 pub fn log_subscription_filters(&self) -> Vec<Filter> {
3967 let mut filters = Vec::new();
3968 for interest in self.log_interests() {
3969 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
3970 }
3971 filters
3972 }
3973
3974 pub fn route_log(&self, log: &Log) -> Vec<ReactiveLogRoute> {
3980 self.log_handler_candidates(log)
3981 .into_iter()
3982 .filter_map(|handler| handler.route_log(log))
3983 .collect()
3984 }
3985
3986 fn log_handler_candidates(&self, log: &Log) -> Vec<&RegisteredHandler<N>> {
3987 let mut indexed_positions = Vec::new();
3988 if let Some(indexed) = self
3989 .indexed_log_handlers
3990 .get(&LogRouteKey::Emitter(log.address()))
3991 {
3992 indexed_positions.extend(indexed.iter().copied());
3993 }
3994 for (index, value) in log.topics().iter().copied().enumerate() {
3995 if let Some(indexed) = self
3996 .indexed_log_handlers
3997 .get(&LogRouteKey::Topic { index, value })
3998 {
3999 indexed_positions.extend(indexed.iter().copied());
4000 }
4001 }
4002 let data = log.inner.data.data.as_ref();
4003 for &(offset, len) in self.data_slice_shapes.keys() {
4004 let Some(end) = offset.checked_add(len) else {
4005 continue;
4006 };
4007 let Some(value) = data.get(offset..end) else {
4008 continue;
4009 };
4010 if let Some(indexed) = self.indexed_log_handlers.get(&LogRouteKey::DataSlice {
4011 offset,
4012 value: value.to_vec(),
4013 }) {
4014 indexed_positions.extend(indexed.iter().copied());
4015 }
4016 }
4017 if indexed_positions.is_empty() {
4018 if self.fallback_log_handlers.is_empty() {
4019 return Vec::new();
4020 }
4021 if !self.indexed_log_handlers.is_empty() {
4022 return self
4023 .fallback_log_handlers
4024 .iter()
4025 .filter_map(|position| self.handlers.get(position))
4026 .collect();
4027 }
4028 return self
4029 .handlers
4030 .values()
4031 .filter(|handler| handler.has_log_interests && handler.log_route_index.is_none())
4032 .collect();
4033 }
4034
4035 indexed_positions.extend(self.fallback_log_handlers.iter().copied());
4036 indexed_positions.sort_unstable();
4037 indexed_positions.dedup();
4038 indexed_positions
4039 .into_iter()
4040 .filter_map(|position| self.handlers.get(&position))
4041 .collect()
4042 }
4043
4044 fn handlers(&self) -> impl Iterator<Item = &RegisteredHandler<N>> {
4045 self.handlers.values()
4046 }
4047
4048 fn log_interests(&self) -> impl Iterator<Item = &LogInterest> {
4049 self.handlers.values().flat_map(|handler| {
4050 handler
4051 .interests
4052 .iter()
4053 .filter_map(|interest| match interest {
4054 ReactiveInterest::Logs(interest) => Some(interest),
4055 ReactiveInterest::Blocks(_) | ReactiveInterest::PendingTransactions(_) => None,
4056 })
4057 })
4058 }
4059
4060 fn compact_handler_positions(&mut self) {
4061 let handlers = std::mem::take(&mut self.handlers);
4062 self.handler_positions.clear();
4063 self.indexed_log_handlers.clear();
4064 self.fallback_log_handlers.clear();
4065 self.data_slice_shapes.clear();
4066
4067 for (position, (_, handler)) in handlers.into_iter().enumerate() {
4068 let position = position as u128;
4069 self.handler_positions.insert(handler.id.clone(), position);
4070 if let Some(index) = &handler.log_route_index {
4071 for key in index.keys() {
4072 if let LogRouteKey::DataSlice { offset, value } = key {
4073 *self
4074 .data_slice_shapes
4075 .entry((*offset, value.len()))
4076 .or_default() += 1;
4077 }
4078 self.indexed_log_handlers
4079 .entry(key.clone())
4080 .or_default()
4081 .insert(position);
4082 }
4083 } else if handler.has_log_interests {
4084 self.fallback_log_handlers.insert(position);
4085 }
4086 self.handlers.insert(position, handler);
4087 }
4088 self.next_handler_position = self.handlers.len() as u128;
4089 }
4090}
4091
4092impl<N: Network> ReactiveRuntime<N> {
4093 pub fn new(config: ReactiveConfig) -> Self {
4095 Self {
4096 registry: ReactiveRegistry::new(),
4097 hooks: Vec::new(),
4098 config,
4099 journal: VecDeque::new(),
4100 coverage_head: None,
4101 pending_resyncs: Vec::new(),
4102 health: CacheHealth::Healthy,
4103 safe_head: None,
4104 finalized_head: None,
4105 metrics: CacheMetrics::default(),
4106 freshness: None,
4107 tracking: HashMap::new(),
4108 tracked_roots: HashMap::new(),
4109 root_gate_cadence: RootGateCadence::default(),
4110 last_gate_block: None,
4111 touched_since_gate: HashSet::new(),
4112 preconfirmed_branch: None,
4113 }
4114 }
4115
4116 fn checkpoint_state(&self) -> ReactiveRuntimeState<N> {
4117 ReactiveRuntimeState {
4118 journal: self.journal.clone(),
4119 coverage_head: self.coverage_head,
4120 pending_resyncs: self.pending_resyncs.clone(),
4121 health: self.health,
4122 safe_head: self.safe_head,
4123 finalized_head: self.finalized_head,
4124 freshness: self.freshness.clone(),
4125 tracking: self.tracking.clone(),
4126 tracked_roots: self.tracked_roots.clone(),
4127 root_gate_cadence: self.root_gate_cadence,
4128 last_gate_block: self.last_gate_block,
4129 touched_since_gate: self.touched_since_gate.clone(),
4130 metrics: self.metrics.snapshot(),
4131 }
4132 }
4133
4134 fn is_pristine_for_checkpoint_restore(&self) -> bool {
4135 self.preconfirmed_branch.is_none()
4136 && self.journal.is_empty()
4137 && self.coverage_head.is_none()
4138 && self.pending_resyncs.is_empty()
4139 && self.health == CacheHealth::Healthy
4140 && self.safe_head.is_none()
4141 && self.finalized_head.is_none()
4142 && self.tracked_roots.is_empty()
4143 && self.last_gate_block.is_none()
4144 && self.touched_since_gate.is_empty()
4145 && self.metrics.snapshot() == CacheMetricsSnapshot::default()
4146 }
4147
4148 fn adopted_baseline_only(&self) -> Option<BlockRef> {
4149 let baseline = self.coverage_head?;
4150 let journal_is_baseline_only = if self.config.journal_depth == 0 {
4151 self.journal.is_empty()
4152 } else {
4153 self.journal.len() == 1
4154 && self.journal.front().is_some_and(|entry| {
4155 entry.block == baseline
4156 && entry.inputs.is_empty()
4157 && entry.applied.is_empty()
4158 && entry.handler_ids.is_empty()
4159 && entry.resynced.is_empty()
4160 && entry.rollback_diffs.is_empty()
4161 })
4162 };
4163 (self.preconfirmed_branch.is_none()
4164 && journal_is_baseline_only
4165 && self.pending_resyncs.is_empty()
4166 && self.health == CacheHealth::Healthy
4167 && self.safe_head.is_none()
4168 && self.finalized_head.is_none()
4169 && self.tracked_roots.is_empty()
4170 && self.last_gate_block.is_none()
4171 && self.touched_since_gate.is_empty()
4172 && self.metrics.snapshot() == CacheMetricsSnapshot::default())
4173 .then_some(baseline)
4174 }
4175
4176 fn restore_state(&mut self, state: ReactiveRuntimeState<N>) {
4177 self.journal = state.journal;
4178 self.coverage_head = state.coverage_head;
4179 self.pending_resyncs = state.pending_resyncs;
4180 self.health = state.health;
4181 self.safe_head = state.safe_head;
4182 self.finalized_head = state.finalized_head;
4183 self.freshness = state.freshness;
4184 self.tracking = state.tracking;
4185 self.tracked_roots = state.tracked_roots;
4186 self.root_gate_cadence = state.root_gate_cadence;
4187 self.last_gate_block = state.last_gate_block;
4188 self.touched_since_gate = state.touched_since_gate;
4189 self.metrics.restore(state.metrics);
4190 }
4191
4192 fn restore_transaction_state(&mut self, state: ReactiveRuntimeState<N>) {
4193 let metrics = self.metrics.snapshot();
4198 self.restore_state(state);
4199 self.metrics.restore(metrics);
4200 }
4201
4202 fn durable_checkpoint_bytes(&self) -> Result<Vec<u8>, ReactiveEngineError> {
4203 let checkpoint = DurableRuntimeCheckpoint {
4204 version: DURABLE_RUNTIME_CHECKPOINT_VERSION,
4205 safe_head: self.safe_head,
4206 finalized_head: self.finalized_head,
4207 health: self.health,
4208 pending_resyncs: self.pending_resyncs.clone(),
4209 coverage_head: self.coverage_head,
4210 journal: self
4211 .journal
4212 .iter()
4213 .map(|entry| DurableBlockJournal {
4214 block: entry.block,
4215 handler_ids: entry.handler_ids.clone(),
4216 rollback_diffs: entry.rollback_diffs.clone(),
4217 })
4218 .collect(),
4219 freshness: self.freshness.clone(),
4220 tracking: self.tracking.clone(),
4221 tracked_roots: self.tracked_roots.clone(),
4222 root_gate_cadence: self.root_gate_cadence,
4223 last_gate_block: self.last_gate_block,
4224 touched_since_gate: self.touched_since_gate.clone(),
4225 metrics: self.metrics.snapshot(),
4226 };
4227 bincode::serialize(&checkpoint)
4228 .map_err(|error| ReactiveEngineError::RuntimeCheckpoint(error.to_string()))
4229 }
4230
4231 fn plan_durable_checkpoint_restore(
4232 &self,
4233 bytes: &[u8],
4234 expected_coverage: &BlockRef,
4235 ) -> Result<DurableRuntimeRestorePlan, ReactiveCheckpointRestoreError> {
4236 let mut cursor = std::io::Cursor::new(bytes);
4237 let mut checkpoint: DurableRuntimeCheckpoint = bincode::DefaultOptions::new()
4238 .with_fixint_encoding()
4239 .with_limit(bytes.len() as u64)
4240 .deserialize_from(&mut cursor)
4241 .map_err(|error| {
4242 ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(error.to_string())
4243 })?;
4244 if cursor.position() != bytes.len() as u64 {
4245 return Err(ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(
4246 "runtime checkpoint has trailing bytes".to_owned(),
4247 ));
4248 }
4249 if checkpoint.version != DURABLE_RUNTIME_CHECKPOINT_VERSION {
4250 return Err(ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(
4251 format!(
4252 "unsupported runtime checkpoint version {}",
4253 checkpoint.version
4254 ),
4255 ));
4256 }
4257 self.validate_durable_runtime_checkpoint(&checkpoint, expected_coverage)?;
4258
4259 let retained = self.config.journal_depth.min(checkpoint.journal.len());
4260 let discard = checkpoint.journal.len() - retained;
4261 checkpoint.journal.drain(..discard);
4262 Ok(DurableRuntimeRestorePlan {
4263 checkpoint: Some(checkpoint),
4264 fallback_history: Vec::new(),
4265 })
4266 }
4267
4268 fn apply_durable_checkpoint_restore(&mut self, plan: DurableRuntimeRestorePlan) {
4269 let Some(checkpoint) = plan.checkpoint else {
4270 self.journal = plan
4271 .fallback_history
4272 .into_iter()
4273 .map(|block| BlockJournal {
4274 block,
4275 inputs: Vec::new(),
4276 applied: Vec::new(),
4277 handler_ids: Vec::new(),
4278 resynced: Vec::new(),
4279 rollback_diffs: Vec::new(),
4280 })
4281 .collect();
4282 return;
4283 };
4284 self.safe_head = checkpoint.safe_head;
4285 self.finalized_head = checkpoint.finalized_head;
4286 self.health = checkpoint.health;
4287 self.pending_resyncs = checkpoint.pending_resyncs;
4288 self.coverage_head = checkpoint.coverage_head;
4289 self.journal = checkpoint
4290 .journal
4291 .into_iter()
4292 .map(|entry| BlockJournal {
4293 block: entry.block,
4294 inputs: Vec::new(),
4295 applied: Vec::new(),
4296 handler_ids: entry.handler_ids,
4297 resynced: Vec::new(),
4298 rollback_diffs: entry.rollback_diffs,
4299 })
4300 .collect();
4301 self.freshness = checkpoint.freshness;
4302 self.tracking = checkpoint.tracking;
4303 self.tracked_roots = checkpoint.tracked_roots;
4304 self.root_gate_cadence = checkpoint.root_gate_cadence;
4305 self.last_gate_block = checkpoint.last_gate_block;
4306 self.touched_since_gate = checkpoint.touched_since_gate;
4307 self.metrics.restore(checkpoint.metrics);
4308 }
4309
4310 fn validate_durable_runtime_checkpoint(
4311 &self,
4312 checkpoint: &DurableRuntimeCheckpoint,
4313 expected_coverage: &BlockRef,
4314 ) -> Result<(), ReactiveCheckpointRestoreError> {
4315 let invalid =
4316 |message: String| ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(message);
4317 let Some(coverage) = checkpoint.coverage_head.as_ref() else {
4318 return Err(invalid(
4319 "runtime checkpoint is missing its canonical coverage head".into(),
4320 ));
4321 };
4322 if !optional_block_refs_are_compatible(Some(coverage), Some(expected_coverage)) {
4323 return Err(invalid(format!(
4324 "runtime coverage {}:{:?} conflicts with checkpoint metadata {}:{:?}",
4325 coverage.number, coverage.hash, expected_coverage.number, expected_coverage.hash
4326 )));
4327 }
4328 for (label, head) in [
4329 ("safe", checkpoint.safe_head.as_ref()),
4330 ("finalized", checkpoint.finalized_head.as_ref()),
4331 ] {
4332 let Some(head) = head else { continue };
4333 if head.number > coverage.number
4334 || (head.number == coverage.number && head.hash != coverage.hash)
4335 {
4336 return Err(invalid(format!(
4337 "{label} head {}:{:?} lies beyond or conflicts with canonical coverage {}:{:?}",
4338 head.number, head.hash, coverage.number, coverage.hash
4339 )));
4340 }
4341 if head.number.checked_add(1) == Some(coverage.number)
4342 && coverage
4343 .parent_hash
4344 .is_some_and(|parent| parent != head.hash)
4345 {
4346 return Err(invalid(format!(
4347 "canonical coverage does not descend from adjacent {label} head"
4348 )));
4349 }
4350 }
4351 if let (Some(finalized), Some(safe)) = (
4352 checkpoint.finalized_head.as_ref(),
4353 checkpoint.safe_head.as_ref(),
4354 ) {
4355 if finalized.number > safe.number
4356 || (finalized.number == safe.number && finalized.hash != safe.hash)
4357 {
4358 return Err(invalid(
4359 "finalized head is above or conflicts with the safe head".into(),
4360 ));
4361 }
4362 if finalized.number.checked_add(1) == Some(safe.number)
4363 && safe.parent_hash != Some(finalized.hash)
4364 {
4365 return Err(invalid(
4366 "adjacent safe head does not descend from finalized head".into(),
4367 ));
4368 }
4369 }
4370
4371 let mut previous: Option<&DurableBlockJournal> = None;
4372 for entry in &checkpoint.journal {
4373 if entry.block.number > coverage.number
4374 || (entry.block.number == coverage.number && entry.block.hash != coverage.hash)
4375 {
4376 return Err(invalid(format!(
4377 "journal block {}:{:?} lies beyond or conflicts with canonical coverage",
4378 entry.block.number, entry.block.hash
4379 )));
4380 }
4381 if let Some(previous) = previous {
4382 if entry.block.number <= previous.block.number {
4383 return Err(invalid(
4384 "runtime journal block numbers are not strictly increasing".into(),
4385 ));
4386 }
4387 if previous.block.number.checked_add(1) == Some(entry.block.number)
4388 && entry.block.parent_hash.is_some()
4389 && entry.block.parent_hash != Some(previous.block.hash)
4390 {
4391 return Err(invalid(
4392 "adjacent runtime journal blocks are not parent-linked".into(),
4393 ));
4394 }
4395 }
4396 for (label, head) in [
4397 ("safe", checkpoint.safe_head.as_ref()),
4398 ("finalized", checkpoint.finalized_head.as_ref()),
4399 ] {
4400 if let Some(head) = head
4401 && head.number == entry.block.number
4402 && !optional_block_refs_are_compatible(Some(head), Some(&entry.block))
4403 {
4404 return Err(invalid(format!(
4405 "{label} head conflicts with the retained journal at block {}",
4406 head.number
4407 )));
4408 }
4409 }
4410 let mut handler_ids = HashSet::new();
4411 if entry
4412 .handler_ids
4413 .iter()
4414 .any(|handler_id| !handler_ids.insert(handler_id))
4415 {
4416 return Err(invalid(
4417 "runtime journal contains duplicate handler generation ids".into(),
4418 ));
4419 }
4420 previous = Some(entry);
4421 }
4422 if let Some(tail) = checkpoint.journal.last()
4423 && tail.block.number == coverage.number
4424 && !optional_block_refs_are_compatible(Some(&tail.block), Some(coverage))
4425 {
4426 return Err(invalid(format!(
4427 "runtime journal tail conflicts with canonical coverage at block {}",
4428 coverage.number
4429 )));
4430 }
4431 if let Some(tail) = checkpoint.journal.last()
4432 && tail.block.number.checked_add(1) == Some(coverage.number)
4433 && coverage
4434 .parent_hash
4435 .is_some_and(|parent_hash| parent_hash != tail.block.hash)
4436 {
4437 return Err(invalid(format!(
4438 "canonical coverage does not descend from adjacent runtime journal tail at block {}",
4439 tail.block.number
4440 )));
4441 }
4442
4443 if let Some(last_gate_block) = checkpoint.last_gate_block {
4444 if last_gate_block > coverage.number {
4445 return Err(invalid(
4446 "root-gate cursor lies beyond canonical coverage".into(),
4447 ));
4448 }
4449 } else if !checkpoint.tracked_roots.is_empty() {
4450 return Err(invalid(
4451 "root-gate baselines exist without a completed gate cursor".into(),
4452 ));
4453 }
4454 for (address, baseline) in &checkpoint.tracked_roots {
4455 let Some(policy) = checkpoint.tracking.get(address) else {
4456 return Err(invalid(
4457 "root-gate baseline has no corresponding tracking policy".into(),
4458 ));
4459 };
4460 if matches!(policy, TrackingPolicy::Slots { .. }) {
4461 return Err(invalid(
4462 "slot-only tracking cannot carry an account root baseline".into(),
4463 ));
4464 }
4465 if baseline.last_block > coverage.number
4466 || checkpoint
4467 .last_gate_block
4468 .is_some_and(|last_gate| baseline.last_block > last_gate)
4469 {
4470 return Err(invalid(
4471 "root-gate baseline lies beyond the committed gate window".into(),
4472 ));
4473 }
4474 }
4475 Ok(())
4476 }
4477
4478 pub fn track_account(&mut self, address: Address, policy: TrackingPolicy) {
4492 self.tracking.insert(address, policy);
4493 self.tracked_roots.remove(&address);
4494 }
4495
4496 pub fn untrack_account(&mut self, address: Address) -> bool {
4500 self.tracked_roots.remove(&address);
4501 self.tracking.remove(&address).is_some()
4502 }
4503
4504 pub fn set_root_gate_cadence(&mut self, cadence: RootGateCadence) {
4512 self.root_gate_cadence = cadence;
4513 self.last_gate_block = None;
4514 self.touched_since_gate.clear();
4515 }
4516
4517 pub fn root_gate_cadence(&self) -> RootGateCadence {
4519 self.root_gate_cadence
4520 }
4521
4522 pub fn enable_freshness_stamping(&mut self) {
4534 if self.freshness.is_none() {
4535 self.freshness = Some(FreshnessRegistry::new());
4536 }
4537 }
4538
4539 pub fn freshness(&self) -> Option<&FreshnessRegistry> {
4544 self.freshness.as_ref()
4545 }
4546
4547 pub fn freshness_mut(&mut self) -> Option<&mut FreshnessRegistry> {
4552 self.freshness.as_mut()
4553 }
4554
4555 pub fn health(&self) -> CacheHealth {
4557 self.health
4558 }
4559
4560 pub fn metrics(&self) -> CacheMetricsSnapshot {
4562 self.metrics.snapshot()
4563 }
4564
4565 pub fn reset_health(&mut self) {
4575 self.health = CacheHealth::Healthy;
4576 }
4577
4578 fn escalate_trust(&mut self, block: u64) -> Option<Arc<ReactiveReport<N>>> {
4592 let to = match self.health {
4593 CacheHealth::Healthy => CacheHealth::Degraded { since_block: block },
4594 CacheHealth::Degraded { .. } => CacheHealth::Unhealthy { since_block: block },
4595 CacheHealth::Unhealthy { .. } => return None,
4596 };
4597 self.transition_health(to, Some(block))
4598 }
4599
4600 fn transition_health(
4608 &mut self,
4609 to: CacheHealth,
4610 block: Option<u64>,
4611 ) -> Option<Arc<ReactiveReport<N>>> {
4612 if to == self.health {
4613 return None;
4614 }
4615 let from = self.health;
4616 self.health = to;
4617 Some(Arc::new(ReactiveReport::Health(HealthReport {
4618 from,
4619 to,
4620 block,
4621 _network: PhantomData,
4622 })))
4623 }
4624
4625 pub fn register_handler(
4632 &mut self,
4633 handler: Arc<dyn ReactiveHandler<N>>,
4634 ) -> Result<(), RegisterError> {
4635 self.registry.register_handler(handler)
4636 }
4637
4638 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
4646 self.registry.unregister_handler(id)
4647 }
4648
4649 pub fn contains_handler(&self, id: &HandlerId) -> bool {
4651 self.registry.contains_handler(id)
4652 }
4653
4654 pub fn handler_ids(&self) -> Vec<HandlerId> {
4656 self.registry.handler_ids()
4657 }
4658
4659 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
4661 self.registry.handler_interests(id)
4662 }
4663
4664 pub fn last_canonical_block(&self) -> Option<BlockRef> {
4674 self.coverage_head
4675 }
4676
4677 pub fn adopt_canonical_baseline(
4694 &mut self,
4695 baseline: BlockRef,
4696 ) -> Result<(), ReactiveBaselineError> {
4697 self.validate_canonical_baseline_adoption(baseline)?;
4698 if self.adopted_baseline_only().is_some() {
4699 return Ok(());
4700 }
4701
4702 self.coverage_head = Some(baseline);
4703 if self.config.journal_depth > 0 {
4704 self.journal.push_back(BlockJournal {
4705 block: baseline,
4706 inputs: Vec::new(),
4707 applied: Vec::new(),
4708 handler_ids: Vec::new(),
4709 resynced: Vec::new(),
4710 rollback_diffs: Vec::new(),
4711 });
4712 }
4713 Ok(())
4714 }
4715
4716 fn validate_canonical_baseline_adoption(
4717 &self,
4718 baseline: BlockRef,
4719 ) -> Result<(), ReactiveBaselineError> {
4720 if let Some(existing) = self.adopted_baseline_only() {
4721 return if existing == baseline {
4722 Ok(())
4723 } else {
4724 Err(ReactiveBaselineError::ConflictingBaseline {
4725 existing_number: existing.number,
4726 existing_hash: existing.hash,
4727 requested_number: baseline.number,
4728 requested_hash: baseline.hash,
4729 })
4730 };
4731 }
4732 if !self.is_pristine_for_checkpoint_restore() {
4733 return Err(ReactiveBaselineError::ActiveRuntime);
4734 }
4735 Ok(())
4736 }
4737
4738 pub const fn safe_head(&self) -> Option<&BlockRef> {
4740 self.safe_head.as_ref()
4741 }
4742
4743 pub const fn finalized_head(&self) -> Option<&BlockRef> {
4745 self.finalized_head.as_ref()
4746 }
4747
4748 pub fn has_journaled_handler_effects(&self, handler_id: &HandlerId) -> bool {
4756 self.journal
4757 .iter()
4758 .any(|entry| entry.handler_ids.contains(handler_id))
4759 }
4760
4761 pub fn journaled_handler_ids(&self) -> HashSet<HandlerId> {
4768 self.journal
4769 .iter()
4770 .flat_map(|entry| entry.handler_ids.iter().cloned())
4771 .collect()
4772 }
4773
4774 pub fn pending_resyncs(&self) -> &[ResyncRequest] {
4785 &self.pending_resyncs
4786 }
4787
4788 pub fn cancel_pending_resync(&mut self, id: &ResyncId) -> Vec<ResyncRequest> {
4797 self.cancel_pending_resyncs_by_id(std::slice::from_ref(id))
4798 }
4799
4800 pub fn cancel_pending_resyncs_by_id(&mut self, ids: &[ResyncId]) -> Vec<ResyncRequest> {
4807 if ids.is_empty() {
4808 return Vec::new();
4809 }
4810 let ids: HashSet<&ResyncId> = ids.iter().collect();
4811 let mut cancelled = Vec::new();
4812 self.pending_resyncs.retain(|request| {
4813 if ids.contains(&request.id) {
4814 cancelled.push(request.clone());
4815 false
4816 } else {
4817 true
4818 }
4819 });
4820 cancelled
4821 }
4822
4823 pub fn cancel_pending_resyncs(&mut self, address: Address) -> Vec<ResyncRequest> {
4838 let mut cancelled = Vec::new();
4839 self.pending_resyncs.retain_mut(|request| {
4840 let (matching, remaining): (Vec<_>, Vec<_>) = request
4841 .targets
4842 .drain(..)
4843 .partition(|target| resync_target_address(target) == address);
4844 request.targets = remaining;
4845 if !matching.is_empty() {
4846 cancelled.push(ResyncRequest {
4847 id: request.id.clone(),
4848 reason: request.reason.clone(),
4849 block: request.block.clone(),
4850 targets: matching,
4851 priority: request.priority,
4852 });
4853 }
4854 !request.targets.is_empty()
4855 });
4856 cancelled
4857 }
4858
4859 pub fn register_hook(&mut self, hook: Arc<dyn ReactiveHook<N>>) -> Result<(), RegisterError> {
4866 self.hooks.push(hook);
4867 Ok(())
4868 }
4869
4870 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
4872 self.registry.interests()
4873 }
4874
4875 pub fn ingest_batch(
4893 &mut self,
4894 cache: &mut EvmCache,
4895 batch: ReactiveInputBatch<N>,
4896 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4897 let preconfirmation = batch_preconfirmation(&batch)?;
4898 if let Some(flashblock) = preconfirmation.as_ref() {
4899 self.prepare_preconfirmed_branch(cache, flashblock)?;
4900 } else {
4901 self.discard_preconfirmed_branch(cache);
4902 }
4903 let cache_state = EvmCacheStateSnapshot::capture(cache);
4904 let runtime_state = self.checkpoint_state();
4905 let batch_report = match self.ingest_batch_direct(cache, batch) {
4906 Ok(report) => report,
4907 Err(error) => {
4908 cache_state.restore(cache);
4909 self.restore_transaction_state(runtime_state);
4910 return Err(error);
4911 }
4912 };
4913 if let Some(flashblock) = preconfirmation {
4914 self.restore_transaction_state(runtime_state);
4915 if let Some(branch) = self.preconfirmed_branch.as_mut() {
4916 branch.flashblock = flashblock;
4917 }
4918 }
4919 self.dispatch_reports(&batch_report.reports);
4920 let _ = &self.config;
4921 Ok(batch_report)
4922 }
4923
4924 pub fn ingest_batch_with_resync(
4941 &mut self,
4942 cache: &mut EvmCache,
4943 batch: ReactiveInputBatch<N>,
4944 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4945 let preconfirmation = batch_preconfirmation(&batch)?;
4946 if let Some(flashblock) = preconfirmation.as_ref() {
4947 self.prepare_preconfirmed_branch(cache, flashblock)?;
4948 } else {
4949 self.discard_preconfirmed_branch(cache);
4950 }
4951 let cache_state = EvmCacheStateSnapshot::capture(cache);
4952 let runtime_state = self.checkpoint_state();
4953 let batch_report = match self.ingest_batch_with_resync_direct(cache, batch) {
4954 Ok(report) => report,
4955 Err(error) => {
4956 cache_state.restore(cache);
4957 self.restore_transaction_state(runtime_state);
4958 return Err(error);
4959 }
4960 };
4961
4962 if let Some(flashblock) = preconfirmation {
4963 self.restore_transaction_state(runtime_state);
4964 if let Some(branch) = self.preconfirmed_branch.as_mut() {
4965 branch.flashblock = flashblock;
4966 }
4967 }
4968
4969 self.dispatch_reports(&batch_report.reports);
4970 let _ = &self.config;
4971 Ok(batch_report)
4972 }
4973
4974 pub fn active_preconfirmation(&self) -> Option<&FlashblockRef> {
4977 self.preconfirmed_branch
4978 .as_ref()
4979 .map(|branch| &branch.flashblock)
4980 }
4981
4982 pub fn discard_preconfirmation(&mut self, cache: &mut EvmCache) {
4985 self.discard_preconfirmed_branch(cache);
4986 }
4987
4988 fn discard_preconfirmed_branch(&mut self, cache: &mut EvmCache) {
4989 if let Some(branch) = self.preconfirmed_branch.take() {
4990 branch.canonical_cache.restore(cache);
4991 }
4992 }
4993
4994 fn prepare_preconfirmed_branch(
4995 &mut self,
4996 cache: &mut EvmCache,
4997 incoming: &FlashblockRef,
4998 ) -> Result<(), ReactiveError> {
4999 let Some(canonical) = self.coverage_head else {
5000 self.discard_preconfirmed_branch(cache);
5001 return Err(ReactiveError::InvalidInputRecord {
5002 message: "pre-confirmed state requires an exact canonical coverage baseline".into(),
5003 });
5004 };
5005 if canonical.number.checked_add(1) != Some(incoming.block_number) {
5006 self.discard_preconfirmed_branch(cache);
5007 return Err(ReactiveError::InvalidInputRecord {
5008 message: format!(
5009 "pre-confirmed block {} is not the exact successor of canonical block {}",
5010 incoming.block_number, canonical.number
5011 ),
5012 });
5013 }
5014 if incoming.parent_hash != Some(canonical.hash) {
5015 self.discard_preconfirmed_branch(cache);
5016 return Err(ReactiveError::InvalidInputRecord {
5017 message: "pre-confirmed block parent does not match the canonical coverage hash"
5018 .into(),
5019 });
5020 }
5021 if let Some(active) = self.preconfirmed_branch.as_ref()
5022 && active.flashblock.same_payload(incoming)
5023 {
5024 if let (Some(active_index), Some(incoming_index)) =
5025 (active.flashblock.index, incoming.index)
5026 && incoming_index < active_index
5027 {
5028 return Err(ReactiveError::InvalidInputRecord {
5029 message: format!(
5030 "Flashblock index regressed from {active_index} to {incoming_index}"
5031 ),
5032 });
5033 }
5034 if active.flashblock.index.is_some()
5035 && active.flashblock.index == incoming.index
5036 && active.flashblock.content_hash != incoming.content_hash
5037 {
5038 self.discard_preconfirmed_branch(cache);
5039 return Err(ReactiveError::InvalidInputRecord {
5040 message: "same Flashblock payload/index carried conflicting cumulative content"
5041 .into(),
5042 });
5043 }
5044 install_preconfirmed_cache_context(cache, incoming);
5045 return Ok(());
5046 }
5047
5048 self.discard_preconfirmed_branch(cache);
5049 self.preconfirmed_branch = Some(PreconfirmedBranch {
5050 flashblock: incoming.clone(),
5051 canonical_cache: EvmCacheStateSnapshot::capture(cache),
5052 });
5053 install_preconfirmed_cache_context(cache, incoming);
5054 Ok(())
5055 }
5056
5057 fn ingest_batch_with_resync_direct(
5058 &mut self,
5059 cache: &mut EvmCache,
5060 batch: ReactiveInputBatch<N>,
5061 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
5062 let mut batch_report = self.ingest_batch_direct(cache, batch)?;
5063 if !batch_report.resyncs.is_empty() {
5064 let resync_report = execute_resync_requests(cache, &batch_report.resyncs);
5065 let unique_requests = resync_report
5069 .requested
5070 .iter()
5071 .map(|request| &request.id)
5072 .collect::<HashSet<_>>()
5073 .len();
5074 self.metrics
5075 .resync_requests
5076 .fetch_add(unique_requests as u64, Ordering::Relaxed);
5077 self.metrics
5078 .resync_failures
5079 .fetch_add(resync_report.failed.len() as u64, Ordering::Relaxed);
5080 self.remove_pending_resyncs(batch_report.resyncs.iter().map(|request| &request.id));
5081 self.record_journal_resync(&resync_report);
5082 batch_report
5083 .reports
5084 .push(Arc::new(ReactiveReport::Resynced(resync_report)));
5085 }
5086 Ok(batch_report)
5087 }
5088
5089 fn ingest_batch_direct(
5090 &mut self,
5091 cache: &mut EvmCache,
5092 batch: ReactiveInputBatch<N>,
5093 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
5094 let (records, chain_controls, batch_chain_id) = batch.into_runtime_parts();
5095 if let Some(chain_id) = batch_chain_id
5096 && chain_id != cache.chain_id()
5097 {
5098 return Err(ReactiveError::InvalidInputRecord {
5099 message: format!(
5100 "batch chain id {chain_id} does not match cache chain id {}",
5101 cache.chain_id()
5102 ),
5103 });
5104 }
5105 if !chain_controls.is_empty() && batch_chain_id.is_none() {
5106 return Err(ReactiveError::InvalidChainControl {
5107 message: "chain-control batches require an authoritative batch chain id".into(),
5108 });
5109 }
5110 for (record, _, _) in &records {
5111 record.validated_identity()?;
5112 if let Some(chain_id) = record.context.chain_id
5113 && chain_id != cache.chain_id()
5114 {
5115 return Err(ReactiveError::InvalidInputRecord {
5116 message: format!(
5117 "input chain id {chain_id} does not match cache chain id {}",
5118 cache.chain_id()
5119 ),
5120 });
5121 }
5122 }
5123 let records = sort_scoped_records(dedupe_scoped_records(records)?);
5124
5125 let mut batch_report = ReactiveBatchReport::default();
5126 let mut reports_to_dispatch = Vec::new();
5127 let control_split = validate_control_phase_order(&chain_controls)?;
5128 let (pre_record_controls, post_record_controls) = chain_controls.split_at(control_split);
5129 let pre_record_state =
5130 self.validate_ingest_sequence(pre_record_controls, post_record_controls, &records)?;
5131 self.validate_owner_catchup_against_journal(&pre_record_state, &records)?;
5132 let mut batch_dropped = BatchDroppedCanonical::default();
5133 for control in pre_record_controls {
5134 if let ChainControl::Reorg {
5135 common_ancestor,
5136 old_tip,
5137 ..
5138 } = control
5139 {
5140 batch_dropped.record_explicit(common_ancestor, old_tip);
5141 let drained = self
5142 .journal
5143 .iter()
5144 .filter(|entry| entry.block.number > common_ancestor.number)
5145 .map(|entry| entry.block)
5146 .collect::<Vec<_>>();
5147 batch_dropped.record_drained(&drained);
5148 }
5149 }
5150 let certified_progress_through = post_record_controls
5151 .iter()
5152 .filter_map(canonical_coverage_control_block)
5153 .map(|block| block.number)
5154 .max();
5155 for control in pre_record_controls.iter().cloned() {
5156 self.apply_chain_control(cache, control, &mut batch_report, &mut reports_to_dispatch);
5157 }
5158 let mut touched_addrs: HashSet<Address> = HashSet::new();
5163 let mut canonical_batch_block: Option<u64> = None;
5164
5165 for (record, audience, delivery_scope) in records {
5166 let raw_canonical_block = canonical_record_block(&record).copied();
5167 let canonical_block = raw_canonical_block.map(|block| {
5168 pre_record_state
5169 .resolved_canonical_blocks
5170 .get(&(block.number, block.hash))
5171 .copied()
5172 .unwrap_or(block)
5173 });
5174 let input_ref = record.input_ref();
5175 reports_to_dispatch.push(Arc::new(ReactiveReport::Input(InputReport {
5176 input_ref,
5177 context: record.context.clone(),
5178 provider: record.provider.clone(),
5179 _network: PhantomData,
5180 })));
5181
5182 let recovered_reorg = if delivery_scope.advances_canonical_state() {
5183 if let Some(block) = canonical_block.as_ref() {
5184 let gap_is_certified = delivery_scope == DeliveryScope::CanonicalProgress
5185 && certified_progress_through
5186 .is_some_and(|through| block.number <= through);
5187 let parentless_replacement_is_proven = raw_canonical_block.is_some_and(|raw| {
5188 raw.parent_hash.is_none()
5189 && batch_dropped.covers_implicit_number(raw.number)
5190 });
5191 self.recover_for_canonical_input(
5192 cache,
5193 block,
5194 gap_is_certified,
5195 parentless_replacement_is_proven,
5196 &mut reports_to_dispatch,
5197 )
5198 } else {
5199 None
5200 }
5201 } else {
5202 None
5203 };
5204 let recovered_reorg_for_input = recovered_reorg.is_some();
5205 if let Some(reorg_report) = recovered_reorg {
5206 self.metrics
5207 .reorgs_recovered
5208 .fetch_add(1, Ordering::Relaxed);
5209 remove_canceled_resyncs_from_batch(
5210 &mut batch_report.resyncs,
5211 &reorg_report.canceled_resyncs,
5212 );
5213 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5214 }
5215
5216 if reorg_signal_block(&record).is_some() {
5222 if delivery_scope.advances_canonical_state()
5223 && let Some(reorg_report) = self.recover_for_reorged_input(
5224 cache,
5225 &record,
5226 &mut batch_dropped,
5227 &mut reports_to_dispatch,
5228 )
5229 {
5230 self.metrics
5231 .reorgs_recovered
5232 .fetch_add(1, Ordering::Relaxed);
5233 remove_canceled_resyncs_from_batch(
5234 &mut batch_report.resyncs,
5235 &reorg_report.canceled_resyncs,
5236 );
5237 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5238 }
5239 continue;
5240 }
5241
5242 if delivery_scope == DeliveryScope::OwnerCatchup {
5249 self.validate_owner_catchup_record_against_current_journal(&record)?;
5250 }
5251
5252 if delivery_scope.advances_canonical_state()
5253 && let Some(block) = canonical_block.as_ref()
5254 {
5255 canonical_batch_block = Some(block.number);
5258 self.record_journal_input(block, input_ref);
5259 }
5260
5261 if delivery_scope.advances_canonical_state()
5267 && let Some(block) = canonical_block.as_ref()
5268 {
5269 match advance_block_for_canonical_record(cache, &record) {
5270 Some(Ok(())) => {
5271 cache.advance_compact_block(block.number, block.hash, block.timestamp, true)
5272 }
5273 Some(Err(err)) => {
5274 cache.advance_compact_block(
5275 block.number,
5276 block.hash,
5277 block.timestamp,
5278 false,
5279 );
5280 reports_to_dispatch.push(Arc::new(ReactiveReport::Error(
5281 ReactiveErrorReport {
5282 input_ref: Some(input_ref),
5283 message: err.to_string(),
5284 _network: PhantomData,
5285 },
5286 )));
5287 }
5288 None => cache.advance_compact_block(
5289 block.number,
5290 block.hash,
5291 block.timestamp,
5292 !recovered_reorg_for_input,
5293 ),
5294 }
5295 }
5296
5297 let executions = self.execute_handlers(cache, &record, input_ref, &audience)?;
5298 if executions.is_empty() {
5299 continue;
5300 }
5301
5302 reports_to_dispatch.push(Arc::new(ReactiveReport::Decoded(DecodedReport {
5303 input_ref,
5304 handler_ids: executions
5305 .iter()
5306 .map(|execution| execution.handler_id.clone())
5307 .collect(),
5308 _network: PhantomData,
5309 })));
5310
5311 detect_conflicts(input_ref, &executions)?;
5312
5313 let canonical_block_number = delivery_scope
5319 .advances_canonical_state()
5320 .then_some(canonical_block)
5321 .flatten()
5322 .map(|block| block.number);
5323
5324 for execution in executions {
5325 let diff = if execution.state_updates.is_empty() {
5326 StateDiff::default()
5327 } else {
5328 cache.apply_updates(&execution.state_updates)
5329 };
5330
5331 batch_report
5332 .resyncs
5333 .extend(execution.resyncs.iter().cloned());
5334 self.pending_resyncs
5335 .extend(execution.resyncs.iter().cloned());
5336 batch_report
5337 .speculative
5338 .extend(execution.speculative.iter().cloned());
5339
5340 let applied = AppliedReport {
5341 input_ref,
5342 handler_id: execution.handler_id,
5343 quality: execution.quality,
5344 tags: execution.tags,
5345 diff,
5346 state_updates: execution.state_updates,
5347 invalidations: execution.invalidations,
5348 resyncs: execution.resyncs,
5349 speculative: execution.speculative,
5350 hook_signals: execution.hook_signals,
5351 _network: PhantomData,
5352 };
5353 if let (Some(number), Some(registry)) =
5362 (canonical_block_number, self.freshness.as_mut())
5363 {
5364 for change in &applied.diff.slots {
5365 registry.valid_through_slot(change.address, change.slot, number);
5366 }
5367 }
5368
5369 if delivery_scope.advances_canonical_state() {
5377 collect_diff_addresses(&applied.diff, &mut touched_addrs);
5378 }
5379
5380 let report = Arc::new(ReactiveReport::Applied(applied.clone()));
5381 reports_to_dispatch.push(report);
5382 if let Some(block) = canonical_block.as_ref() {
5383 if delivery_scope.advances_canonical_state() {
5384 self.record_journal_applied(block, applied.clone());
5385 } else {
5386 self.record_journal_applied_if_present(block, applied.clone());
5387 }
5388 }
5389 batch_report.applied.push(applied);
5390 }
5391 }
5392
5393 for control in post_record_controls.iter().cloned() {
5398 if let Some(block) = canonical_coverage_control_block(&control) {
5399 canonical_batch_block = Some(
5400 canonical_batch_block.map_or(block.number, |current| current.max(block.number)),
5401 );
5402 }
5403 self.apply_chain_control(cache, control, &mut batch_report, &mut reports_to_dispatch);
5404 }
5405
5406 if self.root_gate_runnable(cache) {
5416 self.touched_since_gate
5417 .extend(touched_addrs.iter().copied());
5418 if self.root_gate_due(canonical_batch_block) {
5419 let accumulated = std::mem::take(&mut self.touched_since_gate);
5420 self.run_root_gate(
5421 cache,
5422 canonical_batch_block,
5423 &accumulated,
5424 &mut batch_report.resyncs,
5425 &mut reports_to_dispatch,
5426 );
5427 self.last_gate_block = canonical_batch_block;
5428 }
5429 } else {
5430 self.touched_since_gate.clear();
5437 }
5438
5439 batch_report.reports = reports_to_dispatch;
5440 Ok(batch_report)
5441 }
5442
5443 fn validate_owner_catchup_against_journal(
5457 &self,
5458 control_state: &ChainControlState,
5459 records: &[(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)],
5460 ) -> Result<(), ReactiveError> {
5461 for (record, _, delivery_scope) in records {
5462 if *delivery_scope != DeliveryScope::OwnerCatchup {
5463 continue;
5464 }
5465 if reorg_signal_block(record).is_some() {
5470 continue;
5471 }
5472 let context_block = canonical_record_block(record).ok_or_else(|| {
5473 ReactiveError::InvalidChainControl {
5474 message: "owner catch-up input has no canonical block identity".into(),
5475 }
5476 })?;
5477 let block = resolve_record_block_payload_metadata(record, *context_block)?;
5478 let invalidated_by_control = control_state
5479 .journal_invalidated_from
5480 .is_some_and(|from| block.number >= from);
5481 let rollbackable = !invalidated_by_control
5482 && self.journal.iter().any(|entry| {
5483 optional_block_refs_are_compatible(Some(&entry.block), Some(&block))
5484 });
5485 if !rollbackable {
5486 return Err(ReactiveError::OwnerCatchupOutsideJournal {
5487 number: block.number,
5488 hash: block.hash,
5489 });
5490 }
5491 }
5492 Ok(())
5493 }
5494
5495 fn validate_owner_catchup_record_against_current_journal(
5496 &self,
5497 record: &ReactiveInputRecord<N>,
5498 ) -> Result<(), ReactiveError> {
5499 let context_block =
5500 canonical_record_block(record).ok_or_else(|| ReactiveError::InvalidChainControl {
5501 message: "owner catch-up input has no canonical block identity".into(),
5502 })?;
5503 let block = resolve_record_block_payload_metadata(record, *context_block)?;
5504 if self
5505 .journal
5506 .iter()
5507 .any(|entry| optional_block_refs_are_compatible(Some(&entry.block), Some(&block)))
5508 {
5509 return Ok(());
5510 }
5511 Err(ReactiveError::OwnerCatchupOutsideJournal {
5512 number: block.number,
5513 hash: block.hash,
5514 })
5515 }
5516
5517 fn root_gate_runnable(&self, cache: &EvmCache) -> bool {
5522 if matches!(self.root_gate_cadence, RootGateCadence::Disabled) {
5523 return false;
5524 }
5525 let has_gated_targets = self
5526 .tracking
5527 .values()
5528 .any(|policy| !matches!(policy, TrackingPolicy::Slots { .. }));
5529 has_gated_targets && cache.account_proof_fetcher().is_some()
5530 }
5531
5532 fn root_gate_due(&self, canonical_block: Option<u64>) -> bool {
5536 let Some(block) = canonical_block else {
5537 return false;
5538 };
5539 match self.root_gate_cadence {
5540 RootGateCadence::Disabled => false,
5541 RootGateCadence::EveryNBlocks(n) => match self.last_gate_block {
5542 None => true,
5543 Some(last) => block >= last.saturating_add(n.get()),
5544 },
5545 }
5546 }
5547
5548 fn run_root_gate(
5574 &mut self,
5575 cache: &EvmCache,
5576 canonical_block: Option<u64>,
5577 touched: &HashSet<Address>,
5578 resyncs: &mut Vec<ResyncRequest>,
5579 reports: &mut Vec<Arc<ReactiveReport<N>>>,
5580 ) {
5581 if self.tracking.is_empty() {
5582 return;
5583 }
5584 let Some(block) = canonical_block else {
5585 return;
5586 };
5587 let Some(fetcher) = cache.account_proof_fetcher().cloned() else {
5588 return;
5589 };
5590
5591 let mut targets: Vec<(Address, bool)> = self
5594 .tracking
5595 .iter()
5596 .filter_map(|(address, policy)| match policy {
5597 TrackingPolicy::Slots { .. } => None,
5598 TrackingPolicy::WholeAccount => Some((*address, true)),
5599 TrackingPolicy::Scalars => Some((*address, false)),
5600 })
5601 .collect();
5602 if targets.is_empty() {
5603 return;
5604 }
5605 targets.sort_by_key(|(address, _)| *address);
5606
5607 let block_id = BlockId::number(block);
5608 let mut probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
5613 targets
5614 .iter()
5615 .map(|&(address, _)| (address, vec![]))
5616 .collect(),
5617 block_id,
5618 )
5619 .into_iter()
5620 .collect();
5621 for (address, whole_account) in targets {
5622 let Some(Ok(proof)) = probes.remove(&address) else {
5623 continue;
5626 };
5627
5628 let baseline = self.tracked_roots.get(&address).cloned();
5629 let Some(baseline) = baseline else {
5630 self.adopt_root(address, block, &proof);
5632 continue;
5633 };
5634
5635 if block <= baseline.last_block {
5640 continue;
5641 }
5642
5643 if whole_account {
5644 if proof.storage_hash == baseline.last_root {
5645 continue;
5647 }
5648 if !touched.contains(&address) {
5650 reports.push(Arc::new(ReactiveReport::CoverageGap(CoverageGapReport {
5652 address,
5653 block,
5654 _network: PhantomData,
5655 })));
5656 self.metrics.coverage_gaps.fetch_add(1, Ordering::Relaxed);
5657 resyncs.push(root_moved_account_resync(
5658 address,
5659 block,
5660 AccountFieldMask {
5661 balance: true,
5662 nonce: true,
5663 code: true,
5664 },
5665 ));
5666 }
5667 self.adopt_root(address, block, &proof);
5669 } else {
5670 let balance_moved = proof.balance != baseline.balance;
5673 let nonce_moved = proof.nonce != baseline.nonce;
5674 let code_moved = proof.code_hash != baseline.code_hash;
5675 if (balance_moved || nonce_moved || code_moved) && !touched.contains(&address) {
5676 resyncs.push(root_moved_account_resync(
5677 address,
5678 block,
5679 AccountFieldMask {
5680 balance: balance_moved,
5681 nonce: nonce_moved,
5682 code: code_moved,
5683 },
5684 ));
5685 }
5686 self.adopt_root(address, block, &proof);
5687 }
5688 }
5689 }
5690
5691 fn adopt_root(&mut self, address: Address, block: u64, proof: &AccountProof) {
5693 self.tracked_roots.insert(
5694 address,
5695 TrackedRoot {
5696 last_root: proof.storage_hash,
5697 last_block: block,
5698 balance: proof.balance,
5699 nonce: proof.nonce,
5700 code_hash: proof.code_hash,
5701 },
5702 );
5703 }
5704
5705 fn execute_handlers(
5706 &self,
5707 cache: &EvmCache,
5708 record: &ReactiveInputRecord<N>,
5709 input_ref: InputRef,
5710 audience: &DeliveryAudience,
5711 ) -> Result<Vec<HandlerExecution>, ReactiveError> {
5712 let mut executions = Vec::new();
5713 let candidates: Vec<_> = match &record.input {
5714 ReactiveInput::Log(log) => self.registry.log_handler_candidates(log),
5715 ReactiveInput::BlockHeader(_)
5716 | ReactiveInput::FullBlock(_)
5717 | ReactiveInput::PendingTxHash(_)
5718 | ReactiveInput::PendingTx(_) => self.registry.handlers().collect(),
5719 };
5720 for registered in candidates {
5721 match audience {
5722 DeliveryAudience::Owners(owners) if !owners.contains(®istered.id) => continue,
5723 DeliveryAudience::AllExcept(excluded) if excluded.contains(®istered.id) => {
5724 continue;
5725 }
5726 DeliveryAudience::All
5727 | DeliveryAudience::Owners(_)
5728 | DeliveryAudience::AllExcept(_) => {}
5729 }
5730 if !registered.matches(&record.input) {
5731 continue;
5732 }
5733
5734 let outcome = registered
5735 .handler
5736 .handle(&record.context, &record.input, cache)
5737 .map_err(|source| ReactiveError::HandlerFailed {
5738 handler_id: registered.id.clone(),
5739 source,
5740 })?;
5741
5742 if let Err(error) =
5743 validate_effects(input_ref, &record.context, ®istered.id, &outcome.effects)
5744 {
5745 if matches!(error, ReactiveError::InvalidPendingEffect { .. }) {
5746 self.metrics
5747 .pending_contamination
5748 .fetch_add(1, Ordering::Relaxed);
5749 }
5750 return Err(error);
5751 }
5752 executions.push(HandlerExecution::from_outcome(
5753 registered.id.clone(),
5754 input_ref,
5755 outcome,
5756 matches!(
5757 record.context.chain_status,
5758 ChainStatus::Preconfirmed { .. }
5759 ),
5760 ));
5761 }
5762 Ok(executions)
5763 }
5764
5765 fn dispatch_reports(&self, reports: &[Arc<ReactiveReport<N>>]) {
5766 for report in reports {
5767 for hook in &self.hooks {
5768 hook.on_report(report.clone());
5769 }
5770 }
5771 }
5772
5773 fn apply_chain_control(
5774 &mut self,
5775 cache: &mut EvmCache,
5776 control: ChainControl,
5777 batch_report: &mut ReactiveBatchReport<N>,
5778 reports: &mut Vec<Arc<ReactiveReport<N>>>,
5779 ) {
5780 match &control {
5781 ChainControl::Safe(block) => set_or_enrich_block_ref(&mut self.safe_head, block),
5782 ChainControl::Finalized(block) => {
5783 set_or_enrich_block_ref(&mut self.finalized_head, block);
5784 }
5785 ChainControl::CanonicalProgress(block)
5786 | ChainControl::Barrier {
5787 block: Some(block), ..
5788 } => {
5789 let preserve_env = self.coverage_head.as_ref().is_some_and(|current| {
5790 optional_block_refs_are_compatible(Some(current), Some(block))
5791 });
5792 cache.advance_compact_block(
5793 block.number,
5794 block.hash,
5795 block.timestamp,
5796 preserve_env,
5797 );
5798 advance_or_enrich_coverage(&mut self.coverage_head, block);
5799 let enriched = self.journal_entry_mut(block).block;
5800 advance_or_enrich_coverage(&mut self.coverage_head, &enriched);
5801 self.trim_journal();
5802 }
5803 ChainControl::Barrier { block: None, .. } => {}
5804 ChainControl::Reorg {
5805 common_ancestor,
5806 old_tip,
5807 ..
5808 } => {
5809 cache.invalidate_cached_block_hashes_from(common_ancestor.number.saturating_add(1));
5810 self.rebase_validation_state_from(common_ancestor.number.saturating_add(1));
5811 let dropped = if let Some(ancestor_index) = self.journal.iter().rposition(|entry| {
5812 entry.block.number == common_ancestor.number
5813 && entry.block.hash == common_ancestor.hash
5814 }) {
5815 self.drain_journal_after(ancestor_index)
5816 } else {
5817 if self
5822 .journal
5823 .front()
5824 .is_none_or(|entry| entry.block.number > common_ancestor.number)
5825 {
5826 reports.extend(
5827 self.warn_under_recovery(common_ancestor.number.saturating_add(1)),
5828 );
5829 }
5830 self.drain_journal_from_number(common_ancestor.number.saturating_add(1))
5831 };
5832
5833 let reorg_report = self
5834 .recover_dropped_journals(cache, dropped, ReorgReason::Explicit)
5835 .unwrap_or_else(|| ReorgReport {
5836 dropped: Some(*old_tip),
5837 dropped_blocks: Vec::new(),
5838 dropped_inputs: Vec::new(),
5839 rollback_updates: Vec::new(),
5840 rollback_diff: StateDiff::default(),
5841 purge_updates: Vec::new(),
5842 purge_diff: StateDiff::default(),
5843 canceled_resyncs: self
5844 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(old_tip)),
5845 reason: ReorgReason::Explicit,
5846 _network: PhantomData,
5847 });
5848 remove_canceled_resyncs_from_batch(
5849 &mut batch_report.resyncs,
5850 &reorg_report.canceled_resyncs,
5851 );
5852 self.metrics
5853 .reorgs_recovered
5854 .fetch_add(1, Ordering::Relaxed);
5855 reports.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5856
5857 if self.safe_head.as_ref().is_some_and(|head| {
5858 head.number > common_ancestor.number
5859 || (head.number == common_ancestor.number
5860 && head.hash != common_ancestor.hash)
5861 }) {
5862 self.safe_head = None;
5863 }
5864 if self.finalized_head.as_ref().is_some_and(|head| {
5865 head.number > common_ancestor.number
5866 || (head.number == common_ancestor.number
5867 && head.hash != common_ancestor.hash)
5868 }) {
5869 self.finalized_head = None;
5870 }
5871 let mut enriched_ancestor = *common_ancestor;
5872 if let Some(entry) = self.journal.iter().find(|entry| {
5873 entry.block.number == common_ancestor.number
5874 && entry.block.hash == common_ancestor.hash
5875 }) {
5876 enrich_block_ref(&mut enriched_ancestor, &entry.block);
5877 }
5878 if let Some(current) = self.coverage_head.as_ref()
5879 && current.number == common_ancestor.number
5880 && current.hash == common_ancestor.hash
5881 {
5882 enrich_block_ref(&mut enriched_ancestor, current);
5883 }
5884 self.coverage_head = Some(enriched_ancestor);
5885 cache.advance_compact_block(
5886 enriched_ancestor.number,
5887 enriched_ancestor.hash,
5888 enriched_ancestor.timestamp,
5889 false,
5890 );
5891 let enriched_ancestor = self.journal_entry_mut(&enriched_ancestor).block;
5892 self.coverage_head = Some(enriched_ancestor);
5893 self.trim_journal();
5894 }
5895 }
5896 reports.push(Arc::new(ReactiveReport::ChainControl(ChainControlReport {
5897 control,
5898 })));
5899 }
5900
5901 fn validate_ingest_sequence(
5902 &self,
5903 pre_record_controls: &[ChainControl],
5904 post_record_controls: &[ChainControl],
5905 records: &[(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)],
5906 ) -> Result<ChainControlState, ReactiveError> {
5907 let mut controls =
5908 Vec::with_capacity(pre_record_controls.len() + post_record_controls.len());
5909 controls.extend_from_slice(pre_record_controls);
5910 controls.extend_from_slice(post_record_controls);
5911 let state = CanonicalSequenceState::new(
5912 self.journal.iter().map(|entry| entry.block).collect(),
5913 self.coverage_head,
5914 self.safe_head,
5915 self.finalized_head,
5916 );
5917 let record_metadata = records
5918 .iter()
5919 .map(|(record, _, scope)| (record, *scope))
5920 .collect::<Vec<_>>();
5921 let validation = validate_canonical_sequence_parts(
5922 &state,
5923 &controls,
5924 &record_metadata,
5925 CanonicalSequenceValidationPolicy::ObserveIncompleteRollback,
5926 )
5927 .map_err(CanonicalSequenceError::into_reactive_error)?;
5928 let mut resolved_canonical_blocks = HashMap::new();
5929 for mutation in validation.mutations() {
5930 if let CanonicalSequenceMutation::Canonical(block) = mutation {
5931 resolved_canonical_blocks
5932 .entry((block.number, block.hash))
5933 .and_modify(|known| enrich_block_ref(known, block))
5934 .or_insert(*block);
5935 }
5936 }
5937 Ok(ChainControlState {
5938 journal_invalidated_from: pre_record_controls
5939 .iter()
5940 .filter_map(|control| match control {
5941 ChainControl::Reorg {
5942 common_ancestor, ..
5943 } => Some(common_ancestor.number.saturating_add(1)),
5944 _ => None,
5945 })
5946 .min(),
5947 resolved_canonical_blocks,
5948 })
5949 }
5950
5951 fn recover_for_canonical_input(
5952 &mut self,
5953 cache: &mut EvmCache,
5954 block: &BlockRef,
5955 gap_is_certified: bool,
5956 parentless_replacement_is_proven: bool,
5957 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
5958 ) -> Option<ReorgReport<N>> {
5959 let latest = self
5960 .coverage_head
5961 .or_else(|| self.journal.back().map(|entry| entry.block))?;
5962
5963 if latest.number == block.number && latest.hash == block.hash {
5964 return None;
5965 }
5966
5967 if self
5968 .journal
5969 .iter()
5970 .any(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
5971 {
5972 return None;
5973 }
5974
5975 if latest.number.checked_add(1) == Some(block.number)
5976 && (block.parent_hash == Some(latest.hash)
5977 || (parentless_replacement_is_proven && block.parent_hash.is_none()))
5978 {
5979 return None;
5980 }
5981
5982 if latest
5983 .number
5984 .checked_add(1)
5985 .is_some_and(|next| block.number > next)
5986 {
5987 if !gap_is_certified {
5994 self.metrics.missed_ranges.fetch_add(1, Ordering::Relaxed);
5995 health_reports.extend(self.escalate_trust(block.number));
5996 health_reports.push(Arc::new(ReactiveReport::MissedBlockRange(
5997 MissedRangeReport {
5998 from: latest.number + 1,
5999 to: block.number - 1,
6000 block: block.number,
6001 _network: PhantomData,
6002 },
6003 )));
6004 }
6005 return None;
6006 }
6007
6008 let (dropped, authenticated_anchor) = if let Some(parent_hash) = block.parent_hash {
6009 if let Some(parent_index) = self.journal.iter().rposition(|entry| {
6010 entry.block.number.checked_add(1) == Some(block.number)
6011 && entry.block.hash == parent_hash
6012 }) {
6013 let parent = self.journal[parent_index].block;
6014 cache.invalidate_cached_block_hashes_from(parent.number.saturating_add(1));
6015 (self.drain_journal_after(parent_index), Some(parent))
6016 } else {
6017 let proven_finalized_anchor = self.finalized_head.filter(|finalized| {
6023 finalized.number.checked_add(1) == Some(block.number)
6024 && parent_hash == finalized.hash
6025 });
6026 let invalidated_from = proven_finalized_anchor
6027 .map_or(0, |finalized| finalized.number.saturating_add(1));
6028 cache.invalidate_cached_block_hashes_from(invalidated_from);
6029 if block.number > 0 {
6030 cache.set_cached_block_hash(block.number.saturating_sub(1), parent_hash);
6034 }
6035 health_reports.extend(self.warn_under_recovery(block.number));
6036 let dropped = if let Some(finalized) = proven_finalized_anchor {
6037 self.drain_journal_from_number(finalized.number.saturating_add(1))
6038 } else {
6039 self.drain_journal_from_number(0)
6040 };
6041 (dropped, proven_finalized_anchor)
6042 }
6043 } else {
6044 cache.invalidate_cached_block_hashes_from(0);
6046 health_reports.extend(self.warn_under_recovery(block.number));
6047 (self.drain_journal_from_number(0), None)
6048 };
6049
6050 self.rebase_validation_state_from(
6051 authenticated_anchor.map_or(0, |anchor| anchor.number.saturating_add(1)),
6052 );
6053 let report = self
6054 .recover_dropped_journals(cache, dropped, ReorgReason::ParentMismatch)
6055 .or_else(|| {
6056 Some(ReorgReport {
6057 dropped: Some(latest),
6058 dropped_blocks: Vec::new(),
6059 dropped_inputs: Vec::new(),
6060 rollback_updates: Vec::new(),
6061 rollback_diff: StateDiff::default(),
6062 purge_updates: Vec::new(),
6063 purge_diff: StateDiff::default(),
6064 canceled_resyncs: self
6065 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&latest)),
6066 reason: ReorgReason::ParentMismatch,
6067 _network: PhantomData,
6068 })
6069 });
6070 self.coverage_head = authenticated_anchor;
6071 for head in [&mut self.safe_head, &mut self.finalized_head] {
6072 if head.is_some_and(|head| {
6073 authenticated_anchor.is_none_or(|anchor| {
6074 head.number > anchor.number
6075 || (head.number == anchor.number && head.hash != anchor.hash)
6076 })
6077 }) {
6078 *head = None;
6079 }
6080 }
6081 if let Some(anchor) = authenticated_anchor {
6082 cache.advance_compact_block(anchor.number, anchor.hash, anchor.timestamp, false);
6083 }
6084 report
6085 }
6086
6087 fn recover_for_reorged_input(
6088 &mut self,
6089 cache: &mut EvmCache,
6090 record: &ReactiveInputRecord<N>,
6091 batch_dropped: &mut BatchDroppedCanonical,
6092 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
6093 ) -> Option<ReorgReport<N>> {
6094 let (incoming_dropped_block, reason) = reorg_signal_block(record)?;
6095 if batch_dropped.contains(&incoming_dropped_block) {
6096 let canceled_resyncs = self
6101 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&incoming_dropped_block));
6102 return (!canceled_resyncs.is_empty()).then(|| ReorgReport {
6103 dropped: Some(incoming_dropped_block),
6104 dropped_blocks: vec![incoming_dropped_block],
6105 dropped_inputs: Vec::new(),
6106 rollback_updates: Vec::new(),
6107 rollback_diff: StateDiff::default(),
6108 purge_updates: Vec::new(),
6109 purge_diff: StateDiff::default(),
6110 canceled_resyncs,
6111 reason,
6112 _network: PhantomData,
6113 });
6114 }
6115 let exact_index = self.journal.iter().position(|entry| {
6116 entry.block.number == incoming_dropped_block.number
6117 && entry.block.hash == incoming_dropped_block.hash
6118 });
6119 let mut dropped_block = exact_index
6120 .map(|index| self.journal[index].block)
6121 .or_else(|| {
6122 self.coverage_head.filter(|known| {
6123 known.number == incoming_dropped_block.number
6124 && known.hash == incoming_dropped_block.hash
6125 })
6126 })
6127 .unwrap_or(incoming_dropped_block);
6128 enrich_block_ref(&mut dropped_block, &incoming_dropped_block);
6129 let replacement_is_known = exact_index.is_none()
6130 && (self.journal.iter().any(|entry| {
6131 entry.block.number == dropped_block.number && entry.block.hash != dropped_block.hash
6132 }) || self.coverage_head.is_some_and(|head| {
6133 head.number == dropped_block.number && head.hash != dropped_block.hash
6134 }));
6135
6136 if replacement_is_known {
6137 let canceled_resyncs =
6141 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
6142 return (!canceled_resyncs.is_empty()).then(|| ReorgReport {
6143 dropped: Some(dropped_block),
6144 dropped_blocks: vec![dropped_block],
6145 dropped_inputs: Vec::new(),
6146 rollback_updates: Vec::new(),
6147 rollback_diff: StateDiff::default(),
6148 purge_updates: Vec::new(),
6149 purge_diff: StateDiff::default(),
6150 canceled_resyncs,
6151 reason,
6152 _network: PhantomData,
6153 });
6154 }
6155
6156 let authenticated_anchor = exact_index.and_then(|index| {
6157 let ancestor_number = dropped_block.number.checked_sub(1)?;
6158 let retained = self
6159 .journal
6160 .iter()
6161 .take(index)
6162 .rev()
6163 .find(|entry| entry.block.number == ancestor_number)
6164 .map(|entry| entry.block);
6165 let synthetic_parent = dropped_block.parent_hash.map(|hash| BlockRef {
6166 number: ancestor_number,
6167 hash,
6168 parent_hash: None,
6169 timestamp: None,
6170 });
6171 let finalized_fallback = self
6172 .finalized_head
6173 .filter(|head| head.number == ancestor_number);
6174 let mut anchor = retained.or(synthetic_parent).or(finalized_fallback)?;
6175 for head in [self.safe_head.as_ref(), self.finalized_head.as_ref()]
6176 .into_iter()
6177 .flatten()
6178 {
6179 if head.number == anchor.number && head.hash == anchor.hash {
6180 enrich_block_ref(&mut anchor, head);
6181 }
6182 }
6183 Some(anchor)
6184 });
6185
6186 cache.invalidate_cached_block_hashes_from(dropped_block.number);
6187 let dropped = if let Some(index) = exact_index {
6188 self.drain_journal_from(index)
6189 } else {
6190 health_reports.extend(self.warn_under_recovery(dropped_block.number));
6191 self.drain_journal_from_number(dropped_block.number)
6192 };
6193 let drained_blocks = dropped.iter().map(|entry| entry.block).collect::<Vec<_>>();
6194 batch_dropped.record_drained(&drained_blocks);
6195 batch_dropped.record_identity(&dropped_block);
6196 self.rebase_validation_state_from(dropped_block.number);
6197
6198 let recovered_journal = !dropped.is_empty();
6199 let report = if !recovered_journal {
6200 let canceled_resyncs =
6201 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
6202 Some(ReorgReport {
6203 dropped: Some(dropped_block),
6204 dropped_blocks: Vec::new(),
6205 dropped_inputs: Vec::new(),
6206 rollback_updates: Vec::new(),
6207 rollback_diff: StateDiff::default(),
6208 purge_updates: Vec::new(),
6209 purge_diff: StateDiff::default(),
6210 canceled_resyncs,
6211 reason,
6212 _network: PhantomData,
6213 })
6214 } else {
6215 self.recover_dropped_journals(cache, dropped, reason)
6216 };
6217
6218 if recovered_journal {
6219 if let Some(anchor) = authenticated_anchor {
6220 self.coverage_head = Some(anchor);
6221 }
6222 let coverage = self.coverage_head;
6223 for head in [&mut self.safe_head, &mut self.finalized_head] {
6224 if head.is_some_and(|head| {
6225 coverage.is_none_or(|coverage| {
6226 head.number > coverage.number
6227 || (head.number == coverage.number && head.hash != coverage.hash)
6228 })
6229 }) {
6230 *head = None;
6231 }
6232 }
6233 }
6234
6235 if recovered_journal
6236 && report.is_some()
6237 && let Some(head) = self.coverage_head
6238 {
6239 cache.advance_compact_block(head.number, head.hash, head.timestamp, false);
6240 }
6241 report
6242 }
6243
6244 fn warn_under_recovery(&mut self, reorg_number: u64) -> Option<Arc<ReactiveReport<N>>> {
6256 let oldest_journaled = self.journal.front().map(|entry| entry.block.number);
6257 tracing::warn!(
6258 reorg_block = reorg_number,
6259 oldest_journaled = ?oldest_journaled,
6260 journal_depth = self.config.journal_depth,
6261 "reactive reorg recovery is incomplete: the reorged block is no longer \
6262 in the journal, so effects from blocks aged out of the journal are \
6263 neither rolled back nor purged (the freshness/validation loop is the \
6264 backstop). Increase ReactiveConfig::journal_depth to recover deeper \
6265 reorgs precisely."
6266 );
6267
6268 self.metrics.deep_reorgs.fetch_add(1, Ordering::Relaxed);
6269
6270 self.escalate_trust(reorg_number)
6271 }
6272
6273 fn record_journal_input(&mut self, block: &BlockRef, input_ref: InputRef) {
6274 advance_or_enrich_coverage(&mut self.coverage_head, block);
6275 let entry = self.journal_entry_mut(block);
6276 let enriched = entry.block;
6277 if !entry.inputs.contains(&input_ref) {
6278 entry.inputs.push(input_ref);
6279 }
6280 advance_or_enrich_coverage(&mut self.coverage_head, &enriched);
6281 self.trim_journal();
6282 }
6283
6284 fn record_journal_applied(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
6285 let entry = self.journal_entry_mut(block);
6286 if !entry.handler_ids.contains(&applied.handler_id) {
6287 entry.handler_ids.push(applied.handler_id.clone());
6288 }
6289 entry.rollback_diffs.push(applied.diff.clone());
6290 entry.applied.push(applied);
6291 self.trim_journal();
6292 }
6293
6294 fn record_journal_applied_if_present(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
6295 let Some(entry) = self
6296 .journal
6297 .iter_mut()
6298 .find(|entry| entry.block.number == block.number && entry.block.hash == block.hash)
6299 else {
6300 return;
6301 };
6302 if !entry.handler_ids.contains(&applied.handler_id) {
6303 entry.handler_ids.push(applied.handler_id.clone());
6304 }
6305 entry.rollback_diffs.push(applied.diff.clone());
6306 entry.applied.push(applied);
6307 }
6308
6309 fn record_journal_resync(&mut self, report: &ResyncReport) {
6310 if report.diff.is_empty() {
6311 return;
6312 }
6313 let Some(block) = single_hash_pinned_resync_block(report) else {
6314 return;
6315 };
6316 let entry = self.journal_entry_mut(&block);
6317 entry.rollback_diffs.push(report.diff.clone());
6318 entry.resynced.push(report.clone());
6319 self.trim_journal();
6320 }
6321
6322 fn journal_entry_mut(&mut self, block: &BlockRef) -> &mut BlockJournal<N> {
6323 if let Some(index) = self
6324 .journal
6325 .iter()
6326 .position(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
6327 {
6328 enrich_block_ref(&mut self.journal[index].block, block);
6329 return &mut self.journal[index];
6330 }
6331
6332 self.journal.push_back(BlockJournal {
6333 block: *block,
6334 inputs: Vec::new(),
6335 applied: Vec::new(),
6336 handler_ids: Vec::new(),
6337 resynced: Vec::new(),
6338 rollback_diffs: Vec::new(),
6339 });
6340 let index = self.journal.len() - 1;
6341 &mut self.journal[index]
6342 }
6343
6344 fn trim_journal(&mut self) {
6345 if self.config.journal_depth == 0 {
6346 self.journal.clear();
6347 return;
6348 }
6349 while self.journal.len() > self.config.journal_depth {
6350 self.journal.pop_front();
6351 }
6352 }
6353
6354 fn drain_journal_after(&mut self, index: usize) -> Vec<BlockJournal<N>> {
6355 self.journal.drain((index + 1)..).collect()
6356 }
6357
6358 fn drain_journal_from(&mut self, index: usize) -> Vec<BlockJournal<N>> {
6359 self.journal.drain(index..).collect()
6360 }
6361
6362 fn drain_journal_from_number(&mut self, number: u64) -> Vec<BlockJournal<N>> {
6363 let Some(index) = self
6364 .journal
6365 .iter()
6366 .position(|entry| entry.block.number >= number)
6367 else {
6368 return Vec::new();
6369 };
6370 self.drain_journal_from(index)
6371 }
6372
6373 fn recover_dropped_journals(
6374 &mut self,
6375 cache: &mut EvmCache,
6376 dropped: Vec<BlockJournal<N>>,
6377 reason: ReorgReason,
6378 ) -> Option<ReorgReport<N>> {
6379 if dropped.is_empty() {
6380 return None;
6381 }
6382
6383 let first_dropped_block = dropped
6384 .iter()
6385 .map(|entry| entry.block.number)
6386 .min()
6387 .expect("non-empty dropped journal set");
6388 self.rebase_validation_state_from(first_dropped_block);
6389 if self
6390 .safe_head
6391 .is_some_and(|head| head.number >= first_dropped_block)
6392 {
6393 self.safe_head = None;
6394 }
6395
6396 let dropped_blocks: Vec<_> = dropped.iter().map(|entry| entry.block).collect();
6397 let dropped_inputs: Vec<_> = dropped
6398 .iter()
6399 .flat_map(|entry| entry.inputs.iter().copied())
6400 .collect();
6401 let canceled_resyncs = self.cancel_resyncs_for_dropped_blocks(&dropped_blocks);
6402 let purge_scopes = purge_scopes_for_dropped_journals(&dropped);
6403 let rollback_updates = rollback_updates_for_dropped_journals(&dropped, &purge_scopes);
6404 let purge_updates: Vec<_> = purge_scopes
6405 .iter()
6406 .map(|(address, scope)| StateUpdate::purge(*address, scope.clone()))
6407 .collect();
6408
6409 let rollback_diff = if rollback_updates.is_empty() {
6410 StateDiff::default()
6411 } else {
6412 cache.apply_updates(&rollback_updates)
6413 };
6414 let purge_diff = if purge_updates.is_empty() {
6415 StateDiff::default()
6416 } else {
6417 cache.apply_updates(&purge_updates)
6418 };
6419 self.coverage_head = self.journal.back().map(|entry| entry.block);
6420
6421 Some(ReorgReport {
6422 dropped: dropped_blocks.first().cloned(),
6423 dropped_blocks,
6424 dropped_inputs,
6425 rollback_updates,
6426 rollback_diff,
6427 purge_updates,
6428 purge_diff,
6429 canceled_resyncs,
6430 reason,
6431 _network: PhantomData,
6432 })
6433 }
6434
6435 fn rebase_validation_state_from(&mut self, first_dropped_block: u64) {
6436 if let Some(freshness) = self.freshness.as_mut() {
6437 freshness.invalidate_valid_through_from(first_dropped_block);
6438 }
6439 self.tracked_roots
6440 .retain(|_, baseline| baseline.last_block < first_dropped_block);
6441 if self
6442 .last_gate_block
6443 .is_some_and(|block| block >= first_dropped_block)
6444 {
6445 self.last_gate_block = self
6446 .tracked_roots
6447 .values()
6448 .map(|baseline| baseline.last_block)
6449 .max();
6450 }
6451 self.touched_since_gate.clear();
6455 }
6456
6457 fn cancel_resyncs_for_dropped_blocks(
6458 &mut self,
6459 dropped_blocks: &[BlockRef],
6460 ) -> Vec<ResyncRequest> {
6461 let mut canceled = Vec::new();
6462 self.pending_resyncs.retain(|request| {
6463 let should_cancel = resync_request_targets_dropped_block(request, dropped_blocks);
6464 if should_cancel {
6465 canceled.push(request.clone());
6466 }
6467 !should_cancel
6468 });
6469 canceled
6470 }
6471
6472 fn remove_pending_resyncs<'a>(&mut self, ids: impl IntoIterator<Item = &'a ResyncId>) {
6473 let ids: HashSet<_> = ids.into_iter().cloned().collect();
6474 self.pending_resyncs
6475 .retain(|request| !ids.contains(&request.id));
6476 }
6477}
6478
6479fn install_preconfirmed_cache_context(cache: &mut EvmCache, flashblock: &FlashblockRef) {
6480 cache.set_block(BlockId::pending());
6481 cache.set_block_context(Some(flashblock.block_number), flashblock.base_fee_per_gas);
6482 cache.set_coinbase(flashblock.beneficiary);
6483 cache.set_prevrandao(flashblock.prevrandao);
6484 cache.set_block_gas_limit(flashblock.gas_limit);
6485 cache.set_timestamp(flashblock.timestamp);
6486}
6487
6488pub fn validate_canonical_sequence<N: Network>(
6520 state: &CanonicalSequenceState,
6521 batch: &ReactiveInputBatch<N>,
6522) -> Result<CanonicalSequenceValidation, ReactiveError> {
6523 validate_canonical_sequence_diagnostic(state, batch)
6524 .map_err(CanonicalSequenceError::into_reactive_error)
6525}
6526
6527pub fn validate_canonical_sequence_diagnostic<N: Network>(
6542 state: &CanonicalSequenceState,
6543 batch: &ReactiveInputBatch<N>,
6544) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6545 validate_canonical_sequence_internal(
6546 state,
6547 batch,
6548 CanonicalSequenceValidationPolicy::RequireCompleteRollback,
6549 )
6550}
6551
6552pub fn normalize_and_validate_canonical_sequence<N: Network>(
6575 state: &CanonicalSequenceState,
6576 batch: &ReactiveInputBatch<N>,
6577) -> Result<CanonicalSequenceValidation, ReactiveError> {
6578 normalize_and_validate_canonical_sequence_diagnostic(state, batch)
6579 .map_err(CanonicalSequenceError::into_reactive_error)
6580}
6581
6582pub fn normalize_and_validate_canonical_sequence_diagnostic<N: Network>(
6597 state: &CanonicalSequenceState,
6598 batch: &ReactiveInputBatch<N>,
6599) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6600 validate_canonical_sequence_internal(
6601 state,
6602 batch,
6603 CanonicalSequenceValidationPolicy::RequireCompleteRollbackNormalizeCoverage,
6604 )
6605}
6606
6607fn validate_canonical_sequence_internal<N: Network>(
6608 state: &CanonicalSequenceState,
6609 batch: &ReactiveInputBatch<N>,
6610 policy: CanonicalSequenceValidationPolicy,
6611) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6612 let records = batch
6613 .records()
6614 .iter()
6615 .enumerate()
6616 .map(|(index, record)| {
6617 (
6618 record.clone(),
6619 DeliveryAudience::All,
6620 batch
6621 .record_delivery_scope(index)
6622 .expect("enumerated record always has a delivery scope"),
6623 )
6624 })
6625 .collect::<Vec<_>>();
6626 let records = sort_scoped_records(dedupe_scoped_records(records)?);
6627 let records = records
6628 .iter()
6629 .map(|(record, _, scope)| (record, *scope))
6630 .collect::<Vec<_>>();
6631 validate_canonical_sequence_parts(state, batch.chain_controls(), &records, policy)
6632}
6633
6634#[derive(Clone, Copy)]
6635enum CanonicalSequenceValidationPolicy {
6636 RequireCompleteRollback,
6637 RequireCompleteRollbackNormalizeCoverage,
6638 ObserveIncompleteRollback,
6639}
6640
6641#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6644#[non_exhaustive]
6645pub enum CanonicalRollbackKind {
6646 Explicit,
6648 Removed,
6650 ImplicitParent,
6652 MissingReplacement,
6654}
6655
6656#[derive(Debug, thiserror::Error)]
6661#[non_exhaustive]
6662pub enum CanonicalSequenceError {
6663 #[error(transparent)]
6665 Invalid(#[from] ReactiveError),
6666 #[error(
6669 "{kind:?} rollback after block {common_ancestor} exceeds retained canonical history starting at {oldest_retained:?}"
6670 )]
6671 IncompleteRollback {
6672 common_ancestor: u64,
6674 oldest_retained: Option<u64>,
6676 kind: CanonicalRollbackKind,
6678 },
6679}
6680
6681#[derive(Clone, Copy, Debug)]
6682struct RequiredReorgAnchor {
6683 number: u64,
6684 block: Option<BlockRef>,
6685 permits_missing_child_parent: bool,
6686 must_be_consumed: bool,
6687}
6688
6689#[derive(Debug)]
6690struct SequenceRewind {
6691 common_ancestor: Option<BlockRef>,
6692 dropped: Vec<BlockRef>,
6693}
6694
6695impl RequiredReorgAnchor {
6696 const fn hash(self) -> Option<B256> {
6697 match self.block {
6698 Some(block) => Some(block.hash),
6699 None => None,
6700 }
6701 }
6702}
6703
6704impl CanonicalSequenceError {
6705 pub const fn requires_history(&self) -> bool {
6708 matches!(self, Self::IncompleteRollback { .. })
6709 }
6710
6711 pub fn into_reactive_error(self) -> ReactiveError {
6713 match self {
6714 Self::Invalid(error) => error,
6715 Self::IncompleteRollback {
6716 common_ancestor,
6717 oldest_retained,
6718 kind,
6719 } => ReactiveError::InvalidChainControl {
6720 message: format!(
6721 "{kind:?} rollback after block {common_ancestor} exceeds retained canonical history starting at {oldest_retained:?}"
6722 ),
6723 },
6724 }
6725 }
6726}
6727
6728impl CanonicalSequenceValidationPolicy {
6729 const fn requires_complete_rollback(self) -> bool {
6730 matches!(
6731 self,
6732 Self::RequireCompleteRollback | Self::RequireCompleteRollbackNormalizeCoverage
6733 )
6734 }
6735
6736 const fn normalizes_coverage(self) -> bool {
6737 matches!(self, Self::RequireCompleteRollbackNormalizeCoverage)
6738 }
6739}
6740
6741fn validate_canonical_sequence_parts<N: Network>(
6742 initial: &CanonicalSequenceState,
6743 controls: &[ChainControl],
6744 records: &[(&ReactiveInputRecord<N>, DeliveryScope)],
6745 policy: CanonicalSequenceValidationPolicy,
6746) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6747 validate_canonical_sequence_snapshot(initial)?;
6748 let control_split = validate_control_phase_order(controls)?;
6749 let (pre_record_controls, post_record_controls) = controls.split_at(control_split);
6750 let mut state = initial.clone();
6751 let mut asserted_blocks = HashMap::<u64, BlockRef>::new();
6752 let mut mutations = Vec::new();
6753 let mut normalized_chain_controls = Vec::with_capacity(controls.len());
6754 let mut batch_dropped = BatchDroppedCanonical::default();
6755 let mut removed_assertions = HashMap::<(u64, B256), BlockRef>::new();
6756 let mut removed_heights_by_hash = HashMap::<B256, u64>::new();
6757 let mut record_proof_control_identities = HashSet::<(u64, B256)>::new();
6758 let rollback_oldest = initial
6759 .retained_canonical_history
6760 .first()
6761 .map(|block| block.number);
6762
6763 for control in pre_record_controls {
6764 normalized_chain_controls.push(control.clone());
6765 validate_sequence_control(&state, control)?;
6766 assert_chain_control_identities(&mut asserted_blocks, control)?;
6767 let ChainControl::Reorg {
6768 common_ancestor,
6769 old_tip,
6770 ..
6771 } = control
6772 else {
6773 unreachable!("phase validation leaves only reorg controls before records")
6774 };
6775 let exact_ancestor = state.retained_canonical_history.iter().any(|block| {
6776 block.number == common_ancestor.number && block.hash == common_ancestor.hash
6777 });
6778 let rollback_horizon_covers_ancestor = state
6779 .retained_canonical_history
6780 .first()
6781 .is_some_and(|oldest| oldest.number <= common_ancestor.number);
6782 if policy.requires_complete_rollback()
6783 && !exact_ancestor
6784 && !rollback_horizon_covers_ancestor
6785 {
6786 return Err(CanonicalSequenceError::IncompleteRollback {
6787 common_ancestor: common_ancestor.number,
6788 oldest_retained: rollback_oldest,
6789 kind: CanonicalRollbackKind::Explicit,
6790 });
6791 }
6792 let dropped = state
6793 .retained_canonical_history
6794 .iter()
6795 .copied()
6796 .filter(|block| block.number > common_ancestor.number)
6797 .collect::<Vec<_>>();
6798 state
6799 .retained_canonical_history
6800 .retain(|block| block.number <= common_ancestor.number);
6801 upsert_sequence_history(&mut state.retained_canonical_history, common_ancestor)?;
6802 let mut enriched_ancestor = *common_ancestor;
6803 if let Some(retained) = state.retained_canonical_history.iter().find(|block| {
6804 block.number == common_ancestor.number && block.hash == common_ancestor.hash
6805 }) {
6806 enrich_block_ref(&mut enriched_ancestor, retained);
6807 }
6808 if let Some(coverage) = state.coverage_head.as_ref()
6809 && coverage.number == common_ancestor.number
6810 && coverage.hash == common_ancestor.hash
6811 {
6812 enrich_block_ref(&mut enriched_ancestor, coverage);
6813 }
6814 upsert_sequence_history(&mut state.retained_canonical_history, &enriched_ancestor)?;
6815 state.coverage_head = Some(enriched_ancestor);
6816 clear_sequence_heads_above(&mut state, &enriched_ancestor);
6817 batch_dropped.record_explicit(common_ancestor, old_tip);
6818 batch_dropped.record_drained(&dropped);
6819 mutations.push(CanonicalSequenceMutation::Rewind {
6820 common_ancestor: Some(enriched_ancestor),
6821 dropped,
6822 });
6823 }
6824 let pre_record_state = state.clone();
6825 let mut required_reorg_anchor = None::<RequiredReorgAnchor>;
6826
6827 for (record, scope) in records {
6828 if !scope.advances_canonical_state() {
6829 continue;
6830 }
6831 if let Some((incoming_dropped_block, _)) = reorg_signal_block(record) {
6832 let incoming_dropped_block =
6833 resolve_record_block_payload_metadata(record, incoming_dropped_block)?;
6834 validate_sequence_matching_metadata(&state, &incoming_dropped_block, "removed record")?;
6835 validate_sequence_adjacent_parent_identity(
6836 &state,
6837 &incoming_dropped_block,
6838 "removed record",
6839 )?;
6840 let mut dropped_block = state
6841 .retained_canonical_history
6842 .iter()
6843 .find(|known| {
6844 known.number == incoming_dropped_block.number
6845 && known.hash == incoming_dropped_block.hash
6846 })
6847 .copied()
6848 .or_else(|| {
6849 state.coverage_head.filter(|known| {
6850 known.number == incoming_dropped_block.number
6851 && known.hash == incoming_dropped_block.hash
6852 })
6853 })
6854 .unwrap_or(incoming_dropped_block);
6855 enrich_block_ref(&mut dropped_block, &incoming_dropped_block);
6856 validate_sequence_implicit_finality(&state, record, None)?;
6857 if dropped_block.number == 0 {
6858 return Err(ReactiveError::InvalidChainControl {
6859 message: "a removed/reorged genesis block has no canonical parent anchor"
6860 .into(),
6861 }
6862 .into());
6863 }
6864 let removed_identity = (dropped_block.number, dropped_block.hash);
6865 if let Some(previous_number) =
6866 removed_heights_by_hash.insert(dropped_block.hash, dropped_block.number)
6867 && previous_number != dropped_block.number
6868 {
6869 return Err(ReactiveError::InvalidChainControl {
6870 message: format!(
6871 "removed hash {:?} is reused at heights {} and {}",
6872 dropped_block.hash, previous_number, dropped_block.number
6873 ),
6874 }
6875 .into());
6876 }
6877 if let Some(previous) = removed_assertions.get_mut(&removed_identity) {
6878 if !optional_block_refs_are_compatible(Some(previous), Some(&dropped_block)) {
6879 return Err(ReactiveError::InvalidChainControl {
6880 message: format!(
6881 "duplicate removed block {}:{:?} carries conflicting metadata",
6882 dropped_block.number, dropped_block.hash
6883 ),
6884 }
6885 .into());
6886 }
6887 enrich_block_ref(previous, &dropped_block);
6888 } else {
6889 removed_assertions.insert(removed_identity, dropped_block);
6890 }
6891 if asserted_blocks
6892 .get(&dropped_block.number)
6893 .is_some_and(|asserted| asserted.hash == dropped_block.hash)
6894 {
6895 return Err(ReactiveError::InvalidChainControl {
6896 message: format!(
6897 "removed block {}:{:?} is asserted canonical by the same envelope",
6898 dropped_block.number, dropped_block.hash
6899 ),
6900 }
6901 .into());
6902 }
6903 if batch_dropped.contains(&dropped_block) {
6904 continue;
6905 }
6906 if let Some(index) = state.retained_canonical_history.iter().position(|block| {
6907 block.number == dropped_block.number && block.hash == dropped_block.hash
6908 }) {
6909 let dropped = state.retained_canonical_history.split_off(index);
6910 batch_dropped.record_drained(&dropped);
6911 let ancestor_number = dropped_block
6912 .number
6913 .checked_sub(1)
6914 .expect("genesis removal was rejected above");
6915 let retained_anchor = state
6916 .retained_canonical_history
6917 .iter()
6918 .rev()
6919 .find(|head| head.number == ancestor_number)
6920 .copied();
6921 let authenticated_anchor = retained_anchor
6922 .or_else(|| {
6923 dropped_block.parent_hash.map(|hash| BlockRef {
6924 number: ancestor_number,
6925 hash,
6926 parent_hash: None,
6927 timestamp: None,
6928 })
6929 })
6930 .or_else(|| {
6931 state
6932 .finalized_head
6933 .filter(|head| head.number == ancestor_number)
6934 });
6935 let authenticated_anchor = authenticated_anchor.map(|mut anchor| {
6936 for head in [state.safe_head.as_ref(), state.finalized_head.as_ref()]
6937 .into_iter()
6938 .flatten()
6939 {
6940 if head.number == anchor.number && head.hash == anchor.hash {
6941 enrich_block_ref(&mut anchor, head);
6942 }
6943 }
6944 anchor
6945 });
6946 required_reorg_anchor = Some(RequiredReorgAnchor {
6947 number: ancestor_number,
6948 block: authenticated_anchor,
6949 permits_missing_child_parent: retained_anchor.is_some(),
6950 must_be_consumed: authenticated_anchor.is_none()
6951 && state.retained_canonical_history.is_empty(),
6952 });
6953 state.coverage_head = authenticated_anchor
6954 .or_else(|| state.retained_canonical_history.last().copied());
6955 if let Some(head) = state.coverage_head {
6956 clear_sequence_heads_above(&mut state, &head);
6957 } else {
6958 state.safe_head = None;
6959 state.finalized_head = None;
6960 }
6961 mutations.push(CanonicalSequenceMutation::Rewind {
6962 common_ancestor: state.coverage_head,
6963 dropped,
6964 });
6965 } else {
6966 let replacement_is_known = state.retained_canonical_history.iter().any(|block| {
6967 block.number == dropped_block.number && block.hash != dropped_block.hash
6968 }) || state.coverage_head.is_some_and(|head| {
6969 head.number == dropped_block.number && head.hash != dropped_block.hash
6970 });
6971 if !replacement_is_known {
6972 if policy.requires_complete_rollback() {
6977 return Err(CanonicalSequenceError::IncompleteRollback {
6978 common_ancestor: dropped_block
6979 .number
6980 .checked_sub(1)
6981 .expect("genesis removal was rejected above"),
6982 oldest_retained: rollback_oldest,
6983 kind: CanonicalRollbackKind::Removed,
6984 });
6985 }
6986 continue;
6987 }
6988 }
6989 continue;
6990 }
6991
6992 let Some(context_block) = canonical_record_block(record) else {
6993 continue;
6994 };
6995 let incoming_block = resolve_record_block_payload_metadata(record, *context_block)?;
6996 if post_record_controls
6997 .iter()
6998 .filter_map(canonical_coverage_control_block)
6999 .any(|asserted| {
7000 asserted.number == incoming_block.number
7001 && asserted.hash == incoming_block.hash
7002 && optional_block_refs_are_compatible(Some(asserted), Some(&incoming_block))
7003 && ((incoming_block.parent_hash.is_none() && asserted.parent_hash.is_some())
7004 || (incoming_block.timestamp.is_none() && asserted.timestamp.is_some()))
7005 })
7006 {
7007 record_proof_control_identities.insert((incoming_block.number, incoming_block.hash));
7008 }
7009 let mut resolved_block = incoming_block;
7010 if let Some(asserted) = asserted_blocks
7011 .get(&incoming_block.number)
7012 .filter(|asserted| asserted.hash == incoming_block.hash)
7013 {
7014 if !optional_block_refs_are_compatible(Some(asserted), Some(&incoming_block)) {
7015 return Err(ReactiveError::InvalidChainControl {
7016 message: format!(
7017 "canonical record {}:{:?} conflicts with the same envelope's asserted metadata",
7018 incoming_block.number, incoming_block.hash
7019 ),
7020 }
7021 .into());
7022 }
7023 enrich_block_ref(&mut resolved_block, asserted);
7024 }
7025 for asserted in post_record_controls
7026 .iter()
7027 .filter_map(chain_control_canonical_assertion)
7028 .filter(|asserted| {
7029 asserted.number == incoming_block.number && asserted.hash == incoming_block.hash
7030 })
7031 {
7032 if !optional_block_refs_are_compatible(Some(&resolved_block), Some(asserted)) {
7033 return Err(ReactiveError::InvalidChainControl {
7034 message: format!(
7035 "canonical record {}:{:?} conflicts with the same envelope's asserted metadata",
7036 incoming_block.number, incoming_block.hash
7037 ),
7038 }
7039 .into());
7040 }
7041 enrich_block_ref(&mut resolved_block, asserted);
7042 }
7043 let replacement_anchor =
7044 required_reorg_anchor.filter(|required| resolved_block.number > required.number);
7045 if resolved_block.parent_hash.is_none()
7046 && replacement_anchor.is_some_and(|anchor| {
7047 anchor.permits_missing_child_parent
7048 && anchor.number.checked_add(1) == Some(resolved_block.number)
7049 })
7050 {
7051 resolved_block.parent_hash = replacement_anchor.and_then(RequiredReorgAnchor::hash);
7052 }
7053 let block = &resolved_block;
7054 if removed_assertions.contains_key(&(block.number, block.hash)) {
7055 return Err(ReactiveError::InvalidChainControl {
7056 message: format!(
7057 "canonical block {}:{:?} is also removed by the same envelope",
7058 block.number, block.hash
7059 ),
7060 }
7061 .into());
7062 }
7063 if let Some(removed_number) = removed_heights_by_hash.get(&block.hash)
7064 && *removed_number != block.number
7065 {
7066 return Err(ReactiveError::InvalidChainControl {
7067 message: format!(
7068 "canonical hash {:?} at height {} is removed at height {} by the same envelope",
7069 block.hash, block.number, removed_number
7070 ),
7071 }
7072 .into());
7073 }
7074 let replacement_proven_by_removal =
7075 validate_replacement_reorg_anchor(replacement_anchor, block, policy, rollback_oldest)?;
7076 if replacement_anchor.is_some() {
7077 required_reorg_anchor = None;
7078 }
7079 validate_sequence_matching_metadata(&state, block, "canonical record")?;
7080 validate_sequence_implicit_finality(&state, record, Some(block))?;
7081 let implicit_replacement_requires_history = if replacement_proven_by_removal {
7082 false
7083 } else {
7084 sequence_implicit_replacement_requires_history(&state, block, policy)?
7085 };
7086 if implicit_replacement_requires_history && policy.requires_complete_rollback() {
7087 return Err(CanonicalSequenceError::IncompleteRollback {
7088 common_ancestor: block.number.saturating_sub(1),
7089 oldest_retained: rollback_oldest,
7090 kind: CanonicalRollbackKind::ImplicitParent,
7091 });
7092 }
7093 assert_canonical_block_identity(&mut asserted_blocks, block, "canonical record")?;
7094 let allow_parentless_extension = replacement_anchor.is_some_and(|anchor| {
7095 anchor.permits_missing_child_parent
7096 && anchor.number.checked_add(1) == Some(block.number)
7097 });
7098 if let Some(rewind) =
7099 apply_sequence_canonical_block(&mut state, block, allow_parentless_extension)?
7100 {
7101 mutations.push(CanonicalSequenceMutation::Rewind {
7102 common_ancestor: rewind.common_ancestor,
7103 dropped: rewind.dropped,
7104 });
7105 }
7106 mutations.push(CanonicalSequenceMutation::Canonical(*block));
7107 }
7108
7109 for control in post_record_controls {
7110 if let Some(block) = chain_control_canonical_assertion(control)
7111 && removed_assertions.contains_key(&(block.number, block.hash))
7112 {
7113 return Err(ReactiveError::InvalidChainControl {
7114 message: format!(
7115 "canonical block {}:{:?} is also removed by the same envelope",
7116 block.number, block.hash
7117 ),
7118 }
7119 .into());
7120 }
7121 if let Some(block) = chain_control_canonical_assertion(control)
7122 && let Some(removed_number) = removed_heights_by_hash.get(&block.hash)
7123 && *removed_number != block.number
7124 {
7125 return Err(ReactiveError::InvalidChainControl {
7126 message: format!(
7127 "canonical hash {:?} at height {} is removed at height {} by the same envelope",
7128 block.hash, block.number, removed_number
7129 ),
7130 }
7131 .into());
7132 }
7133 let replacement_anchor = canonical_coverage_control_block(control).and_then(|block| {
7134 required_reorg_anchor.filter(|required| block.number > required.number)
7135 });
7136 if let Some(block) = canonical_coverage_control_block(control) {
7137 validate_replacement_reorg_anchor(replacement_anchor, block, policy, rollback_oldest)?;
7138 if replacement_anchor.is_some() {
7139 required_reorg_anchor = None;
7140 }
7141 }
7142 assert_chain_control_identities(&mut asserted_blocks, control)?;
7143 let preserves_record_proof =
7144 canonical_coverage_control_block(control).is_some_and(|block| {
7145 record_proof_control_identities.contains(&(block.number, block.hash))
7146 });
7147 if policy.normalizes_coverage()
7148 && !preserves_record_proof
7149 && let Some(block) = canonical_coverage_control_block(control)
7150 && state
7151 .coverage_head
7152 .is_some_and(|head| block.number <= head.number)
7153 {
7154 let is_equal_coverage = state
7155 .coverage_head
7156 .is_some_and(|head| block.number == head.number);
7157 let known = state
7158 .coverage_head
7159 .as_ref()
7160 .filter(|head| head.number == block.number && head.hash == block.hash)
7161 .or_else(|| {
7162 state
7163 .retained_canonical_history
7164 .iter()
7165 .find(|entry| entry.number == block.number && entry.hash == block.hash)
7166 });
7167 if let Some(known) = known
7168 && optional_block_refs_are_compatible(Some(known), Some(block))
7169 && (!is_equal_coverage || !sequence_block_adds_metadata(&state, block))
7170 {
7171 if let ChainControl::Barrier { id, .. } = control {
7172 normalized_chain_controls.push(ChainControl::Barrier {
7173 id: id.clone(),
7174 block: None,
7175 });
7176 }
7177 continue;
7178 }
7179 }
7180 validate_sequence_control(&state, control)?;
7181 normalized_chain_controls.push(control.clone());
7182 match control {
7183 ChainControl::Safe(block) => {
7184 set_or_enrich_block_ref(&mut state.safe_head, block);
7185 mutations.push(CanonicalSequenceMutation::Safe(
7186 state.safe_head.expect("safe head was just installed"),
7187 ));
7188 }
7189 ChainControl::Finalized(block) => {
7190 set_or_enrich_block_ref(&mut state.finalized_head, block);
7191 mutations.push(CanonicalSequenceMutation::Finalized(
7192 state
7193 .finalized_head
7194 .expect("finalized head was just installed"),
7195 ));
7196 }
7197 ChainControl::CanonicalProgress(block)
7198 | ChainControl::Barrier {
7199 block: Some(block), ..
7200 } => {
7201 let allow_parentless_extension = replacement_anchor.is_some_and(|anchor| {
7202 anchor.permits_missing_child_parent
7203 && anchor.number.checked_add(1) == Some(block.number)
7204 }) || (replacement_anchor.is_none()
7205 && block.parent_hash.is_none()
7206 && state
7207 .coverage_head
7208 .is_some_and(|head| head.number.checked_add(1) == Some(block.number)));
7209 if let Some(rewind) =
7210 apply_sequence_canonical_block(&mut state, block, allow_parentless_extension)?
7211 {
7212 mutations.push(CanonicalSequenceMutation::Rewind {
7213 common_ancestor: rewind.common_ancestor,
7214 dropped: rewind.dropped,
7215 });
7216 }
7217 mutations.push(CanonicalSequenceMutation::Canonical(*block));
7218 }
7219 ChainControl::Barrier { block: None, .. } => {}
7220 ChainControl::Reorg { .. } => {
7221 unreachable!("phase validation excludes post-record reorg controls")
7222 }
7223 }
7224 }
7225
7226 if let Some(required) = required_reorg_anchor
7227 && required.must_be_consumed
7228 && policy.requires_complete_rollback()
7229 {
7230 return Err(CanonicalSequenceError::IncompleteRollback {
7231 common_ancestor: required.number,
7232 oldest_retained: rollback_oldest,
7233 kind: CanonicalRollbackKind::MissingReplacement,
7234 });
7235 }
7236
7237 validate_canonical_sequence_snapshot(&state)?;
7238 Ok(CanonicalSequenceValidation {
7239 pre_record_state,
7240 next_state: state,
7241 mutations,
7242 normalized_chain_controls,
7243 })
7244}
7245
7246fn validate_canonical_sequence_snapshot(
7247 state: &CanonicalSequenceState,
7248) -> Result<(), ReactiveError> {
7249 let invalid = |message: String| ReactiveError::InvalidChainControl { message };
7250 let supplied_blocks = state
7251 .retained_canonical_history
7252 .iter()
7253 .chain(state.coverage_head.iter())
7254 .chain(state.safe_head.iter())
7255 .chain(state.finalized_head.iter())
7256 .collect::<Vec<_>>();
7257 validate_known_parent_hash_heights(&supplied_blocks)?;
7258 let mut prior = None::<BlockRef>;
7259 for block in &state.retained_canonical_history {
7260 if let Some(previous) = prior {
7261 if block.number < previous.number {
7262 return Err(invalid(
7263 "retained canonical history is not ordered by block number".into(),
7264 ));
7265 }
7266 if block.number == previous.number {
7267 let qualifier = if optional_block_refs_are_compatible(Some(&previous), Some(block))
7268 {
7269 "duplicate"
7270 } else {
7271 "conflicting"
7272 };
7273 return Err(invalid(format!(
7274 "retained canonical history contains {qualifier} identities at block {}",
7275 block.number
7276 )));
7277 }
7278 if previous.number.checked_add(1) == Some(block.number)
7279 && block.parent_hash.is_some()
7280 && block.parent_hash != Some(previous.hash)
7281 {
7282 return Err(invalid(format!(
7283 "adjacent retained block {}:{:?} does not descend from {}:{:?}",
7284 block.number, block.hash, previous.number, previous.hash
7285 )));
7286 }
7287 }
7288 prior = Some(*block);
7289 }
7290 if state.coverage_head.is_none() && !state.retained_canonical_history.is_empty() {
7291 return Err(invalid(
7292 "retained canonical history requires an authoritative coverage head".into(),
7293 ));
7294 }
7295 if let Some(head) = state.coverage_head.as_ref() {
7296 if let Some(retained) = state
7297 .retained_canonical_history
7298 .iter()
7299 .find(|entry| entry.number == head.number)
7300 && !optional_block_refs_are_compatible(Some(retained), Some(head))
7301 {
7302 return Err(invalid(format!(
7303 "coverage head {}:{:?} conflicts with retained identity {:?}",
7304 head.number, head.hash, retained
7305 )));
7306 }
7307 if state
7308 .retained_canonical_history
7309 .last()
7310 .is_some_and(|retained| retained.number > head.number)
7311 {
7312 return Err(invalid(
7313 "retained canonical history advances beyond the coverage head".into(),
7314 ));
7315 }
7316 if let Some(retained) = state.retained_canonical_history.last()
7317 && retained.number.checked_add(1) == Some(head.number)
7318 && head.parent_hash.is_some()
7319 && head.parent_hash != Some(retained.hash)
7320 {
7321 return Err(invalid(format!(
7322 "coverage head {}:{:?} does not descend from adjacent retained block {}:{:?}",
7323 head.number, head.hash, retained.number, retained.hash
7324 )));
7325 }
7326 }
7327 if let Some(safe) = state.safe_head.as_ref() {
7328 validate_sequence_known_identity(state, safe, "safe")?;
7329 validate_sequence_head_within_coverage(state, safe, "safe")?;
7330 validate_coverage_descends_from_adjacent_head(state.coverage_head.as_ref(), safe, "safe")?;
7331 }
7332 if let Some(finalized) = state.finalized_head.as_ref() {
7333 validate_sequence_known_identity(state, finalized, "finalized")?;
7334 validate_sequence_head_within_coverage(state, finalized, "finalized")?;
7335 validate_coverage_descends_from_adjacent_head(
7336 state.coverage_head.as_ref(),
7337 finalized,
7338 "finalized",
7339 )?;
7340 }
7341 validate_adjacent_finality(state.finalized_head.as_ref(), state.safe_head.as_ref())?;
7342 if let (Some(finalized), Some(safe)) = (state.finalized_head, state.safe_head)
7343 && (finalized.number > safe.number
7344 || (finalized.number == safe.number && finalized.hash != safe.hash))
7345 {
7346 return Err(invalid(
7347 "finalized head cannot advance beyond or conflict with safe head".into(),
7348 ));
7349 }
7350 Ok(())
7351}
7352
7353fn validate_known_parent_hash_heights(blocks: &[&BlockRef]) -> Result<(), ReactiveError> {
7354 let mut heights_by_hash = HashMap::<B256, u64>::with_capacity(blocks.len());
7355 let mut resolved_by_height = HashMap::<u64, BlockRef>::with_capacity(blocks.len());
7356 for block in blocks.iter().copied() {
7357 if let Some(previous_height) = heights_by_hash.insert(block.hash, block.number)
7358 && previous_height != block.number
7359 {
7360 return Err(ReactiveError::InvalidChainControl {
7361 message: format!(
7362 "canonical hash {:?} is reused at heights {} and {}",
7363 block.hash, previous_height, block.number
7364 ),
7365 });
7366 }
7367 if let Some(resolved) = resolved_by_height.get_mut(&block.number) {
7368 if !optional_block_refs_are_compatible(Some(resolved), Some(block)) {
7369 return Err(ReactiveError::InvalidChainControl {
7370 message: format!(
7371 "canonical aliases at height {} carry conflicting identities or metadata",
7372 block.number
7373 ),
7374 });
7375 }
7376 enrich_block_ref(resolved, block);
7377 } else {
7378 resolved_by_height.insert(block.number, *block);
7379 }
7380 }
7381 for child in resolved_by_height.values() {
7382 let Some(parent_hash) = child.parent_hash else {
7383 continue;
7384 };
7385 if let Some(parent_number) = heights_by_hash.get(&parent_hash)
7386 && parent_number.checked_add(1) != Some(child.number)
7387 {
7388 return Err(ReactiveError::InvalidChainControl {
7389 message: format!(
7390 "block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
7391 child.number, child.hash, parent_hash, parent_number
7392 ),
7393 });
7394 }
7395 if let Some(parent_number) = child.number.checked_sub(1)
7396 && let Some(parent) = resolved_by_height.get(&parent_number)
7397 && parent.hash != parent_hash
7398 {
7399 return Err(ReactiveError::InvalidChainControl {
7400 message: format!(
7401 "block {}:{:?} does not descend from supplied adjacent identity {}:{:?}",
7402 child.number, child.hash, parent.number, parent.hash
7403 ),
7404 });
7405 }
7406 }
7407 Ok(())
7408}
7409
7410fn validate_coverage_descends_from_adjacent_head(
7411 coverage: Option<&BlockRef>,
7412 head: &BlockRef,
7413 label: &str,
7414) -> Result<(), ReactiveError> {
7415 let Some(coverage) = coverage else {
7416 return Ok(());
7417 };
7418 if head.number.checked_add(1) == Some(coverage.number)
7419 && coverage
7420 .parent_hash
7421 .is_some_and(|parent| parent != head.hash)
7422 {
7423 return Err(ReactiveError::InvalidChainControl {
7424 message: format!(
7425 "canonical coverage {}:{:?} does not descend from adjacent {label} head {}:{:?}",
7426 coverage.number, coverage.hash, head.number, head.hash
7427 ),
7428 });
7429 }
7430 Ok(())
7431}
7432
7433fn validate_sequence_control(
7434 state: &CanonicalSequenceState,
7435 control: &ChainControl,
7436) -> Result<(), ReactiveError> {
7437 let invalid = |message: String| ReactiveError::InvalidChainControl { message };
7438 match control {
7439 ChainControl::Safe(block) => {
7440 validate_sequence_known_identity(state, block, "safe")?;
7441 validate_sequence_head_within_coverage(state, block, "safe")?;
7442 if let Some(current) = state.safe_head.as_ref()
7443 && (block.number < current.number
7444 || (block.number == current.number
7445 && (block.hash != current.hash
7446 || !optional_block_refs_are_compatible(Some(block), Some(current)))))
7447 {
7448 return Err(invalid(format!(
7449 "safe head {}:{:?} conflicts with current {}:{:?}",
7450 block.number, block.hash, current.number, current.hash
7451 )));
7452 }
7453 if let Some(finalized) = state.finalized_head.as_ref()
7454 && (block.number < finalized.number
7455 || (block.number == finalized.number && block.hash != finalized.hash))
7456 {
7457 return Err(invalid(
7458 "safe head cannot precede or conflict with finalized head".into(),
7459 ));
7460 }
7461 validate_adjacent_finality(state.finalized_head.as_ref(), Some(block))?;
7462 }
7463 ChainControl::Finalized(block) => {
7464 validate_sequence_known_identity(state, block, "finalized")?;
7465 validate_sequence_head_within_coverage(state, block, "finalized")?;
7466 if let Some(current) = state.finalized_head.as_ref()
7467 && (block.number < current.number
7468 || (block.number == current.number
7469 && (block.hash != current.hash
7470 || !optional_block_refs_are_compatible(Some(block), Some(current)))))
7471 {
7472 return Err(invalid(format!(
7473 "finalized head {}:{:?} conflicts with current {}:{:?}",
7474 block.number, block.hash, current.number, current.hash
7475 )));
7476 }
7477 if let Some(safe) = state.safe_head.as_ref()
7478 && (block.number > safe.number
7479 || (block.number == safe.number && block.hash != safe.hash))
7480 {
7481 return Err(invalid(
7482 "finalized head cannot advance beyond or conflict with safe head".into(),
7483 ));
7484 }
7485 validate_adjacent_finality(Some(block), state.safe_head.as_ref())?;
7486 }
7487 ChainControl::CanonicalProgress(block)
7488 | ChainControl::Barrier {
7489 block: Some(block), ..
7490 } => {
7491 validate_sequence_known_identity(state, block, "canonical coverage")?;
7492 if let Some(current) = state.coverage_head.as_ref()
7493 && (block.number < current.number
7494 || (block.number == current.number && block.hash != current.hash))
7495 {
7496 return Err(invalid(format!(
7497 "canonical coverage {}:{:?} conflicts with current {}:{:?}",
7498 block.number, block.hash, current.number, current.hash
7499 )));
7500 }
7501 if let Some(current) = state.coverage_head.as_ref()
7502 && current.number.checked_add(1) == Some(block.number)
7503 && block.parent_hash.is_some()
7504 && block.parent_hash != Some(current.hash)
7505 {
7506 return Err(invalid(format!(
7507 "canonical coverage {}:{:?} does not descend from current {}:{:?}",
7508 block.number, block.hash, current.number, current.hash
7509 )));
7510 }
7511 }
7512 ChainControl::Barrier { block: None, .. } => {}
7513 ChainControl::Reorg {
7514 common_ancestor,
7515 old_tip,
7516 new_tip,
7517 } => {
7518 validate_sequence_known_identity(state, common_ancestor, "reorg common ancestor")?;
7519 validate_reorg_ancestor_against_retained_branch(state, common_ancestor)?;
7520 validate_sequence_known_hash_height(state, old_tip, "reorg old tip")?;
7521 validate_sequence_known_hash_height(state, new_tip, "reorg new tip")?;
7522 validate_sequence_known_parent_height(state, old_tip, "reorg old tip")?;
7523 validate_sequence_known_parent_height(state, new_tip, "reorg new tip")?;
7524 validate_sequence_adjacent_parent_identity(state, old_tip, "reorg old tip")?;
7525 if let Some(current) = state.coverage_head.as_ref()
7526 && (old_tip.number != current.number
7527 || old_tip.hash != current.hash
7528 || !optional_block_refs_are_compatible(Some(old_tip), Some(current)))
7529 {
7530 return Err(invalid(format!(
7531 "reorg old tip {}:{:?} does not exactly match current metadata {}:{:?}",
7532 old_tip.number, old_tip.hash, current.number, current.hash
7533 )));
7534 }
7535 if common_ancestor.number > old_tip.number || common_ancestor.number > new_tip.number {
7536 return Err(invalid(
7537 "reorg common ancestor cannot be above either branch tip".into(),
7538 ));
7539 }
7540 if common_ancestor.number == old_tip.number || common_ancestor.number == new_tip.number
7541 {
7542 return Err(invalid(
7543 "reorg must replace non-empty old and new branches above the common ancestor"
7544 .into(),
7545 ));
7546 }
7547 if old_tip.number == new_tip.number && old_tip.hash == new_tip.hash {
7548 return Err(invalid(
7549 "reorg old and new tips cannot have the same canonical identity".into(),
7550 ));
7551 }
7552 for (label, tip) in [("old", old_tip), ("new", new_tip)] {
7553 if common_ancestor.number.checked_add(1) == Some(tip.number)
7554 && tip.parent_hash != Some(common_ancestor.hash)
7555 {
7556 return Err(invalid(format!(
7557 "reorg {label} tip does not descend from the common ancestor"
7558 )));
7559 }
7560 }
7561 if let Some(finalized) = state.finalized_head.as_ref()
7562 && (common_ancestor.number < finalized.number
7563 || (common_ancestor.number == finalized.number
7564 && common_ancestor.hash != finalized.hash))
7565 {
7566 return Err(invalid(
7567 "reorg would cross or conflict with the finalized head".into(),
7568 ));
7569 }
7570 }
7571 }
7572 Ok(())
7573}
7574
7575fn validate_sequence_known_identity(
7576 state: &CanonicalSequenceState,
7577 block: &BlockRef,
7578 label: &str,
7579) -> Result<(), ReactiveError> {
7580 validate_sequence_known_hash_height(state, block, label)?;
7581 validate_sequence_known_parent_height(state, block, label)?;
7582 let known = state
7583 .coverage_head
7584 .as_ref()
7585 .filter(|head| head.number == block.number)
7586 .or_else(|| {
7587 state
7588 .retained_canonical_history
7589 .iter()
7590 .find(|entry| entry.number == block.number)
7591 });
7592 if let Some(known) = known
7593 && !optional_block_refs_are_compatible(Some(known), Some(block))
7594 {
7595 return Err(ReactiveError::InvalidChainControl {
7596 message: format!(
7597 "{label} block {}:{:?} conflicts with known canonical block {:?}",
7598 block.number, block.hash, known
7599 ),
7600 });
7601 }
7602 Ok(())
7603}
7604
7605fn validate_sequence_known_parent_height(
7606 state: &CanonicalSequenceState,
7607 block: &BlockRef,
7608 label: &str,
7609) -> Result<(), ReactiveError> {
7610 let Some(parent_hash) = block.parent_hash else {
7611 return Ok(());
7612 };
7613 let known_parent = state
7614 .retained_canonical_history
7615 .iter()
7616 .chain(state.coverage_head.iter())
7617 .chain(state.safe_head.iter())
7618 .chain(state.finalized_head.iter())
7619 .find(|known| known.hash == parent_hash);
7620 if let Some(parent) = known_parent
7621 && parent.number.checked_add(1) != Some(block.number)
7622 {
7623 return Err(ReactiveError::InvalidChainControl {
7624 message: format!(
7625 "{label} block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
7626 block.number, block.hash, parent.hash, parent.number
7627 ),
7628 });
7629 }
7630 Ok(())
7631}
7632
7633fn validate_sequence_head_within_coverage(
7634 state: &CanonicalSequenceState,
7635 block: &BlockRef,
7636 label: &str,
7637) -> Result<(), ReactiveError> {
7638 let Some(coverage) = state.coverage_head.as_ref() else {
7639 return Err(ReactiveError::InvalidChainControl {
7640 message: format!("{label} head requires an authoritative coverage head"),
7641 });
7642 };
7643 if block.number > coverage.number
7644 || (block.number == coverage.number
7645 && !optional_block_refs_are_compatible(Some(block), Some(coverage)))
7646 {
7647 return Err(ReactiveError::InvalidChainControl {
7648 message: format!(
7649 "{label} head {}:{:?} advances beyond or conflicts with coverage {}:{:?}",
7650 block.number, block.hash, coverage.number, coverage.hash
7651 ),
7652 });
7653 }
7654 Ok(())
7655}
7656
7657fn validate_sequence_matching_metadata(
7658 state: &CanonicalSequenceState,
7659 block: &BlockRef,
7660 label: &str,
7661) -> Result<(), ReactiveError> {
7662 validate_sequence_known_hash_height(state, block, label)?;
7663 validate_sequence_known_parent_height(state, block, label)?;
7664 let known = state
7665 .coverage_head
7666 .as_ref()
7667 .filter(|head| head.number == block.number && head.hash == block.hash)
7668 .or_else(|| {
7669 state
7670 .retained_canonical_history
7671 .iter()
7672 .find(|entry| entry.number == block.number && entry.hash == block.hash)
7673 });
7674 if let Some(known) = known
7675 && !optional_block_refs_are_compatible(Some(known), Some(block))
7676 {
7677 return Err(ReactiveError::InvalidChainControl {
7678 message: format!(
7679 "{label} block {}:{:?} carries metadata conflicting with known canonical block {:?}",
7680 block.number, block.hash, known
7681 ),
7682 });
7683 }
7684 Ok(())
7685}
7686
7687fn validate_sequence_known_hash_height(
7688 state: &CanonicalSequenceState,
7689 block: &BlockRef,
7690 label: &str,
7691) -> Result<(), ReactiveError> {
7692 let known = state
7693 .retained_canonical_history
7694 .iter()
7695 .chain(state.coverage_head.iter())
7696 .chain(state.safe_head.iter())
7697 .chain(state.finalized_head.iter())
7698 .find(|known| known.hash == block.hash);
7699 if let Some(known) = known
7700 && known.number != block.number
7701 {
7702 return Err(ReactiveError::InvalidChainControl {
7703 message: format!(
7704 "{label} block {}:{:?} reuses a canonical hash already known at height {}",
7705 block.number, block.hash, known.number
7706 ),
7707 });
7708 }
7709 Ok(())
7710}
7711
7712fn validate_reorg_ancestor_against_retained_branch(
7713 state: &CanonicalSequenceState,
7714 ancestor: &BlockRef,
7715) -> Result<(), ReactiveError> {
7716 let adjacent_number = ancestor.number.checked_add(1);
7717 for retained in state
7718 .retained_canonical_history
7719 .iter()
7720 .chain(state.coverage_head.iter())
7721 .chain(state.safe_head.iter())
7722 .chain(state.finalized_head.iter())
7723 {
7724 if Some(retained.number) == adjacent_number
7725 && retained
7726 .parent_hash
7727 .is_some_and(|parent| parent != ancestor.hash)
7728 {
7729 return Err(ReactiveError::InvalidChainControl {
7730 message: format!(
7731 "reorg common ancestor {}:{:?} conflicts with retained child {}:{:?} parent {:?}",
7732 ancestor.number,
7733 ancestor.hash,
7734 retained.number,
7735 retained.hash,
7736 retained.parent_hash
7737 ),
7738 });
7739 }
7740 if retained.parent_hash == Some(ancestor.hash) && Some(retained.number) != adjacent_number {
7741 return Err(ReactiveError::InvalidChainControl {
7742 message: format!(
7743 "reorg common ancestor {}:{:?} is named as the non-adjacent parent of retained block {}:{:?}",
7744 ancestor.number, ancestor.hash, retained.number, retained.hash
7745 ),
7746 });
7747 }
7748 }
7749 Ok(())
7750}
7751
7752fn validate_sequence_adjacent_parent_identity(
7753 state: &CanonicalSequenceState,
7754 block: &BlockRef,
7755 label: &str,
7756) -> Result<(), ReactiveError> {
7757 let Some(parent_hash) = block.parent_hash else {
7758 return Ok(());
7759 };
7760 let Some(parent_number) = block.number.checked_sub(1) else {
7761 return Ok(());
7762 };
7763 let known_parent = state
7764 .retained_canonical_history
7765 .iter()
7766 .chain(state.coverage_head.iter())
7767 .chain(state.safe_head.iter())
7768 .chain(state.finalized_head.iter())
7769 .find(|known| known.number == parent_number);
7770 if let Some(known_parent) = known_parent
7771 && known_parent.hash != parent_hash
7772 {
7773 return Err(ReactiveError::InvalidChainControl {
7774 message: format!(
7775 "{label} block {}:{:?} names parent {:?}, which conflicts with known adjacent block {}:{:?}",
7776 block.number, block.hash, parent_hash, known_parent.number, known_parent.hash
7777 ),
7778 });
7779 }
7780 Ok(())
7781}
7782
7783fn sequence_block_adds_metadata(state: &CanonicalSequenceState, incoming: &BlockRef) -> bool {
7784 state
7785 .coverage_head
7786 .iter()
7787 .chain(state.retained_canonical_history.iter())
7788 .filter(|known| known.number == incoming.number && known.hash == incoming.hash)
7789 .any(|known| {
7790 (known.parent_hash.is_none() && incoming.parent_hash.is_some())
7791 || (known.timestamp.is_none() && incoming.timestamp.is_some())
7792 })
7793}
7794
7795fn validate_sequence_implicit_finality<N: Network>(
7796 state: &CanonicalSequenceState,
7797 record: &ReactiveInputRecord<N>,
7798 resolved_canonical_block: Option<&BlockRef>,
7799) -> Result<(), ReactiveError> {
7800 let Some(finalized) = state.finalized_head.as_ref() else {
7801 return Ok(());
7802 };
7803 if let Some((dropped, _)) = reorg_signal_block(record) {
7804 if dropped.number <= finalized.number {
7805 return Err(ReactiveError::InvalidChainControl {
7806 message: format!(
7807 "implicit reorg at {}:{:?} would cross finalized head {}:{:?}",
7808 dropped.number, dropped.hash, finalized.number, finalized.hash
7809 ),
7810 });
7811 }
7812 return Ok(());
7813 }
7814 let Some(block) = resolved_canonical_block.or_else(|| canonical_record_block(record)) else {
7815 return Ok(());
7816 };
7817 let Some(latest) = state.coverage_head.as_ref() else {
7818 return Ok(());
7819 };
7820 if (block.number == latest.number && block.hash == latest.hash)
7821 || state
7822 .retained_canonical_history
7823 .iter()
7824 .any(|entry| entry.number == block.number && entry.hash == block.hash)
7825 || (latest.number.checked_add(1) == Some(block.number)
7826 && block.parent_hash == Some(latest.hash))
7827 || latest
7828 .number
7829 .checked_add(1)
7830 .is_some_and(|next| block.number > next)
7831 {
7832 return Ok(());
7833 }
7834 let crosses_finalized = if block.number <= finalized.number {
7835 true
7836 } else if let Some(parent_hash) = block.parent_hash {
7837 if finalized.number.checked_add(1) == Some(block.number) && parent_hash == finalized.hash {
7838 false
7839 } else if let Some(parent_index) =
7840 state.retained_canonical_history.iter().rposition(|entry| {
7841 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
7842 })
7843 {
7844 state
7845 .retained_canonical_history
7846 .iter()
7847 .skip(parent_index + 1)
7848 .any(|entry| entry.number <= finalized.number)
7849 } else {
7850 true
7851 }
7852 } else {
7853 true
7854 };
7855 if crosses_finalized {
7856 return Err(ReactiveError::InvalidChainControl {
7857 message: format!(
7858 "canonical input {}:{:?} would replace finalized head {}:{:?}",
7859 block.number, block.hash, finalized.number, finalized.hash
7860 ),
7861 });
7862 }
7863 Ok(())
7864}
7865
7866fn validate_required_reorg_anchor(
7867 required: Option<RequiredReorgAnchor>,
7868 block: &BlockRef,
7869) -> Result<(), ReactiveError> {
7870 let Some(required) = required else {
7871 return Ok(());
7872 };
7873 let ancestor_hash = required.hash();
7874 let restores_ancestor =
7875 block.number == required.number && ancestor_hash.is_some_and(|hash| block.hash == hash);
7876 let replaces_removed_child = required.number.checked_add(1) == Some(block.number)
7877 && ancestor_hash.is_some()
7878 && (block.parent_hash == ancestor_hash
7879 || (block.parent_hash.is_none() && required.permits_missing_child_parent));
7880 if restores_ancestor || replaces_removed_child {
7881 return Ok(());
7882 }
7883 Err(ReactiveError::InvalidChainControl {
7884 message: format!(
7885 "canonical replacement {}:{:?} does not prove the removed tip's parent at block {}",
7886 block.number, block.hash, required.number
7887 ),
7888 })
7889}
7890
7891fn validate_replacement_reorg_anchor(
7892 required: Option<RequiredReorgAnchor>,
7893 block: &BlockRef,
7894 policy: CanonicalSequenceValidationPolicy,
7895 oldest_retained: Option<u64>,
7896) -> Result<bool, CanonicalSequenceError> {
7897 let Some(required) = required else {
7898 return Ok(false);
7899 };
7900 match validate_required_reorg_anchor(Some(required), block) {
7901 Ok(()) => Ok(true),
7902 Err(error) if required.block.is_some() => Err(error.into()),
7903 Err(_) if policy.requires_complete_rollback() => {
7904 Err(CanonicalSequenceError::IncompleteRollback {
7905 common_ancestor: required.number,
7906 oldest_retained,
7907 kind: CanonicalRollbackKind::MissingReplacement,
7908 })
7909 }
7910 Err(_) => Ok(false),
7911 }
7912}
7913
7914fn apply_sequence_canonical_block(
7915 state: &mut CanonicalSequenceState,
7916 block: &BlockRef,
7917 allow_parentless_adjacent_extension: bool,
7918) -> Result<Option<SequenceRewind>, ReactiveError> {
7919 let latest = state.coverage_head;
7920 let already_known = state
7921 .retained_canonical_history
7922 .iter()
7923 .any(|entry| entry.number == block.number && entry.hash == block.hash);
7924 let repeats_tip =
7925 latest.is_some_and(|head| head.number == block.number && head.hash == block.hash);
7926 let extends_tip = latest.is_some_and(|head| {
7927 head.number.checked_add(1) == Some(block.number)
7928 && (block.parent_hash == Some(head.hash)
7929 || (allow_parentless_adjacent_extension && block.parent_hash.is_none()))
7930 });
7931 let forward_gap = latest.is_some_and(|head| {
7932 head.number
7933 .checked_add(1)
7934 .is_some_and(|next| block.number > next)
7935 });
7936 let mut rewind = None;
7937
7938 if latest.is_some() && !already_known && !repeats_tip && !extends_tip && !forward_gap {
7939 let retained_parent = block.parent_hash.and_then(|parent_hash| {
7940 state
7941 .retained_canonical_history
7942 .iter()
7943 .rposition(|entry| {
7944 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
7945 })
7946 .map(|index| (index, state.retained_canonical_history[index]))
7947 });
7948 let finalized_parent = block.parent_hash.and_then(|parent_hash| {
7949 state.finalized_head.filter(|finalized| {
7950 finalized.number.checked_add(1) == Some(block.number)
7951 && finalized.hash == parent_hash
7952 })
7953 });
7954 let (common_ancestor, dropped) = if let Some((parent_index, parent)) = retained_parent {
7955 let dropped = state.retained_canonical_history.split_off(parent_index + 1);
7956 (Some(parent), dropped)
7957 } else if let Some(finalized) = finalized_parent {
7958 let dropped = state
7959 .retained_canonical_history
7960 .iter()
7961 .position(|entry| entry.number > finalized.number)
7962 .map_or_else(Vec::new, |index| {
7963 state.retained_canonical_history.split_off(index)
7964 });
7965 (Some(finalized), dropped)
7966 } else {
7967 (None, std::mem::take(&mut state.retained_canonical_history))
7973 };
7974 state.coverage_head = common_ancestor;
7975 if let Some(common_ancestor) = common_ancestor {
7976 clear_sequence_heads_above(state, &common_ancestor);
7977 } else {
7978 state.safe_head = None;
7979 state.finalized_head = None;
7980 }
7981 rewind = Some(SequenceRewind {
7982 common_ancestor,
7983 dropped,
7984 });
7985 }
7986 upsert_sequence_history(&mut state.retained_canonical_history, block)?;
7987 advance_or_enrich_coverage(&mut state.coverage_head, block);
7988 Ok(rewind)
7989}
7990
7991fn sequence_implicit_replacement_requires_history(
7992 state: &CanonicalSequenceState,
7993 block: &BlockRef,
7994 policy: CanonicalSequenceValidationPolicy,
7995) -> Result<bool, ReactiveError> {
7996 let Some(latest) = state.coverage_head else {
7997 return Ok(false);
7998 };
7999 let already_known = state
8000 .retained_canonical_history
8001 .iter()
8002 .any(|entry| entry.number == block.number && entry.hash == block.hash);
8003 let repeats_tip = block.number == latest.number && block.hash == latest.hash;
8004 let extends_tip = latest.number.checked_add(1) == Some(block.number)
8005 && block.parent_hash == Some(latest.hash);
8006 let forward_gap = latest
8007 .number
8008 .checked_add(1)
8009 .is_some_and(|next| block.number > next);
8010 if already_known || repeats_tip || extends_tip || forward_gap {
8011 return Ok(false);
8012 }
8013 let Some(parent_hash) = block.parent_hash else {
8014 if policy.requires_complete_rollback() {
8015 return Err(ReactiveError::InvalidChainControl {
8016 message: format!(
8017 "implicit canonical replacement {}:{:?} must identify its parent",
8018 block.number, block.hash
8019 ),
8020 });
8021 }
8022 return Ok(true);
8023 };
8024 let known_adjacent_parent = block.number.checked_sub(1).and_then(|parent_number| {
8025 state
8026 .retained_canonical_history
8027 .iter()
8028 .chain(state.coverage_head.iter())
8029 .chain(state.safe_head.iter())
8030 .chain(state.finalized_head.iter())
8031 .find(|known| known.number == parent_number)
8032 });
8033 if let Some(known_parent) = known_adjacent_parent
8034 && known_parent.hash != parent_hash
8035 && policy.requires_complete_rollback()
8036 {
8037 return Err(ReactiveError::InvalidChainControl {
8038 message: format!(
8039 "implicit canonical replacement {}:{:?} names parent {:?}, which conflicts with known adjacent block {}:{:?}",
8040 block.number, block.hash, parent_hash, known_parent.number, known_parent.hash
8041 ),
8042 });
8043 }
8044 let retained_parent = state.retained_canonical_history.iter().any(|entry| {
8045 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
8046 });
8047 let finalized_parent = state.finalized_head.is_some_and(|finalized| {
8048 finalized.number.checked_add(1) == Some(block.number) && parent_hash == finalized.hash
8049 });
8050 Ok(!retained_parent && !finalized_parent)
8051}
8052
8053fn upsert_sequence_history(
8054 history: &mut Vec<BlockRef>,
8055 block: &BlockRef,
8056) -> Result<(), ReactiveError> {
8057 if let Some(existing) = history
8058 .iter_mut()
8059 .find(|entry| entry.number == block.number)
8060 {
8061 if existing.hash != block.hash {
8062 return Err(ReactiveError::InvalidChainControl {
8063 message: format!(
8064 "canonical block {}:{:?} conflicts with retained identity {:?}",
8065 block.number, block.hash, existing
8066 ),
8067 });
8068 }
8069 if !optional_block_refs_are_compatible(Some(existing), Some(block)) {
8070 return Err(ReactiveError::InvalidChainControl {
8071 message: format!(
8072 "canonical block {}:{:?} carries conflicting retained metadata",
8073 block.number, block.hash
8074 ),
8075 });
8076 }
8077 enrich_block_ref(existing, block);
8078 } else {
8079 history.push(*block);
8080 history.sort_by_key(|entry| entry.number);
8081 }
8082 Ok(())
8083}
8084
8085fn clear_sequence_heads_above(state: &mut CanonicalSequenceState, ancestor: &BlockRef) {
8086 if state.safe_head.as_ref().is_some_and(|head| {
8087 head.number > ancestor.number
8088 || (head.number == ancestor.number && head.hash != ancestor.hash)
8089 }) {
8090 state.safe_head = None;
8091 }
8092 if state.finalized_head.as_ref().is_some_and(|head| {
8093 head.number > ancestor.number
8094 || (head.number == ancestor.number && head.hash != ancestor.hash)
8095 }) {
8096 state.finalized_head = None;
8097 }
8098}
8099
8100fn validate_control_phase_order(controls: &[ChainControl]) -> Result<usize, ReactiveError> {
8101 let split = controls
8102 .iter()
8103 .position(|control| !matches!(control, ChainControl::Reorg { .. }))
8104 .unwrap_or(controls.len());
8105 if controls[split..]
8106 .iter()
8107 .any(|control| matches!(control, ChainControl::Reorg { .. }))
8108 {
8109 return Err(ReactiveError::InvalidChainControl {
8110 message: "reorg controls must precede records and all post-record controls in a batch"
8111 .into(),
8112 });
8113 }
8114 Ok(split)
8115}
8116
8117fn canonical_coverage_control_block(control: &ChainControl) -> Option<&BlockRef> {
8118 match control {
8119 ChainControl::CanonicalProgress(block)
8120 | ChainControl::Barrier {
8121 block: Some(block), ..
8122 } => Some(block),
8123 ChainControl::Reorg { .. }
8124 | ChainControl::Safe(_)
8125 | ChainControl::Finalized(_)
8126 | ChainControl::Barrier { block: None, .. } => None,
8127 }
8128}
8129
8130fn chain_control_canonical_assertion(control: &ChainControl) -> Option<&BlockRef> {
8131 match control {
8132 ChainControl::Safe(block)
8133 | ChainControl::Finalized(block)
8134 | ChainControl::CanonicalProgress(block)
8135 | ChainControl::Barrier {
8136 block: Some(block), ..
8137 } => Some(block),
8138 ChainControl::Reorg { .. } | ChainControl::Barrier { block: None, .. } => None,
8139 }
8140}
8141
8142fn assert_chain_control_identities(
8143 asserted_blocks: &mut HashMap<u64, BlockRef>,
8144 control: &ChainControl,
8145) -> Result<(), ReactiveError> {
8146 match control {
8147 ChainControl::Safe(block)
8148 | ChainControl::Finalized(block)
8149 | ChainControl::CanonicalProgress(block)
8150 | ChainControl::Barrier {
8151 block: Some(block), ..
8152 } => assert_canonical_block_identity(asserted_blocks, block, "chain control"),
8153 ChainControl::Barrier { block: None, .. } => Ok(()),
8154 ChainControl::Reorg {
8155 common_ancestor,
8156 new_tip,
8157 ..
8158 } => {
8159 asserted_blocks.retain(|number, _| *number <= common_ancestor.number);
8160 assert_canonical_block_identity(
8161 asserted_blocks,
8162 common_ancestor,
8163 "reorg common ancestor",
8164 )?;
8165 assert_canonical_block_identity(asserted_blocks, new_tip, "reorg new tip")
8166 }
8167 }
8168}
8169
8170fn assert_canonical_block_identity(
8171 asserted_blocks: &mut HashMap<u64, BlockRef>,
8172 block: &BlockRef,
8173 label: &str,
8174) -> Result<(), ReactiveError> {
8175 for asserted in asserted_blocks.values() {
8176 if asserted.hash == block.hash && asserted.number != block.number {
8177 return Err(ReactiveError::InvalidChainControl {
8178 message: format!(
8179 "{label} hash {:?} is already asserted at height {}, not {}",
8180 block.hash, asserted.number, block.number
8181 ),
8182 });
8183 }
8184 if block
8185 .parent_hash
8186 .is_some_and(|parent| parent == asserted.hash)
8187 && asserted.number.checked_add(1) != Some(block.number)
8188 {
8189 return Err(ReactiveError::InvalidChainControl {
8190 message: format!(
8191 "{label} block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
8192 block.number, block.hash, asserted.hash, asserted.number
8193 ),
8194 });
8195 }
8196 if asserted
8197 .parent_hash
8198 .is_some_and(|parent| parent == block.hash)
8199 && block.number.checked_add(1) != Some(asserted.number)
8200 {
8201 return Err(ReactiveError::InvalidChainControl {
8202 message: format!(
8203 "block {}:{:?} asserted earlier names {label} hash {:?} from non-adjacent height {} as its parent",
8204 asserted.number, asserted.hash, block.hash, block.number
8205 ),
8206 });
8207 }
8208 }
8209 if let Some(known) = asserted_blocks.get_mut(&block.number) {
8210 if !optional_block_refs_are_compatible(Some(known), Some(block)) {
8211 return Err(ReactiveError::InvalidChainControl {
8212 message: format!(
8213 "{label} block {}:{:?} conflicts with block identity {:?} asserted earlier in the batch",
8214 block.number, block.hash, known
8215 ),
8216 });
8217 }
8218 enrich_block_ref(known, block);
8219 } else {
8220 asserted_blocks.insert(block.number, *block);
8221 }
8222 Ok(())
8223}
8224
8225fn set_or_enrich_block_ref(current: &mut Option<BlockRef>, incoming: &BlockRef) {
8226 match current {
8227 Some(current) if current.number == incoming.number && current.hash == incoming.hash => {
8228 enrich_block_ref(current, incoming);
8229 }
8230 _ => *current = Some(*incoming),
8231 }
8232}
8233
8234fn advance_or_enrich_coverage(current: &mut Option<BlockRef>, incoming: &BlockRef) {
8235 match current {
8236 Some(current) if current.number == incoming.number && current.hash == incoming.hash => {
8237 enrich_block_ref(current, incoming);
8238 }
8239 Some(current) if current.number >= incoming.number => {}
8240 _ => *current = Some(*incoming),
8241 }
8242}
8243
8244fn validate_adjacent_finality(
8245 finalized: Option<&BlockRef>,
8246 safe: Option<&BlockRef>,
8247) -> Result<(), ReactiveError> {
8248 let Some((finalized, safe)) = finalized.zip(safe) else {
8249 return Ok(());
8250 };
8251 if finalized.number.checked_add(1) == Some(safe.number)
8252 && safe.parent_hash != Some(finalized.hash)
8253 {
8254 return Err(ReactiveError::InvalidChainControl {
8255 message: "adjacent safe head does not descend from finalized head".into(),
8256 });
8257 }
8258 Ok(())
8259}
8260
8261fn collect_diff_addresses(diff: &StateDiff, into: &mut HashSet<Address>) {
8267 into.extend(diff.slots.iter().map(|change| change.address));
8268 into.extend(diff.accounts.iter().map(|change| change.address));
8269 into.extend(diff.purged.iter().map(|purge| purge.address));
8270 into.extend(diff.skipped.iter().map(|skipped| skipped.address));
8271 into.extend(diff.skipped_balances.iter().map(|skipped| skipped.address));
8272 into.extend(diff.skipped_masks.iter().map(|skipped| skipped.address));
8273 into.extend(diff.skipped_accounts.iter().map(|skipped| skipped.address));
8274}
8275
8276fn root_moved_account_resync(
8281 address: Address,
8282 block: u64,
8283 fields: AccountFieldMask,
8284) -> ResyncRequest {
8285 ResyncRequest {
8286 id: ResyncId::new(format!("root-moved:{address:#x}:{block}")),
8287 reason: ResyncReason::RootMoved,
8288 block: ResyncBlock::Number(block),
8289 targets: vec![ResyncTarget::Account { address, fields }],
8290 priority: ResyncPriority::Normal,
8291 }
8292}
8293
8294fn batch_preconfirmation<N: Network>(
8295 batch: &ReactiveInputBatch<N>,
8296) -> Result<Option<FlashblockRef>, ReactiveError> {
8297 let mut flashblock: Option<FlashblockRef> = None;
8298 let mut has_non_preconfirmed = false;
8299 for (index, record) in batch.records().iter().enumerate() {
8300 match &record.context.chain_status {
8301 ChainStatus::Preconfirmed {
8302 flashblock: current,
8303 } => {
8304 if batch.record_delivery_scope(index) != Some(DeliveryScope::Preconfirmed) {
8305 return Err(ReactiveError::InvalidInputRecord {
8306 message: "pre-confirmed input requires pre-confirmed delivery scope".into(),
8307 });
8308 }
8309 if flashblock
8310 .as_ref()
8311 .is_some_and(|known| known != current.as_ref())
8312 {
8313 return Err(ReactiveError::InvalidInputRecord {
8314 message: "one batch cannot mix distinct Flashblock snapshots".into(),
8315 });
8316 }
8317 flashblock.get_or_insert_with(|| current.as_ref().clone());
8318 }
8319 _ => has_non_preconfirmed = true,
8320 }
8321 }
8322 if flashblock.is_some() && (has_non_preconfirmed || !batch.chain_controls().is_empty()) {
8323 return Err(ReactiveError::InvalidInputRecord {
8324 message: "pre-confirmed delivery cannot mix canonical inputs or chain controls".into(),
8325 });
8326 }
8327 Ok(flashblock)
8328}
8329
8330fn canonical_record_block<N: Network>(record: &ReactiveInputRecord<N>) -> Option<&BlockRef> {
8331 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
8332 return None;
8333 }
8334 if is_canonical_status(&record.context.chain_status) {
8335 return context_block_ref(&record.context);
8336 }
8337 None
8338}
8339
8340fn resolve_record_block_payload_metadata<N: Network>(
8341 record: &ReactiveInputRecord<N>,
8342 mut block: BlockRef,
8343) -> Result<BlockRef, ReactiveError> {
8344 let ReactiveInput::Log(log) = &record.input else {
8345 return Ok(block);
8346 };
8347 if log.block_number != Some(block.number) || log.block_hash != Some(block.hash) {
8348 return Err(ReactiveError::InvalidInputRecord {
8349 message: "log payload and canonical context carry different block identities".into(),
8350 });
8351 }
8352 if let Some(timestamp) = log.block_timestamp {
8353 if block.timestamp.is_some_and(|known| known != timestamp) {
8354 return Err(ReactiveError::InvalidInputRecord {
8355 message: "log payload and canonical context carry different block timestamps"
8356 .into(),
8357 });
8358 }
8359 block.timestamp = Some(timestamp);
8360 }
8361 Ok(block)
8362}
8363
8364fn validate_input_record<N: Network>(record: &ReactiveInputRecord<N>) -> Result<(), ReactiveError> {
8365 let invalid = |message: String| ReactiveError::InvalidInputRecord { message };
8366 if let ChainStatus::Preconfirmed { flashblock } = &record.context.chain_status
8367 && record.context.block != Some(flashblock.block_ref())
8368 {
8369 return Err(invalid(
8370 "pre-confirmed status and context carry different partial block identities".into(),
8371 ));
8372 }
8373 let status_block = match &record.context.chain_status {
8374 ChainStatus::Included { block, .. }
8375 | ChainStatus::Safe { block }
8376 | ChainStatus::Finalized { block }
8377 | ChainStatus::Reorged {
8378 dropped_from: block,
8379 } => Some(block),
8380 ChainStatus::Preconfirmed { .. } => record.context.block.as_ref(),
8381 ChainStatus::Pending => None,
8382 };
8383 match (status_block, record.context.block.as_ref()) {
8384 (Some(status), Some(context)) if status == context => {}
8385 (Some(_), Some(_)) => {
8386 return Err(invalid(
8387 "chain status and context carry different block identities".into(),
8388 ));
8389 }
8390 (Some(_), None) => {
8391 return Err(invalid(
8392 "included or reorged input is missing its context block".into(),
8393 ));
8394 }
8395 (None, Some(_)) => {
8396 return Err(invalid(
8397 "pending input cannot carry a canonical context block".into(),
8398 ));
8399 }
8400 (None, None) => {}
8401 }
8402
8403 match &record.input {
8404 ReactiveInput::Log(log) => {
8405 let Some(block) = status_block else {
8406 return Err(invalid(
8407 "log input must carry an included or reorged block identity".into(),
8408 ));
8409 };
8410 if log.removed && !matches!(record.context.chain_status, ChainStatus::Reorged { .. }) {
8411 return Err(invalid(
8412 "removed log must carry reorged chain status".into(),
8413 ));
8414 }
8415 let block_number = log
8416 .block_number
8417 .ok_or_else(|| invalid("log is missing its block number".into()))?;
8418 let block_hash = log
8419 .block_hash
8420 .ok_or_else(|| invalid("log is missing its block hash".into()))?;
8421 log.transaction_hash
8422 .ok_or_else(|| invalid("log is missing its transaction hash".into()))?;
8423 let transaction_index = log
8424 .transaction_index
8425 .ok_or_else(|| invalid("log is missing its transaction index".into()))?;
8426 let log_index = log
8427 .log_index
8428 .ok_or_else(|| invalid("log is missing its log index".into()))?;
8429 if block_number != block.number
8430 || block_hash != block.hash
8431 || !optional_metadata_compatible(
8432 log.block_timestamp.as_ref(),
8433 block.timestamp.as_ref(),
8434 )
8435 {
8436 return Err(invalid(
8437 "log payload and context carry different block identities".into(),
8438 ));
8439 }
8440 if record.context.transaction_index != Some(transaction_index)
8441 || record.context.log_index != Some(log_index)
8442 {
8443 return Err(invalid(
8444 "log payload and context carry different transaction/log positions".into(),
8445 ));
8446 }
8447 }
8448 ReactiveInput::BlockHeader(header) => {
8449 if let Some(block) = status_block {
8450 if header.number() != block.number
8451 || header.hash() != block.hash
8452 || Some(header.parent_hash()) != block.parent_hash
8453 || Some(header.timestamp()) != block.timestamp
8454 {
8455 return Err(invalid(
8456 "block header payload and context carry different block identities".into(),
8457 ));
8458 }
8459 } else if !matches!(record.context.chain_status, ChainStatus::Pending) {
8460 return Err(invalid("block header has an unsupported lifecycle".into()));
8461 }
8462 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8463 return Err(invalid(
8464 "block header context cannot carry transaction/log positions".into(),
8465 ));
8466 }
8467 }
8468 ReactiveInput::FullBlock(block_response) => {
8469 let header = block_response.header();
8470 if let Some(block) = status_block {
8471 if header.number() != block.number
8472 || header.hash() != block.hash
8473 || Some(header.parent_hash()) != block.parent_hash
8474 || Some(header.timestamp()) != block.timestamp
8475 {
8476 return Err(invalid(
8477 "full-block payload and context carry different block identities".into(),
8478 ));
8479 }
8480 } else if !matches!(record.context.chain_status, ChainStatus::Pending) {
8481 return Err(invalid("full block has an unsupported lifecycle".into()));
8482 }
8483 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8484 return Err(invalid(
8485 "full-block context cannot carry transaction/log positions".into(),
8486 ));
8487 }
8488 if let Some(transactions) = block_response.transactions().as_transactions() {
8489 for (index, transaction) in transactions.iter().enumerate() {
8490 if transaction
8491 .block_hash()
8492 .is_some_and(|hash| hash != header.hash())
8493 || transaction
8494 .block_number()
8495 .is_some_and(|number| number != header.number())
8496 || transaction
8497 .transaction_index()
8498 .is_some_and(|position| position != index as u64)
8499 {
8500 return Err(invalid(format!(
8501 "full-block transaction {index} carries contradictory inclusion metadata"
8502 )));
8503 }
8504 if transaction
8505 .chain_id()
8506 .zip(record.context.chain_id)
8507 .is_some_and(|(transaction, context)| transaction != context)
8508 {
8509 return Err(invalid(format!(
8510 "full-block transaction {index} carries a chain id conflicting with its context"
8511 )));
8512 }
8513 }
8514 }
8515 }
8516 ReactiveInput::PendingTxHash(_) => {
8517 if !matches!(record.context.chain_status, ChainStatus::Pending) {
8518 return Err(invalid(
8519 "pending transaction input must carry pending chain status".into(),
8520 ));
8521 }
8522 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8523 return Err(invalid(
8524 "pending transaction context cannot carry canonical positions".into(),
8525 ));
8526 }
8527 }
8528 ReactiveInput::PendingTx(transaction) => {
8529 if !matches!(record.context.chain_status, ChainStatus::Pending) {
8530 return Err(invalid(
8531 "pending transaction input must carry pending chain status".into(),
8532 ));
8533 }
8534 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8535 return Err(invalid(
8536 "pending transaction context cannot carry canonical positions".into(),
8537 ));
8538 }
8539 if transaction.block_hash().is_some()
8540 || transaction.block_number().is_some()
8541 || transaction.transaction_index().is_some()
8542 {
8543 return Err(invalid(
8544 "hydrated pending transaction cannot carry inclusion metadata".into(),
8545 ));
8546 }
8547 if transaction
8548 .chain_id()
8549 .zip(record.context.chain_id)
8550 .is_some_and(|(transaction, context)| transaction != context)
8551 {
8552 return Err(invalid(
8553 "pending transaction carries a chain id conflicting with its context".into(),
8554 ));
8555 }
8556 }
8557 }
8558 Ok(())
8559}
8560
8561fn advance_block_for_canonical_record<N: Network>(
8570 cache: &mut EvmCache,
8571 record: &ReactiveInputRecord<N>,
8572) -> Option<Result<(), BlockContextError>> {
8573 if !is_canonical_status(&record.context.chain_status) {
8574 return None;
8575 }
8576 match &record.input {
8577 ReactiveInput::BlockHeader(header) => Some(cache.advance_block(header)),
8578 ReactiveInput::FullBlock(block) => Some(cache.advance_block(block.header())),
8579 _ => None,
8580 }
8581}
8582
8583fn context_block_ref(ctx: &ReactiveContext) -> Option<&BlockRef> {
8584 match &ctx.chain_status {
8585 ChainStatus::Included { block, .. }
8586 | ChainStatus::Safe { block }
8587 | ChainStatus::Finalized { block } => Some(block),
8588 ChainStatus::Reorged { dropped_from } => Some(dropped_from),
8589 ChainStatus::Preconfirmed { .. } => ctx.block.as_ref(),
8590 ChainStatus::Pending => ctx.block.as_ref(),
8591 }
8592}
8593
8594fn reorg_signal_block<N: Network>(
8595 record: &ReactiveInputRecord<N>,
8596) -> Option<(BlockRef, ReorgReason)> {
8597 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
8598 return block_ref_from_record(record).map(|block| (block, ReorgReason::RemovedLog));
8599 }
8600
8601 if let ChainStatus::Reorged { dropped_from } = &record.context.chain_status {
8602 return Some((*dropped_from, ReorgReason::ReorgedInput));
8603 }
8604
8605 None
8606}
8607
8608fn block_ref_from_record<N: Network>(record: &ReactiveInputRecord<N>) -> Option<BlockRef> {
8609 context_block_ref(&record.context)
8610 .cloned()
8611 .or_else(|| match &record.input {
8612 ReactiveInput::Log(log) => Some(BlockRef {
8613 number: log.block_number?,
8614 hash: log.block_hash?,
8615 parent_hash: None,
8616 timestamp: log.block_timestamp,
8617 }),
8618 ReactiveInput::BlockHeader(header) => Some(BlockRef {
8619 number: header.number(),
8620 hash: header.hash(),
8621 parent_hash: Some(header.parent_hash()),
8622 timestamp: Some(header.timestamp()),
8623 }),
8624 ReactiveInput::FullBlock(block) => {
8625 let header = block.header();
8626 Some(BlockRef {
8627 number: header.number(),
8628 hash: header.hash(),
8629 parent_hash: Some(header.parent_hash()),
8630 timestamp: Some(header.timestamp()),
8631 })
8632 }
8633 ReactiveInput::PendingTxHash(_) | ReactiveInput::PendingTx(_) => None,
8634 })
8635}
8636
8637fn remove_canceled_resyncs_from_batch(
8638 resyncs: &mut Vec<ResyncRequest>,
8639 canceled: &[ResyncRequest],
8640) {
8641 if canceled.is_empty() {
8642 return;
8643 }
8644 let canceled_ids: HashSet<_> = canceled.iter().map(|request| request.id.clone()).collect();
8645 resyncs.retain(|request| !canceled_ids.contains(&request.id));
8646}
8647
8648fn resync_target_address(target: &ResyncTarget) -> Address {
8649 match target {
8650 ResyncTarget::StorageSlot { address, .. }
8651 | ResyncTarget::StorageSlots { address, .. }
8652 | ResyncTarget::Account { address, .. } => *address,
8653 }
8654}
8655
8656fn resync_request_targets_dropped_block(
8657 request: &ResyncRequest,
8658 dropped_blocks: &[BlockRef],
8659) -> bool {
8660 let ResyncBlock::Hash { number, hash, .. } = &request.block else {
8661 return false;
8662 };
8663 dropped_blocks
8664 .iter()
8665 .any(|block| block.hash == *hash && block.number == *number)
8666}
8667
8668fn single_hash_pinned_resync_block(report: &ResyncReport) -> Option<BlockRef> {
8669 let first = report.requested.first()?.block.clone();
8670 if !report
8671 .requested
8672 .iter()
8673 .all(|request| request.block == first)
8674 {
8675 return None;
8676 }
8677
8678 let ResyncBlock::Hash { number, hash, .. } = first else {
8679 return None;
8680 };
8681
8682 Some(BlockRef {
8683 number,
8684 hash,
8685 parent_hash: None,
8686 timestamp: None,
8687 })
8688}
8689
8690fn purge_scopes_for_dropped_journals<N: Network>(
8691 dropped: &[BlockJournal<N>],
8692) -> Vec<(Address, PurgeScope)> {
8693 let mut scopes: Vec<(Address, PurgeScope)> = Vec::new();
8694 for entry in dropped.iter().rev() {
8695 for diff in entry.rollback_diffs.iter().rev() {
8696 merge_purge_scopes_for_diff(&mut scopes, diff);
8697 }
8698 }
8699 scopes
8700}
8701
8702fn rollback_updates_for_dropped_journals<N: Network>(
8703 dropped: &[BlockJournal<N>],
8704 purge_scopes: &[(Address, PurgeScope)],
8705) -> Vec<StateUpdate> {
8706 let purge_addresses: HashSet<_> = purge_scopes
8707 .iter()
8708 .map(|(address, _scope)| *address)
8709 .collect();
8710 let mut updates = Vec::new();
8711 for entry in dropped.iter().rev() {
8712 for diff in entry.rollback_diffs.iter().rev() {
8713 push_rollback_updates_for_diff(&mut updates, diff, &purge_addresses);
8714 }
8715 }
8716 updates
8717}
8718
8719fn merge_purge_scopes_for_diff(scopes: &mut Vec<(Address, PurgeScope)>, diff: &StateDiff) {
8720 for change in &diff.accounts {
8721 merge_purge_scope(scopes, change.address, PurgeScope::Account);
8722 }
8723 for record in &diff.purged {
8724 merge_purge_scope(scopes, record.address, record.scope.clone());
8725 }
8726}
8727
8728fn push_rollback_updates_for_diff(
8729 updates: &mut Vec<StateUpdate>,
8730 diff: &StateDiff,
8731 purge_addresses: &HashSet<Address>,
8732) {
8733 for change in diff.slots.iter().rev() {
8734 if purge_addresses.contains(&change.address) {
8735 continue;
8736 }
8737 updates.push(StateUpdate::slot(change.address, change.slot, change.old));
8738 }
8739}
8740
8741fn merge_purge_scope(scopes: &mut Vec<(Address, PurgeScope)>, address: Address, scope: PurgeScope) {
8742 if let Some((_existing_address, existing_scope)) = scopes
8743 .iter_mut()
8744 .find(|(existing_address, _scope)| *existing_address == address)
8745 {
8746 *existing_scope = merged_purge_scope(existing_scope.clone(), scope);
8747 } else {
8748 scopes.push((address, scope));
8749 }
8750}
8751
8752fn merged_purge_scope(left: PurgeScope, right: PurgeScope) -> PurgeScope {
8753 match (left, right) {
8754 (PurgeScope::Account, _) | (_, PurgeScope::Account) => PurgeScope::Account,
8755 (PurgeScope::AllStorage, _) | (_, PurgeScope::AllStorage) => PurgeScope::AllStorage,
8756 (PurgeScope::Slots(mut left), PurgeScope::Slots(right)) => {
8757 for slot in right {
8758 if !left.contains(&slot) {
8759 left.push(slot);
8760 }
8761 }
8762 PurgeScope::Slots(left)
8763 }
8764 }
8765}
8766
8767#[derive(Clone, Debug)]
8768struct StorageFetchSlot {
8769 address: Address,
8770 slot: U256,
8771 origins: Vec<StorageFetchOrigin>,
8772}
8773
8774#[derive(Clone, Debug)]
8775struct StorageFetchOrigin {
8776 request_id: ResyncId,
8777 target: ResyncTarget,
8778}
8779
8780#[derive(Clone, Debug)]
8781struct StorageFetchGroup {
8782 block: ResyncBlock,
8783 slots: Vec<StorageFetchSlot>,
8784 seen: HashSet<(Address, U256)>,
8785}
8786
8787#[derive(Clone, Debug)]
8790struct AccountResyncTarget {
8791 request_id: ResyncId,
8792 block: ResyncBlock,
8793 address: Address,
8794 fields: AccountFieldMask,
8795}
8796
8797fn resolve_trace_resyncs(
8798 cache: &EvmCache,
8799 storage_groups: &mut Vec<StorageFetchGroup>,
8800 account_targets: &mut Vec<AccountResyncTarget>,
8801 state_updates: &mut Vec<StateUpdate>,
8802) {
8803 let Some(fetcher) = cache.block_state_diff_fetcher().cloned() else {
8804 return;
8805 };
8806
8807 let mut blocks = Vec::new();
8808 let mut seen = HashSet::new();
8809 for block in storage_groups
8810 .iter()
8811 .map(|group| group.block.clone())
8812 .chain(account_targets.iter().map(|target| target.block.clone()))
8813 {
8814 if seen.insert(block.clone()) {
8815 blocks.push(block);
8816 }
8817 }
8818
8819 let mut traces = HashMap::new();
8820 for block in blocks {
8821 match (fetcher)(resync_block_to_block_id(&block)) {
8822 Ok(diff) => {
8823 traces.insert(block, diff);
8824 }
8825 Err(error) => {
8826 tracing::debug!(
8827 block = ?block,
8828 error = %error,
8829 "block trace resync source failed; falling back to point resync"
8830 );
8831 }
8832 }
8833 }
8834
8835 for group in storage_groups.iter_mut() {
8836 let Some(trace) = traces.get(&group.block) else {
8837 continue;
8838 };
8839 group.slots.retain(|slot| {
8840 if let Some(value) = trace_storage_value(trace, slot.address, slot.slot) {
8841 state_updates.push(StateUpdate::slot(slot.address, slot.slot, value));
8842 return false;
8843 }
8844 cache
8845 .cached_storage_value(slot.address, slot.slot)
8846 .is_none()
8847 });
8848 group.seen = group
8849 .slots
8850 .iter()
8851 .map(|slot| (slot.address, slot.slot))
8852 .collect();
8853 }
8854 storage_groups.retain(|group| !group.slots.is_empty());
8855
8856 let mut unresolved_accounts = Vec::new();
8857 for mut account in account_targets.drain(..) {
8858 let Some(trace) = traces.get(&account.block) else {
8859 unresolved_accounts.push(account);
8860 continue;
8861 };
8862 let Some(trace_account) = trace
8863 .accounts
8864 .iter()
8865 .find(|diff| diff.address == account.address)
8866 else {
8867 unresolved_accounts.push(account);
8868 continue;
8869 };
8870
8871 let mut patch = AccountPatch::default();
8872 let mut unresolved = AccountFieldMask::default();
8873 if account.fields.balance {
8874 if let Some(balance) = trace_account.balance {
8875 patch = patch.balance(balance);
8876 } else {
8877 unresolved.balance = true;
8878 }
8879 }
8880 if account.fields.nonce {
8881 if let Some(nonce) = trace_account.nonce {
8882 patch = patch.nonce(nonce);
8883 } else {
8884 unresolved.nonce = true;
8885 }
8886 }
8887 if account.fields.code {
8888 if let Some(code) = &trace_account.code {
8889 patch = patch.code(code.clone());
8890 } else {
8891 unresolved.code = true;
8892 }
8893 }
8894
8895 if patch.balance.is_some() || patch.nonce.is_some() || patch.code.is_some() {
8896 state_updates.push(StateUpdate::account_upsert(account.address, patch));
8897 }
8898 if !account_field_mask_empty(unresolved) {
8899 account.fields = unresolved;
8900 unresolved_accounts.push(account);
8901 }
8902 }
8903 *account_targets = unresolved_accounts;
8904}
8905
8906fn trace_storage_value(trace: &BlockStateDiff, address: Address, slot: U256) -> Option<U256> {
8907 trace
8908 .accounts
8909 .iter()
8910 .find(|account| account.address == address)
8911 .and_then(|account| {
8912 account
8913 .storage
8914 .iter()
8915 .find(|entry| entry.slot == slot)
8916 .map(|entry| entry.value)
8917 })
8918}
8919
8920fn account_field_mask_empty(mask: AccountFieldMask) -> bool {
8921 !mask.balance && !mask.nonce && !mask.code
8922}
8923
8924fn execute_resync_requests(cache: &mut EvmCache, requests: &[ResyncRequest]) -> ResyncReport {
8925 let mut failed = Vec::new();
8926 let mut storage_groups: Vec<StorageFetchGroup> = Vec::new();
8927 let mut account_targets: Vec<AccountResyncTarget> = Vec::new();
8928
8929 for request in requests {
8930 for target in &request.targets {
8931 match target {
8932 ResyncTarget::StorageSlot { address, slot } => {
8933 push_storage_resync_slot(
8934 &mut storage_groups,
8935 &request.id,
8936 &request.block,
8937 *address,
8938 *slot,
8939 );
8940 }
8941 ResyncTarget::StorageSlots { address, slots } => {
8942 for slot in slots {
8943 push_storage_resync_slot(
8944 &mut storage_groups,
8945 &request.id,
8946 &request.block,
8947 *address,
8948 *slot,
8949 );
8950 }
8951 }
8952 ResyncTarget::Account { address, fields } => {
8953 account_targets.push(AccountResyncTarget {
8954 request_id: request.id.clone(),
8955 block: request.block.clone(),
8956 address: *address,
8957 fields: *fields,
8958 });
8959 }
8960 }
8961 }
8962 }
8963
8964 let mut state_updates = Vec::new();
8965 resolve_trace_resyncs(
8966 cache,
8967 &mut storage_groups,
8968 &mut account_targets,
8969 &mut state_updates,
8970 );
8971
8972 if !storage_groups.is_empty() {
8973 if let Some(fetcher) = cache.storage_batch_fetcher().cloned() {
8974 for group in storage_groups {
8975 let block = group.block.clone();
8976 let fetches: Vec<(Address, U256)> = group
8977 .slots
8978 .iter()
8979 .map(|slot| (slot.address, slot.slot))
8980 .collect();
8981 let results = (fetcher)(fetches, resync_block_to_block_id(&block));
8982 let mut pending: HashMap<(Address, U256), StorageFetchSlot> = group
8983 .slots
8984 .iter()
8985 .cloned()
8986 .map(|slot| ((slot.address, slot.slot), slot))
8987 .collect();
8988
8989 for (address, slot, fetched) in results {
8990 let Some(requested_slot) = pending.remove(&(address, slot)) else {
8991 continue;
8992 };
8993 match fetched {
8994 Ok(value) => state_updates.push(StateUpdate::slot(address, slot, value)),
8995 Err(error) => {
8996 let message = error.to_string();
8997 push_resync_failures(
8998 &mut failed,
8999 &block,
9000 requested_slot.origins,
9001 ResyncFailureKind::StorageFetchFailed,
9002 message,
9003 );
9004 }
9005 }
9006 }
9007
9008 for requested_slot in group.slots {
9009 if pending
9010 .remove(&(requested_slot.address, requested_slot.slot))
9011 .is_some()
9012 {
9013 push_resync_failures(
9014 &mut failed,
9015 &block,
9016 requested_slot.origins,
9017 ResyncFailureKind::StorageFetchOmitted,
9018 "storage batch fetcher did not return a value for slot".to_string(),
9019 );
9020 }
9021 }
9022 }
9023 } else {
9024 for group in storage_groups {
9025 let block = group.block.clone();
9026 for slot in group.slots {
9027 push_resync_failures(
9028 &mut failed,
9029 &block,
9030 slot.origins,
9031 ResyncFailureKind::MissingStorageFetcher,
9032 "storage resync requires a storage batch fetcher".to_string(),
9033 );
9034 }
9035 }
9036 }
9037 }
9038
9039 if !account_targets.is_empty() {
9040 if let Some(fetcher) = cache.account_proof_fetcher().cloned() {
9041 let mut groups: Vec<(BlockId, Vec<_>)> = Vec::new();
9047 for account in account_targets {
9048 let block_id = resync_block_to_block_id(&account.block);
9049 match groups
9050 .iter_mut()
9051 .find(|(group_block, _)| *group_block == block_id)
9052 {
9053 Some((_, group)) => group.push(account),
9054 None => groups.push((block_id, vec![account])),
9055 }
9056 }
9057 for (block_id, group) in groups {
9058 let probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
9059 group
9060 .iter()
9061 .map(|account| (account.address, vec![]))
9062 .collect(),
9063 block_id,
9064 )
9065 .into_iter()
9066 .collect();
9067 for account in group {
9068 match probes.get(&account.address).cloned() {
9072 Some(Ok(proof)) => {
9073 let mut patch = AccountPatch::default();
9078 if account.fields.balance {
9079 patch = patch.balance(proof.balance);
9080 }
9081 if account.fields.nonce {
9082 patch = patch.nonce(proof.nonce);
9083 }
9084 state_updates.push(StateUpdate::account_upsert(account.address, patch));
9090 }
9091 Some(Err(error)) => {
9092 failed.push(ResyncFailure {
9093 request_id: account.request_id,
9094 block: account.block,
9095 target: ResyncTarget::Account {
9096 address: account.address,
9097 fields: account.fields,
9098 },
9099 kind: ResyncFailureKind::AccountFetchFailed,
9100 message: error.to_string(),
9101 });
9102 }
9103 None => {
9104 failed.push(ResyncFailure {
9105 request_id: account.request_id,
9106 block: account.block,
9107 target: ResyncTarget::Account {
9108 address: account.address,
9109 fields: account.fields,
9110 },
9111 kind: ResyncFailureKind::AccountFetchOmitted,
9112 message:
9113 "account proof fetcher did not return a result for address"
9114 .to_string(),
9115 });
9116 }
9117 }
9118 }
9119 }
9120 } else {
9121 for account in account_targets {
9122 failed.push(ResyncFailure {
9123 request_id: account.request_id,
9124 block: account.block,
9125 target: ResyncTarget::Account {
9126 address: account.address,
9127 fields: account.fields,
9128 },
9129 kind: ResyncFailureKind::MissingAccountFetcher,
9130 message: "account resync requires an account proof fetcher".to_string(),
9131 });
9132 }
9133 }
9134 }
9135
9136 let diff = if state_updates.is_empty() {
9137 StateDiff::default()
9138 } else {
9139 cache.apply_updates(&state_updates)
9140 };
9141
9142 ResyncReport {
9143 requested: requests.to_vec(),
9144 state_updates,
9145 diff,
9146 failed,
9147 }
9148}
9149
9150fn push_resync_failures(
9151 failed: &mut Vec<ResyncFailure>,
9152 block: &ResyncBlock,
9153 origins: Vec<StorageFetchOrigin>,
9154 kind: ResyncFailureKind,
9155 message: String,
9156) {
9157 for origin in origins {
9158 failed.push(ResyncFailure {
9159 request_id: origin.request_id,
9160 block: block.clone(),
9161 target: origin.target,
9162 kind,
9163 message: message.clone(),
9164 });
9165 }
9166}
9167
9168fn push_storage_resync_slot(
9169 groups: &mut Vec<StorageFetchGroup>,
9170 request_id: &ResyncId,
9171 block: &ResyncBlock,
9172 address: Address,
9173 slot: U256,
9174) {
9175 let group_index = if let Some(index) = groups.iter().position(|group| group.block == *block) {
9176 index
9177 } else {
9178 groups.push(StorageFetchGroup {
9179 block: block.clone(),
9180 slots: Vec::new(),
9181 seen: HashSet::new(),
9182 });
9183 groups.len() - 1
9184 };
9185
9186 let group = &mut groups[group_index];
9187 let origin = StorageFetchOrigin {
9188 request_id: request_id.clone(),
9189 target: ResyncTarget::StorageSlot { address, slot },
9190 };
9191 if group.seen.insert((address, slot)) {
9192 group.slots.push(StorageFetchSlot {
9193 address,
9194 slot,
9195 origins: vec![origin],
9196 });
9197 } else if let Some(existing) = group
9198 .slots
9199 .iter_mut()
9200 .find(|existing| existing.address == address && existing.slot == slot)
9201 {
9202 existing.origins.push(origin);
9203 }
9204}
9205
9206fn resync_block_to_block_id(block: &ResyncBlock) -> BlockId {
9207 match block {
9208 ResyncBlock::Latest => BlockId::latest(),
9209 ResyncBlock::Pending => BlockId::pending(),
9210 ResyncBlock::Safe => BlockId::safe(),
9211 ResyncBlock::Finalized => BlockId::finalized(),
9212 ResyncBlock::Number(number) => BlockId::number(*number),
9213 ResyncBlock::Hash {
9214 number: _,
9215 hash,
9216 require_canonical,
9217 } => BlockId::from((*hash, Some(*require_canonical))),
9218 }
9219}
9220
9221impl<N: Network> RegisteredHandler<N> {
9222 fn matches(&self, input: &ReactiveInput<N>) -> bool {
9223 self.interests
9224 .iter()
9225 .any(|interest| interest_matches(interest, input))
9226 }
9227
9228 fn route_log(&self, log: &Log) -> Option<ReactiveLogRoute> {
9229 self.interests.iter().find_map(|interest| match interest {
9230 ReactiveInterest::Logs(interest) if interest.matches(log) => Some(ReactiveLogRoute {
9231 handler_id: self.id.clone(),
9232 route_key: interest.route_key(log),
9233 }),
9234 ReactiveInterest::Logs(_)
9235 | ReactiveInterest::Blocks(_)
9236 | ReactiveInterest::PendingTransactions(_) => None,
9237 })
9238 }
9239}
9240
9241fn merge_log_subscription_filter(filters: &mut Vec<Filter>, next: &Filter) {
9242 let mut candidate = next.clone();
9243 let mut insertion_index = filters.len();
9244 let mut index = 0;
9245 while index < filters.len() {
9246 if filters[index].block_option != candidate.block_option {
9247 index += 1;
9248 continue;
9249 }
9250 if let Some(merged) = exact_filter_union(&candidate, &filters[index]) {
9251 candidate = merged;
9252 insertion_index = insertion_index.min(index);
9253 filters.remove(index);
9254 index = 0;
9255 } else {
9256 index += 1;
9257 }
9258 }
9259 filters.insert(insertion_index.min(filters.len()), candidate);
9260}
9261
9262fn exact_filter_union(left: &Filter, right: &Filter) -> Option<Filter> {
9263 if filter_subsumes(left, right) {
9264 return Some(left.clone());
9265 }
9266 if filter_subsumes(right, left) {
9267 return Some(right.clone());
9268 }
9269 let differing_dimensions = usize::from(left.address != right.address)
9270 + left
9271 .topics
9272 .iter()
9273 .zip(right.topics.iter())
9274 .filter(|(left, right)| left != right)
9275 .count();
9276 if differing_dimensions != 1 {
9277 return None;
9278 }
9279
9280 let mut merged = left.clone();
9281 if merged.address != right.address {
9282 merge_filter_set(&mut merged.address, &right.address);
9283 } else {
9284 for (merged_topic, right_topic) in merged.topics.iter_mut().zip(right.topics.iter()) {
9285 if merged_topic != right_topic {
9286 merge_filter_set(merged_topic, right_topic);
9287 break;
9288 }
9289 }
9290 }
9291 Some(merged)
9292}
9293
9294fn filter_subsumes(left: &Filter, right: &Filter) -> bool {
9295 filter_set_subsumes(&left.address, &right.address)
9296 && left
9297 .topics
9298 .iter()
9299 .zip(right.topics.iter())
9300 .all(|(left, right)| filter_set_subsumes(left, right))
9301}
9302
9303fn filter_set_subsumes<T: Eq + Hash>(left: &FilterSet<T>, right: &FilterSet<T>) -> bool {
9304 left.is_empty()
9305 || (!right.is_empty()
9306 && right
9307 .iter()
9308 .all(|value| left.iter().any(|known| known == value)))
9309}
9310
9311fn merge_filter_set<T: Clone + Eq + Hash>(target: &mut FilterSet<T>, source: &FilterSet<T>) {
9312 if target.is_empty() {
9313 return;
9314 }
9315 if source.is_empty() {
9316 *target = FilterSet::default();
9317 return;
9318 }
9319 for value in source.iter() {
9320 target.insert(value.clone());
9321 }
9322}
9323
9324#[derive(Clone, Debug)]
9325struct HandlerExecution {
9326 handler_id: HandlerId,
9327 quality: StateEffectQuality,
9328 tags: Vec<ReportTag>,
9329 state_updates: Vec<StateUpdate>,
9330 invalidations: Vec<InvalidationRequest>,
9331 resyncs: Vec<ResyncRequest>,
9332 speculative: Vec<SpeculativeRequest>,
9333 hook_signals: Vec<HookSignal>,
9334}
9335
9336impl HandlerExecution {
9337 fn from_outcome(
9338 handler_id: HandlerId,
9339 input_ref: InputRef,
9340 outcome: HandlerOutcome,
9341 preconfirmed: bool,
9342 ) -> Self {
9343 let mut state_updates = Vec::new();
9344 let mut invalidations = Vec::new();
9345 let mut resyncs = Vec::new();
9346 let mut speculative = Vec::new();
9347 let mut hook_signals = Vec::new();
9348
9349 for effect in outcome.effects {
9350 match effect {
9351 ReactiveEffect::StateUpdate(update) => state_updates.push(update),
9352 ReactiveEffect::Invalidate(invalidation) => {
9353 state_updates.push(StateUpdate::purge(
9354 invalidation.address,
9355 invalidation.scope.clone(),
9356 ));
9357 invalidations.push(invalidation);
9358 }
9359 ReactiveEffect::Resync(mut request) => {
9360 if preconfirmed {
9361 request.block = ResyncBlock::Pending;
9362 }
9363 resyncs.push(request);
9364 }
9365 ReactiveEffect::Hook(signal) => hook_signals.push(signal),
9366 ReactiveEffect::Speculative(mut request) => {
9367 request.input_ref = input_ref;
9368 speculative.push(request);
9369 }
9370 }
9371 }
9372
9373 Self {
9374 handler_id,
9375 quality: outcome.quality,
9376 tags: outcome.tags,
9377 state_updates,
9378 invalidations,
9379 resyncs,
9380 speculative,
9381 hook_signals,
9382 }
9383 }
9384}
9385
9386fn dedupe_records<N: Network>(
9387 records: Vec<ReactiveInputRecord<N>>,
9388) -> Result<Vec<ReactiveInputRecord<N>>, ReactiveError> {
9389 let mut positions = HashMap::<ReactiveInputIdentity, usize>::new();
9390 let mut deduped = Vec::with_capacity(records.len());
9391 for record in records {
9392 let identity = record.validated_identity()?;
9393 if !record.is_payload_deduplicable() {
9394 deduped.push(record);
9395 continue;
9396 }
9397 if let Some(index) = positions.get(&identity).copied() {
9398 let merged = deduped[index].merge_compatible_duplicate(&record)?;
9399 debug_assert!(merged, "same indexed identity is deduplicable");
9400 } else {
9401 positions.insert(identity, deduped.len());
9402 deduped.push(record);
9403 }
9404 }
9405 Ok(deduped)
9406}
9407
9408fn dedupe_scoped_records<N: Network>(
9409 records: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
9410) -> Result<Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>, ReactiveError> {
9411 let mut positions: HashMap<ReactiveInputIdentity, usize> = HashMap::new();
9412 let mut deduped: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)> =
9413 Vec::with_capacity(records.len());
9414 for (record, audience, delivery_scope) in records {
9415 let identity = record.validated_identity()?;
9416 if !record.is_payload_deduplicable() {
9417 deduped.push((record, audience, delivery_scope));
9418 continue;
9419 }
9420 if let Some(index) = positions.get(&identity).copied() {
9421 let merged = deduped[index].0.merge_compatible_duplicate(&record)?;
9422 debug_assert!(merged, "same indexed identity is deduplicable");
9423 merge_delivery_audience(&mut deduped[index].1, audience);
9424 merge_delivery_scope(&mut deduped[index].2, delivery_scope);
9425 } else {
9426 positions.insert(identity, deduped.len());
9427 deduped.push((record, audience, delivery_scope));
9428 }
9429 }
9430 Ok(deduped)
9431}
9432
9433fn merge_delivery_scope(into: &mut DeliveryScope, incoming: DeliveryScope) {
9434 *into = match (*into, incoming) {
9435 (DeliveryScope::Canonical, _) | (_, DeliveryScope::Canonical) => DeliveryScope::Canonical,
9436 (DeliveryScope::CanonicalProgress, _) | (_, DeliveryScope::CanonicalProgress) => {
9437 DeliveryScope::CanonicalProgress
9438 }
9439 (DeliveryScope::Preconfirmed, DeliveryScope::Preconfirmed)
9440 | (DeliveryScope::Preconfirmed, DeliveryScope::OwnerCatchup)
9441 | (DeliveryScope::OwnerCatchup, DeliveryScope::Preconfirmed) => DeliveryScope::Preconfirmed,
9442 (DeliveryScope::OwnerCatchup, DeliveryScope::OwnerCatchup) => DeliveryScope::OwnerCatchup,
9443 };
9444}
9445
9446fn merge_delivery_audience(into: &mut DeliveryAudience, incoming: DeliveryAudience) {
9447 match (&mut *into, incoming) {
9448 (DeliveryAudience::All, _) => {}
9449 (current, DeliveryAudience::All) => *current = DeliveryAudience::All,
9450 (DeliveryAudience::Owners(current), DeliveryAudience::Owners(incoming)) => {
9451 for owner in incoming {
9452 if !current.contains(&owner) {
9453 current.push(owner);
9454 }
9455 }
9456 }
9457 (DeliveryAudience::AllExcept(current), DeliveryAudience::AllExcept(incoming)) => {
9458 current.retain(|owner| incoming.contains(owner));
9459 }
9460 (DeliveryAudience::AllExcept(excluded), DeliveryAudience::Owners(included)) => {
9461 excluded.retain(|owner| !included.contains(owner));
9462 }
9463 (current @ DeliveryAudience::Owners(_), DeliveryAudience::AllExcept(mut excluded)) => {
9464 let DeliveryAudience::Owners(included) = current else {
9465 unreachable!("match arm restricts the audience variant")
9466 };
9467 excluded.retain(|owner| !included.contains(owner));
9468 *current = DeliveryAudience::AllExcept(excluded);
9469 }
9470 }
9471}
9472
9473fn sort_records<N: Network>(records: Vec<ReactiveInputRecord<N>>) -> Vec<ReactiveInputRecord<N>> {
9474 let mut indexed: Vec<(usize, ReactiveInputRecord<N>)> =
9475 records.into_iter().enumerate().collect();
9476 indexed.sort_by_key(|(index, record)| record_sort_key(*index, record));
9477 indexed.into_iter().map(|(_, record)| record).collect()
9478}
9479
9480fn sort_scoped_records<N: Network>(
9481 records: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
9482) -> Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)> {
9483 let mut indexed: Vec<_> = records.into_iter().enumerate().collect();
9484 indexed.sort_by_key(|(index, (record, _, _))| record_sort_key(*index, record));
9485 indexed
9486 .into_iter()
9487 .map(|(_, scoped_record)| scoped_record)
9488 .collect()
9489}
9490
9491fn record_sort_key<N: Network>(index: usize, record: &ReactiveInputRecord<N>) -> RecordSortKey {
9492 if let Some((block, _)) = reorg_signal_block(record) {
9493 return RecordSortKey {
9494 class: 0,
9495 block_number: block.number,
9496 record_class: 0,
9497 transaction_index: record.context.transaction_index.unwrap_or(u64::MAX),
9498 log_index: record.context.log_index.unwrap_or(u64::MAX),
9499 original_index: index,
9500 };
9501 }
9502 if is_canonical_status(&record.context.chain_status)
9503 && let Some(block) = record.context.block.as_ref()
9504 {
9505 let (record_class, transaction_index, log_index) = match &record.input {
9506 ReactiveInput::BlockHeader(_) | ReactiveInput::FullBlock(_) => (0, 0, 0),
9507 ReactiveInput::Log(log) if !log.removed => (
9508 1,
9509 log.transaction_index
9510 .or(record.context.transaction_index)
9511 .unwrap_or(u64::MAX),
9512 log.log_index
9513 .or(record.context.log_index)
9514 .unwrap_or(u64::MAX),
9515 ),
9516 ReactiveInput::Log(_)
9517 | ReactiveInput::PendingTxHash(_)
9518 | ReactiveInput::PendingTx(_) => (2, u64::MAX, u64::MAX),
9519 };
9520 return RecordSortKey {
9521 class: 1,
9522 block_number: block.number,
9523 record_class,
9524 transaction_index,
9525 log_index,
9526 original_index: index,
9527 };
9528 }
9529
9530 RecordSortKey {
9531 class: 2,
9532 block_number: 0,
9533 record_class: 0,
9534 transaction_index: 0,
9535 log_index: 0,
9536 original_index: index,
9537 }
9538}
9539
9540#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
9541struct RecordSortKey {
9542 class: u8,
9543 block_number: u64,
9544 record_class: u8,
9545 transaction_index: u64,
9546 log_index: u64,
9547 original_index: usize,
9548}
9549
9550fn interest_matches<N: Network>(interest: &ReactiveInterest<N>, input: &ReactiveInput<N>) -> bool {
9551 match (interest, input) {
9552 (ReactiveInterest::Logs(interest), ReactiveInput::Log(log)) => interest.matches(log),
9553 (
9554 ReactiveInterest::Blocks(BlockInterest {
9555 mode: BlockInterestMode::Header,
9556 }),
9557 ReactiveInput::BlockHeader(_),
9558 ) => true,
9559 (
9560 ReactiveInterest::Blocks(BlockInterest {
9561 mode: BlockInterestMode::FullBlock,
9562 }),
9563 ReactiveInput::FullBlock(_),
9564 ) => true,
9565 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTxHash(_)) => {
9566 interest.matches_hash_only()
9567 }
9568 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTx(tx)) => {
9569 interest.matches_tx(tx)
9570 }
9571 _ => false,
9572 }
9573}
9574
9575fn validate_effects(
9576 input_ref: InputRef,
9577 ctx: &ReactiveContext,
9578 handler_id: &HandlerId,
9579 effects: &[ReactiveEffect],
9580) -> Result<(), ReactiveError> {
9581 let pending = matches!(ctx.chain_status, ChainStatus::Pending)
9582 || matches!(input_ref, InputRef::PendingTx { .. });
9583 if !pending {
9584 return Ok(());
9585 }
9586
9587 for effect in effects {
9588 let effect_kind = match effect {
9589 ReactiveEffect::StateUpdate(_) => Some("state_update"),
9590 ReactiveEffect::Invalidate(_) => Some("invalidate"),
9591 ReactiveEffect::Resync(_) => Some("resync"),
9592 ReactiveEffect::Hook(_) | ReactiveEffect::Speculative(_) => None,
9593 };
9594 if let Some(effect_kind) = effect_kind {
9595 return Err(ReactiveError::InvalidPendingEffect {
9596 input_ref: Box::new(input_ref),
9597 handler_id: handler_id.clone(),
9598 effect_kind,
9599 });
9600 }
9601 }
9602 Ok(())
9603}
9604
9605fn detect_conflicts(
9606 input_ref: InputRef,
9607 executions: &[HandlerExecution],
9608) -> Result<(), ReactiveError> {
9609 let mut writes: HashMap<EffectTarget, (AbsoluteValue, HandlerId)> = HashMap::new();
9610 for execution in executions {
9611 for update in &execution.state_updates {
9612 for (target, value) in absolute_writes(update) {
9613 if let Some((previous_value, previous_handler)) = writes.get(&target) {
9614 if previous_value != &value {
9615 return Err(ReactiveError::ConflictingEffects {
9616 input_ref: Box::new(input_ref),
9617 target: Box::new(target),
9618 first: previous_handler.clone(),
9619 second: execution.handler_id.clone(),
9620 });
9621 }
9622 } else {
9623 writes.insert(target, (value, execution.handler_id.clone()));
9624 }
9625 }
9626 }
9627 }
9628 Ok(())
9629}
9630
9631fn absolute_writes(update: &StateUpdate) -> Vec<(EffectTarget, AbsoluteValue)> {
9632 match update {
9633 StateUpdate::Slot {
9634 address,
9635 slot,
9636 value,
9637 } => vec![(
9638 EffectTarget::StorageSlot {
9639 address: *address,
9640 slot: *slot,
9641 },
9642 AbsoluteValue::U256(*value),
9643 )],
9644 StateUpdate::SlotMasked {
9645 address,
9646 slot,
9647 mask,
9648 value,
9649 } => vec![(
9650 EffectTarget::MaskedStorageSlot {
9651 address: *address,
9652 slot: *slot,
9653 mask: *mask,
9654 },
9655 AbsoluteValue::U256(*value),
9656 )],
9657 StateUpdate::Account { address, patch } | StateUpdate::AccountUpsert { address, patch } => {
9658 account_patch_writes(*address, patch)
9659 }
9660 StateUpdate::SlotDelta { .. }
9661 | StateUpdate::BalanceDelta { .. }
9662 | StateUpdate::Purge { .. } => Vec::new(),
9663 }
9664}
9665
9666fn account_patch_writes(
9667 address: Address,
9668 patch: &AccountPatch,
9669) -> Vec<(EffectTarget, AbsoluteValue)> {
9670 let mut writes = Vec::new();
9671 if let Some(balance) = patch.balance {
9672 writes.push((
9673 EffectTarget::AccountBalance { address },
9674 AbsoluteValue::U256(balance),
9675 ));
9676 }
9677 if let Some(nonce) = patch.nonce {
9678 writes.push((
9679 EffectTarget::AccountNonce { address },
9680 AbsoluteValue::U64(nonce),
9681 ));
9682 }
9683 if let Some(code) = &patch.code {
9684 writes.push((
9685 EffectTarget::AccountCode { address },
9686 AbsoluteValue::Bytes(code.clone()),
9687 ));
9688 }
9689 writes
9690}
9691
9692fn input_ref<N: Network>(input: &ReactiveInput<N>, ctx: &ReactiveContext) -> InputRef {
9693 match input {
9694 ReactiveInput::Log(log) => InputRef::Log {
9695 chain_id: ctx.chain_id,
9696 block_hash: log
9697 .block_hash
9698 .or(ctx.block.as_ref().map(|block| block.hash))
9699 .unwrap_or_default(),
9700 transaction_hash: log.transaction_hash.unwrap_or_default(),
9701 log_index: log.log_index.or(ctx.log_index).unwrap_or_default(),
9702 },
9703 ReactiveInput::PendingTxHash(hash) => InputRef::PendingTx {
9704 chain_id: ctx.chain_id,
9705 hash: *hash,
9706 },
9707 ReactiveInput::PendingTx(tx) => InputRef::PendingTx {
9708 chain_id: ctx.chain_id,
9709 hash: tx.tx_hash(),
9710 },
9711 ReactiveInput::BlockHeader(header) => InputRef::Block {
9712 chain_id: ctx.chain_id,
9713 hash: header.hash(),
9714 number: header.number(),
9715 },
9716 ReactiveInput::FullBlock(block) => {
9717 let header = block.header();
9718 InputRef::Block {
9719 chain_id: ctx.chain_id,
9720 hash: header.hash(),
9721 number: header.number(),
9722 }
9723 }
9724 }
9725}
9726
9727fn is_canonical_status(status: &ChainStatus) -> bool {
9728 matches!(
9729 status,
9730 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. }
9731 )
9732}
9733
9734pub struct EventDecoderHandler {
9736 id: HandlerId,
9737 decoder: Arc<dyn EventDecoder>,
9738 interest: LogInterest,
9739}
9740
9741impl EventDecoderHandler {
9742 pub fn new(id: HandlerId, decoder: Arc<dyn EventDecoder>, interest: LogInterest) -> Self {
9744 Self {
9745 id,
9746 decoder,
9747 interest,
9748 }
9749 }
9750}
9751
9752impl<N: Network> ReactiveHandler<N> for EventDecoderHandler {
9753 fn id(&self) -> HandlerId {
9754 self.id.clone()
9755 }
9756
9757 fn interests(&self) -> Vec<ReactiveInterest<N>> {
9758 vec![ReactiveInterest::Logs(self.interest.clone())]
9759 }
9760
9761 fn handle(
9762 &self,
9763 _ctx: &ReactiveContext,
9764 input: &ReactiveInput<N>,
9765 state: &dyn StateView,
9766 ) -> Result<HandlerOutcome, HandlerError> {
9767 let ReactiveInput::Log(log) = input else {
9768 return Ok(HandlerOutcome::empty(StateEffectQuality::NoStateEffect));
9769 };
9770
9771 Ok(HandlerOutcome {
9772 effects: self
9773 .decoder
9774 .decode(&log.inner, state)
9775 .into_iter()
9776 .map(ReactiveEffect::StateUpdate)
9777 .collect(),
9778 quality: StateEffectQuality::ExactFromInput,
9779 tags: Vec::new(),
9780 })
9781 }
9782}
9783
9784#[derive(
9786 Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
9787)]
9788#[non_exhaustive]
9789pub enum SubscriberCapability {
9790 Logs,
9792 BlockHeaders,
9794 FullBlocks,
9796 PendingTransactionHashes,
9798 PendingTransactions,
9800 HistoricalBackfill,
9802 Live,
9804 DurableReplay,
9814 OwnerScopedDelivery,
9816 DynamicInterests,
9818 ExplicitReorgs,
9820 FinalityUpdates,
9822 Barriers,
9824 Preconfirmations,
9826}
9827
9828#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9834pub struct SubscriberCapabilities {
9835 supported: BTreeSet<SubscriberCapability>,
9836}
9837
9838impl SubscriberCapabilities {
9839 pub fn new(capabilities: impl IntoIterator<Item = SubscriberCapability>) -> Self {
9841 Self {
9842 supported: capabilities.into_iter().collect(),
9843 }
9844 }
9845
9846 pub fn supports(&self, capability: SubscriberCapability) -> bool {
9848 self.supported.contains(&capability)
9849 }
9850
9851 pub fn iter(&self) -> impl Iterator<Item = SubscriberCapability> + '_ {
9853 self.supported.iter().copied()
9854 }
9855
9856 pub fn supports_live(&self) -> bool {
9858 self.supports(SubscriberCapability::Live)
9859 }
9860
9861 pub fn supports_durable_replay(&self) -> bool {
9864 self.supports(SubscriberCapability::DurableReplay)
9865 }
9866
9867 pub fn supports_explicit_reorgs(&self) -> bool {
9869 self.supports(SubscriberCapability::ExplicitReorgs)
9870 }
9871}
9872
9873impl FromIterator<SubscriberCapability> for SubscriberCapabilities {
9874 fn from_iter<T: IntoIterator<Item = SubscriberCapability>>(iter: T) -> Self {
9875 Self::new(iter)
9876 }
9877}
9878
9879pub trait EventSubscriber<N: Network = Ethereum>: Send {
9881 fn chain_id(&self) -> Option<u64> {
9888 None
9889 }
9890
9891 fn capabilities(&self) -> SubscriberCapabilities {
9893 SubscriberCapabilities::default()
9894 }
9895
9896 fn register_interests(
9915 &mut self,
9916 interests: &[ReactiveInterest<N>],
9917 ) -> SubscriberOperation<'_, ()>;
9918
9919 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N>;
9931
9932 fn restore_position(
9955 &mut self,
9956 _position: &SubscriberResumePosition,
9957 ) -> Result<(), SubscriberError> {
9958 Ok(())
9959 }
9960
9961 fn acknowledge_delivery(
9975 &mut self,
9976 _token: SubscriberDeliveryToken,
9977 ) -> SubscriberOperation<'_, ()> {
9978 Box::pin(async { Ok(()) })
9979 }
9980}
9981
9982pub type SubscriberOperation<'a, T> =
9987 Pin<Box<dyn Future<Output = Result<T, SubscriberError>> + Send + 'a>>;
9988
9989pub type SubscriberNextBatch<'a, N> = Pin<
9991 Box<dyn Future<Output = Result<Option<ReactiveInputBatch<N>>, SubscriberError>> + Send + 'a>,
9992>;
9993
9994pub type SubscriberNextScopedBatch<'a, N> = Pin<
9996 Box<dyn Future<Output = Result<Option<SubscriberInputBatch<N>>, SubscriberError>> + Send + 'a>,
9997>;
9998
9999#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
10001pub enum SubscriberMode {
10002 #[default]
10008 Auto,
10009 PubSub,
10011 Polling,
10013}
10014
10015#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10016enum FlashblocksAdapter {
10017 NativeSubscriptions,
10018 PendingStatePolling,
10019}
10020
10021fn flashblocks_adapter(chain_id: u64) -> Option<FlashblocksAdapter> {
10022 match chain_id {
10023 8_453 | 84_532 => Some(FlashblocksAdapter::NativeSubscriptions),
10024 10 | 11_155_420 => Some(FlashblocksAdapter::PendingStatePolling),
10025 _ => None,
10026 }
10027}
10028
10029#[derive(Clone, Debug, PartialEq, Eq)]
10031pub struct SubscriberConfig {
10032 pub preconfirmations: PreconfirmationMode,
10035 pub canonical_head_poll_interval: Duration,
10038 pub canonical_head_request_timeout: Duration,
10041 pub flashblock_poll_interval: Duration,
10049 pub max_consecutive_flashblock_poll_failures: usize,
10055 pub max_pending_transaction_receipts_per_tick: usize,
10063 pub max_flashblock_rpc_requests_per_second: usize,
10073 pub hydrate_pending_transactions: bool,
10075 pub verify_log_block_context: bool,
10086 pub max_batch_size: usize,
10088 pub max_log_addresses_per_subscription: usize,
10092 pub max_pending_records: usize,
10097 pub max_pending_backfills: usize,
10099 pub max_backfill_log_bytes: usize,
10102 pub max_reconcile_requests_in_flight: usize,
10105 pub reconnect: SubscriberReconnectConfig,
10107}
10108
10109impl Default for SubscriberConfig {
10110 fn default() -> Self {
10111 Self {
10112 preconfirmations: PreconfirmationMode::Disabled,
10113 canonical_head_poll_interval: Duration::from_millis(500),
10114 canonical_head_request_timeout: Duration::from_secs(3),
10115 flashblock_poll_interval: Duration::from_millis(250),
10116 max_consecutive_flashblock_poll_failures: 10,
10117 max_pending_transaction_receipts_per_tick: 32,
10118 max_flashblock_rpc_requests_per_second: 40,
10119 hydrate_pending_transactions: false,
10120 verify_log_block_context: false,
10121 max_batch_size: 1024,
10122 max_log_addresses_per_subscription: 1024,
10123 max_pending_records: 16_384,
10124 max_pending_backfills: 4_096,
10125 max_backfill_log_bytes: 64 * 1024 * 1024,
10126 max_reconcile_requests_in_flight: 8,
10127 reconnect: SubscriberReconnectConfig::default(),
10128 }
10129 }
10130}
10131
10132#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10134pub enum FlashblocksDelivery {
10135 NativeSubscriptions,
10137 PendingStatePolling,
10139 #[cfg(feature = "raw-flashblocks-json")]
10141 ExternalUpdates,
10142}
10143
10144#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10146pub struct FlashblocksRpcMetrics {
10147 capability_requests: u64,
10148 provider_pair_chain_requests: u64,
10149 canonical_head_requests: u64,
10150 pending_block_requests: u64,
10151 pending_log_requests: u64,
10152 pending_receipt_requests: u64,
10153 pending_receipts_completed: u64,
10154 pending_receipts_unavailable: u64,
10155 failed_requests: u64,
10156 raced_samples: u64,
10157}
10158
10159impl FlashblocksRpcMetrics {
10160 pub const fn capability_requests(self) -> u64 {
10162 self.capability_requests
10163 }
10164
10165 pub const fn provider_pair_chain_requests(self) -> u64 {
10168 self.provider_pair_chain_requests
10169 }
10170
10171 pub const fn canonical_head_requests(self) -> u64 {
10173 self.canonical_head_requests
10174 }
10175
10176 pub const fn pending_block_requests(self) -> u64 {
10178 self.pending_block_requests
10179 }
10180
10181 pub const fn pending_log_requests(self) -> u64 {
10183 self.pending_log_requests
10184 }
10185
10186 pub const fn pending_receipt_requests(self) -> u64 {
10189 self.pending_receipt_requests
10190 }
10191
10192 pub const fn pending_receipts_completed(self) -> u64 {
10194 self.pending_receipts_completed
10195 }
10196
10197 pub const fn pending_receipts_unavailable(self) -> u64 {
10200 self.pending_receipts_unavailable
10201 }
10202
10203 pub const fn failed_requests(self) -> u64 {
10205 self.failed_requests
10206 }
10207
10208 pub const fn raced_samples(self) -> u64 {
10212 self.raced_samples
10213 }
10214
10215 pub const fn total_requests(self) -> u64 {
10218 self.capability_requests
10219 .saturating_add(self.provider_pair_chain_requests)
10220 .saturating_add(self.canonical_head_requests)
10221 .saturating_add(self.pending_block_requests)
10222 .saturating_add(self.pending_log_requests)
10223 .saturating_add(self.pending_receipt_requests)
10224 }
10225}
10226
10227#[derive(Clone, Debug, PartialEq, Eq)]
10236pub struct FlashblocksPreflight {
10237 chain_id: u64,
10238 provider: ProviderRef,
10239 delivery: FlashblocksDelivery,
10240 pending_log_subscriptions: usize,
10241 pending_log_filters: usize,
10242 advertised_capabilities: Option<serde_json::Value>,
10243}
10244
10245impl FlashblocksPreflight {
10246 pub const fn chain_id(&self) -> u64 {
10248 self.chain_id
10249 }
10250
10251 pub const fn provider(&self) -> &ProviderRef {
10257 &self.provider
10258 }
10259
10260 pub const fn delivery(&self) -> FlashblocksDelivery {
10262 self.delivery
10263 }
10264
10265 pub const fn pending_log_subscriptions(&self) -> usize {
10269 self.pending_log_subscriptions
10270 }
10271
10272 pub const fn pending_log_filters(&self) -> usize {
10275 self.pending_log_filters
10276 }
10277
10278 pub const fn advertised_capabilities(&self) -> Option<&serde_json::Value> {
10281 self.advertised_capabilities.as_ref()
10282 }
10283}
10284
10285#[derive(Clone, Debug, PartialEq, Eq)]
10291pub struct SubscriberReconnectConfig {
10292 pub enabled: bool,
10294 pub initial_delay: Duration,
10296 pub retry_delay: Duration,
10299 pub max_delay: Duration,
10301 pub max_attempts: Option<usize>,
10303 pub dedupe_window: usize,
10306}
10307
10308impl Default for SubscriberReconnectConfig {
10309 fn default() -> Self {
10310 Self {
10311 enabled: true,
10312 initial_delay: Duration::ZERO,
10313 retry_delay: Duration::from_millis(250),
10314 max_delay: Duration::from_secs(30),
10315 max_attempts: Some(3),
10316 dedupe_window: 4096,
10317 }
10318 }
10319}
10320
10321#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10333pub struct SubscriberBackfill {
10334 from_block: u64,
10335 to_block: Option<u64>,
10336 retained_anchor: Option<BlockRef>,
10337}
10338
10339impl SubscriberBackfill {
10340 pub fn range(from_block: u64, to_block: u64) -> Self {
10342 Self {
10343 from_block,
10344 to_block: Some(to_block),
10345 retained_anchor: None,
10346 }
10347 }
10348
10349 pub fn from_block(from_block: u64) -> Self {
10351 Self {
10352 from_block,
10353 to_block: None,
10354 retained_anchor: None,
10355 }
10356 }
10357
10358 pub fn from_canonical_block(block: BlockRef) -> Self {
10365 Self {
10366 from_block: block.number,
10367 to_block: None,
10368 retained_anchor: Some(block),
10369 }
10370 }
10371
10372 pub fn from_canonical_block_through(
10380 block: BlockRef,
10381 to_block: u64,
10382 ) -> Result<Self, SubscriberError> {
10383 if to_block < block.number {
10384 return Err(SubscriberError::InvalidConfig(
10385 "inclusive backfill upper bound precedes its retained anchor",
10386 ));
10387 }
10388 Ok(Self {
10389 from_block: block.number,
10390 to_block: Some(to_block),
10391 retained_anchor: Some(block),
10392 })
10393 }
10394
10395 pub fn after_canonical_block(block: BlockRef) -> Result<Self, SubscriberError> {
10411 Self::after_canonical_block_inner(block, None)
10412 }
10413
10414 pub fn after_canonical_block_through(
10426 block: BlockRef,
10427 to_block: u64,
10428 ) -> Result<Self, SubscriberError> {
10429 if to_block < block.number {
10430 return Err(SubscriberError::InvalidConfig(
10431 "exclusive backfill upper bound precedes its retained baseline",
10432 ));
10433 }
10434 Self::after_canonical_block_inner(block, Some(to_block))
10435 }
10436
10437 fn after_canonical_block_inner(
10438 block: BlockRef,
10439 to_block: Option<u64>,
10440 ) -> Result<Self, SubscriberError> {
10441 let from_block = block
10442 .number
10443 .checked_add(1)
10444 .ok_or(SubscriberError::InvalidConfig(
10445 "cannot construct an exclusive backfill after block u64::MAX",
10446 ))?;
10447 Ok(Self {
10448 from_block,
10449 to_block,
10450 retained_anchor: Some(block),
10451 })
10452 }
10453
10454 pub fn start_block(&self) -> u64 {
10456 self.from_block
10457 }
10458
10459 pub fn end_block(&self) -> Option<u64> {
10461 self.to_block
10462 }
10463
10464 pub fn retained_anchor(&self) -> Option<&BlockRef> {
10466 self.retained_anchor.as_ref()
10467 }
10468}
10469
10470#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
10477pub struct SubscriberOwnerEpoch {
10478 owner: HandlerId,
10479 sequence: u64,
10480}
10481
10482#[derive(Clone, Debug, PartialEq, Eq)]
10489#[non_exhaustive]
10490pub enum SubscriberInputScope {
10491 Canonical {
10493 owners: Vec<SubscriberOwnerEpoch>,
10495 },
10496 CanonicalResidual {
10500 owners: Vec<SubscriberOwnerEpoch>,
10502 excluded: Vec<HandlerId>,
10504 },
10505 OwnerOnly {
10507 owners: Vec<SubscriberOwnerEpoch>,
10509 },
10510 OwnerOnlyHandlers {
10512 owners: Vec<HandlerId>,
10514 },
10515 Preconfirmed,
10518}
10519
10520impl SubscriberInputScope {
10521 pub fn owners(&self) -> &[SubscriberOwnerEpoch] {
10523 match self {
10524 Self::Canonical { owners }
10525 | Self::CanonicalResidual { owners, .. }
10526 | Self::OwnerOnly { owners } => owners,
10527 Self::OwnerOnlyHandlers { .. } | Self::Preconfirmed => &[],
10528 }
10529 }
10530
10531 pub const fn is_canonical(&self) -> bool {
10533 matches!(
10534 self,
10535 Self::Canonical { .. } | Self::CanonicalResidual { .. }
10536 )
10537 }
10538
10539 pub const fn is_preconfirmed(&self) -> bool {
10541 matches!(self, Self::Preconfirmed)
10542 }
10543}
10544
10545#[derive(Clone, Debug)]
10547pub struct SubscriberInputRecord<N: Network = Ethereum> {
10548 record: ReactiveInputRecord<N>,
10549 scope: SubscriberInputScope,
10550 preconfirmation_timing: Option<FlashblockIngressTiming>,
10551}
10552
10553impl<N: Network> SubscriberInputRecord<N> {
10554 pub const fn record(&self) -> &ReactiveInputRecord<N> {
10556 &self.record
10557 }
10558
10559 pub const fn scope(&self) -> &SubscriberInputScope {
10561 &self.scope
10562 }
10563
10564 pub const fn preconfirmation_timing(&self) -> Option<FlashblockIngressTiming> {
10566 self.preconfirmation_timing
10567 }
10568
10569 pub fn into_record(self) -> ReactiveInputRecord<N> {
10571 self.record
10572 }
10573}
10574
10575impl<N: Network> std::ops::Deref for SubscriberInputRecord<N> {
10576 type Target = ReactiveInputRecord<N>;
10577
10578 fn deref(&self) -> &Self::Target {
10579 &self.record
10580 }
10581}
10582
10583#[derive(Clone, Debug)]
10585pub struct SubscriberInputBatch<N: Network = Ethereum> {
10586 records: Vec<SubscriberInputRecord<N>>,
10587 chain_id: Option<u64>,
10588 chain_controls: Vec<ChainControl>,
10589 preconfirmation_invalidated: bool,
10590 preconfirmation_timing: Option<FlashblockIngressTiming>,
10591}
10592
10593#[derive(Debug)]
10596#[non_exhaustive]
10597pub enum SubscriberDriverPoll<C, N: Network = Ethereum> {
10598 Control(C),
10600 Batch(Option<SubscriberInputBatch<N>>),
10602}
10603
10604impl<N: Network> SubscriberInputBatch<N> {
10605 pub fn records(&self) -> &[SubscriberInputRecord<N>] {
10607 &self.records
10608 }
10609
10610 pub fn into_records(self) -> Vec<SubscriberInputRecord<N>> {
10612 self.records
10613 }
10614
10615 pub fn chain_controls(&self) -> &[ChainControl] {
10617 &self.chain_controls
10618 }
10619
10620 pub const fn preconfirmation_invalidated(&self) -> bool {
10623 self.preconfirmation_invalidated
10624 }
10625
10626 pub const fn preconfirmation_timing(&self) -> Option<FlashblockIngressTiming> {
10628 self.preconfirmation_timing
10629 }
10630
10631 pub fn into_reactive_batch(self) -> ReactiveInputBatch<N> {
10637 let chain_id = self.chain_id;
10638 let chain_controls = self.chain_controls;
10639 let preconfirmation_timing = self.preconfirmation_timing;
10640 let mut batch = ReactiveInputBatch::from_scoped_records_with_delivery_scope(
10641 self.records.into_iter().map(|scoped| {
10642 let source = scoped.record.context.source;
10643 let (audience, delivery_scope) = match scoped.scope {
10644 SubscriberInputScope::Canonical { .. } => (
10645 DeliveryAudience::All,
10646 if source == InputSource::Backfill {
10647 DeliveryScope::CanonicalProgress
10648 } else {
10649 DeliveryScope::Canonical
10650 },
10651 ),
10652 SubscriberInputScope::CanonicalResidual { excluded, .. } => (
10653 DeliveryAudience::AllExcept(excluded),
10654 if source == InputSource::Backfill {
10655 DeliveryScope::CanonicalProgress
10656 } else {
10657 DeliveryScope::Canonical
10658 },
10659 ),
10660 SubscriberInputScope::OwnerOnly { owners } => {
10661 let mut handler_ids = Vec::with_capacity(owners.len());
10662 for epoch in owners {
10663 if !handler_ids.contains(epoch.owner()) {
10664 handler_ids.push(epoch.owner().clone());
10665 }
10666 }
10667 (
10668 DeliveryAudience::Owners(handler_ids),
10669 DeliveryScope::OwnerCatchup,
10670 )
10671 }
10672 SubscriberInputScope::OwnerOnlyHandlers { owners } => (
10673 DeliveryAudience::Owners(owners),
10674 DeliveryScope::OwnerCatchup,
10675 ),
10676 SubscriberInputScope::Preconfirmed => {
10677 (DeliveryAudience::All, DeliveryScope::Preconfirmed)
10678 }
10679 };
10680 (scoped.record, audience, delivery_scope)
10681 }),
10682 )
10683 .with_chain_controls(chain_controls);
10684 if let Some(chain_id) = chain_id {
10685 batch = batch.with_chain_id(chain_id);
10686 }
10687 if let Some(timing) = preconfirmation_timing {
10688 batch = batch.with_preconfirmation_timing(timing);
10689 }
10690 batch
10691 }
10692}
10693
10694impl SubscriberOwnerEpoch {
10695 pub const fn owner(&self) -> &HandlerId {
10697 &self.owner
10698 }
10699
10700 pub const fn sequence(&self) -> u64 {
10702 self.sequence
10703 }
10704}
10705
10706#[derive(Clone, Debug, PartialEq, Eq)]
10708#[non_exhaustive]
10709pub enum SubscriberOwnerStart {
10710 Live,
10712 PostBlock(BlockRef),
10719}
10720
10721#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10723#[non_exhaustive]
10724pub enum SubscriberOwnerState {
10725 Staged,
10728 Active,
10730 Removing,
10732}
10733
10734#[derive(Clone, Debug, PartialEq, Eq)]
10740pub struct SubscriberOwnerProgress {
10741 owner: SubscriberOwnerEpoch,
10742 through: BlockRef,
10743}
10744
10745impl SubscriberOwnerProgress {
10746 pub const fn owner(&self) -> &SubscriberOwnerEpoch {
10748 &self.owner
10749 }
10750
10751 pub const fn through(&self) -> &BlockRef {
10753 &self.through
10754 }
10755}
10756
10757#[derive(Debug, thiserror::Error)]
10759#[non_exhaustive]
10760pub enum SubscriberOwnerError {
10761 #[error(transparent)]
10763 Subscriber(#[from] SubscriberError),
10764 #[error("subscriber interest owner `{0}` is already registered")]
10766 AlreadyRegistered(HandlerId),
10767 #[error("post-block subscriber baseline {0} has no following block")]
10769 PostBlockOverflow(u64),
10770 #[error("subscriber owner epoch sequence exhausted")]
10772 EpochExhausted,
10773 #[error("subscriber owner epoch is not staged")]
10775 NotStaged,
10776 #[error("subscriber owner was staged live-only and has no catch-up baseline")]
10778 MissingBaseline,
10779 #[error("post-block subscriber owners support log interests only")]
10781 UnsupportedPostBlockInterest,
10782 #[error("subscriber reconcile target block {0} was not found")]
10784 BlockUnavailable(u64),
10785 #[error(
10787 "subscriber reconcile target mismatch: expected block {expected_number} {expected_hash}, got block {actual_number} {actual_hash}"
10788 )]
10789 BlockMismatch {
10790 expected_number: u64,
10792 expected_hash: B256,
10794 actual_number: u64,
10796 actual_hash: B256,
10798 },
10799 #[error("subscriber reconcile target block {target} precedes current owner position {current}")]
10801 ProgressRegression {
10802 current: u64,
10804 target: u64,
10806 },
10807 #[error(
10810 "subscriber reconcile conflicts with retained block {number} {current_hash}: target chain references {target_hash}"
10811 )]
10812 ProgressConflict {
10813 number: u64,
10815 current_hash: B256,
10817 target_hash: B256,
10819 },
10820 #[error("subscriber reconcile returned an invalid catch-up log: {0}")]
10822 InvalidBackfillLog(&'static str),
10823}
10824
10825pub trait InterestOwnerSubscriber<N: Network = Ethereum>: EventSubscriber<N> {
10840 fn upsert_interest_owners(
10853 &mut self,
10854 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
10855 ) -> SubscriberOperation<'_, ()> {
10856 Box::pin(async {
10857 Err(SubscriberError::Unsupported(
10858 "subscriber does not implement atomic bulk owner upsert",
10859 ))
10860 })
10861 }
10862
10863 fn replace_interest_owners(
10876 &mut self,
10877 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
10878 ) -> SubscriberOperation<'_, ()> {
10879 Box::pin(async {
10880 Err(SubscriberError::Unsupported(
10881 "subscriber does not implement atomic exact owner replacement",
10882 ))
10883 })
10884 }
10885
10886 fn replace_interest_owners_with_global_backfill(
10910 &mut self,
10911 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
10912 _backfill: SubscriberBackfill,
10913 ) -> SubscriberOperation<'_, ()> {
10914 Box::pin(async {
10915 Err(SubscriberError::Unsupported(
10916 "subscriber does not implement atomic owner replacement with global backfill",
10917 ))
10918 })
10919 }
10920
10921 fn add_interest_owner(
10933 &mut self,
10934 owner: HandlerId,
10935 interests: &[ReactiveInterest<N>],
10936 ) -> SubscriberOperation<'_, ()>;
10937
10938 fn add_interest_owner_with_backfill(
10946 &mut self,
10947 owner: HandlerId,
10948 interests: &[ReactiveInterest<N>],
10949 backfill: SubscriberBackfill,
10950 ) -> SubscriberOperation<'_, ()>;
10951
10952 fn add_interest_owner_with_canonical_catchup(
10970 &mut self,
10971 _owner: HandlerId,
10972 _interests: &[ReactiveInterest<N>],
10973 _retained: BlockRef,
10974 ) -> SubscriberOperation<'_, ()> {
10975 Box::pin(async {
10976 Err(SubscriberError::Unsupported(
10977 "subscriber does not implement coordinated canonical owner catch-up",
10978 ))
10979 })
10980 }
10981
10982 fn remove_interest_owner(
10993 &mut self,
10994 owner: &HandlerId,
10995 ) -> SubscriberOperation<'_, Option<Vec<ReactiveInterest<N>>>>;
10996
10997 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]>;
10999}
11000
11001pub struct ReactiveEngine<S, N: Network = Ethereum> {
11042 runtime: ReactiveRuntime<N>,
11043 subscriber: S,
11044 pending_acknowledgement: Option<PendingAcknowledgement<N>>,
11045 pending_checkpoint: Option<PendingCheckpoint<N>>,
11046 last_checkpoint_block: Option<DurableCheckpointBlock>,
11047 last_checkpoint_delivery_token: Option<SubscriberDeliveryToken>,
11048 last_checkpoint_delivery_witness: Option<B256>,
11049 last_subscriber_checkpoint: Option<SubscriberCheckpoint>,
11050 checkpoint_identity: Option<DurableCheckpointIdentity>,
11051}
11052
11053struct PendingAcknowledgement<N: Network> {
11054 token: SubscriberDeliveryToken,
11055 report: ReactiveBatchReport<N>,
11056}
11057
11058struct PendingCheckpoint<N: Network> {
11059 metadata: DurableCheckpointMetadata,
11060 delivery_token: Option<SubscriberDeliveryToken>,
11061 report: ReactiveBatchReport<N>,
11062 saved_to: Option<PathBuf>,
11063 staged_generation: u64,
11064}
11065
11066struct CheckpointStage<N: Network> {
11067 incoming_block: Option<DurableCheckpointBlock>,
11068 delivery_token: Option<SubscriberDeliveryToken>,
11069 delivery_witness: Option<B256>,
11070 subscriber_checkpoint: Option<SubscriberCheckpoint>,
11071 staged_generation: u64,
11072 report: ReactiveBatchReport<N>,
11073}
11074
11075struct DurableResumePlan {
11076 runtime: DurableRuntimeRestorePlan,
11077 position: SubscriberResumePosition,
11078 delivery_witness: Option<B256>,
11079}
11080
11081enum HandlerRegistrationCatchup {
11082 LiveOnly,
11083 OwnerBackfill(SubscriberBackfill),
11084 CoordinatedCanonical(BlockRef),
11085}
11086
11087const DELIVERY_WITNESS_VERSION: u32 = 1;
11088const DELIVERY_WITNESS_DOMAIN: &[u8] = b"evm-fork-cache/reactive-delivery-witness";
11089
11090#[derive(serde::Serialize)]
11091struct DeliveryWitnessEnvelope<'a> {
11092 version: u32,
11093 chain_id: Option<u64>,
11094 records: Vec<DeliveryRecordWitness<'a>>,
11095 chain_controls: &'a [ChainControl],
11096 subscriber_checkpoint: Option<&'a [u8]>,
11097 payload_commitment: Option<B256>,
11098}
11099
11100#[derive(serde::Serialize)]
11101struct DeliveryRecordWitness<'a> {
11102 identity: ReactiveInputIdentity,
11103 context: &'a ReactiveContext,
11104 audience: &'a DeliveryAudience,
11105 scope: DeliveryScope,
11106 payload: DeliveryPayloadWitness<'a>,
11107}
11108
11109#[derive(serde::Serialize)]
11110enum DeliveryPayloadWitness<'a> {
11111 Log {
11114 address: Address,
11115 topics: &'a [B256],
11116 data: &'a Bytes,
11117 block_hash: Option<B256>,
11118 block_number: Option<u64>,
11119 block_timestamp: Option<u64>,
11120 transaction_hash: Option<B256>,
11121 transaction_index: Option<u64>,
11122 log_index: Option<u64>,
11123 removed: bool,
11124 },
11125 IdentityCommitted,
11132}
11133
11134fn durable_delivery_witness<N: Network>(
11135 batch: &ReactiveInputBatch<N>,
11136) -> Result<B256, ReactiveEngineError> {
11137 let requires_payload_commitment = batch.records.iter().any(|record| {
11138 matches!(
11139 &record.input,
11140 ReactiveInput::BlockHeader(_)
11141 | ReactiveInput::FullBlock(_)
11142 | ReactiveInput::PendingTx(_)
11143 )
11144 });
11145 if requires_payload_commitment && batch.payload_commitment.is_none() {
11146 return Err(ReactiveEngineError::MissingPayloadCommitment);
11147 }
11148 let records = batch
11149 .records
11150 .iter()
11151 .enumerate()
11152 .map(|(index, record)| {
11153 let payload = match &record.input {
11154 ReactiveInput::Log(log) => DeliveryPayloadWitness::Log {
11155 address: log.address(),
11156 topics: log.topics(),
11157 data: &log.inner.data.data,
11158 block_hash: log.block_hash,
11159 block_number: log.block_number,
11160 block_timestamp: log.block_timestamp,
11161 transaction_hash: log.transaction_hash,
11162 transaction_index: log.transaction_index,
11163 log_index: log.log_index,
11164 removed: log.removed,
11165 },
11166 ReactiveInput::BlockHeader(_)
11167 | ReactiveInput::FullBlock(_)
11168 | ReactiveInput::PendingTxHash(_)
11169 | ReactiveInput::PendingTx(_) => DeliveryPayloadWitness::IdentityCommitted,
11170 };
11171 Ok(DeliveryRecordWitness {
11172 identity: record.validated_identity()?,
11173 context: &record.context,
11174 audience: batch
11175 .record_audience(index)
11176 .expect("enumerated record always has an audience"),
11177 scope: batch
11178 .record_delivery_scope(index)
11179 .expect("enumerated record always has a delivery scope"),
11180 payload,
11181 })
11182 })
11183 .collect::<Result<Vec<_>, ReactiveError>>()?;
11184 let envelope = DeliveryWitnessEnvelope {
11185 version: DELIVERY_WITNESS_VERSION,
11186 chain_id: batch.chain_id,
11187 records,
11188 chain_controls: &batch.chain_controls,
11189 subscriber_checkpoint: batch
11190 .subscriber_checkpoint
11191 .as_ref()
11192 .map(SubscriberCheckpoint::as_bytes),
11193 payload_commitment: batch
11194 .payload_commitment
11195 .as_ref()
11196 .map(SubscriberPayloadCommitment::digest),
11197 };
11198 let encoded = bincode::DefaultOptions::new()
11199 .with_fixint_encoding()
11200 .serialize(&envelope)
11201 .map_err(|error| ReactiveEngineError::DeliveryWitness(error.to_string()))?;
11202 let mut witness = Keccak256::new();
11203 witness.update(DELIVERY_WITNESS_DOMAIN);
11204 witness.update(encoded);
11205 Ok(witness.finalize())
11206}
11207
11208impl<S, N> ReactiveEngine<S, N>
11209where
11210 N: Network,
11211 S: EventSubscriber<N>,
11212{
11213 pub fn new(runtime: ReactiveRuntime<N>, subscriber: S) -> Self {
11215 Self {
11216 runtime,
11217 subscriber,
11218 pending_acknowledgement: None,
11219 pending_checkpoint: None,
11220 last_checkpoint_block: None,
11221 last_checkpoint_delivery_token: None,
11222 last_checkpoint_delivery_witness: None,
11223 last_subscriber_checkpoint: None,
11224 checkpoint_identity: None,
11225 }
11226 }
11227
11228 pub fn into_parts(self) -> Result<(ReactiveRuntime<N>, S), Box<Self>> {
11242 if self.pending_acknowledgement.is_some() || self.pending_checkpoint.is_some() {
11243 return Err(Box::new(self));
11244 }
11245 Ok((self.runtime, self.subscriber))
11246 }
11247
11248 fn durable_resume_plan(
11249 &self,
11250 metadata: &DurableCheckpointMetadata,
11251 ) -> Result<DurableResumePlan, ReactiveCheckpointRestoreError> {
11252 if !self.subscriber.capabilities().supports_durable_replay() {
11253 return Err(ReactiveCheckpointRestoreError::SubscriberNotDurable);
11254 }
11255 self.ensure_subscriber_restore_chain(metadata.identity.chain_id)?;
11256 if !self.runtime.is_pristine_for_checkpoint_restore()
11257 || self.pending_acknowledgement.is_some()
11258 || self.pending_checkpoint.is_some()
11259 || self.last_checkpoint_block.is_some()
11260 || self.last_checkpoint_delivery_token.is_some()
11261 || self.last_checkpoint_delivery_witness.is_some()
11262 || self.last_subscriber_checkpoint.is_some()
11263 || self.checkpoint_identity.is_some()
11264 {
11265 return Err(ReactiveCheckpointRestoreError::ActiveRuntime);
11266 }
11267
11268 let block = BlockRef {
11269 number: metadata.block.number,
11270 hash: metadata.block.hash,
11271 parent_hash: metadata.block.parent_hash,
11272 timestamp: metadata.block.timestamp,
11273 };
11274 let runtime = match metadata.runtime_checkpoint.as_deref() {
11275 Some(bytes) => self
11276 .runtime
11277 .plan_durable_checkpoint_restore(bytes, &block)?,
11278 None => DurableRuntimeRestorePlan {
11279 checkpoint: None,
11280 fallback_history: (self.runtime.config.journal_depth > 0)
11281 .then_some(block)
11282 .into_iter()
11283 .collect(),
11284 },
11285 };
11286 let delivery_token = metadata
11287 .delivery_token
11288 .clone()
11289 .map(SubscriberDeliveryToken::new);
11290 let subscriber_checkpoint = metadata
11291 .subscriber_checkpoint
11292 .clone()
11293 .map(SubscriberCheckpoint::new);
11294 let position = SubscriberResumePosition::new(
11295 metadata.identity.chain_id,
11296 block,
11297 runtime.canonical_history(),
11298 delivery_token,
11299 subscriber_checkpoint,
11300 );
11301 Ok(DurableResumePlan {
11302 runtime,
11303 position,
11304 delivery_witness: metadata.delivery_witness,
11305 })
11306 }
11307
11308 pub fn preview_durable_resume_position(
11337 &self,
11338 metadata: &DurableCheckpointMetadata,
11339 ) -> Result<SubscriberResumePosition, ReactiveCheckpointRestoreError> {
11340 Ok(self.durable_resume_plan(metadata)?.position)
11341 }
11342
11343 pub fn resume_from_durable_checkpoint(
11365 &mut self,
11366 metadata: &DurableCheckpointMetadata,
11367 ) -> Result<(), ReactiveCheckpointRestoreError> {
11368 let plan = self.durable_resume_plan(metadata)?;
11369 let prior_runtime = self.runtime.checkpoint_state();
11370
11371 let DurableResumePlan {
11372 runtime,
11373 position,
11374 delivery_witness,
11375 } = plan;
11376 self.runtime.apply_durable_checkpoint_restore(runtime);
11377 self.runtime.coverage_head = Some(position.coverage_head);
11378 if let Err(error) = self.subscriber.restore_position(&position) {
11379 self.runtime.restore_state(prior_runtime);
11380 return Err(ReactiveCheckpointRestoreError::Subscriber(error));
11381 }
11382 if let Err(error) = self.ensure_subscriber_restore_chain(metadata.identity.chain_id) {
11383 self.runtime.restore_state(prior_runtime);
11384 return Err(error);
11385 }
11386 self.last_checkpoint_block = Some(metadata.block.clone());
11387 self.last_checkpoint_delivery_token = position.delivery_token;
11388 self.last_checkpoint_delivery_witness = delivery_witness;
11389 self.last_subscriber_checkpoint = position.subscriber_checkpoint;
11390 self.checkpoint_identity = Some(metadata.identity.clone());
11391 Ok(())
11392 }
11393
11394 pub fn restore_durable_checkpoint(
11410 &mut self,
11411 cache: &mut EvmCache,
11412 loaded: LoadedDurableCheckpoint,
11413 expected: &DurableCheckpointIdentity,
11414 ) -> Result<DurableCheckpointMetadata, ReactiveCheckpointRestoreError> {
11415 if !self.subscriber.capabilities().supports_durable_replay() {
11416 return Err(ReactiveCheckpointRestoreError::SubscriberNotDurable);
11417 }
11418 self.ensure_subscriber_restore_chain(expected.chain_id)?;
11419 if !self.runtime.is_pristine_for_checkpoint_restore()
11420 || self.pending_acknowledgement.is_some()
11421 || self.pending_checkpoint.is_some()
11422 || self.last_checkpoint_block.is_some()
11423 || self.last_checkpoint_delivery_token.is_some()
11424 || self.last_checkpoint_delivery_witness.is_some()
11425 || self.last_subscriber_checkpoint.is_some()
11426 || self.checkpoint_identity.is_some()
11427 {
11428 return Err(ReactiveCheckpointRestoreError::ActiveRuntime);
11429 }
11430
11431 let prior_cache = EvmCacheStateSnapshot::capture(cache);
11432 let metadata = loaded.restore_into(cache, expected)?;
11433 if let Err(error) = self.resume_from_durable_checkpoint(&metadata) {
11434 prior_cache.restore(cache);
11435 return Err(error);
11436 }
11437 Ok(metadata)
11438 }
11439
11440 pub fn runtime(&self) -> &ReactiveRuntime<N> {
11442 &self.runtime
11443 }
11444
11445 pub fn runtime_mut(&mut self) -> &mut ReactiveRuntime<N> {
11447 &mut self.runtime
11448 }
11449
11450 pub fn subscriber(&self) -> &S {
11452 &self.subscriber
11453 }
11454
11455 pub fn subscriber_mut(&mut self) -> &mut S {
11457 &mut self.subscriber
11458 }
11459
11460 pub fn adopt_canonical_baseline(
11478 &mut self,
11479 cache: &EvmCache,
11480 baseline: ReactiveCanonicalBaseline,
11481 ) -> Result<(), ReactiveEngineError> {
11482 if self.pending_acknowledgement.is_some()
11483 || self.pending_checkpoint.is_some()
11484 || self.last_checkpoint_block.is_some()
11485 || self.last_checkpoint_delivery_token.is_some()
11486 || self.last_checkpoint_delivery_witness.is_some()
11487 || self.last_subscriber_checkpoint.is_some()
11488 || self.checkpoint_identity.is_some()
11489 {
11490 return Err(ReactiveBaselineError::ActiveRuntime.into());
11491 }
11492 self.runtime
11496 .validate_canonical_baseline_adoption(baseline.block)?;
11497 if baseline.chain_id != cache.chain_id() {
11498 return Err(ReactiveBaselineError::CacheChainMismatch {
11499 baseline_chain_id: baseline.chain_id,
11500 cache_chain_id: cache.chain_id(),
11501 }
11502 .into());
11503 }
11504 self.ensure_subscriber_chain(cache)?;
11505 let exact_selector = BlockId::from((baseline.block.hash, Some(true)));
11506 let context_matches = cache.block_number() == Some(baseline.block.number)
11507 && baseline
11508 .block
11509 .timestamp
11510 .is_none_or(|timestamp| cache.timestamp() == Some(timestamp));
11511 if cache.block() != exact_selector || !context_matches {
11512 return Err(ReactiveBaselineError::CacheBlockMismatch {
11513 number: baseline.block.number,
11514 hash: baseline.block.hash,
11515 }
11516 .into());
11517 }
11518 self.runtime.adopt_canonical_baseline(baseline.block)?;
11519 Ok(())
11520 }
11521
11522 pub fn next_batch(
11535 &mut self,
11536 cache: &EvmCache,
11537 ) -> Result<SubscriberNextBatch<'_, N>, ReactiveEngineError> {
11538 if self.pending_checkpoint.is_some() {
11539 return Err(ReactiveEngineError::PendingCheckpointCommit);
11540 }
11541 if self.pending_acknowledgement.is_some() {
11542 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11543 }
11544 self.ensure_subscriber_chain(cache)?;
11545 Ok(self.subscriber.next_batch())
11546 }
11547
11548 pub fn ingest_batch(
11557 &mut self,
11558 cache: &mut EvmCache,
11559 batch: ReactiveInputBatch<N>,
11560 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
11561 self.ensure_raw_ingest_is_safe(cache, &batch)?;
11562 Ok(self.runtime.ingest_batch(cache, batch)?)
11563 }
11564
11565 pub fn ingest_batch_with_resync(
11575 &mut self,
11576 cache: &mut EvmCache,
11577 batch: ReactiveInputBatch<N>,
11578 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
11579 self.ensure_raw_ingest_is_safe(cache, &batch)?;
11580 Ok(self.runtime.ingest_batch_with_resync(cache, batch)?)
11581 }
11582
11583 fn ensure_raw_ingest_is_safe(
11584 &self,
11585 cache: &EvmCache,
11586 batch: &ReactiveInputBatch<N>,
11587 ) -> Result<(), ReactiveEngineError> {
11588 if self.pending_checkpoint.is_some() {
11589 return Err(ReactiveEngineError::PendingCheckpointCommit);
11590 }
11591 if self.pending_acknowledgement.is_some() {
11592 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11593 }
11594 if batch.delivery_token().is_some() || batch.subscriber_checkpoint().is_some() {
11595 return Err(ReactiveEngineError::UncommittedDeliveryMetadata);
11596 }
11597 self.ensure_subscriber_chain(cache)?;
11598 Ok(())
11599 }
11600
11601 fn ensure_subscriber_chain(&self, cache: &EvmCache) -> Result<(), ReactiveEngineError> {
11602 if let Some(subscriber_chain_id) = self.subscriber.chain_id()
11603 && subscriber_chain_id != cache.chain_id()
11604 {
11605 return Err(ReactiveEngineError::SubscriberChainMismatch {
11606 subscriber_chain_id,
11607 cache_chain_id: cache.chain_id(),
11608 });
11609 }
11610 Ok(())
11611 }
11612
11613 fn ensure_subscriber_restore_chain(
11614 &self,
11615 checkpoint_chain_id: u64,
11616 ) -> Result<(), ReactiveCheckpointRestoreError> {
11617 if let Some(subscriber_chain_id) = self.subscriber.chain_id()
11618 && subscriber_chain_id != checkpoint_chain_id
11619 {
11620 return Err(ReactiveCheckpointRestoreError::SubscriberChainMismatch {
11621 subscriber_chain_id,
11622 checkpoint_chain_id,
11623 });
11624 }
11625 Ok(())
11626 }
11627
11628 pub async fn next_ingest(
11638 &mut self,
11639 cache: &mut EvmCache,
11640 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
11641 self.ensure_subscriber_chain(cache)?;
11642 if self.pending_checkpoint.is_some() {
11643 return Err(ReactiveEngineError::PendingCheckpointCommit);
11644 }
11645 if self.pending_acknowledgement.is_some() {
11646 return self.commit_pending_acknowledgement().await.map(Some);
11647 }
11648 let batch = self.subscriber.next_batch().await?;
11649 self.ensure_subscriber_chain(cache)?;
11650 let Some(mut batch) = batch else {
11651 return Ok(None);
11652 };
11653 let delivery_token = batch.take_delivery_token();
11654 let report = self.runtime.ingest_batch(cache, batch)?;
11655 self.stage_or_return_acknowledgement(delivery_token, report)
11656 .await
11657 }
11658
11659 pub async fn next_ingest_with_resync(
11670 &mut self,
11671 cache: &mut EvmCache,
11672 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
11673 self.ensure_subscriber_chain(cache)?;
11674 if self.pending_checkpoint.is_some() {
11675 return Err(ReactiveEngineError::PendingCheckpointCommit);
11676 }
11677 if self.pending_acknowledgement.is_some() {
11678 return self.commit_pending_acknowledgement().await.map(Some);
11679 }
11680 let batch = self.subscriber.next_batch().await?;
11681 self.ensure_subscriber_chain(cache)?;
11682 let Some(mut batch) = batch else {
11683 return Ok(None);
11684 };
11685 let delivery_token = batch.take_delivery_token();
11686 let report = self.runtime.ingest_batch_with_resync(cache, batch)?;
11687 self.stage_or_return_acknowledgement(delivery_token, report)
11688 .await
11689 }
11690
11691 pub async fn next_ingest_checkpointed(
11722 &mut self,
11723 cache: &mut EvmCache,
11724 store: &DurableCheckpointStore,
11725 identity: &DurableCheckpointIdentity,
11726 ) -> Result<Option<CheckpointedIngest<N>>, ReactiveEngineError> {
11727 if !self.subscriber.capabilities().supports_durable_replay() {
11728 return Err(ReactiveEngineError::SubscriberNotDurable);
11729 }
11730 self.ensure_subscriber_chain(cache)?;
11731 if self.pending_acknowledgement.is_some() {
11732 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11733 }
11734 self.ensure_checkpoint_identity(cache, identity)?;
11735 if self.pending_checkpoint.is_some() {
11736 return self.commit_pending_checkpoint(cache, store).await.map(Some);
11737 }
11738
11739 let batch = self.subscriber.next_batch().await?;
11740 self.ensure_subscriber_chain(cache)?;
11741 let Some(mut batch) = batch else {
11742 return Ok(None);
11743 };
11744 if batch_preconfirmation(&batch)?.is_some() {
11745 return Err(ReactiveEngineError::PreconfirmationNotCheckpointable);
11746 }
11747 self.runtime.discard_preconfirmed_branch(cache);
11748 let delivery_witness = batch
11749 .delivery_token()
11750 .map(|_| durable_delivery_witness(&batch))
11751 .transpose()?;
11752 let delivery_token = batch.take_delivery_token();
11753 let subscriber_checkpoint = batch.take_subscriber_checkpoint();
11754 if let (Some(replay_token), Some(committed_token)) = (
11755 delivery_token.as_ref(),
11756 self.last_checkpoint_delivery_token.as_ref(),
11757 ) && replay_token == committed_token
11758 {
11759 let committed_witness = self
11760 .last_checkpoint_delivery_witness
11761 .ok_or(ReactiveEngineError::MissingReplayWitness)?;
11762 if delivery_witness != Some(committed_witness) {
11763 return Err(ReactiveEngineError::ReplayDeliveryMismatch);
11764 }
11765 self.subscriber
11766 .acknowledge_delivery(replay_token.clone())
11767 .await
11768 .map_err(ReactiveEngineError::Acknowledgement)?;
11769 return Ok(Some(CheckpointedIngest::ReplayAcknowledged));
11770 }
11771
11772 self.ensure_checkpointable_reorgs(&batch)?;
11773
11774 let incoming_block = latest_canonical_batch_block(&batch);
11775 let cache_state = EvmCacheStateSnapshot::capture(cache);
11776 let runtime_state = self.runtime.checkpoint_state();
11777 let report = match self.runtime.ingest_batch_direct(cache, batch) {
11778 Ok(report) => report,
11779 Err(error) => {
11780 cache_state.restore(cache);
11781 self.runtime.restore_transaction_state(runtime_state);
11782 return Err(error.into());
11783 }
11784 };
11785 let reports = report.reports.clone();
11786 let stage = CheckpointStage {
11787 incoming_block,
11788 delivery_token,
11789 delivery_witness,
11790 subscriber_checkpoint,
11791 staged_generation: cache.snapshot_generation(),
11792 report,
11793 };
11794 if let Err(error) = self.stage_checkpoint(identity, stage) {
11795 cache_state.restore(cache);
11796 self.runtime.restore_transaction_state(runtime_state);
11797 return Err(error);
11798 }
11799 self.runtime.dispatch_reports(&reports);
11800 self.commit_pending_checkpoint(cache, store).await.map(Some)
11801 }
11802
11803 pub async fn next_ingest_with_resync_checkpointed(
11814 &mut self,
11815 cache: &mut EvmCache,
11816 store: &DurableCheckpointStore,
11817 identity: &DurableCheckpointIdentity,
11818 ) -> Result<Option<CheckpointedIngest<N>>, ReactiveEngineError> {
11819 if !self.subscriber.capabilities().supports_durable_replay() {
11820 return Err(ReactiveEngineError::SubscriberNotDurable);
11821 }
11822 self.ensure_subscriber_chain(cache)?;
11823 if self.pending_acknowledgement.is_some() {
11824 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11825 }
11826 self.ensure_checkpoint_identity(cache, identity)?;
11827 if self.pending_checkpoint.is_some() {
11828 return self.commit_pending_checkpoint(cache, store).await.map(Some);
11829 }
11830
11831 let batch = self.subscriber.next_batch().await?;
11832 self.ensure_subscriber_chain(cache)?;
11833 let Some(mut batch) = batch else {
11834 return Ok(None);
11835 };
11836 if batch_preconfirmation(&batch)?.is_some() {
11837 return Err(ReactiveEngineError::PreconfirmationNotCheckpointable);
11838 }
11839 self.runtime.discard_preconfirmed_branch(cache);
11840 let delivery_witness = batch
11841 .delivery_token()
11842 .map(|_| durable_delivery_witness(&batch))
11843 .transpose()?;
11844 let delivery_token = batch.take_delivery_token();
11845 let subscriber_checkpoint = batch.take_subscriber_checkpoint();
11846 if let (Some(replay_token), Some(committed_token)) = (
11847 delivery_token.as_ref(),
11848 self.last_checkpoint_delivery_token.as_ref(),
11849 ) && replay_token == committed_token
11850 {
11851 let committed_witness = self
11852 .last_checkpoint_delivery_witness
11853 .ok_or(ReactiveEngineError::MissingReplayWitness)?;
11854 if delivery_witness != Some(committed_witness) {
11855 return Err(ReactiveEngineError::ReplayDeliveryMismatch);
11856 }
11857 self.subscriber
11858 .acknowledge_delivery(replay_token.clone())
11859 .await
11860 .map_err(ReactiveEngineError::Acknowledgement)?;
11861 return Ok(Some(CheckpointedIngest::ReplayAcknowledged));
11862 }
11863
11864 self.ensure_checkpointable_reorgs(&batch)?;
11865
11866 let incoming_block = latest_canonical_batch_block(&batch);
11867 let cache_state = EvmCacheStateSnapshot::capture(cache);
11868 let runtime_state = self.runtime.checkpoint_state();
11869 let report = match self.runtime.ingest_batch_with_resync_direct(cache, batch) {
11870 Ok(report) => report,
11871 Err(error) => {
11872 cache_state.restore(cache);
11873 self.runtime.restore_transaction_state(runtime_state);
11874 return Err(error.into());
11875 }
11876 };
11877 let reports = report.reports.clone();
11878 let stage = CheckpointStage {
11879 incoming_block,
11880 delivery_token,
11881 delivery_witness,
11882 subscriber_checkpoint,
11883 staged_generation: cache.snapshot_generation(),
11884 report,
11885 };
11886 if let Err(error) = self.stage_checkpoint(identity, stage) {
11887 cache_state.restore(cache);
11888 self.runtime.restore_transaction_state(runtime_state);
11889 return Err(error);
11890 }
11891 self.runtime.dispatch_reports(&reports);
11892 self.commit_pending_checkpoint(cache, store).await.map(Some)
11893 }
11894
11895 fn stage_checkpoint(
11896 &mut self,
11897 identity: &DurableCheckpointIdentity,
11898 stage: CheckpointStage<N>,
11899 ) -> Result<(), ReactiveEngineError> {
11900 let CheckpointStage {
11901 incoming_block,
11902 delivery_token,
11903 delivery_witness,
11904 subscriber_checkpoint,
11905 staged_generation,
11906 report,
11907 } = stage;
11908 if delivery_token.is_some() != delivery_witness.is_some() {
11909 return Err(ReactiveEngineError::DeliveryWitness(
11910 "delivery token and witness must be staged together".into(),
11911 ));
11912 }
11913 let runtime_checkpoint = self.runtime.durable_checkpoint_bytes()?;
11914 let block = self
11915 .runtime
11916 .last_canonical_block()
11917 .map(|block| DurableCheckpointBlock {
11918 number: block.number,
11919 hash: block.hash,
11920 parent_hash: block.parent_hash,
11921 timestamp: block.timestamp,
11922 })
11923 .or(incoming_block)
11924 .or_else(|| self.last_checkpoint_block.clone())
11925 .ok_or(ReactiveEngineError::MissingCheckpointBlock)?;
11926 let metadata = DurableCheckpointMetadata {
11927 identity: identity.clone(),
11928 block,
11929 delivery_token: delivery_token
11930 .as_ref()
11931 .or(self.last_checkpoint_delivery_token.as_ref())
11932 .map(|token| token.as_bytes().to_vec()),
11933 delivery_witness: if delivery_token.is_some() {
11934 delivery_witness
11935 } else {
11936 self.last_checkpoint_delivery_witness
11937 },
11938 subscriber_checkpoint: subscriber_checkpoint
11939 .as_ref()
11940 .or(self.last_subscriber_checkpoint.as_ref())
11941 .map(|checkpoint| checkpoint.as_bytes().to_vec()),
11942 runtime_checkpoint: Some(runtime_checkpoint),
11943 };
11944 self.pending_checkpoint = Some(PendingCheckpoint {
11945 metadata,
11946 delivery_token,
11947 report,
11948 saved_to: None,
11949 staged_generation,
11950 });
11951 Ok(())
11952 }
11953
11954 fn ensure_checkpointable_reorgs(
11955 &self,
11956 batch: &ReactiveInputBatch<N>,
11957 ) -> Result<(), ReactiveEngineError> {
11958 let state = CanonicalSequenceState::new(
11959 self.runtime
11960 .journal
11961 .iter()
11962 .map(|entry| entry.block)
11963 .collect(),
11964 self.runtime.coverage_head,
11965 self.runtime.safe_head,
11966 self.runtime.finalized_head,
11967 );
11968 match validate_canonical_sequence_internal(
11969 &state,
11970 batch,
11971 CanonicalSequenceValidationPolicy::RequireCompleteRollback,
11972 ) {
11973 Ok(_) => Ok(()),
11974 Err(CanonicalSequenceError::Invalid(error)) => Err(error.into()),
11975 Err(CanonicalSequenceError::IncompleteRollback {
11976 common_ancestor,
11977 oldest_retained,
11978 ..
11979 }) => Err(ReactiveEngineError::CheckpointReorgOutsideJournal {
11980 common_ancestor,
11981 oldest_journaled: oldest_retained,
11982 journal_depth: self.runtime.config.journal_depth,
11983 }),
11984 }
11985 }
11986
11987 async fn stage_or_return_acknowledgement(
11988 &mut self,
11989 delivery_token: Option<SubscriberDeliveryToken>,
11990 report: ReactiveBatchReport<N>,
11991 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
11992 let Some(token) = delivery_token else {
11993 return Ok(Some(report));
11994 };
11995 self.pending_acknowledgement = Some(PendingAcknowledgement { token, report });
11996 self.commit_pending_acknowledgement().await.map(Some)
11997 }
11998
11999 async fn commit_pending_acknowledgement(
12000 &mut self,
12001 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
12002 let token = self
12003 .pending_acknowledgement
12004 .as_ref()
12005 .expect("caller checked pending acknowledgement")
12006 .token
12007 .clone();
12008 self.subscriber
12009 .acknowledge_delivery(token)
12010 .await
12011 .map_err(ReactiveEngineError::Acknowledgement)?;
12012 Ok(self
12013 .pending_acknowledgement
12014 .take()
12015 .expect("pending acknowledgement remains until commit")
12016 .report)
12017 }
12018
12019 async fn commit_pending_checkpoint(
12020 &mut self,
12021 cache: &EvmCache,
12022 store: &DurableCheckpointStore,
12023 ) -> Result<CheckpointedIngest<N>, ReactiveEngineError> {
12024 let pending = self
12025 .pending_checkpoint
12026 .as_mut()
12027 .expect("caller checked pending checkpoint");
12028 let cache_generation = cache.snapshot_generation();
12029 if cache_generation != pending.staged_generation {
12030 return Err(ReactiveEngineError::PendingCheckpointCacheChanged {
12031 staged_generation: pending.staged_generation,
12032 current_generation: cache_generation,
12033 });
12034 }
12035 if pending.saved_to.as_deref() != Some(store.path()) {
12036 store
12037 .save_async(cache, pending.metadata.clone())
12038 .await
12039 .map_err(ReactiveEngineError::Checkpoint)?;
12040 pending.saved_to = Some(store.path().to_path_buf());
12041 }
12042 if let Some(token) = pending.delivery_token.clone() {
12043 self.subscriber
12044 .acknowledge_delivery(token)
12045 .await
12046 .map_err(ReactiveEngineError::Acknowledgement)?;
12047 }
12048
12049 let pending = self
12050 .pending_checkpoint
12051 .take()
12052 .expect("pending checkpoint remains until commit");
12053 self.last_checkpoint_block = Some(pending.metadata.block);
12054 self.checkpoint_identity = Some(pending.metadata.identity);
12055 self.last_checkpoint_delivery_token = pending
12056 .metadata
12057 .delivery_token
12058 .map(SubscriberDeliveryToken::new);
12059 self.last_checkpoint_delivery_witness = pending.metadata.delivery_witness;
12060 self.last_subscriber_checkpoint = pending
12061 .metadata
12062 .subscriber_checkpoint
12063 .map(SubscriberCheckpoint::new);
12064 Ok(CheckpointedIngest::Applied(pending.report))
12065 }
12066
12067 fn ensure_checkpoint_identity(
12068 &self,
12069 cache: &EvmCache,
12070 identity: &DurableCheckpointIdentity,
12071 ) -> Result<(), ReactiveEngineError> {
12072 if identity.chain_id != cache.chain_id() {
12073 return Err(ReactiveEngineError::Checkpoint(
12074 DurableCheckpointError::CacheChainMismatch {
12075 cache_chain_id: cache.chain_id(),
12076 checkpoint_chain_id: identity.chain_id,
12077 },
12078 ));
12079 }
12080 if let Some(actual) = self.checkpoint_identity.as_ref()
12081 && actual != identity
12082 {
12083 return Err(ReactiveEngineError::Checkpoint(
12084 DurableCheckpointError::IdentityMismatch {
12085 expected: identity.clone(),
12086 actual: actual.clone(),
12087 },
12088 ));
12089 }
12090 if let Some(pending) = self.pending_checkpoint.as_ref()
12091 && &pending.metadata.identity != identity
12092 {
12093 return Err(ReactiveEngineError::Checkpoint(
12094 DurableCheckpointError::IdentityMismatch {
12095 expected: identity.clone(),
12096 actual: pending.metadata.identity.clone(),
12097 },
12098 ));
12099 }
12100 Ok(())
12101 }
12102}
12103
12104fn latest_canonical_batch_block<N: Network>(
12105 batch: &ReactiveInputBatch<N>,
12106) -> Option<DurableCheckpointBlock> {
12107 let record_block = batch
12108 .records()
12109 .iter()
12110 .enumerate()
12111 .filter(|(index, _)| {
12112 batch
12113 .record_delivery_scope(*index)
12114 .is_some_and(DeliveryScope::advances_canonical_state)
12115 })
12116 .filter_map(|(_, record)| canonical_record_block(record))
12117 .max_by_key(|block| block.number)
12118 .cloned();
12119 let control_block = batch
12120 .chain_controls()
12121 .iter()
12122 .filter_map(|control| match control {
12123 ChainControl::Reorg {
12124 common_ancestor, ..
12125 } => Some(common_ancestor),
12126 ChainControl::Barrier {
12127 block: Some(block), ..
12128 }
12129 | ChainControl::CanonicalProgress(block) => Some(block),
12130 ChainControl::Safe(_)
12131 | ChainControl::Finalized(_)
12132 | ChainControl::Barrier { block: None, .. } => None,
12133 })
12134 .max_by_key(|block| block.number)
12135 .cloned();
12136
12137 record_block
12138 .into_iter()
12139 .chain(control_block)
12140 .max_by_key(|block| block.number)
12141 .map(|block| DurableCheckpointBlock {
12142 number: block.number,
12143 hash: block.hash,
12144 parent_hash: block.parent_hash,
12145 timestamp: block.timestamp,
12146 })
12147}
12148
12149impl<S, N> ReactiveEngine<S, N>
12150where
12151 N: Network,
12152 S: InterestOwnerSubscriber<N>,
12153{
12154 pub async fn register_handler(
12180 &mut self,
12181 handler: Arc<dyn ReactiveHandler<N>>,
12182 ) -> Result<(), ReactiveEngineRegisterError> {
12183 let backfill = self
12184 .runtime
12185 .last_canonical_block()
12186 .filter(|retained| {
12187 self.runtime.journal.iter().any(|entry| {
12188 optional_block_refs_are_compatible(Some(&entry.block), Some(retained))
12189 })
12190 })
12191 .map(HandlerRegistrationCatchup::CoordinatedCanonical)
12192 .unwrap_or(HandlerRegistrationCatchup::LiveOnly);
12193 self.register_handler_inner(handler, backfill).await
12194 }
12195
12196 pub async fn register_handler_with_backfill(
12215 &mut self,
12216 handler: Arc<dyn ReactiveHandler<N>>,
12217 backfill: SubscriberBackfill,
12218 ) -> Result<(), ReactiveEngineRegisterError> {
12219 self.register_handler_inner(handler, HandlerRegistrationCatchup::OwnerBackfill(backfill))
12220 .await
12221 }
12222
12223 pub async fn register_handler_live_only(
12234 &mut self,
12235 handler: Arc<dyn ReactiveHandler<N>>,
12236 ) -> Result<(), ReactiveEngineRegisterError> {
12237 self.register_handler_inner(handler, HandlerRegistrationCatchup::LiveOnly)
12238 .await
12239 }
12240
12241 async fn register_handler_inner(
12242 &mut self,
12243 handler: Arc<dyn ReactiveHandler<N>>,
12244 catchup: HandlerRegistrationCatchup,
12245 ) -> Result<(), ReactiveEngineRegisterError> {
12246 let id = handler.id();
12247 if self.runtime.contains_handler(&id) {
12248 return Err(RegisterError::DuplicateHandler(id).into());
12249 }
12250 let interests = handler.interests();
12251
12252 if let HandlerRegistrationCatchup::OwnerBackfill(backfill) = &catchup {
12253 let retained_anchor = backfill.retained_anchor().copied();
12254 let is_exact_retained_block = retained_anchor.is_some_and(|anchor| {
12255 backfill.start_block() == anchor.number
12256 && backfill.end_block() == Some(anchor.number)
12257 && self.runtime.journal.iter().any(|entry| {
12258 optional_block_refs_are_compatible(Some(&entry.block), Some(&anchor))
12259 })
12260 });
12261 if !is_exact_retained_block {
12262 return Err(ReactiveEngineRegisterError::BackfillOutsideJournal {
12263 start_block: backfill.start_block(),
12264 end_block: backfill.end_block(),
12265 retained_anchor,
12266 });
12267 }
12268 }
12269
12270 let subscribed = match catchup {
12271 HandlerRegistrationCatchup::OwnerBackfill(backfill) => {
12272 self.subscriber
12273 .add_interest_owner_with_backfill(id.clone(), &interests, backfill)
12274 .await
12275 }
12276 HandlerRegistrationCatchup::CoordinatedCanonical(retained) => {
12277 self.subscriber
12278 .add_interest_owner_with_canonical_catchup(id.clone(), &interests, retained)
12279 .await
12280 }
12281 HandlerRegistrationCatchup::LiveOnly => {
12282 self.subscriber
12283 .add_interest_owner(id.clone(), &interests)
12284 .await
12285 }
12286 };
12287 if let Err(error) = subscribed {
12288 return Err(error.into());
12289 }
12290
12291 self.runtime
12296 .registry
12297 .insert_handler_prepared(id, handler, interests);
12298 Ok(())
12299 }
12300
12301 pub async fn sync_handler_interests(&mut self) -> Result<(), SubscriberError> {
12326 let owners = self
12327 .runtime
12328 .handler_ids()
12329 .into_iter()
12330 .map(|id| {
12331 let interests = self
12332 .runtime
12333 .handler_interests(&id)
12334 .map(<[ReactiveInterest<N>]>::to_vec)
12335 .unwrap_or_default();
12336 (id, interests)
12337 })
12338 .collect();
12339 self.subscriber.replace_interest_owners(owners).await
12340 }
12341
12342 pub async fn sync_handler_interests_with_backfill(&mut self) -> Result<(), SubscriberError> {
12370 let baseline =
12371 self.runtime
12372 .last_canonical_block()
12373 .ok_or(SubscriberError::InvalidConfig(
12374 "cannot continuity-sync handlers before a canonical runtime position exists",
12375 ))?;
12376 let backfill = SubscriberBackfill::after_canonical_block(baseline)?;
12377 let owners = self
12378 .runtime
12379 .handler_ids()
12380 .into_iter()
12381 .map(|id| {
12382 let interests = self
12383 .runtime
12384 .handler_interests(&id)
12385 .map(<[ReactiveInterest<N>]>::to_vec)
12386 .unwrap_or_default();
12387 (id, interests)
12388 })
12389 .collect();
12390 self.subscriber
12391 .replace_interest_owners_with_global_backfill(owners, backfill)
12392 .await
12393 }
12394
12395 pub async fn unregister_handler(
12427 &mut self,
12428 id: &HandlerId,
12429 ) -> Result<Option<Arc<dyn ReactiveHandler<N>>>, SubscriberError> {
12430 self.subscriber.remove_interest_owner(id).await?;
12431 Ok(self.runtime.unregister_handler(id))
12432 }
12433}
12434
12435type FlashblockReconnectFuture<N> = Pin<
12436 Box<
12437 dyn Future<
12438 Output = (
12439 SubscriberStreamSource,
12440 Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>,
12441 ),
12442 > + Send,
12443 >,
12444>;
12445
12446pub struct AlloySubscriber<P, N: Network = Ethereum> {
12467 provider: P,
12468 #[cfg(feature = "raw-flashblocks-json")]
12471 external_flashblocks_provider: Option<ProviderRef>,
12472 #[cfg(feature = "raw-flashblocks-json")]
12474 external_flashblock_updates:
12475 Option<tokio::sync::mpsc::Receiver<raw_json_flashblocks::QueuedFlashblockUpdate>>,
12476 #[cfg(feature = "raw-flashblocks-json")]
12478 external_flashblock_update_channel_opened: bool,
12479 #[cfg(feature = "raw-flashblocks-json")]
12481 rejected_external_flashblock_generation: Option<u64>,
12482 #[cfg(feature = "raw-flashblocks-json")]
12485 last_external_flashblock_snapshot: Option<FlashblockSnapshot>,
12486 flashblocks_state_provider: Option<P>,
12490 provider_ref: Option<ProviderRef>,
12493 log_verification_provider: Option<P>,
12497 chain_id: Option<u64>,
12499 mode: SubscriberMode,
12500 config: SubscriberConfig,
12501 base_interests: Vec<ReactiveInterest<N>>,
12502 owned_interests: Vec<OwnedSubscriberInterests<N>>,
12503 next_owner_epoch: u64,
12504 interests: Vec<ReactiveInterest<N>>,
12505 log_source_ids: HashMap<Filter, usize>,
12510 next_log_source_id: usize,
12511 pending_backfills: VecDeque<QueuedSubscriberBackfill>,
12512 pending_source_backfills: VecDeque<SubscriberStreamSource>,
12517 sources_dirty: bool,
12520 stream_revision: u64,
12523 state: AlloySubscriberState<N>,
12524 pending_records: VecDeque<SubscriberInputRecord<N>>,
12525 pending_chain_controls: VecDeque<ChainControl>,
12526 pending_reconcile_owner_records: VecDeque<BufferedSubscriberOwnerRecord<N>>,
12531 resource_error: Option<String>,
12534 last_seen_log_blocks: HashMap<usize, u64>,
12535 verified_log_blocks: HashMap<(u64, B256), BlockRef>,
12536 verified_log_block_order: VecDeque<(u64, B256)>,
12537 recent_input_refs: VecDeque<InputRef>,
12538 recent_input_ref_set: HashSet<InputRef>,
12539 recent_owner_input_refs: HashMap<SubscriberOwnerEpoch, VecDeque<InputRef>>,
12540 recent_owner_input_ref_sets: HashMap<SubscriberOwnerEpoch, HashSet<InputRef>>,
12541 recent_compat_owner_input_refs: HashMap<HandlerId, VecDeque<InputRef>>,
12542 recent_compat_owner_input_ref_sets: HashMap<HandlerId, HashSet<InputRef>>,
12543 base_flashblock_header: Option<(FixedBytes<8>, BaseFlashblockBase)>,
12544 base_flashblock_transactions: Option<(FixedBytes<8>, u64, Vec<B256>, Vec<B256>)>,
12545 unmatched_pending_logs: VecDeque<(usize, Log, FlashblockIngressTiming)>,
12546 latest_preconfirmation: Option<FlashblockRef>,
12547 preconfirmed_seen_logs: HashSet<(B256, u64)>,
12548 preconfirmed_receipted_transactions: HashSet<B256>,
12552 preconfirmed_unavailable_receipts: HashSet<B256>,
12557 last_certified_canonical_head: Option<BlockRef>,
12558 pending_preconfirmation_invalidation: bool,
12559 pending_flashblock_reconnects: FuturesUnordered<FlashblockReconnectFuture<N>>,
12560 pending_flashblock_reconnect_sources: Vec<SubscriberStreamSource>,
12561 flashblocks_rpc_metrics: FlashblocksRpcMetrics,
12562 consecutive_flashblock_poll_failures: usize,
12563 flashblock_rpc_request_times: VecDeque<Instant>,
12564 _network: PhantomData<N>,
12565}
12566
12567struct OwnedSubscriberInterests<N: Network = Ethereum> {
12568 owner: HandlerId,
12569 interests: Vec<ReactiveInterest<N>>,
12570 epoch: Option<SubscriberOwnerEpoch>,
12571 state: SubscriberOwnerState,
12572 baseline: Option<BlockRef>,
12573 progress: Option<SubscriberOwnerProgress>,
12574 progress_stream_revision: Option<u64>,
12575}
12576
12577#[derive(Clone)]
12578struct SubscriberOwnerReconcilePlan<N: Network = Ethereum> {
12579 epoch: SubscriberOwnerEpoch,
12580 interests: Vec<ReactiveInterest<N>>,
12581 retained: BlockRef,
12582 from_block: u64,
12583}
12584
12585struct SubscriberOwnerCatchup {
12586 logs: Vec<Log>,
12587 certified: BlockRef,
12588}
12589
12590#[derive(Clone, Copy)]
12591struct SubscriberOwnerCatchupOptions {
12592 target_preverified: bool,
12593 max_logs: usize,
12594 max_log_bytes: usize,
12595 max_requests_in_flight: usize,
12596}
12597
12598struct SubscriberOwnerReconcileFilter {
12599 filter: Filter,
12600 from_block: u64,
12601}
12602
12603struct BufferedSubscriberOwnerRecord<N: Network = Ethereum> {
12604 record: ReactiveInputRecord<N>,
12605 owners: Vec<SubscriberOwnerEpoch>,
12606}
12607
12608const OWNER_RECONCILE_FILTERS_PER_CHUNK: usize = 256;
12609
12610struct QueuedSubscriberBackfill {
12611 owner: Option<HandlerId>,
12614 epoch: Option<SubscriberOwnerEpoch>,
12615 filters: Vec<Filter>,
12617 backfill: SubscriberBackfill,
12618}
12619
12620#[cfg(feature = "reactive-ws")]
12626fn ensure_ring_crypto_provider() {
12627 use std::sync::Once;
12628 static INSTALL: Once = Once::new();
12629 INSTALL.call_once(|| {
12630 let _ = rustls::crypto::ring::default_provider().install_default();
12631 });
12632}
12633
12634impl<P, N: Network> AlloySubscriber<P, N> {
12635 pub fn new(provider: P, mode: SubscriberMode, config: SubscriberConfig) -> Self {
12637 #[cfg(feature = "reactive-ws")]
12638 ensure_ring_crypto_provider();
12639 Self {
12640 provider,
12641 #[cfg(feature = "raw-flashblocks-json")]
12642 external_flashblocks_provider: None,
12643 #[cfg(feature = "raw-flashblocks-json")]
12644 external_flashblock_updates: None,
12645 #[cfg(feature = "raw-flashblocks-json")]
12646 external_flashblock_update_channel_opened: false,
12647 #[cfg(feature = "raw-flashblocks-json")]
12648 rejected_external_flashblock_generation: None,
12649 #[cfg(feature = "raw-flashblocks-json")]
12650 last_external_flashblock_snapshot: None,
12651 flashblocks_state_provider: None,
12652 provider_ref: None,
12653 log_verification_provider: None,
12654 chain_id: None,
12655 mode,
12656 config,
12657 base_interests: Vec::new(),
12658 owned_interests: Vec::new(),
12659 next_owner_epoch: 0,
12660 interests: Vec::new(),
12661 log_source_ids: HashMap::new(),
12662 next_log_source_id: 0,
12663 pending_backfills: VecDeque::new(),
12664 pending_source_backfills: VecDeque::new(),
12665 sources_dirty: true,
12666 stream_revision: 0,
12667 state: AlloySubscriberState::Uninitialized,
12668 pending_records: VecDeque::new(),
12669 pending_chain_controls: VecDeque::new(),
12670 pending_reconcile_owner_records: VecDeque::new(),
12671 resource_error: None,
12672 last_seen_log_blocks: HashMap::new(),
12673 verified_log_blocks: HashMap::new(),
12674 verified_log_block_order: VecDeque::new(),
12675 recent_input_refs: VecDeque::new(),
12676 recent_input_ref_set: HashSet::new(),
12677 recent_owner_input_refs: HashMap::new(),
12678 recent_owner_input_ref_sets: HashMap::new(),
12679 recent_compat_owner_input_refs: HashMap::new(),
12680 recent_compat_owner_input_ref_sets: HashMap::new(),
12681 base_flashblock_header: None,
12682 base_flashblock_transactions: None,
12683 unmatched_pending_logs: VecDeque::new(),
12684 latest_preconfirmation: None,
12685 preconfirmed_seen_logs: HashSet::new(),
12686 preconfirmed_receipted_transactions: HashSet::new(),
12687 preconfirmed_unavailable_receipts: HashSet::new(),
12688 last_certified_canonical_head: None,
12689 pending_preconfirmation_invalidation: false,
12690 pending_flashblock_reconnects: FuturesUnordered::new(),
12691 pending_flashblock_reconnect_sources: Vec::new(),
12692 flashblocks_rpc_metrics: FlashblocksRpcMetrics::default(),
12693 consecutive_flashblock_poll_failures: 0,
12694 flashblock_rpc_request_times: VecDeque::new(),
12695 _network: PhantomData,
12696 }
12697 }
12698
12699 pub fn provider(&self) -> &P {
12701 &self.provider
12702 }
12703
12704 #[must_use]
12708 pub fn with_provider_ref(mut self, provider: ProviderRef) -> Self {
12709 self.provider_ref = Some(provider);
12710 self
12711 }
12712
12713 #[cfg(feature = "raw-flashblocks-json")]
12739 pub fn configure_external_flashblock_updates(
12740 &mut self,
12741 provider: ProviderRef,
12742 ) -> Result<(), SubscriberError> {
12743 if self.external_flashblocks_provider.is_some() {
12744 return Err(SubscriberError::InvalidConfig(
12745 "external Flashblock updates were already configured",
12746 ));
12747 }
12748 if self.external_flashblock_update_channel_opened
12749 || self.external_flashblock_updates.is_some()
12750 || self.chain_id.is_some()
12751 || !self.base_interests.is_empty()
12752 || !self.owned_interests.is_empty()
12753 || !self.interests.is_empty()
12754 || !self.pending_records.is_empty()
12755 || !self.pending_chain_controls.is_empty()
12756 || !self.pending_backfills.is_empty()
12757 || !matches!(self.state, AlloySubscriberState::Uninitialized)
12758 {
12759 return Err(SubscriberError::InvalidConfig(
12760 "external Flashblock updates must be configured before subscriber registration",
12761 ));
12762 }
12763 self.external_flashblocks_provider = Some(provider);
12764 Ok(())
12765 }
12766
12767 #[cfg(feature = "raw-flashblocks-json")]
12782 pub fn open_external_flashblock_update_channel(
12783 &mut self,
12784 capacity: usize,
12785 ) -> Result<FlashblockUpdateSender, SubscriberError> {
12786 if capacity == 0 {
12787 return Err(SubscriberError::InvalidConfig(
12788 "external Flashblock update channel capacity must be greater than zero",
12789 ));
12790 }
12791 let provider = self.external_flashblocks_provider.clone().ok_or(
12792 SubscriberError::InvalidConfig(
12793 "external Flashblock update channel requires configure_external_flashblock_updates",
12794 ),
12795 )?;
12796 if self.external_flashblock_update_channel_opened {
12797 return Err(SubscriberError::InvalidConfig(
12798 "external Flashblock update channel was already opened",
12799 ));
12800 }
12801 let (sender, receiver) =
12802 raw_json_flashblocks::flashblock_update_channel(provider, capacity);
12803 self.external_flashblock_updates = Some(receiver);
12804 self.external_flashblock_update_channel_opened = true;
12805 self.sources_dirty = true;
12806 Ok(sender)
12807 }
12808
12809 fn uses_external_flashblock_updates(&self) -> bool {
12810 #[cfg(feature = "raw-flashblocks-json")]
12811 {
12812 self.external_flashblocks_provider.is_some()
12813 }
12814 #[cfg(not(feature = "raw-flashblocks-json"))]
12815 {
12816 false
12817 }
12818 }
12819
12820 #[must_use]
12828 pub fn with_flashblocks_state_provider(mut self, provider: P) -> Self {
12829 self.flashblocks_state_provider = Some(provider);
12830 self
12831 }
12832
12833 #[must_use]
12840 pub fn with_log_verification_provider(mut self, provider: P) -> Self {
12841 self.log_verification_provider = Some(provider);
12842 self
12843 }
12844
12845 pub fn mode(&self) -> SubscriberMode {
12847 self.mode
12848 }
12849
12850 pub fn config(&self) -> &SubscriberConfig {
12852 &self.config
12853 }
12854
12855 pub const fn flashblocks_rpc_metrics(&self) -> FlashblocksRpcMetrics {
12858 self.flashblocks_rpc_metrics
12859 }
12860
12861 pub fn registered_interests(&self) -> &[ReactiveInterest<N>] {
12863 &self.interests
12864 }
12865
12866 pub fn stage_interest_owner(
12883 &mut self,
12884 owner: HandlerId,
12885 interests: &[ReactiveInterest<N>],
12886 start: SubscriberOwnerStart,
12887 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
12888 validate_subscriber_config(&self.config)?;
12889 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
12890 && interests
12891 .iter()
12892 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
12893 {
12894 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
12895 }
12896 if self
12897 .owned_interests
12898 .iter()
12899 .any(|entry| entry.owner == owner)
12900 {
12901 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
12902 }
12903
12904 let mut next_owned = self.clone_owned_interests();
12905 next_owned.push(OwnedSubscriberInterests {
12906 owner: owner.clone(),
12907 interests: interests.to_vec(),
12908 epoch: None,
12909 state: SubscriberOwnerState::Staged,
12910 baseline: None,
12911 progress: None,
12912 progress_stream_revision: None,
12913 });
12914 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
12915 validate_supported_interests(self.mode, &self.config, &next_registered)?;
12916
12917 let baseline = match start {
12918 SubscriberOwnerStart::Live => None,
12919 SubscriberOwnerStart::PostBlock(block) => {
12920 block
12921 .number
12922 .checked_add(1)
12923 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
12924 Some(block)
12925 }
12926 };
12927 let sequence = self
12928 .next_owner_epoch
12929 .checked_add(1)
12930 .ok_or(SubscriberOwnerError::EpochExhausted)?;
12931 let epoch = SubscriberOwnerEpoch {
12932 owner: owner.clone(),
12933 sequence,
12934 };
12935
12936 self.next_owner_epoch = sequence;
12937 let entry = next_owned
12938 .last_mut()
12939 .expect("staged owner was appended during preflight");
12940 entry.epoch = Some(epoch.clone());
12941 entry.baseline = baseline;
12942 self.owned_interests = next_owned;
12943 self.interests = next_registered;
12944 self.sources_dirty = true;
12945
12946 Ok(epoch)
12947 }
12948
12949 pub fn stage_interest_owner_replacement(
12963 &mut self,
12964 owner: HandlerId,
12965 interests: &[ReactiveInterest<N>],
12966 start: SubscriberOwnerStart,
12967 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
12968 validate_subscriber_config(&self.config)?;
12969 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
12970 && interests
12971 .iter()
12972 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
12973 {
12974 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
12975 }
12976 let active_count = self
12977 .owned_interests
12978 .iter()
12979 .filter(|entry| {
12980 entry.owner == owner
12981 && entry.state == SubscriberOwnerState::Active
12982 && entry.epoch.is_some()
12983 })
12984 .count();
12985 if active_count != 1
12986 || self
12987 .owned_interests
12988 .iter()
12989 .any(|entry| entry.owner == owner && entry.state != SubscriberOwnerState::Active)
12990 {
12991 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
12992 }
12993
12994 let mut next_owned = self.clone_owned_interests();
12995 next_owned.push(OwnedSubscriberInterests {
12996 owner: owner.clone(),
12997 interests: interests.to_vec(),
12998 epoch: None,
12999 state: SubscriberOwnerState::Staged,
13000 baseline: None,
13001 progress: None,
13002 progress_stream_revision: None,
13003 });
13004 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
13005 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13006
13007 let baseline = match start {
13008 SubscriberOwnerStart::Live => None,
13009 SubscriberOwnerStart::PostBlock(block) => {
13010 block
13011 .number
13012 .checked_add(1)
13013 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
13014 Some(block)
13015 }
13016 };
13017 let sequence = self
13018 .next_owner_epoch
13019 .checked_add(1)
13020 .ok_or(SubscriberOwnerError::EpochExhausted)?;
13021 let epoch = SubscriberOwnerEpoch {
13022 owner: owner.clone(),
13023 sequence,
13024 };
13025
13026 self.next_owner_epoch = sequence;
13027 let entry = next_owned
13028 .last_mut()
13029 .expect("staged replacement owner was appended during preflight");
13030 entry.epoch = Some(epoch.clone());
13031 entry.baseline = baseline;
13032 self.owned_interests = next_owned;
13033 self.interests = next_registered;
13034 self.sources_dirty = true;
13035 Ok(epoch)
13036 }
13037
13038 pub fn interest_owner_state(
13040 &self,
13041 epoch: &SubscriberOwnerEpoch,
13042 ) -> Option<SubscriberOwnerState> {
13043 self.owned_interests
13044 .iter()
13045 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13046 .map(|entry| entry.state)
13047 }
13048
13049 pub fn interest_owner_progress(
13051 &self,
13052 epoch: &SubscriberOwnerEpoch,
13053 ) -> Option<&SubscriberOwnerProgress> {
13054 self.owned_interests
13055 .iter()
13056 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13057 .and_then(|entry| entry.progress.as_ref())
13058 }
13059
13060 pub fn activate_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
13064 let stream_revision = self.stream_revision;
13065 let sources_dirty = self.sources_dirty;
13066 let Some(entry) = self
13067 .owned_interests
13068 .iter_mut()
13069 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13070 else {
13071 return false;
13072 };
13073 if entry.state != SubscriberOwnerState::Staged
13074 || (entry.baseline.is_some()
13075 && (entry.progress.is_none()
13076 || entry.progress_stream_revision != Some(stream_revision)
13077 || sources_dirty))
13078 {
13079 return false;
13080 }
13081 entry.state = SubscriberOwnerState::Active;
13082 true
13083 }
13084
13085 pub fn commit_interest_owner_replacement(
13087 &mut self,
13088 active: &SubscriberOwnerEpoch,
13089 replacement: &SubscriberOwnerEpoch,
13090 ) -> bool {
13091 let Some(active_index) = self
13092 .owned_interests
13093 .iter()
13094 .position(|entry| entry.epoch.as_ref() == Some(active))
13095 else {
13096 return false;
13097 };
13098 let Some(replacement_index) = self
13099 .owned_interests
13100 .iter()
13101 .position(|entry| entry.epoch.as_ref() == Some(replacement))
13102 else {
13103 return false;
13104 };
13105 if active_index == replacement_index
13106 || active.owner() != replacement.owner()
13107 || self.owned_interests[active_index].state != SubscriberOwnerState::Active
13108 || self.owned_interests[replacement_index].state != SubscriberOwnerState::Staged
13109 || (self.owned_interests[replacement_index].baseline.is_some()
13110 && (self.owned_interests[replacement_index].progress.is_none()
13111 || self.owned_interests[replacement_index].progress_stream_revision
13112 != Some(self.stream_revision)
13113 || self.sources_dirty))
13114 {
13115 return false;
13116 }
13117
13118 self.owned_interests[replacement_index].state = SubscriberOwnerState::Active;
13119 self.owned_interests.remove(active_index);
13120 self.purge_owner_epoch(active);
13121 self.rebuild_registered_interests();
13122 self.retire_unreferenced_filters();
13123 self.sources_dirty = true;
13124 true
13125 }
13126
13127 pub fn prepare_interest_owner_removal(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
13136 let Some(entry) = self
13137 .owned_interests
13138 .iter_mut()
13139 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13140 else {
13141 return false;
13142 };
13143 if entry.state != SubscriberOwnerState::Active {
13144 return false;
13145 }
13146 entry.state = SubscriberOwnerState::Removing;
13147 true
13148 }
13149
13150 pub fn finalize_interest_owner_removal(
13156 &mut self,
13157 epoch: &SubscriberOwnerEpoch,
13158 ) -> Option<Vec<ReactiveInterest<N>>> {
13159 let index = self.owned_interests.iter().position(|entry| {
13160 entry.epoch.as_ref() == Some(epoch) && entry.state == SubscriberOwnerState::Removing
13161 })?;
13162 let removed = self.owned_interests.remove(index).interests;
13163 self.purge_owner_epoch(epoch);
13164 self.rebuild_registered_interests();
13165 self.retire_unreferenced_filters();
13166 self.sources_dirty = true;
13167 Some(removed)
13168 }
13169
13170 pub fn abort_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
13176 let Some(index) = self
13177 .owned_interests
13178 .iter()
13179 .position(|entry| entry.epoch.as_ref() == Some(epoch))
13180 else {
13181 return false;
13182 };
13183 match self.owned_interests[index].state {
13184 SubscriberOwnerState::Staged => {
13185 self.owned_interests.remove(index);
13186 self.purge_owner_epoch(epoch);
13187 self.rebuild_registered_interests();
13188 self.retire_unreferenced_filters();
13189 self.sources_dirty = true;
13190 true
13191 }
13192 SubscriberOwnerState::Removing => {
13193 self.owned_interests[index].state = SubscriberOwnerState::Active;
13194 true
13195 }
13196 SubscriberOwnerState::Active => false,
13197 }
13198 }
13199
13200 fn purge_owner_epoch(&mut self, epoch: &SubscriberOwnerEpoch) {
13201 self.pending_backfills
13202 .retain(|backfill| backfill.epoch.as_ref() != Some(epoch));
13203 self.pending_records
13204 .retain_mut(|pending| match &mut pending.scope {
13205 SubscriberInputScope::Canonical { owners }
13206 | SubscriberInputScope::CanonicalResidual { owners, .. } => {
13207 owners.retain(|owner| owner != epoch);
13208 true
13209 }
13210 SubscriberInputScope::OwnerOnly { owners } => {
13211 owners.retain(|owner| owner != epoch);
13212 !owners.is_empty()
13213 }
13214 SubscriberInputScope::OwnerOnlyHandlers { .. }
13215 | SubscriberInputScope::Preconfirmed => true,
13216 });
13217 self.pending_reconcile_owner_records.retain_mut(|pending| {
13218 pending.owners.retain(|owner| owner != epoch);
13219 !pending.owners.is_empty()
13220 });
13221 self.recent_owner_input_refs.remove(epoch);
13222 self.recent_owner_input_ref_sets.remove(epoch);
13223 }
13224
13225 pub fn upsert_interest_owners(
13233 &mut self,
13234 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13235 ) -> Result<(), SubscriberError> {
13236 self.upsert_interest_owners_inner(owners, None)
13237 }
13238
13239 pub fn upsert_interest_owners_with_backfill(
13248 &mut self,
13249 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13250 backfill: SubscriberBackfill,
13251 ) -> Result<(), SubscriberError> {
13252 self.upsert_interest_owners_inner(owners, Some(backfill))
13253 }
13254
13255 fn upsert_interest_owners_inner(
13256 &mut self,
13257 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13258 explicit_backfill: Option<SubscriberBackfill>,
13259 ) -> Result<(), SubscriberError> {
13260 validate_subscriber_config(&self.config)?;
13261 let mut seen = HashSet::with_capacity(owners.len());
13262 let mut next_owned = self.clone_owned_interests();
13263 for (owner, interests) in &owners {
13264 if !seen.insert(owner.clone()) {
13265 return Err(SubscriberError::InvalidConfig(
13266 "bulk owner upsert contains a duplicate owner",
13267 ));
13268 }
13269 if self
13270 .owned_interests
13271 .iter()
13272 .any(|entry| &entry.owner == owner && entry.epoch.is_some())
13273 {
13274 return Err(SubscriberError::InvalidConfig(
13275 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
13276 ));
13277 }
13278 if let Some(entry) = next_owned.iter_mut().find(|entry| &entry.owner == owner) {
13279 entry.interests = interests.clone();
13280 entry.state = SubscriberOwnerState::Active;
13281 entry.baseline = None;
13282 entry.progress = None;
13283 entry.progress_stream_revision = None;
13284 } else {
13285 next_owned.push(OwnedSubscriberInterests {
13286 owner: owner.clone(),
13287 interests: interests.clone(),
13288 epoch: None,
13289 state: SubscriberOwnerState::Active,
13290 baseline: None,
13291 progress: None,
13292 progress_stream_revision: None,
13293 });
13294 }
13295 }
13296 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
13297 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13298
13299 let mut replacement_backfills = Vec::new();
13304 for (owner, interests) in &owners {
13305 let previous_filters: Vec<Filter> = self
13306 .owner_interests(owner)
13307 .map(log_filters)
13308 .unwrap_or_default();
13309 let continuity_anchor = previous_filters
13310 .iter()
13311 .filter_map(|filter| self.log_anchor(filter))
13312 .min();
13313 let filters = log_filters(interests);
13314 if let Some(backfill) = explicit_backfill
13315 && !filters.is_empty()
13316 {
13317 replacement_backfills.push(QueuedSubscriberBackfill {
13318 owner: Some(owner.clone()),
13319 epoch: None,
13320 filters: filters.clone(),
13321 backfill,
13322 });
13323 }
13324 let explicit_covers = explicit_backfill.is_some_and(|explicit| {
13325 explicit.end_block().is_none()
13326 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
13327 });
13328 let continuity_filters: Vec<_> = filters
13329 .into_iter()
13330 .filter(|filter| !previous_filters.contains(filter))
13331 .collect();
13332 if let Some(anchor) = continuity_anchor
13333 && !continuity_filters.is_empty()
13334 && !explicit_covers
13335 {
13336 replacement_backfills.push(QueuedSubscriberBackfill {
13337 owner: Some(owner.clone()),
13338 epoch: None,
13339 filters: continuity_filters,
13340 backfill: SubscriberBackfill::from_block(anchor),
13341 });
13342 }
13343 }
13344
13345 let retained_backfills = self
13346 .pending_backfills
13347 .iter()
13348 .filter(|queued| {
13349 queued
13350 .owner
13351 .as_ref()
13352 .is_none_or(|owner| !seen.contains(owner))
13353 })
13354 .map(|queued| queued.filters.len())
13355 .sum::<usize>();
13356 let replacement_units = replacement_backfills
13357 .iter()
13358 .map(|queued| queued.filters.len())
13359 .sum::<usize>();
13360 if retained_backfills.saturating_add(replacement_units) > self.config.max_pending_backfills
13361 {
13362 return Err(SubscriberError::ResourceExhausted(format!(
13363 "bulk owner update would queue more than {} lazy backfills",
13364 self.config.max_pending_backfills
13365 )));
13366 }
13367
13368 self.owned_interests = next_owned;
13372 self.interests = next_registered;
13373 for owner in &seen {
13374 self.recent_compat_owner_input_refs.remove(owner);
13375 self.recent_compat_owner_input_ref_sets.remove(owner);
13376 }
13377 self.retire_unreferenced_filters();
13378 self.sources_dirty = true;
13379 self.pending_backfills.retain(|queued| {
13380 queued
13381 .owner
13382 .as_ref()
13383 .is_none_or(|owner| !seen.contains(owner))
13384 });
13385 self.pending_backfills.extend(replacement_backfills);
13386 Ok(())
13387 }
13388
13389 pub fn replace_interest_owners(
13398 &mut self,
13399 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13400 ) -> Result<(), SubscriberError> {
13401 self.replace_interest_owners_inner(owners, None)
13402 }
13403
13404 pub fn replace_interest_owners_with_global_backfill(
13417 &mut self,
13418 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13419 backfill: SubscriberBackfill,
13420 ) -> Result<(), SubscriberError> {
13421 self.replace_interest_owners_inner(owners, Some(backfill))
13422 }
13423
13424 fn replace_interest_owners_inner(
13425 &mut self,
13426 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13427 backfill: Option<SubscriberBackfill>,
13428 ) -> Result<(), SubscriberError> {
13429 validate_subscriber_config(&self.config)?;
13430 if self
13431 .owned_interests
13432 .iter()
13433 .any(|entry| entry.epoch.is_some())
13434 {
13435 return Err(SubscriberError::InvalidConfig(
13436 "cannot replace compatibility owners while an epoch-scoped lifecycle exists",
13437 ));
13438 }
13439
13440 let mut seen = HashSet::with_capacity(owners.len());
13441 let mut next_owned = Vec::with_capacity(owners.len());
13442 for (owner, interests) in owners {
13443 if !seen.insert(owner.clone()) {
13444 return Err(SubscriberError::InvalidConfig(
13445 "owner replacement contains a duplicate owner",
13446 ));
13447 }
13448 next_owned.push(OwnedSubscriberInterests {
13449 owner,
13450 interests,
13451 epoch: None,
13452 state: SubscriberOwnerState::Active,
13453 baseline: None,
13454 progress: None,
13455 progress_stream_revision: None,
13456 });
13457 }
13458 let next_registered = aggregate_interests(&[], &next_owned);
13459 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13460 let mut filters = log_filters(&next_registered);
13461 let mut unique_filters = Vec::with_capacity(filters.len());
13462 for filter in filters.drain(..) {
13463 if !unique_filters.contains(&filter) {
13464 unique_filters.push(filter);
13465 }
13466 }
13467 let replacement_backfills: VecDeque<_> = match backfill {
13468 Some(backfill) if !unique_filters.is_empty() => {
13469 VecDeque::from([QueuedSubscriberBackfill {
13470 owner: None,
13471 epoch: None,
13472 filters: unique_filters,
13473 backfill,
13474 }])
13475 }
13476 Some(_) | None => VecDeque::new(),
13477 };
13478 let replacement_units = replacement_backfills
13479 .iter()
13480 .map(|queued| queued.filters.len())
13481 .sum::<usize>();
13482 if replacement_units > self.config.max_pending_backfills {
13483 return Err(SubscriberError::ResourceExhausted(format!(
13484 "owner replacement would queue more than {} lazy backfills",
13485 self.config.max_pending_backfills
13486 )));
13487 }
13488
13489 let revoke_preconfirmation = self.latest_preconfirmation.is_some()
13494 || self.pending_preconfirmation_invalidation
13495 || self.pending_records.iter().any(|record| {
13496 record.scope == SubscriberInputScope::Preconfirmed
13497 || matches!(
13498 &record.record.context.chain_status,
13499 ChainStatus::Preconfirmed { .. }
13500 )
13501 });
13502 self.base_interests.clear();
13503 self.owned_interests = next_owned;
13504 self.interests = next_registered;
13505 self.reset_delivery_state();
13506 self.pending_preconfirmation_invalidation = revoke_preconfirmation;
13507 self.pending_backfills = replacement_backfills;
13508 self.reset_stream_topology();
13509 Ok(())
13510 }
13511
13512 pub fn add_interest_owner(
13535 &mut self,
13536 owner: HandlerId,
13537 interests: &[ReactiveInterest<N>],
13538 ) -> Result<(), SubscriberError> {
13539 self.set_interest_owner(owner, interests, None)
13540 }
13541
13542 pub fn add_interest_owner_with_backfill(
13561 &mut self,
13562 owner: HandlerId,
13563 interests: &[ReactiveInterest<N>],
13564 backfill: SubscriberBackfill,
13565 ) -> Result<(), SubscriberError> {
13566 self.set_interest_owner(owner, interests, Some(backfill))
13567 }
13568
13569 pub fn add_interest_owner_with_canonical_catchup(
13588 &mut self,
13589 owner: HandlerId,
13590 interests: &[ReactiveInterest<N>],
13591 retained: BlockRef,
13592 ) -> Result<(), SubscriberError> {
13593 validate_subscriber_config(&self.config)?;
13594 if self
13595 .owned_interests
13596 .iter()
13597 .any(|entry| entry.owner == owner && entry.epoch.is_some())
13598 {
13599 return Err(SubscriberError::InvalidConfig(
13600 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
13601 ));
13602 }
13603
13604 let mut next_owned = self.clone_owned_interests();
13605 if let Some(entry) = next_owned.iter_mut().find(|entry| entry.owner == owner) {
13606 entry.interests = interests.to_vec();
13607 entry.state = SubscriberOwnerState::Active;
13608 entry.baseline = None;
13609 entry.progress = None;
13610 entry.progress_stream_revision = None;
13611 entry.epoch = None;
13612 } else {
13613 next_owned.push(OwnedSubscriberInterests {
13614 owner: owner.clone(),
13615 interests: interests.to_vec(),
13616 epoch: None,
13617 state: SubscriberOwnerState::Active,
13618 baseline: None,
13619 progress: None,
13620 progress_stream_revision: None,
13621 });
13622 }
13623 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
13624 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13625 if next_registered
13626 .iter()
13627 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
13628 {
13629 return Err(SubscriberError::Unsupported(
13630 "Alloy coordinated registration supports log-only interest topologies",
13631 ));
13632 }
13633
13634 let mut owner_filters = Vec::new();
13635 for filter in log_filters(interests) {
13636 if !owner_filters.contains(&filter) {
13637 owner_filters.push(filter);
13638 }
13639 }
13640 let mut global_filters = Vec::new();
13641 for filter in log_filters(&next_registered) {
13642 if !global_filters.contains(&filter) {
13643 global_filters.push(filter);
13644 }
13645 }
13646 let owner_backfill =
13647 SubscriberBackfill::from_canonical_block_through(retained, retained.number)?;
13648 let global_backfill = SubscriberBackfill::after_canonical_block(retained)?;
13649 let replacement_units = owner_filters.len().saturating_add(global_filters.len());
13650 let retained_units = self
13651 .pending_backfills
13652 .iter()
13653 .filter(|queued| queued.owner.as_ref() != Some(&owner))
13654 .map(|queued| queued.filters.len())
13655 .sum::<usize>();
13656 if retained_units.saturating_add(replacement_units) > self.config.max_pending_backfills {
13657 return Err(SubscriberError::ResourceExhausted(format!(
13658 "coordinated owner registration would queue more than {} lazy backfills",
13659 self.config.max_pending_backfills
13660 )));
13661 }
13662
13663 let mut replacement_backfills = VecDeque::new();
13664 if !owner_filters.is_empty() {
13665 replacement_backfills.push_back(QueuedSubscriberBackfill {
13666 owner: Some(owner.clone()),
13667 epoch: None,
13668 filters: owner_filters,
13669 backfill: owner_backfill,
13670 });
13671 }
13672 replacement_backfills.push_back(QueuedSubscriberBackfill {
13675 owner: None,
13676 epoch: None,
13677 filters: global_filters,
13678 backfill: global_backfill,
13679 });
13680
13681 self.owned_interests = next_owned;
13684 self.interests = next_registered;
13685 self.recent_compat_owner_input_refs.remove(&owner);
13686 self.recent_compat_owner_input_ref_sets.remove(&owner);
13687 self.pending_backfills
13688 .retain(|queued| queued.owner.as_ref() != Some(&owner));
13689 self.pending_backfills.extend(replacement_backfills);
13690 self.retire_unreferenced_filters();
13691 self.sources_dirty = true;
13692 Ok(())
13693 }
13694
13695 pub fn remove_interest_owner(&mut self, owner: &HandlerId) -> Option<Vec<ReactiveInterest<N>>> {
13704 let index = self
13705 .owned_interests
13706 .iter()
13707 .position(|entry| &entry.owner == owner && entry.epoch.is_none())?;
13708 let removed = self.owned_interests.remove(index);
13709 if let Some(epoch) = &removed.epoch {
13710 self.purge_owner_epoch(epoch);
13711 } else {
13712 self.pending_backfills
13713 .retain(|backfill| backfill.owner.as_ref() != Some(owner));
13714 self.recent_compat_owner_input_refs.remove(owner);
13715 self.recent_compat_owner_input_ref_sets.remove(owner);
13716 }
13717 self.rebuild_registered_interests();
13718 self.retire_unreferenced_filters();
13719 self.sources_dirty = true;
13720 Some(removed.interests)
13721 }
13722
13723 pub fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
13725 self.owned_interests
13726 .iter()
13727 .find(|entry| &entry.owner == owner)
13728 .map(|entry| entry.interests.as_slice())
13729 }
13730
13731 fn set_interest_owner(
13732 &mut self,
13733 owner: HandlerId,
13734 interests: &[ReactiveInterest<N>],
13735 backfill: Option<SubscriberBackfill>,
13736 ) -> Result<(), SubscriberError> {
13737 validate_subscriber_config(&self.config)?;
13738 if self
13739 .owned_interests
13740 .iter()
13741 .any(|entry| entry.owner == owner && entry.epoch.is_some())
13742 {
13743 return Err(SubscriberError::InvalidConfig(
13744 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
13745 ));
13746 }
13747
13748 let mut next_owned = self.clone_owned_interests();
13749 let replaced_epoch = match next_owned.iter_mut().find(|entry| entry.owner == owner) {
13750 Some(entry) => {
13751 entry.interests = interests.to_vec();
13752 entry.state = SubscriberOwnerState::Active;
13753 entry.baseline = None;
13754 entry.progress = None;
13755 entry.progress_stream_revision = None;
13756 entry.epoch.take()
13757 }
13758 None => {
13759 next_owned.push(OwnedSubscriberInterests {
13760 owner: owner.clone(),
13761 interests: interests.to_vec(),
13762 epoch: None,
13763 state: SubscriberOwnerState::Active,
13764 baseline: None,
13765 progress: None,
13766 progress_stream_revision: None,
13767 });
13768 None
13769 }
13770 };
13771 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
13772 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13773
13774 let previous_filters: Vec<Filter> = self
13781 .owner_interests(&owner)
13782 .map(log_filters)
13783 .unwrap_or_default();
13784 let continuity_anchor: Option<u64> = previous_filters
13785 .iter()
13786 .filter_map(|filter| self.log_anchor(filter))
13787 .min();
13788
13789 let mut replacement_backfills = Vec::new();
13793 let filters = log_filters(interests);
13794 if let Some(backfill) = backfill
13795 && !filters.is_empty()
13796 {
13797 replacement_backfills.push(QueuedSubscriberBackfill {
13798 owner: Some(owner.clone()),
13799 epoch: None,
13800 filters: filters.clone(),
13801 backfill,
13802 });
13803 }
13804 let explicit_covers = backfill.is_some_and(|explicit| {
13805 explicit.end_block().is_none()
13806 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
13807 });
13808 let continuity_filters: Vec<_> = filters
13809 .into_iter()
13810 .filter(|filter| !previous_filters.contains(filter))
13811 .collect();
13812 if let Some(anchor) = continuity_anchor
13813 && !continuity_filters.is_empty()
13814 && !explicit_covers
13815 {
13816 replacement_backfills.push(QueuedSubscriberBackfill {
13817 owner: Some(owner.clone()),
13818 epoch: None,
13819 filters: continuity_filters,
13820 backfill: SubscriberBackfill::from_block(anchor),
13821 });
13822 }
13823 let retained_backfills = self
13824 .pending_backfills
13825 .iter()
13826 .filter(|queued| queued.owner.as_ref() != Some(&owner))
13827 .map(|queued| queued.filters.len())
13828 .sum::<usize>();
13829 let replacement_units = replacement_backfills
13830 .iter()
13831 .map(|queued| queued.filters.len())
13832 .sum::<usize>();
13833 if retained_backfills.saturating_add(replacement_units) > self.config.max_pending_backfills
13834 {
13835 return Err(SubscriberError::ResourceExhausted(format!(
13836 "owner update would queue more than {} lazy backfills",
13837 self.config.max_pending_backfills
13838 )));
13839 }
13840
13841 self.owned_interests = next_owned;
13842 self.interests = next_registered;
13843 if let Some(epoch) = replaced_epoch {
13844 self.purge_owner_epoch(&epoch);
13845 } else {
13846 self.recent_compat_owner_input_refs.remove(&owner);
13847 self.recent_compat_owner_input_ref_sets.remove(&owner);
13848 }
13849 self.retire_unreferenced_filters();
13850 self.sources_dirty = true;
13851
13852 self.pending_backfills
13855 .retain(|queued| queued.owner.as_ref() != Some(&owner));
13856 self.pending_backfills.extend(replacement_backfills);
13857 Ok(())
13858 }
13859
13860 fn clone_owned_interests(&self) -> Vec<OwnedSubscriberInterests<N>> {
13861 self.owned_interests
13862 .iter()
13863 .map(|entry| OwnedSubscriberInterests {
13864 owner: entry.owner.clone(),
13865 interests: entry.interests.clone(),
13866 epoch: entry.epoch.clone(),
13867 state: entry.state,
13868 baseline: entry.baseline,
13869 progress: entry.progress.clone(),
13870 progress_stream_revision: entry.progress_stream_revision,
13871 })
13872 .collect()
13873 }
13874
13875 fn rebuild_registered_interests(&mut self) {
13876 self.interests = aggregate_interests(&self.base_interests, &self.owned_interests);
13877 }
13878
13879 fn log_anchor(&self, filter: &Filter) -> Option<u64> {
13882 if let Some(anchor) = self
13883 .log_source_ids
13884 .get(filter)
13885 .and_then(|id| self.last_seen_log_blocks.get(id))
13886 {
13887 return Some(*anchor);
13888 }
13889
13890 self.log_source_ids
13894 .values()
13895 .filter_map(|id| self.last_seen_log_blocks.get(id).copied())
13896 .min()
13897 }
13898
13899 #[allow(clippy::mutable_key_type)]
13906 fn logical_log_filters(&self) -> Vec<Filter> {
13907 let mut filters = log_filters(&self.base_interests);
13908 for entry in &self.owned_interests {
13909 filters.extend(log_filters(&entry.interests));
13910 }
13911 let mut seen = HashSet::new();
13912 filters.retain(|filter| seen.insert(filter.clone()));
13913 filters
13914 }
13915
13916 fn log_stream_filters(&self) -> Vec<Filter> {
13922 let mut merged = Vec::new();
13923 for filter in self.logical_log_filters() {
13924 merge_log_subscription_filter(&mut merged, &filter);
13925 }
13926
13927 let max_addresses = self.config.max_log_addresses_per_subscription.max(1);
13928 let mut planned = Vec::new();
13929 for filter in merged {
13930 let mut addresses: Vec<_> = filter.address.iter().copied().collect();
13931 if addresses.len() <= max_addresses {
13932 planned.push(filter);
13933 continue;
13934 }
13935 addresses.sort_unstable();
13936 for chunk in addresses.chunks(max_addresses) {
13937 let mut split = filter.clone();
13938 split.address = FilterSet::default();
13939 for address in chunk {
13940 split.address.insert(*address);
13941 }
13942 planned.push(split);
13943 }
13944 }
13945 planned
13946 }
13947
13948 #[allow(clippy::mutable_key_type)]
13955 fn retire_unreferenced_filters(&mut self) {
13956 let mut live: HashSet<Filter> = self.log_stream_filters().into_iter().collect();
13957 if let AlloySubscriberState::Active(streams) = &self.state {
13958 for entry in &streams.entries {
13959 match &entry.source {
13960 SubscriberStreamSource::PubSubLog { filter, .. }
13961 | SubscriberStreamSource::BasePendingLog { filter, .. }
13962 | SubscriberStreamSource::PollingLog { filter } => {
13963 live.insert(filter.clone());
13964 }
13965 SubscriberStreamSource::BaseFlashblocks
13966 | SubscriberStreamSource::OpPendingFlashblocks
13967 | SubscriberStreamSource::CanonicalHeadPolling
13968 | SubscriberStreamSource::PubSubPendingHashes
13969 | SubscriberStreamSource::PubSubBlockHeaders
13970 | SubscriberStreamSource::PollingPendingHashes => {}
13971 #[cfg(feature = "raw-flashblocks-json")]
13972 SubscriberStreamSource::ExternalFlashblockUpdates => {}
13973 }
13974 }
13975 }
13976 self.log_source_ids
13977 .retain(|filter, _| live.contains(filter));
13978 let live_ids: HashSet<usize> = self.log_source_ids.values().copied().collect();
13979 self.last_seen_log_blocks
13980 .retain(|id, _| live_ids.contains(id));
13981 }
13982
13983 fn drain_next_scoped_batch(&mut self) -> Option<SubscriberInputBatch<N>> {
13984 if self.pending_records.is_empty()
13985 && self.pending_chain_controls.is_empty()
13986 && !self.pending_preconfirmation_invalidation
13987 {
13988 return None;
13989 }
13990
13991 let first_preconfirmation = self.pending_records.front().and_then(|record| {
13992 if record.scope != SubscriberInputScope::Preconfirmed {
13993 return None;
13994 }
13995 match &record.record.context.chain_status {
13996 ChainStatus::Preconfirmed { flashblock } => Some(flashblock.clone()),
13997 _ => None,
13998 }
13999 });
14000 let len = self
14001 .pending_records
14002 .iter()
14003 .take(self.config.max_batch_size)
14004 .take_while(|record| match &first_preconfirmation {
14005 Some(expected) => {
14006 record.scope == SubscriberInputScope::Preconfirmed
14007 && matches!(
14008 &record.record.context.chain_status,
14009 ChainStatus::Preconfirmed { flashblock } if flashblock == expected
14010 )
14011 }
14012 None => record.scope != SubscriberInputScope::Preconfirmed,
14013 })
14014 .count();
14015 let preconfirmation_timing = self
14016 .pending_records
14017 .iter()
14018 .take(len)
14019 .filter_map(SubscriberInputRecord::preconfirmation_timing)
14020 .reduce(FlashblockIngressTiming::earliest);
14021 let records = self.pending_records.drain(..len).collect();
14022 let chain_controls = if first_preconfirmation.is_none() && self.pending_records.is_empty() {
14023 self.pending_chain_controls.drain(..).collect()
14024 } else {
14025 Vec::new()
14026 };
14027 Some(SubscriberInputBatch {
14028 records,
14029 chain_id: self.chain_id,
14030 chain_controls,
14031 preconfirmation_invalidated: std::mem::take(
14032 &mut self.pending_preconfirmation_invalidation,
14033 ),
14034 preconfirmation_timing,
14035 })
14036 }
14037
14038 fn reset_delivery_state(&mut self) {
14039 self.pending_records.clear();
14040 self.pending_chain_controls.clear();
14041 self.pending_reconcile_owner_records.clear();
14042 self.resource_error = None;
14043 self.last_seen_log_blocks.clear();
14044 self.verified_log_blocks.clear();
14045 self.verified_log_block_order.clear();
14046 self.recent_input_refs.clear();
14047 self.recent_input_ref_set.clear();
14048 self.recent_owner_input_refs.clear();
14049 self.recent_owner_input_ref_sets.clear();
14050 self.recent_compat_owner_input_refs.clear();
14051 self.recent_compat_owner_input_ref_sets.clear();
14052 self.pending_backfills.clear();
14053 self.pending_source_backfills.clear();
14054 self.pending_preconfirmation_invalidation = false;
14055 self.pending_flashblock_reconnects.clear();
14056 self.pending_flashblock_reconnect_sources.clear();
14057 self.flashblocks_rpc_metrics = FlashblocksRpcMetrics::default();
14058 self.log_source_ids.clear();
14059 self.next_log_source_id = 0;
14060 self.sources_dirty = true;
14061 self.last_certified_canonical_head = None;
14062 self.reset_flashblock_tracking();
14063 }
14064
14065 fn reset_stream_topology(&mut self) {
14066 #[cfg(feature = "raw-flashblocks-json")]
14067 let external = match &mut self.state {
14068 AlloySubscriberState::Active(streams) => streams
14069 .entries
14070 .iter()
14071 .position(|entry| entry.source.is_external_flashblocks())
14072 .map(|index| streams.entries.remove(index)),
14073 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => None,
14074 };
14075
14076 #[cfg(feature = "raw-flashblocks-json")]
14077 if let Some(external) = external {
14078 let mut streams = SubscriberStreams::new();
14079 streams.entries.push(external);
14080 self.state = AlloySubscriberState::Active(streams);
14081 return;
14082 }
14083
14084 self.state = AlloySubscriberState::Uninitialized;
14085 }
14086
14087 fn reset_flashblock_tracking(&mut self) {
14088 self.base_flashblock_header = None;
14089 self.base_flashblock_transactions = None;
14090 self.unmatched_pending_logs.clear();
14091 self.latest_preconfirmation = None;
14092 self.preconfirmed_seen_logs.clear();
14093 self.preconfirmed_receipted_transactions.clear();
14094 self.preconfirmed_unavailable_receipts.clear();
14095 #[cfg(feature = "raw-flashblocks-json")]
14096 {
14097 self.last_external_flashblock_snapshot = None;
14098 }
14099 self.consecutive_flashblock_poll_failures = 0;
14100 }
14101
14102 fn invalidate_preconfirmation_snapshot(&mut self) {
14109 self.pending_records
14110 .retain(|record| record.scope != SubscriberInputScope::Preconfirmed);
14111 self.pending_preconfirmation_invalidation = true;
14112 self.latest_preconfirmation = None;
14113 self.preconfirmed_seen_logs.clear();
14114 self.preconfirmed_receipted_transactions.clear();
14115 self.preconfirmed_unavailable_receipts.clear();
14116 }
14117
14118 fn bump_stream_revision(&mut self) {
14119 self.stream_revision = self.stream_revision.saturating_add(1);
14120 }
14121}
14122
14123impl<P, N> InterestOwnerSubscriber<N> for AlloySubscriber<P, N>
14124where
14125 P: Provider<N> + Send + Sync,
14126 N: Network + 'static,
14127 N::HeaderResponse: Send + 'static,
14128{
14129 fn upsert_interest_owners(
14130 &mut self,
14131 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14132 ) -> SubscriberOperation<'_, ()> {
14133 Box::pin(async move {
14134 if !owners.is_empty() {
14135 self.ensure_chain_id().await?;
14136 }
14137 AlloySubscriber::upsert_interest_owners(self, owners)
14138 })
14139 }
14140
14141 fn replace_interest_owners(
14142 &mut self,
14143 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14144 ) -> SubscriberOperation<'_, ()> {
14145 Box::pin(async move {
14146 if owners.iter().any(|(_, interests)| !interests.is_empty()) {
14147 self.ensure_chain_id().await?;
14148 }
14149 AlloySubscriber::replace_interest_owners(self, owners)
14150 })
14151 }
14152
14153 fn replace_interest_owners_with_global_backfill(
14154 &mut self,
14155 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14156 backfill: SubscriberBackfill,
14157 ) -> SubscriberOperation<'_, ()> {
14158 Box::pin(async move {
14159 if owners.iter().any(|(_, interests)| !interests.is_empty()) {
14160 self.ensure_chain_id().await?;
14161 }
14162 AlloySubscriber::replace_interest_owners_with_global_backfill(self, owners, backfill)
14163 })
14164 }
14165
14166 fn add_interest_owner(
14167 &mut self,
14168 owner: HandlerId,
14169 interests: &[ReactiveInterest<N>],
14170 ) -> SubscriberOperation<'_, ()> {
14171 let interests = interests.to_vec();
14172 Box::pin(async move {
14173 if !interests.is_empty() {
14174 self.ensure_chain_id().await?;
14175 }
14176 AlloySubscriber::add_interest_owner(self, owner, &interests)
14177 })
14178 }
14179
14180 fn add_interest_owner_with_backfill(
14181 &mut self,
14182 owner: HandlerId,
14183 interests: &[ReactiveInterest<N>],
14184 backfill: SubscriberBackfill,
14185 ) -> SubscriberOperation<'_, ()> {
14186 let interests = interests.to_vec();
14187 Box::pin(async move {
14188 if !interests.is_empty() {
14189 self.ensure_chain_id().await?;
14190 }
14191 AlloySubscriber::add_interest_owner_with_backfill(self, owner, &interests, backfill)
14192 })
14193 }
14194
14195 fn add_interest_owner_with_canonical_catchup(
14196 &mut self,
14197 owner: HandlerId,
14198 interests: &[ReactiveInterest<N>],
14199 retained: BlockRef,
14200 ) -> SubscriberOperation<'_, ()> {
14201 let interests = interests.to_vec();
14202 Box::pin(async move {
14203 self.ensure_chain_id().await?;
14206 AlloySubscriber::add_interest_owner_with_canonical_catchup(
14207 self, owner, &interests, retained,
14208 )
14209 })
14210 }
14211
14212 fn remove_interest_owner(
14213 &mut self,
14214 owner: &HandlerId,
14215 ) -> SubscriberOperation<'_, Option<Vec<ReactiveInterest<N>>>> {
14216 let owner = owner.clone();
14217 Box::pin(async move { Ok(AlloySubscriber::remove_interest_owner(self, &owner)) })
14218 }
14219
14220 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
14221 AlloySubscriber::owner_interests(self, owner)
14222 }
14223}
14224
14225enum AlloySubscriberState<N: Network> {
14226 Uninitialized,
14227 Active(SubscriberStreams<N>),
14228 Empty,
14229}
14230
14231struct SubscriberStreams<N: Network> {
14232 entries: Vec<SubscriberStreamEntry<N>>,
14233 next_index: usize,
14234}
14235
14236struct SubscriberStreamEntry<N: Network> {
14237 source: SubscriberStreamSource,
14238 stream: BoxStream<'static, SubscriberEvent<N>>,
14239}
14240
14241impl<N: Network> SubscriberStreams<N> {
14242 fn new() -> Self {
14243 Self {
14244 entries: Vec::new(),
14245 next_index: 0,
14246 }
14247 }
14248
14249 fn is_empty(&self) -> bool {
14250 self.entries.is_empty()
14251 }
14252
14253 fn push(
14254 &mut self,
14255 source: SubscriberStreamSource,
14256 stream: BoxStream<'static, SubscriberEvent<N>>,
14257 ) {
14258 self.entries.push(SubscriberStreamEntry { source, stream });
14259 }
14260
14261 #[cfg(all(test, any(feature = "reactive-polling", feature = "reactive-ws")))]
14262 fn len(&self) -> usize {
14263 self.entries.len()
14264 }
14265
14266 fn contains_source(&self, source: &SubscriberStreamSource) -> bool {
14267 self.entries
14268 .iter()
14269 .any(|entry| entry.source.same_key(source))
14270 }
14271
14272 fn retain_sources(&mut self, sources: &[SubscriberStreamSource]) {
14273 self.entries
14274 .retain(|entry| sources.iter().any(|source| entry.source.same_key(source)));
14275 self.normalize_next_index();
14276 }
14277
14278 fn normalize_next_index(&mut self) {
14279 if self.entries.is_empty() {
14280 self.next_index = 0;
14281 } else if self.next_index >= self.entries.len() {
14282 self.next_index %= self.entries.len();
14283 }
14284 }
14285
14286 async fn next(&mut self) -> Option<SubscriberEvent<N>> {
14287 poll_fn(|cx| {
14288 self.normalize_next_index();
14289 if self.entries.is_empty() {
14290 return std::task::Poll::Ready(None);
14291 }
14292
14293 let mut index = self.next_index;
14294 let mut checked = 0usize;
14295 while checked < self.entries.len() {
14296 if index >= self.entries.len() {
14297 index = 0;
14298 }
14299 match self.entries[index].stream.as_mut().poll_next(cx) {
14300 std::task::Poll::Ready(Some(event)) => {
14301 if matches!(event, SubscriberEvent::StreamTerminated(_)) {
14302 self.entries.remove(index);
14303 self.next_index = if self.entries.is_empty() {
14304 0
14305 } else {
14306 index % self.entries.len()
14307 };
14308 } else {
14309 self.next_index = (index + 1) % self.entries.len();
14310 }
14311 return std::task::Poll::Ready(Some(event));
14312 }
14313 std::task::Poll::Ready(None) => {
14314 self.entries.remove(index);
14315 if self.entries.is_empty() {
14316 self.next_index = 0;
14317 return std::task::Poll::Ready(None);
14318 }
14319 }
14320 std::task::Poll::Pending => {
14321 checked += 1;
14322 index += 1;
14323 }
14324 }
14325 }
14326
14327 if self.entries.is_empty() {
14328 std::task::Poll::Ready(None)
14329 } else {
14330 self.next_index = index % self.entries.len();
14331 std::task::Poll::Pending
14332 }
14333 })
14334 .await
14335 }
14336}
14337
14338#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14339#[allow(dead_code)]
14340enum SubscriberTransport {
14341 PubSub,
14342 Polling,
14343}
14344
14345#[derive(Clone, Debug)]
14346enum SubscriberStreamSource {
14347 PubSubLog {
14348 id: usize,
14349 filter: Filter,
14350 },
14351 BasePendingLog {
14352 id: usize,
14353 filter: Filter,
14354 },
14355 BaseFlashblocks,
14356 OpPendingFlashblocks,
14357 CanonicalHeadPolling,
14358 PubSubPendingHashes,
14359 PubSubBlockHeaders,
14360 PollingLog {
14361 filter: Filter,
14362 },
14363 PollingPendingHashes,
14364 #[cfg(feature = "raw-flashblocks-json")]
14365 ExternalFlashblockUpdates,
14366}
14367
14368impl SubscriberStreamSource {
14369 fn label(&self) -> &'static str {
14370 match self {
14371 Self::PubSubLog { .. } => "pubsub log",
14372 Self::BasePendingLog { .. } => "OP Stack pendingLogs",
14373 Self::BaseFlashblocks => "OP Stack newFlashblocks",
14374 Self::OpPendingFlashblocks => "Optimism pending Flashblocks",
14375 Self::CanonicalHeadPolling => "certified canonical head",
14376 Self::PubSubPendingHashes => "pubsub pending transaction hash",
14377 Self::PubSubBlockHeaders => "pubsub block header",
14378 Self::PollingLog { .. } => "polling log",
14379 Self::PollingPendingHashes => "polling pending transaction hash",
14380 #[cfg(feature = "raw-flashblocks-json")]
14381 Self::ExternalFlashblockUpdates => "external standardized Flashblock update",
14382 }
14383 }
14384
14385 fn is_pubsub(&self) -> bool {
14386 matches!(
14387 self,
14388 Self::PubSubLog { .. }
14389 | Self::BasePendingLog { .. }
14390 | Self::BaseFlashblocks
14391 | Self::OpPendingFlashblocks
14392 | Self::PubSubPendingHashes
14393 | Self::PubSubBlockHeaders
14394 )
14395 }
14396
14397 fn is_flashblocks(&self) -> bool {
14398 matches!(
14399 self,
14400 Self::BasePendingLog { .. } | Self::BaseFlashblocks | Self::OpPendingFlashblocks
14401 )
14402 }
14403
14404 fn same_key(&self, other: &Self) -> bool {
14405 match (self, other) {
14406 (Self::PubSubLog { filter: left, .. }, Self::PubSubLog { filter: right, .. })
14407 | (
14408 Self::BasePendingLog { filter: left, .. },
14409 Self::BasePendingLog { filter: right, .. },
14410 )
14411 | (Self::PollingLog { filter: left }, Self::PollingLog { filter: right }) => {
14412 left == right
14413 }
14414 (Self::BaseFlashblocks, Self::BaseFlashblocks)
14415 | (Self::OpPendingFlashblocks, Self::OpPendingFlashblocks)
14416 | (Self::CanonicalHeadPolling, Self::CanonicalHeadPolling)
14417 | (Self::PubSubPendingHashes, Self::PubSubPendingHashes)
14418 | (Self::PubSubBlockHeaders, Self::PubSubBlockHeaders)
14419 | (Self::PollingPendingHashes, Self::PollingPendingHashes) => true,
14420 #[cfg(feature = "raw-flashblocks-json")]
14421 (Self::ExternalFlashblockUpdates, Self::ExternalFlashblockUpdates) => true,
14422 _ => false,
14423 }
14424 }
14425
14426 fn is_external_flashblocks(&self) -> bool {
14427 #[cfg(feature = "raw-flashblocks-json")]
14428 {
14429 matches!(self, Self::ExternalFlashblockUpdates)
14430 }
14431 #[cfg(not(feature = "raw-flashblocks-json"))]
14432 {
14433 false
14434 }
14435 }
14436}
14437
14438#[allow(dead_code)]
14439enum SubscriberEvent<N: Network> {
14440 Log {
14441 source_id: usize,
14442 log: Log,
14443 },
14444 BackfilledLogs {
14445 source_id: usize,
14446 logs: Vec<Log>,
14447 },
14448 Logs(Vec<Log>),
14449 BlockHeader(N::HeaderResponse),
14450 PendingHash(B256),
14451 PendingHashes(Vec<B256>),
14452 BasePendingLog {
14453 source_id: usize,
14454 log: Log,
14455 },
14456 BasePendingLogTimed {
14457 source_id: usize,
14458 log: Log,
14459 timing: FlashblockIngressTiming,
14460 },
14461 BaseFlashblock(BaseFlashblockWirePayload),
14462 BaseFlashblockTimed {
14463 payload: BaseFlashblockWirePayload,
14464 timing: FlashblockIngressTiming,
14465 },
14466 OpFlashblockTick,
14467 OpFlashblockTickTimed(FlashblockIngressTiming),
14468 CanonicalHeadTick,
14469 PreconfirmedLogs {
14470 flashblock: FlashblockRef,
14471 logs: Vec<Log>,
14472 timing: FlashblockIngressTiming,
14473 },
14474 FlashblockInvalidated,
14475 FlashblockObserved,
14476 #[cfg(feature = "raw-flashblocks-json")]
14477 ExternalFlashblockUpdate(raw_json_flashblocks::QueuedFlashblockUpdate),
14478 StreamTerminated(SubscriberStreamSource),
14479}
14480
14481enum SubscriberReady<N: Network> {
14482 Event(Option<SubscriberEvent<N>>),
14483 FlashblockReconnect(
14484 SubscriberStreamSource,
14485 Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>,
14486 ),
14487}
14488
14489#[derive(Debug)]
14490enum PendingFlashblockPollError {
14491 Request(SubscriberError),
14492 Integrity(SubscriberError),
14493}
14494
14495impl PendingFlashblockPollError {
14496 fn into_subscriber(self) -> SubscriberError {
14497 match self {
14498 Self::Request(error) | Self::Integrity(error) => error,
14499 }
14500 }
14501}
14502
14503fn pending_flashblock_request_error(error: impl fmt::Display) -> PendingFlashblockPollError {
14504 PendingFlashblockPollError::Request(provider_error(error))
14505}
14506
14507fn normalize_op_pending_block<N: Network>(
14508 mut value: serde_json::Value,
14509) -> Result<N::BlockResponse, SubscriberError> {
14510 let object = value.as_object_mut().ok_or_else(|| {
14511 SubscriberError::Provider("OP pending block response is not an object".into())
14512 })?;
14513 let transactions = object
14514 .get_mut("transactions")
14515 .and_then(serde_json::Value::as_array_mut)
14516 .ok_or_else(|| {
14517 SubscriberError::Provider(
14518 "OP pending block response is missing its transaction array".into(),
14519 )
14520 })?;
14521 for transaction in transactions {
14522 if transaction.is_string() {
14523 continue;
14524 }
14525 let hash = transaction
14526 .as_object()
14527 .and_then(|object| object.get("hash"))
14528 .filter(|hash| hash.is_string())
14529 .cloned()
14530 .ok_or_else(|| {
14531 SubscriberError::Provider("OP pending block transaction is missing its hash".into())
14532 })?;
14533 *transaction = hash;
14534 }
14535 if object.get("hash").is_none_or(serde_json::Value::is_null) {
14536 object.insert(
14537 "hash".into(),
14538 serde_json::Value::String(B256::ZERO.to_string()),
14539 );
14540 }
14541 if object.get("nonce").is_none_or(serde_json::Value::is_null) {
14542 object.insert(
14543 "nonce".into(),
14544 serde_json::Value::String("0x0000000000000000".into()),
14545 );
14546 }
14547 if object.get("miner").is_none_or(serde_json::Value::is_null)
14548 || object
14549 .get("beneficiary")
14550 .is_none_or(serde_json::Value::is_null)
14551 {
14552 object.insert(
14553 "miner".into(),
14554 serde_json::Value::String(Address::ZERO.to_string()),
14555 );
14556 }
14557 serde_json::from_value(value).map_err(|error| {
14558 SubscriberError::Provider(format!(
14559 "failed to decode normalized OP pending block: {error}"
14560 ))
14561 })
14562}
14563
14564fn normalize_pending_transaction_receipt(
14565 expected_transaction_hash: B256,
14566 value: serde_json::Value,
14567) -> Result<Option<Vec<Log>>, SubscriberError> {
14568 if value.is_null() {
14569 return Ok(None);
14570 }
14571 let receipt = value.as_object().ok_or_else(|| {
14572 SubscriberError::Provider("pending transaction receipt response is not an object".into())
14573 })?;
14574 let transaction_hash: B256 =
14575 serde_json::from_value(receipt.get("transactionHash").cloned().ok_or_else(|| {
14576 SubscriberError::Provider(
14577 "pending transaction receipt is missing its transaction hash".into(),
14578 )
14579 })?)
14580 .map_err(|error| {
14581 SubscriberError::Provider(format!(
14582 "failed to decode pending transaction receipt hash: {error}"
14583 ))
14584 })?;
14585 if transaction_hash != expected_transaction_hash {
14586 return Err(SubscriberError::Provider(
14587 "pending transaction receipt hash disagrees with its request".into(),
14588 ));
14589 }
14590 let receipt_logs = receipt
14591 .get("logs")
14592 .and_then(serde_json::Value::as_array)
14593 .ok_or_else(|| {
14594 SubscriberError::Provider("pending transaction receipt is missing its log array".into())
14595 })?;
14596 let mut logs = Vec::new();
14597 for log in receipt_logs {
14598 let log: Log = serde_json::from_value(log.clone()).map_err(|error| {
14599 SubscriberError::Provider(format!(
14600 "failed to decode pending transaction receipt log: {error}"
14601 ))
14602 })?;
14603 if log.transaction_hash != Some(expected_transaction_hash) {
14604 return Err(SubscriberError::Provider(
14605 "pending transaction receipt log hash disagrees with its receipt".into(),
14606 ));
14607 }
14608 logs.push(log);
14609 }
14610 Ok(Some(logs))
14611}
14612
14613impl<P, N> EventSubscriber<N> for AlloySubscriber<P, N>
14614where
14615 P: Provider<N> + Send + Sync,
14616 N: Network + 'static,
14617 N::HeaderResponse: Send + 'static,
14618{
14619 fn chain_id(&self) -> Option<u64> {
14620 self.chain_id
14621 }
14622
14623 fn capabilities(&self) -> SubscriberCapabilities {
14624 let Ok(transport) = resolve_subscriber_transport(self.mode) else {
14625 return SubscriberCapabilities::default();
14626 };
14627 let mut capabilities = vec![
14628 SubscriberCapability::Logs,
14629 SubscriberCapability::PendingTransactionHashes,
14630 SubscriberCapability::HistoricalBackfill,
14631 SubscriberCapability::Live,
14632 SubscriberCapability::OwnerScopedDelivery,
14633 SubscriberCapability::DynamicInterests,
14634 ];
14635 if transport == SubscriberTransport::PubSub {
14636 capabilities.push(SubscriberCapability::BlockHeaders);
14637 }
14638 if self.config.preconfirmations != PreconfirmationMode::Disabled
14639 && (self.uses_external_flashblock_updates()
14640 || (self.provider_ref.is_some()
14641 && self.chain_id.and_then(flashblocks_adapter).is_some()))
14642 {
14643 capabilities.push(SubscriberCapability::Preconfirmations);
14644 }
14645 SubscriberCapabilities::new(capabilities)
14646 }
14647
14648 fn register_interests(
14649 &mut self,
14650 interests: &[ReactiveInterest<N>],
14651 ) -> SubscriberOperation<'_, ()> {
14652 let interests = interests.to_vec();
14653 Box::pin(async move {
14654 validate_subscriber_config(&self.config)?;
14655 validate_supported_interests(self.mode, &self.config, &interests)?;
14656 if !interests.is_empty() {
14657 self.ensure_chain_id().await?;
14658 }
14659 self.validate_flashblocks_setup()?;
14660
14661 self.base_interests = interests;
14662 self.owned_interests.clear();
14663 self.rebuild_registered_interests();
14664 self.reset_delivery_state();
14665 self.reset_stream_topology();
14666 Ok(())
14667 })
14668 }
14669
14670 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N> {
14671 Box::pin(async {
14672 Ok(self
14673 .next_scoped_batch()
14674 .await?
14675 .map(SubscriberInputBatch::into_reactive_batch))
14676 })
14677 }
14678}
14679
14680impl<P, N> AlloySubscriber<P, N>
14681where
14682 P: Provider<N> + Send + Sync,
14683 N: Network + 'static,
14684 N::HeaderResponse: Send + 'static,
14685{
14686 pub async fn establish_flashblocks_preflight(
14708 &mut self,
14709 expected_chain_id: u64,
14710 ) -> Result<FlashblocksPreflight, SubscriberError> {
14711 validate_subscriber_config(&self.config)?;
14712 if self.config.preconfirmations == PreconfirmationMode::Disabled {
14713 return Err(SubscriberError::InvalidConfig(
14714 "Flashblocks preflight requires preconfirmations",
14715 ));
14716 }
14717 if !self
14718 .interests
14719 .iter()
14720 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)))
14721 {
14722 return Err(SubscriberError::InvalidConfig(
14723 "Flashblocks preflight requires at least one active log interest",
14724 ));
14725 }
14726 let chain_id = self.ensure_chain_id().await?;
14727 if chain_id != expected_chain_id {
14728 return Err(SubscriberError::ChainMismatch {
14729 expected: expected_chain_id,
14730 actual: chain_id,
14731 });
14732 }
14733 self.validate_flashblocks_setup()?;
14734 #[cfg(feature = "raw-flashblocks-json")]
14735 if let Some(provider) = self.external_flashblocks_provider.clone() {
14736 self.ensure_streams().await?;
14737 return Ok(FlashblocksPreflight {
14738 chain_id,
14739 provider,
14740 delivery: FlashblocksDelivery::ExternalUpdates,
14741 pending_log_subscriptions: 0,
14742 pending_log_filters: self.log_stream_filters().len(),
14743 advertised_capabilities: None,
14744 });
14745 }
14746 let adapter = flashblocks_adapter(chain_id).ok_or(SubscriberError::Unsupported(
14747 "Flashblocks are currently implemented for Base and OP chains",
14748 ))?;
14749 let provider = self
14750 .provider_ref
14751 .clone()
14752 .ok_or(SubscriberError::InvalidConfig(
14753 "Flashblocks preflight requires a stable provider ref",
14754 ))?;
14755 self.flashblocks_rpc_metrics.capability_requests = self
14756 .flashblocks_rpc_metrics
14757 .capability_requests
14758 .saturating_add(1);
14759 let capability_provider = if adapter == FlashblocksAdapter::PendingStatePolling {
14760 self.flashblocks_state_provider
14761 .as_ref()
14762 .unwrap_or(&self.provider)
14763 } else {
14764 &self.provider
14765 };
14766 let advertised_capabilities = capability_provider
14767 .client()
14768 .request::<_, serde_json::Value>("op_supportedCapabilities", ())
14769 .await
14770 .ok();
14771
14772 self.ensure_streams().await?;
14773 let pending_log_filters = self.log_stream_filters();
14774 if adapter == FlashblocksAdapter::PendingStatePolling
14775 && self.pending_receipt_requests_per_tick_capacity() == 0
14776 {
14777 return Err(SubscriberError::InvalidConfig(
14778 "Flashblocks RPC budget leaves no capacity for OP transaction receipts",
14779 ));
14780 }
14781 let (delivery, pending_log_subscriptions) = match adapter {
14782 FlashblocksAdapter::NativeSubscriptions => {
14783 if resolve_subscriber_transport(self.mode)? != SubscriberTransport::PubSub {
14784 return Err(SubscriberError::Unsupported(
14785 "Base Flashblocks preflight requires pubsub",
14786 ));
14787 }
14788 let pending_sources = self
14789 .pubsub_stream_sources()
14790 .into_iter()
14791 .filter(|source| {
14792 matches!(source, SubscriberStreamSource::BasePendingLog { .. })
14793 })
14794 .collect::<Vec<_>>();
14795 let AlloySubscriberState::Active(streams) = &self.state else {
14796 return Err(SubscriberError::Provider(
14797 "Flashblocks preflight subscriptions did not become active".to_owned(),
14798 ));
14799 };
14800 if !streams.contains_source(&SubscriberStreamSource::BaseFlashblocks)
14801 || pending_sources
14802 .iter()
14803 .any(|source| !streams.contains_source(source))
14804 {
14805 return Err(SubscriberError::Provider(
14806 "Base Flashblocks preflight did not retain both subscription lanes"
14807 .to_owned(),
14808 ));
14809 }
14810 (
14811 FlashblocksDelivery::NativeSubscriptions,
14812 pending_sources.len(),
14813 )
14814 }
14815 FlashblocksAdapter::PendingStatePolling => {
14816 if let Some(state_provider) = self.flashblocks_state_provider.as_ref() {
14817 self.flashblocks_rpc_metrics.provider_pair_chain_requests = self
14818 .flashblocks_rpc_metrics
14819 .provider_pair_chain_requests
14820 .saturating_add(1);
14821 let actual = state_provider
14822 .get_chain_id()
14823 .await
14824 .map_err(provider_error)?;
14825 if actual != expected_chain_id {
14826 return Err(SubscriberError::ChainMismatch {
14827 expected: expected_chain_id,
14828 actual,
14829 });
14830 }
14831 }
14832 let AlloySubscriberState::Active(streams) = &self.state else {
14833 return Err(SubscriberError::Provider(
14834 "Flashblocks preflight streams did not become active".to_owned(),
14835 ));
14836 };
14837 if !streams.contains_source(&SubscriberStreamSource::OpPendingFlashblocks) {
14838 return Err(SubscriberError::Provider(
14839 "Optimism Flashblocks preflight did not retain its pending-state sampler"
14840 .to_owned(),
14841 ));
14842 }
14843 self.probe_pending_state(&pending_log_filters).await?;
14844 (FlashblocksDelivery::PendingStatePolling, 0)
14845 }
14846 };
14847 Ok(FlashblocksPreflight {
14848 chain_id,
14849 provider,
14850 delivery,
14851 pending_log_subscriptions,
14852 pending_log_filters: pending_log_filters.len(),
14853 advertised_capabilities,
14854 })
14855 }
14856
14857 #[cfg(feature = "raw-flashblocks-json")]
14868 pub fn ingest_flashblock_update(
14869 &mut self,
14870 update: FlashblockUpdate,
14871 ) -> Result<(), SubscriberError> {
14872 self.ingest_flashblock_update_with_ingress(
14873 update,
14874 FlashblockIngressTiming::new(Instant::now()),
14875 )
14876 }
14877
14878 #[cfg(feature = "raw-flashblocks-json")]
14888 pub fn ingest_flashblock_update_with_ingress(
14889 &mut self,
14890 update: FlashblockUpdate,
14891 timing: FlashblockIngressTiming,
14892 ) -> Result<(), SubscriberError> {
14893 validate_subscriber_config(&self.config)?;
14894 self.validate_flashblocks_setup()?;
14895 let configured =
14896 self.external_flashblocks_provider
14897 .as_ref()
14898 .ok_or(SubscriberError::InvalidConfig(
14899 "standardized Flashblock updates require configure_external_flashblock_updates",
14900 ))?;
14901
14902 match update {
14903 FlashblockUpdate::Snapshot(batch) => {
14904 if batch.flashblock.provider.endpoint != configured.endpoint {
14905 return Err(SubscriberError::Provider(
14906 "external Flashblock update came from an unexpected provider endpoint"
14907 .into(),
14908 ));
14909 }
14910 if batch.flashblock.provider.generation < configured.generation
14911 || self.latest_preconfirmation.as_ref().is_some_and(|latest| {
14912 latest.provider.endpoint == batch.flashblock.provider.endpoint
14913 && latest.provider.generation > batch.flashblock.provider.generation
14914 })
14915 {
14916 return Ok(());
14917 }
14918 if self
14919 .rejected_external_flashblock_generation
14920 .is_some_and(|rejected| batch.flashblock.provider.generation <= rejected)
14921 {
14922 return Err(SubscriberError::Provider(
14923 "external Flashblock provider generation was previously rejected".into(),
14924 ));
14925 }
14926 validate_standard_flashblock_snapshot(&batch)?;
14927 if self.validate_external_flashblock_sequence(&batch)? {
14928 return Ok(());
14929 }
14930 let required = self.pending_record_count().saturating_add(batch.logs.len());
14931 if required > self.config.max_pending_records {
14932 self.invalidate_preconfirmation_snapshot();
14933 self.last_external_flashblock_snapshot = None;
14934 return Err(SubscriberError::ResourceExhausted(format!(
14935 "external preconfirmation records require {required} pending records, above the configured limit of {}",
14936 self.config.max_pending_records
14937 )));
14938 }
14939 let accepted_snapshot = (*batch).clone();
14940 let FlashblockSnapshot { flashblock, logs } = *batch;
14941 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
14942 self.last_external_flashblock_snapshot = Some(accepted_snapshot);
14943 if let Some(provider) = self.external_flashblocks_provider.as_mut() {
14944 provider.generation = provider.generation.max(flashblock.provider.generation);
14945 }
14946 if !logs.is_empty() {
14947 self.enqueue_event(SubscriberEvent::PreconfirmedLogs {
14948 flashblock,
14949 logs,
14950 timing,
14951 });
14952 }
14953 }
14954 FlashblockUpdate::Invalidated(invalidation) => {
14955 if invalidation.provider.endpoint != configured.endpoint {
14956 return Err(SubscriberError::Provider(
14957 "external Flashblock invalidation came from an unexpected provider endpoint"
14958 .into(),
14959 ));
14960 }
14961 if self.latest_preconfirmation.as_ref().is_some_and(|latest| {
14962 latest.provider == invalidation.provider
14963 && latest.payload_id == Some(invalidation.payload_id)
14964 }) {
14965 self.invalidate_preconfirmation_snapshot();
14966 self.last_external_flashblock_snapshot = None;
14967 }
14968 }
14969 }
14970 Ok(())
14971 }
14972
14973 #[cfg(feature = "raw-flashblocks-json")]
14974 fn validate_external_flashblock_sequence(
14975 &self,
14976 snapshot: &FlashblockSnapshot,
14977 ) -> Result<bool, SubscriberError> {
14978 let Some(previous) = self.last_external_flashblock_snapshot.as_ref() else {
14979 if snapshot.flashblock.index != Some(0) {
14980 return Err(SubscriberError::Provider(
14981 "external Flashblock payload generation must begin at index zero".into(),
14982 ));
14983 }
14984 return Ok(false);
14985 };
14986
14987 if previous.flashblock.provider == snapshot.flashblock.provider
14988 && previous.flashblock.payload_id == snapshot.flashblock.payload_id
14989 {
14990 let previous_index = previous
14991 .flashblock
14992 .index
14993 .expect("validated indexed snapshot");
14994 let current_index = snapshot
14995 .flashblock
14996 .index
14997 .expect("validated indexed snapshot");
14998 if current_index == previous_index {
14999 if previous == snapshot {
15000 return Ok(true);
15001 }
15002 return Err(SubscriberError::Provider(
15003 "external Flashblock repeated the same index with conflicting content".into(),
15004 ));
15005 }
15006 if current_index < previous_index {
15007 return Err(SubscriberError::Provider(format!(
15008 "external Flashblock index regressed from {previous_index} to {current_index}"
15009 )));
15010 }
15011 if current_index > previous_index.saturating_add(1) {
15012 return Err(SubscriberError::Provider(format!(
15013 "external Flashblock index skipped from {previous_index} to {current_index}"
15014 )));
15015 }
15016 if current_index == previous_index.saturating_add(1)
15017 && !previous.flashblock.same_base_identity(&snapshot.flashblock)
15018 {
15019 return Err(SubscriberError::Provider(
15020 "external Flashblock base identity changed within one payload generation"
15021 .into(),
15022 ));
15023 }
15024 if current_index == previous_index.saturating_add(1)
15025 && !snapshot
15026 .flashblock
15027 .transaction_hashes
15028 .starts_with(&previous.flashblock.transaction_hashes)
15029 {
15030 return Err(SubscriberError::Provider(
15031 "external Flashblock cumulative transaction membership changed its prior prefix"
15032 .into(),
15033 ));
15034 }
15035 let prior_transaction_count =
15036 u64::try_from(previous.flashblock.transaction_hashes.len()).unwrap_or(u64::MAX);
15037 if current_index == previous_index.saturating_add(1)
15038 && snapshot.logs.iter().any(|log| {
15039 log.transaction_index
15040 .is_some_and(|index| index < prior_transaction_count)
15041 })
15042 {
15043 return Err(SubscriberError::Provider(
15044 "external Flashblock delta log does not belong to a newly appended transaction"
15045 .into(),
15046 ));
15047 }
15048 } else if snapshot.flashblock.index != Some(0) {
15049 return Err(SubscriberError::Provider(
15050 "external Flashblock payload generation must begin at index zero".into(),
15051 ));
15052 }
15053 Ok(false)
15054 }
15055
15056 async fn probe_pending_state(&mut self, filters: &[Filter]) -> Result<(), SubscriberError> {
15057 self.flashblocks_rpc_metrics.pending_block_requests = self
15058 .flashblocks_rpc_metrics
15059 .pending_block_requests
15060 .saturating_add(1);
15061 let pending = self
15062 .fetch_op_pending_block()
15063 .await
15064 .map_err(PendingFlashblockPollError::into_subscriber)?
15065 .ok_or_else(|| {
15066 SubscriberError::Provider(
15067 "provider returned no pending block during Flashblocks preflight".into(),
15068 )
15069 })?;
15070 self.certify_op_pending_parent(&pending)
15071 .await
15072 .map_err(PendingFlashblockPollError::into_subscriber)?;
15073 for filter in filters {
15074 self.flashblocks_rpc_metrics.pending_log_requests = self
15075 .flashblocks_rpc_metrics
15076 .pending_log_requests
15077 .saturating_add(1);
15078 self.flashblocks_state_provider
15079 .as_ref()
15080 .unwrap_or(&self.provider)
15081 .get_logs(
15082 &filter
15083 .clone()
15084 .from_block(BlockNumberOrTag::Latest)
15085 .to_block(BlockNumberOrTag::Pending),
15086 )
15087 .await
15088 .map_err(provider_error)?;
15089 }
15090 self.flashblocks_rpc_metrics.pending_receipt_requests = self
15091 .flashblocks_rpc_metrics
15092 .pending_receipt_requests
15093 .saturating_add(1);
15094 let _: serde_json::Value = self
15095 .flashblocks_state_provider
15096 .as_ref()
15097 .unwrap_or(&self.provider)
15098 .raw_request(Cow::Borrowed("eth_getTransactionReceipt"), (B256::ZERO,))
15099 .await
15100 .map_err(provider_error)?;
15101 Ok(())
15102 }
15103
15104 async fn certify_op_pending_parent(
15105 &mut self,
15106 pending: &N::BlockResponse,
15107 ) -> Result<N::HeaderResponse, PendingFlashblockPollError> {
15108 let pending_header = pending.header();
15109 let pending_number = pending_header.number();
15110 let parent_hash = pending_header.parent_hash();
15111 if pending_number == 0 || parent_hash.is_zero() {
15112 return Err(PendingFlashblockPollError::Integrity(
15113 SubscriberError::Provider(
15114 "OP pending block omitted a certifiable canonical parent".into(),
15115 ),
15116 ));
15117 }
15118 self.flashblocks_rpc_metrics.canonical_head_requests = self
15119 .flashblocks_rpc_metrics
15120 .canonical_head_requests
15121 .saturating_add(1);
15122 let parent = self
15123 .flashblocks_state_provider
15124 .as_ref()
15125 .unwrap_or(&self.provider)
15126 .get_block_by_hash(parent_hash)
15127 .await
15128 .map_err(pending_flashblock_request_error)?
15129 .ok_or_else(|| {
15130 PendingFlashblockPollError::Request(SubscriberError::Provider(
15131 "Flashblocks provider returned no exact OP pending parent block".into(),
15132 ))
15133 })?;
15134 let parent_header = parent.header();
15135 if parent_header.hash() != parent_hash
15136 || parent_header.number().checked_add(1) != Some(pending_number)
15137 {
15138 return Err(PendingFlashblockPollError::Integrity(
15139 SubscriberError::Provider(
15140 "OP pending block does not extend its exact certified parent".into(),
15141 ),
15142 ));
15143 }
15144 Ok(parent_header.clone())
15145 }
15146
15147 async fn fetch_op_pending_block(
15148 &mut self,
15149 ) -> Result<Option<N::BlockResponse>, PendingFlashblockPollError> {
15150 let state_provider = self
15151 .flashblocks_state_provider
15152 .as_ref()
15153 .unwrap_or(&self.provider);
15154 let value: Option<serde_json::Value> = state_provider
15155 .raw_request(
15156 Cow::Borrowed("eth_getBlockByNumber"),
15157 (BlockNumberOrTag::Pending, true),
15158 )
15159 .await
15160 .map_err(pending_flashblock_request_error)?;
15161 value
15162 .map(normalize_op_pending_block::<N>)
15163 .transpose()
15164 .map_err(PendingFlashblockPollError::Integrity)
15165 }
15166
15167 async fn ensure_chain_id(&mut self) -> Result<u64, SubscriberError> {
15171 if let Some(chain_id) = self.chain_id {
15172 return Ok(chain_id);
15173 }
15174 let chain_id = self.provider.get_chain_id().await.map_err(provider_error)?;
15175 self.chain_id = Some(chain_id);
15176 Ok(chain_id)
15177 }
15178
15179 fn validate_flashblocks_setup(&self) -> Result<(), SubscriberError> {
15180 if self.config.preconfirmations == PreconfirmationMode::Disabled {
15181 if self.uses_external_flashblock_updates() {
15182 return Err(SubscriberError::InvalidConfig(
15183 "external Flashblock updates require preconfirmations to be preferred or required",
15184 ));
15185 }
15186 return Ok(());
15187 }
15188 if self.uses_external_flashblock_updates() {
15189 return Ok(());
15190 }
15191 if self.provider_ref.is_none() {
15192 return Err(SubscriberError::InvalidConfig(
15193 "Flashblocks require a stable provider ref from a pinned provider lease",
15194 ));
15195 }
15196 let Some(chain_id) = self.chain_id else {
15197 return Ok(());
15198 };
15199 match flashblocks_adapter(chain_id) {
15200 Some(FlashblocksAdapter::NativeSubscriptions)
15201 if resolve_subscriber_transport(self.mode)? != SubscriberTransport::PubSub
15202 && self.config.preconfirmations == PreconfirmationMode::Required =>
15203 {
15204 return Err(SubscriberError::Unsupported(
15205 "Base Flashblocks require pubsub for newFlashblocks and pendingLogs",
15206 ));
15207 }
15208 Some(FlashblocksAdapter::NativeSubscriptions) => {}
15209 Some(_) => {}
15210 None if self.config.preconfirmations == PreconfirmationMode::Required => {
15211 return Err(SubscriberError::Unsupported(
15212 "Flashblocks are currently implemented for Base and OP chains",
15213 ));
15214 }
15215 None => {}
15216 }
15217 Ok(())
15218 }
15219
15220 pub async fn reconcile_interest_owner(
15234 &mut self,
15235 epoch: &SubscriberOwnerEpoch,
15236 through: BlockRef,
15237 ) -> Result<SubscriberOwnerProgress, SubscriberOwnerError>
15238 where
15239 P: Clone,
15240 {
15241 self.reconcile_interest_owners(std::slice::from_ref(epoch), through)
15242 .await?
15243 .pop()
15244 .ok_or(SubscriberOwnerError::NotStaged)
15245 }
15246
15247 pub async fn reconcile_interest_owners(
15271 &mut self,
15272 epochs: &[SubscriberOwnerEpoch],
15273 through: BlockRef,
15274 ) -> Result<Vec<SubscriberOwnerProgress>, SubscriberOwnerError>
15275 where
15276 P: Clone,
15277 {
15278 if epochs.is_empty() {
15279 return Ok(Vec::new());
15280 }
15281
15282 self.ensure_chain_id().await?;
15283
15284 let mut seen = HashSet::new();
15285 let mut plans = Vec::with_capacity(epochs.len());
15286 for epoch in epochs {
15287 if !seen.insert(epoch.clone()) {
15288 continue;
15289 }
15290 let entry = self
15291 .owned_interests
15292 .iter()
15293 .find(|entry| {
15294 entry.epoch.as_ref() == Some(epoch)
15295 && entry.state == SubscriberOwnerState::Staged
15296 })
15297 .ok_or(SubscriberOwnerError::NotStaged)?;
15298 let position = entry
15299 .progress
15300 .as_ref()
15301 .map(|progress| &progress.through)
15302 .or(entry.baseline.as_ref())
15303 .ok_or(SubscriberOwnerError::MissingBaseline)?;
15304 let baseline = position.number;
15305 if through.number < baseline {
15306 return Err(SubscriberOwnerError::ProgressRegression {
15307 current: baseline,
15308 target: through.number,
15309 });
15310 }
15311 let from_block = baseline
15312 .checked_add(1)
15313 .ok_or(SubscriberOwnerError::PostBlockOverflow(baseline))?;
15314 if through.number == baseline && through.hash != position.hash {
15315 return Err(SubscriberOwnerError::ProgressConflict {
15316 number: baseline,
15317 current_hash: position.hash,
15318 target_hash: through.hash,
15319 });
15320 }
15321 if through.number == from_block
15322 && through
15323 .parent_hash
15324 .is_some_and(|parent| parent != position.hash)
15325 {
15326 return Err(SubscriberOwnerError::ProgressConflict {
15327 number: baseline,
15328 current_hash: position.hash,
15329 target_hash: through.parent_hash.expect("checked as present above"),
15330 });
15331 }
15332 if entry
15333 .interests
15334 .iter()
15335 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
15336 {
15337 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
15338 }
15339 plans.push(SubscriberOwnerReconcilePlan {
15340 epoch: epoch.clone(),
15341 interests: entry.interests.clone(),
15342 retained: *position,
15343 from_block,
15344 });
15345 }
15346
15347 self.ensure_streams().await?;
15350 let provider = self.provider.clone();
15351 let filters = merged_owner_reconcile_filters(&plans, through.number);
15352 let retained = plans.iter().map(|plan| plan.retained).collect();
15353 let target_epochs: HashSet<_> = plans.iter().map(|plan| plan.epoch.clone()).collect();
15354 let fetch = fetch_owner_catchup::<P, N>(
15355 provider,
15356 filters,
15357 retained,
15358 through,
15359 SubscriberOwnerCatchupOptions {
15360 target_preverified: false,
15361 max_logs: self.config.max_pending_records,
15362 max_log_bytes: self.config.max_backfill_log_bytes,
15363 max_requests_in_flight: self.config.max_reconcile_requests_in_flight,
15364 },
15365 );
15366 let SubscriberOwnerCatchup { logs, certified } =
15367 self.drive_reconcile_fetch(fetch, &target_epochs).await?;
15368
15369 let records = logs
15370 .into_iter()
15371 .map(|log| log_input_record(log, InputSource::Backfill))
15372 .collect();
15373 let mut routed_records = Vec::new();
15374 for record in dedupe_records(sort_records(records)).map_err(|error| {
15375 SubscriberError::InvalidBackfill(format!(
15376 "conflicting duplicate owner catch-up record: {error}"
15377 ))
15378 })? {
15379 let block_number = match &record.input {
15380 ReactiveInput::Log(log) => log
15381 .block_number
15382 .expect("bulk catch-up logs were validated before commit"),
15383 _ => unreachable!("bulk owner catch-up contains log records only"),
15384 };
15385 let owners: Vec<SubscriberOwnerEpoch> = plans
15386 .iter()
15387 .filter(|plan| block_number >= plan.from_block)
15388 .filter(|plan| {
15389 plan.interests
15390 .iter()
15391 .any(|interest| interest_matches(interest, &record.input))
15392 })
15393 .map(|plan| plan.epoch.clone())
15394 .collect();
15395 if !owners.is_empty() {
15396 routed_records.push((record, owners));
15397 }
15398 }
15399 self.ensure_pending_record_capacity(
15400 routed_records.len(),
15401 "owner reconciliation historical records",
15402 )?;
15403
15404 self.pending_backfills.retain(|queued| {
15408 queued
15409 .epoch
15410 .as_ref()
15411 .is_none_or(|epoch| !target_epochs.contains(epoch))
15412 });
15413 for (record, owners) in routed_records {
15414 self.enqueue_owner_record_for_owners_unmerged(record, owners);
15415 }
15416 self.promote_reconcile_owner_records(&target_epochs);
15417 self.seed_reconciled_filter_anchors(&plans, certified.number);
15418
15419 let stream_revision = self.stream_revision;
15420 let mut progress = Vec::with_capacity(plans.len());
15421 for plan in plans {
15422 let item = SubscriberOwnerProgress {
15423 owner: plan.epoch.clone(),
15424 through: certified,
15425 };
15426 let entry = self
15427 .owned_interests
15428 .iter_mut()
15429 .find(|entry| entry.epoch.as_ref() == Some(&plan.epoch))
15430 .expect("bulk reconcile holds exclusive access after epoch preflight");
15431 entry.progress = Some(item.clone());
15432 entry.progress_stream_revision = Some(stream_revision);
15433 progress.push(item);
15434 }
15435 Ok(progress)
15436 }
15437
15438 async fn drive_reconcile_fetch<T, F>(
15439 &mut self,
15440 fetch: F,
15441 target_epochs: &HashSet<SubscriberOwnerEpoch>,
15442 ) -> Result<T, SubscriberOwnerError>
15443 where
15444 F: Future<Output = Result<T, SubscriberOwnerError>>,
15445 {
15446 if !matches!(&self.state, AlloySubscriberState::Active(_)) {
15447 return fetch.await;
15448 }
15449 let mut fetch = Box::pin(fetch);
15450 loop {
15451 let event = {
15452 let live = Box::pin(self.next_event());
15453 match select(fetch, live).await {
15454 Either::Left((result, pending_live)) => {
15455 drop(pending_live);
15456 return result;
15457 }
15458 Either::Right((event, pending_fetch)) => {
15459 fetch = pending_fetch;
15460 event
15461 }
15462 }
15463 };
15464 let event = event?.ok_or_else(|| {
15465 SubscriberError::Provider(
15466 "Alloy subscriber streams ended during owner reconcile".to_owned(),
15467 )
15468 })?;
15469 self.buffer_reconcile_event_for_owners(&event, target_epochs);
15470 self.enqueue_event_excluding_owners(event, target_epochs);
15471 self.check_resource_error()?;
15472 }
15473 }
15474
15475 pub async fn next_scoped_batch_or<C, F>(
15494 &mut self,
15495 control: Pin<&mut F>,
15496 ) -> Result<SubscriberDriverPoll<C, N>, SubscriberError>
15497 where
15498 C: Send,
15499 F: Future<Output = C> + Send,
15500 {
15501 let batch = self.next_scoped_batch();
15502 match select(control, batch).await {
15503 Either::Left((control, pending_batch)) => {
15504 drop(pending_batch);
15505 Ok(SubscriberDriverPoll::Control(control))
15506 }
15507 Either::Right((batch, _pending_control)) => batch.map(SubscriberDriverPoll::Batch),
15508 }
15509 }
15510
15511 pub fn next_scoped_batch(&mut self) -> SubscriberNextScopedBatch<'_, N> {
15522 Box::pin(async {
15523 self.check_resource_error()?;
15524 if self.chain_id.is_none()
15525 && (!self.pending_records.is_empty()
15526 || !self.pending_chain_controls.is_empty()
15527 || !self.pending_backfills.is_empty()
15528 || !self.interests.is_empty())
15529 {
15530 self.ensure_chain_id().await?;
15531 }
15532 if let Some(batch) = self.drain_next_scoped_batch() {
15533 return Ok(Some(batch));
15534 }
15535
15536 self.ensure_streams().await?;
15541 self.check_resource_error()?;
15542 if let Some(batch) = self.drain_next_scoped_batch() {
15543 return Ok(Some(batch));
15544 }
15545
15546 self.drain_pending_backfills().await?;
15547 self.check_resource_error()?;
15548 if let Some(batch) = self.drain_next_scoped_batch() {
15549 return Ok(Some(batch));
15550 }
15551
15552 if self.interests.is_empty() {
15553 return Ok(None);
15554 }
15555
15556 loop {
15557 let Some(event) = self.next_event().await? else {
15558 return Ok(None);
15559 };
15560
15561 self.enqueue_event(event);
15562 self.check_resource_error()?;
15563 if let Some(batch) = self.drain_next_scoped_batch() {
15564 return Ok(Some(batch));
15565 }
15566 }
15567 })
15568 }
15569
15570 async fn ensure_streams(&mut self) -> Result<(), SubscriberError> {
15585 if !self.sources_dirty {
15586 return Ok(());
15587 }
15588 if matches!(self.state, AlloySubscriberState::Uninitialized) && self.interests.is_empty() {
15593 self.bump_stream_revision();
15594 self.sources_dirty = false;
15595 return Ok(());
15596 }
15597
15598 let desired = self.stream_sources()?;
15599 let missing: Vec<SubscriberStreamSource> = match &self.state {
15600 AlloySubscriberState::Active(streams) => desired
15601 .iter()
15602 .filter(|source| !streams.contains_source(source))
15603 .cloned()
15604 .collect(),
15605 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => desired.clone(),
15606 };
15607
15608 for source in missing {
15609 let stream = match self.connect_source_stream(source.clone()).await {
15610 Ok(stream) => stream,
15611 Err(error)
15612 if source.is_flashblocks()
15613 && self.config.preconfirmations == PreconfirmationMode::Preferred =>
15614 {
15615 tracing::warn!(
15616 stream = source.label(),
15617 error = %error,
15618 "Flashblocks source unavailable; canonical delivery remains active"
15619 );
15620 if self.config.reconnect.enabled {
15621 self.schedule_flashblock_reconnect(
15622 source,
15623 self.config.reconnect.retry_delay,
15624 );
15625 }
15626 continue;
15627 }
15628 Err(error) => return Err(error),
15629 };
15630 self.install_source_stream(source.clone(), stream);
15634 if self.source_requires_backfill(&source) {
15635 self.queue_source_backfill(source);
15636 }
15637 }
15638
15639 while let Some(source) = self.pending_source_backfills.front().cloned() {
15640 let desired_and_live = desired.iter().any(|item| item.same_key(&source))
15641 && matches!(
15642 &self.state,
15643 AlloySubscriberState::Active(streams) if streams.contains_source(&source)
15644 );
15645 if !desired_and_live {
15646 self.pending_source_backfills.pop_front();
15647 continue;
15648 }
15649
15650 let event = self.backfill_reconnected_source(&source).await?;
15656 self.pending_source_backfills.pop_front();
15657 if let Some(event) = event {
15658 self.enqueue_event(event);
15659 }
15660 }
15661
15662 if let AlloySubscriberState::Active(streams) = &mut self.state {
15663 streams.retain_sources(&desired);
15664 if streams.is_empty() {
15665 self.state = AlloySubscriberState::Empty;
15666 }
15667 }
15668
15669 self.bump_stream_revision();
15670 self.sources_dirty = false;
15671 self.retire_unreferenced_filters();
15672 Ok(())
15673 }
15674
15675 fn install_source_stream(
15676 &mut self,
15677 source: SubscriberStreamSource,
15678 stream: BoxStream<'static, SubscriberEvent<N>>,
15679 ) {
15680 match &mut self.state {
15681 AlloySubscriberState::Active(streams) => {
15682 if streams.contains_source(&source) {
15683 return;
15684 }
15685 streams.push(source, stream);
15686 }
15687 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
15688 let mut streams = SubscriberStreams::new();
15689 streams.push(source, stream);
15690 self.state = AlloySubscriberState::Active(streams);
15691 }
15692 }
15693 self.bump_stream_revision();
15696 }
15697
15698 fn schedule_flashblock_reconnect(
15699 &mut self,
15700 source: SubscriberStreamSource,
15701 first_delay: Duration,
15702 ) {
15703 if self
15704 .pending_flashblock_reconnect_sources
15705 .iter()
15706 .any(|pending| pending.same_key(&source))
15707 {
15708 return;
15709 }
15710 self.pending_flashblock_reconnect_sources
15711 .push(source.clone());
15712 self.pending_flashblock_reconnects
15713 .push(flashblock_reconnect_future(
15714 self.provider.root().clone(),
15715 source,
15716 self.config.max_batch_size,
15717 self.config.reconnect.clone(),
15718 first_delay,
15719 self.config.flashblock_poll_interval,
15720 ));
15721 }
15722
15723 fn reschedule_preferred_flashblock(&mut self, source: SubscriberStreamSource) {
15724 if !self.config.reconnect.enabled {
15725 return;
15726 }
15727 self.schedule_flashblock_reconnect(source, self.config.reconnect.max_delay);
15728 }
15729
15730 fn source_requires_backfill(&self, source: &SubscriberStreamSource) -> bool {
15731 matches!(source, SubscriberStreamSource::PubSubLog { id, .. }
15732 if self.last_seen_log_blocks.contains_key(id))
15733 }
15734
15735 fn queue_source_backfill(&mut self, source: SubscriberStreamSource) {
15736 if !self
15737 .pending_source_backfills
15738 .iter()
15739 .any(|pending| pending.same_key(&source))
15740 {
15741 self.pending_source_backfills.push_back(source);
15742 }
15743 }
15744
15745 async fn drain_pending_backfills(&mut self) -> Result<(), SubscriberError> {
15757 while let Some(queued) = self.pending_backfills.front() {
15758 let epoch = queued.epoch.clone();
15760 let owner = queued.owner.clone();
15761 let owner_exists = match (&epoch, &owner) {
15762 (Some(epoch), _) => self.interest_owner_state(epoch).is_some(),
15763 (None, Some(owner)) => self.owner_interests(owner).is_some(),
15764 (None, None) => true,
15765 };
15766 if !owner_exists {
15767 self.pending_backfills.pop_front();
15768 continue;
15769 }
15770 let filters = queued.filters.clone();
15771 let backfill = queued.backfill;
15772
15773 let to_block = match backfill.end_block() {
15774 Some(to_block) => to_block,
15775 None => self
15776 .provider
15777 .get_block_number()
15778 .await
15779 .map_err(provider_error)?,
15780 };
15781 if to_block < backfill.start_block() {
15782 let certified = if let Some(retained) = backfill.retained_anchor() {
15787 let actual =
15788 fetch_provider_block_ref::<P, N>(&self.provider, retained.number).await?;
15789 if !block_ref_satisfies_expected(&actual, retained) {
15790 return Err(SubscriberError::InvalidBackfill(format!(
15791 "retained anchor {}:{:?} conflicts with provider block {}:{:?}",
15792 retained.number, retained.hash, actual.number, actual.hash
15793 )));
15794 }
15795 if to_block < retained.number {
15796 return Err(SubscriberError::InvalidBackfill(format!(
15797 "backfill upper bound {to_block} precedes retained anchor {}",
15798 retained.number
15799 )));
15800 }
15801 Some(actual)
15802 } else {
15803 None
15804 };
15805 self.pending_backfills.pop_front();
15806 for filter in &filters {
15807 let source_id = self.log_source_id(filter);
15808 if let Some(certified) = certified {
15809 self.last_seen_log_blocks
15810 .entry(source_id)
15811 .and_modify(|anchor| *anchor = (*anchor).max(certified.number))
15812 .or_insert(certified.number);
15813 }
15814 }
15815 if owner.is_none()
15816 && let Some(certified) = certified
15817 {
15818 self.pending_chain_controls
15819 .push_back(global_backfill_barrier(backfill, certified));
15820 }
15821 if !self.pending_chain_controls.is_empty() {
15822 break;
15823 }
15824 continue;
15825 }
15826
15827 let through = fetch_provider_block_ref::<P, N>(&self.provider, to_block).await?;
15828 let request_filters =
15829 merged_lazy_backfill_filters(&filters, backfill.start_block(), through.number);
15830 let retained = backfill.retained_anchor().copied().into_iter().collect();
15831 let SubscriberOwnerCatchup {
15832 mut logs,
15833 certified,
15834 } = fetch_owner_catchup::<&P, N>(
15835 &self.provider,
15836 request_filters,
15837 retained,
15838 through,
15839 SubscriberOwnerCatchupOptions {
15840 target_preverified: true,
15841 max_logs: self.config.max_pending_records,
15842 max_log_bytes: self.config.max_backfill_log_bytes,
15843 max_requests_in_flight: self.config.max_reconcile_requests_in_flight,
15844 },
15845 )
15846 .await
15847 .map_err(lazy_backfill_error)?;
15848 logs.sort_by_key(|log| {
15849 (
15850 log.block_number.unwrap_or_default(),
15851 log.transaction_index.unwrap_or_default(),
15852 log.log_index.unwrap_or_default(),
15853 )
15854 });
15855 logs.dedup();
15856 self.ensure_pending_record_capacity(logs.len(), "lazy subscriber backfill records")?;
15857
15858 self.pending_backfills.pop_front();
15861 if let Some(epoch) = epoch.as_ref() {
15862 self.enqueue_backfilled_logs(logs, None, Some(epoch), Some(backfill));
15863 } else if let Some(owner) = owner.as_ref() {
15864 self.enqueue_compat_owner_backfilled_logs(logs, owner, backfill);
15865 } else {
15866 self.enqueue_backfilled_logs(logs, None, None, Some(backfill));
15867 self.pending_chain_controls
15868 .push_back(global_backfill_barrier(backfill, certified));
15869 }
15870 for filter in &filters {
15871 let source_id = self.log_source_id(filter);
15872 let anchor = self
15873 .last_seen_log_blocks
15874 .entry(source_id)
15875 .or_insert(certified.number);
15876 *anchor = (*anchor).max(certified.number);
15877 }
15878
15879 if !self.pending_records.is_empty() || !self.pending_chain_controls.is_empty() {
15880 break;
15881 }
15882 }
15883 Ok(())
15884 }
15885
15886 fn stream_sources(&mut self) -> Result<Vec<SubscriberStreamSource>, SubscriberError> {
15887 let sources = match resolve_subscriber_transport(self.mode)? {
15888 SubscriberTransport::PubSub => self.pubsub_stream_sources(),
15889 SubscriberTransport::Polling => self.polling_stream_sources(),
15890 };
15891 #[cfg(feature = "raw-flashblocks-json")]
15892 let sources = {
15893 let mut sources = sources;
15894 if self.external_flashblock_update_channel_opened {
15895 sources.push(SubscriberStreamSource::ExternalFlashblockUpdates);
15896 }
15897 sources
15898 };
15899 Ok(sources)
15900 }
15901
15902 fn pubsub_stream_sources(&mut self) -> Vec<SubscriberStreamSource> {
15903 let mut sources = Vec::new();
15904 let inherited_anchor = self.last_seen_log_blocks.values().copied().min();
15905
15906 for filter in self.log_stream_filters() {
15907 let id = self.log_source_id(&filter);
15908 if let Some(anchor) = inherited_anchor {
15909 self.last_seen_log_blocks.entry(id).or_insert(anchor);
15910 }
15911 sources.push(SubscriberStreamSource::PubSubLog { id, filter });
15912 }
15913
15914 if needs_pending_hash_stream(&self.interests) {
15915 sources.push(SubscriberStreamSource::PubSubPendingHashes);
15916 }
15917
15918 if needs_header_block_stream(&self.interests) {
15919 if !self.uses_external_flashblock_updates()
15920 && self.config.preconfirmations != PreconfirmationMode::Disabled
15921 && self.chain_id.and_then(flashblocks_adapter).is_some()
15922 {
15923 sources.push(SubscriberStreamSource::CanonicalHeadPolling);
15924 } else {
15925 sources.push(SubscriberStreamSource::PubSubBlockHeaders);
15926 }
15927 }
15928
15929 if self.config.preconfirmations != PreconfirmationMode::Disabled
15930 && !self.uses_external_flashblock_updates()
15931 {
15932 match self.chain_id.and_then(flashblocks_adapter) {
15933 Some(FlashblocksAdapter::NativeSubscriptions) => {
15934 sources.push(SubscriberStreamSource::BaseFlashblocks);
15935 for filter in self.log_stream_filters() {
15936 let id = self.log_source_id(&filter);
15937 sources.push(SubscriberStreamSource::BasePendingLog { id, filter });
15938 }
15939 }
15940 Some(FlashblocksAdapter::PendingStatePolling) => {
15941 sources.push(SubscriberStreamSource::OpPendingFlashblocks);
15942 }
15943 None => {}
15944 }
15945 }
15946
15947 sources
15948 }
15949
15950 fn polling_stream_sources(&self) -> Vec<SubscriberStreamSource> {
15951 let mut sources = Vec::new();
15952
15953 for filter in self.log_stream_filters() {
15954 sources.push(SubscriberStreamSource::PollingLog { filter });
15955 }
15956
15957 if needs_pending_hash_stream(&self.interests) {
15958 sources.push(SubscriberStreamSource::PollingPendingHashes);
15959 }
15960
15961 if self.config.preconfirmations != PreconfirmationMode::Disabled
15962 && !self.uses_external_flashblock_updates()
15963 && self.chain_id.and_then(flashblocks_adapter)
15964 == Some(FlashblocksAdapter::PendingStatePolling)
15965 {
15966 sources.push(SubscriberStreamSource::OpPendingFlashblocks);
15967 }
15968
15969 sources
15970 }
15971
15972 fn log_source_id(&mut self, filter: &Filter) -> usize {
15973 if let Some(id) = self.log_source_ids.get(filter) {
15974 return *id;
15975 }
15976
15977 let id = self.next_log_source_id;
15978 self.next_log_source_id = self.next_log_source_id.saturating_add(1);
15979 self.log_source_ids.insert(filter.clone(), id);
15980 id
15981 }
15982
15983 async fn connect_source_stream(
15984 &mut self,
15985 source: SubscriberStreamSource,
15986 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15987 match source {
15988 SubscriberStreamSource::PubSubLog { id, filter } => {
15989 self.connect_pubsub_log_stream(id, filter).await
15990 }
15991 SubscriberStreamSource::BasePendingLog { id, filter } => {
15992 self.connect_base_pending_log_stream(id, filter).await
15993 }
15994 SubscriberStreamSource::BaseFlashblocks => self.connect_base_flashblock_stream().await,
15995 SubscriberStreamSource::OpPendingFlashblocks => {
15996 self.connect_op_flashblock_tick_stream()
15997 }
15998 SubscriberStreamSource::CanonicalHeadPolling => {
15999 self.connect_canonical_head_tick_stream()
16000 }
16001 SubscriberStreamSource::PubSubPendingHashes => {
16002 self.connect_pubsub_pending_hash_stream().await
16003 }
16004 SubscriberStreamSource::PubSubBlockHeaders => {
16005 self.connect_pubsub_block_header_stream().await
16006 }
16007 SubscriberStreamSource::PollingLog { filter } => {
16008 self.connect_polling_log_stream(filter).await
16009 }
16010 SubscriberStreamSource::PollingPendingHashes => {
16011 self.connect_polling_pending_hash_stream().await
16012 }
16013 #[cfg(feature = "raw-flashblocks-json")]
16014 SubscriberStreamSource::ExternalFlashblockUpdates => {
16015 let receiver = self.external_flashblock_updates.take().ok_or_else(|| {
16016 SubscriberError::Provider(
16017 "external Flashblock update channel receiver is unavailable".into(),
16018 )
16019 })?;
16020 let updates = stream::unfold(receiver, |mut receiver| async move {
16021 receiver
16022 .recv()
16023 .await
16024 .map(|update| (SubscriberEvent::ExternalFlashblockUpdate(update), receiver))
16025 });
16026 Ok(stream_with_termination(
16027 updates,
16028 SubscriberStreamSource::ExternalFlashblockUpdates,
16029 ))
16030 }
16031 }
16032 }
16033
16034 async fn connect_pubsub_log_stream(
16035 &mut self,
16036 id: usize,
16037 filter: Filter,
16038 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16039 #[cfg(feature = "reactive-ws")]
16040 {
16041 let source = SubscriberStreamSource::PubSubLog {
16042 id,
16043 filter: filter.clone(),
16044 };
16045 let stream = self
16046 .provider
16047 .subscribe_logs(&filter)
16048 .channel_size(self.config.max_batch_size.max(1))
16049 .await
16050 .map_err(provider_error)?
16051 .into_stream()
16052 .map(move |log| SubscriberEvent::Log { source_id: id, log });
16053 Ok(stream_with_termination(stream, source))
16054 }
16055
16056 #[cfg(not(feature = "reactive-ws"))]
16057 {
16058 let _ = (id, filter);
16059 Err(SubscriberError::Unsupported(
16060 "AlloySubscriber pubsub mode requires the reactive-ws feature",
16061 ))
16062 }
16063 }
16064
16065 async fn connect_base_pending_log_stream(
16066 &mut self,
16067 id: usize,
16068 filter: Filter,
16069 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16070 #[cfg(feature = "reactive-ws")]
16071 {
16072 let source = SubscriberStreamSource::BasePendingLog {
16073 id,
16074 filter: filter.clone(),
16075 };
16076 let params = base_pending_log_filter(&filter)?;
16077 let stream = self
16078 .provider
16079 .subscribe::<_, Log>(("pendingLogs", params))
16080 .channel_size(self.config.max_batch_size.max(1))
16081 .await
16082 .map_err(provider_error)?
16083 .into_stream()
16084 .map(move |log| SubscriberEvent::BasePendingLogTimed {
16085 source_id: id,
16086 log,
16087 timing: FlashblockIngressTiming::new(Instant::now()),
16088 });
16089 Ok(stream_with_termination(stream, source))
16090 }
16091
16092 #[cfg(not(feature = "reactive-ws"))]
16093 {
16094 let _ = (id, filter);
16095 Err(SubscriberError::Unsupported(
16096 "Base Flashblocks require the reactive-ws feature",
16097 ))
16098 }
16099 }
16100
16101 async fn connect_base_flashblock_stream(
16102 &mut self,
16103 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16104 #[cfg(feature = "reactive-ws")]
16105 {
16106 let stream = self
16107 .provider
16108 .subscribe::<_, BaseFlashblockWirePayload>(("newFlashblocks",))
16109 .channel_size(self.config.max_batch_size.max(1))
16110 .await
16111 .map_err(provider_error)?
16112 .into_stream()
16113 .map(|payload| SubscriberEvent::BaseFlashblockTimed {
16114 payload,
16115 timing: FlashblockIngressTiming::new(Instant::now()),
16116 });
16117 Ok(stream_with_termination(
16118 stream,
16119 SubscriberStreamSource::BaseFlashblocks,
16120 ))
16121 }
16122
16123 #[cfg(not(feature = "reactive-ws"))]
16124 {
16125 Err(SubscriberError::Unsupported(
16126 "Base Flashblocks require the reactive-ws feature",
16127 ))
16128 }
16129 }
16130
16131 fn connect_canonical_head_tick_stream(
16132 &self,
16133 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16134 let mut interval = tokio::time::interval(self.config.canonical_head_poll_interval);
16135 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
16136 let stream = stream::unfold(interval, |mut interval| async move {
16137 interval.tick().await;
16138 Some((SubscriberEvent::CanonicalHeadTick, interval))
16139 });
16140 Ok(stream_with_termination(
16141 stream,
16142 SubscriberStreamSource::CanonicalHeadPolling,
16143 ))
16144 }
16145
16146 fn connect_op_flashblock_tick_stream(
16147 &self,
16148 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16149 let first_tick = tokio::time::Instant::now() + self.config.flashblock_poll_interval;
16150 let mut interval =
16151 tokio::time::interval_at(first_tick, self.config.flashblock_poll_interval);
16152 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
16153 let stream = stream::unfold(interval, |mut interval| async move {
16154 interval.tick().await;
16155 Some((
16156 SubscriberEvent::OpFlashblockTickTimed(
16157 FlashblockIngressTiming::new(Instant::now()),
16158 ),
16159 interval,
16160 ))
16161 });
16162 Ok(stream_with_termination(
16163 stream,
16164 SubscriberStreamSource::OpPendingFlashblocks,
16165 ))
16166 }
16167
16168 async fn connect_pubsub_pending_hash_stream(
16169 &mut self,
16170 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16171 #[cfg(feature = "reactive-ws")]
16172 {
16173 let stream = self
16174 .provider
16175 .subscribe_pending_transactions()
16176 .channel_size(self.config.max_batch_size.max(1))
16177 .await
16178 .map_err(provider_error)?
16179 .into_stream()
16180 .map(SubscriberEvent::PendingHash);
16181 Ok(stream_with_termination(
16182 stream,
16183 SubscriberStreamSource::PubSubPendingHashes,
16184 ))
16185 }
16186
16187 #[cfg(not(feature = "reactive-ws"))]
16188 {
16189 Err(SubscriberError::Unsupported(
16190 "AlloySubscriber pubsub mode requires the reactive-ws feature",
16191 ))
16192 }
16193 }
16194
16195 async fn connect_pubsub_block_header_stream(
16196 &mut self,
16197 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16198 #[cfg(feature = "reactive-ws")]
16199 {
16200 let stream = self
16201 .provider
16202 .subscribe_blocks()
16203 .channel_size(self.config.max_batch_size.max(1))
16204 .await
16205 .map_err(provider_error)?
16206 .into_stream()
16207 .map(SubscriberEvent::BlockHeader);
16208 Ok(stream_with_termination(
16209 stream,
16210 SubscriberStreamSource::PubSubBlockHeaders,
16211 ))
16212 }
16213
16214 #[cfg(not(feature = "reactive-ws"))]
16215 {
16216 Err(SubscriberError::Unsupported(
16217 "AlloySubscriber pubsub mode requires the reactive-ws feature",
16218 ))
16219 }
16220 }
16221
16222 async fn connect_polling_log_stream(
16223 &mut self,
16224 filter: Filter,
16225 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16226 #[cfg(feature = "reactive-polling")]
16227 {
16228 let source = SubscriberStreamSource::PollingLog {
16229 filter: filter.clone(),
16230 };
16231 let stream = self
16232 .provider
16233 .watch_logs(&filter)
16234 .await
16235 .map_err(provider_error)?
16236 .with_channel_size(self.config.max_batch_size.max(1))
16237 .into_stream()
16238 .map(SubscriberEvent::Logs);
16239 Ok(stream_with_termination(stream, source))
16240 }
16241
16242 #[cfg(not(feature = "reactive-polling"))]
16243 {
16244 let _ = filter;
16245 Err(SubscriberError::Unsupported(
16246 "AlloySubscriber polling mode requires the reactive-polling feature",
16247 ))
16248 }
16249 }
16250
16251 async fn connect_polling_pending_hash_stream(
16252 &mut self,
16253 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16254 #[cfg(feature = "reactive-polling")]
16255 {
16256 let stream = self
16257 .provider
16258 .watch_pending_transactions()
16259 .await
16260 .map_err(provider_error)?
16261 .with_channel_size(self.config.max_batch_size.max(1))
16262 .into_stream()
16263 .map(SubscriberEvent::PendingHashes);
16264 Ok(stream_with_termination(
16265 stream,
16266 SubscriberStreamSource::PollingPendingHashes,
16267 ))
16268 }
16269
16270 #[cfg(not(feature = "reactive-polling"))]
16271 {
16272 Err(SubscriberError::Unsupported(
16273 "AlloySubscriber polling mode requires the reactive-polling feature",
16274 ))
16275 }
16276 }
16277
16278 async fn next_event(&mut self) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
16279 loop {
16280 let ready = match &mut self.state {
16281 AlloySubscriberState::Active(streams)
16282 if !self.pending_flashblock_reconnects.is_empty() =>
16283 {
16284 let stream_event = Box::pin(streams.next());
16285 let reconnect = Box::pin(self.pending_flashblock_reconnects.next());
16286 match select(reconnect, stream_event).await {
16287 Either::Left((reconnect, pending_event)) => {
16288 drop(pending_event);
16289 let Some((source, result)) = reconnect else {
16290 continue;
16291 };
16292 SubscriberReady::FlashblockReconnect(source, result)
16293 }
16294 Either::Right((event, pending_reconnect)) => {
16295 drop(pending_reconnect);
16296 SubscriberReady::Event(event)
16297 }
16298 }
16299 }
16300 AlloySubscriberState::Active(streams) => {
16301 SubscriberReady::Event(streams.next().await)
16302 }
16303 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty
16304 if !self.pending_flashblock_reconnects.is_empty() =>
16305 {
16306 let Some((source, result)) = self.pending_flashblock_reconnects.next().await
16307 else {
16308 continue;
16309 };
16310 SubscriberReady::FlashblockReconnect(source, result)
16311 }
16312 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
16313 return Ok(None);
16314 }
16315 };
16316
16317 let event = match ready {
16318 SubscriberReady::Event(event) => event,
16319 SubscriberReady::FlashblockReconnect(source, result) => {
16320 self.pending_flashblock_reconnect_sources
16321 .retain(|pending| !pending.same_key(&source));
16322 match result {
16323 Ok(stream) => {
16324 self.install_source_stream(source, stream);
16325 }
16326 Err(error)
16327 if self.config.preconfirmations == PreconfirmationMode::Preferred =>
16328 {
16329 tracing::warn!(
16330 stream = source.label(),
16331 error = %error,
16332 "Flashblocks reconnect window exhausted; canonical delivery remains active"
16333 );
16334 self.reschedule_preferred_flashblock(source);
16335 }
16336 Err(error) => return Err(error),
16337 }
16338 continue;
16339 }
16340 };
16341
16342 let Some(event) = event else {
16343 return Err(SubscriberError::Provider(
16344 "Alloy subscriber streams terminated before the subscriber was stopped"
16345 .to_owned(),
16346 ));
16347 };
16348
16349 match event {
16350 SubscriberEvent::StreamTerminated(source) => {
16351 if source.is_external_flashblocks() {
16352 #[cfg(feature = "raw-flashblocks-json")]
16353 {
16354 self.external_flashblock_update_channel_opened = false;
16355 }
16356 if let AlloySubscriberState::Active(streams) = &mut self.state {
16357 streams
16358 .entries
16359 .retain(|entry| !entry.source.is_external_flashblocks());
16360 streams.normalize_next_index();
16361 }
16362 self.invalidate_preconfirmation_snapshot();
16363 if self.config.preconfirmations == PreconfirmationMode::Required {
16364 return Err(SubscriberError::Provider(
16365 "required external Flashblock update channel closed".into(),
16366 ));
16367 }
16368 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
16369 }
16370 if source.is_flashblocks() {
16374 self.invalidate_flashblock_generation();
16375 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
16376 }
16377 self.sources_dirty = true;
16378 self.bump_stream_revision();
16379 if let Some(backfill_event) = self.reconnect_source_stream(source).await? {
16380 self.sources_dirty = false;
16381 if let Some(backfill_event) =
16382 self.normalize_flashblock_event(backfill_event).await?
16383 {
16384 self.verify_event_log_blocks(&backfill_event).await?;
16385 return Ok(Some(backfill_event));
16386 }
16387 }
16388 self.sources_dirty = false;
16389 }
16390 event => {
16391 let Some(event) = self.normalize_flashblock_event(event).await? else {
16392 continue;
16393 };
16394 self.verify_event_log_blocks(&event).await?;
16395 return Ok(Some(event));
16396 }
16397 }
16398 }
16399 }
16400
16401 fn invalidate_flashblock_generation(&mut self) {
16402 self.pending_records
16403 .retain(|record| record.scope != SubscriberInputScope::Preconfirmed);
16404 self.pending_preconfirmation_invalidation = true;
16405 self.reset_flashblock_tracking();
16406 if let Some(provider) = self.provider_ref.as_mut() {
16407 provider.generation = provider.generation.saturating_add(1);
16408 }
16409 if let AlloySubscriberState::Active(streams) = &mut self.state {
16410 streams
16411 .entries
16412 .retain(|entry| !entry.source.is_flashblocks());
16413 streams.normalize_next_index();
16414 }
16415 let reconnect_sources = self
16416 .stream_sources()
16417 .unwrap_or_default()
16418 .into_iter()
16419 .filter(SubscriberStreamSource::is_flashblocks)
16420 .collect::<Vec<_>>();
16421 self.pending_flashblock_reconnects.clear();
16422 self.pending_flashblock_reconnect_sources.clear();
16423 if self.config.preconfirmations == PreconfirmationMode::Required
16424 || self.config.reconnect.enabled
16425 {
16426 for source in reconnect_sources {
16427 self.schedule_flashblock_reconnect(source, self.config.reconnect.initial_delay);
16428 }
16429 }
16430 self.sources_dirty = false;
16431 self.bump_stream_revision();
16432 }
16433
16434 async fn normalize_flashblock_event(
16435 &mut self,
16436 event: SubscriberEvent<N>,
16437 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
16438 let event = match event {
16439 SubscriberEvent::BasePendingLog { source_id, log } => {
16440 SubscriberEvent::BasePendingLogTimed {
16441 source_id,
16442 log,
16443 timing: FlashblockIngressTiming::new(Instant::now()),
16444 }
16445 }
16446 SubscriberEvent::BaseFlashblock(payload) => SubscriberEvent::BaseFlashblockTimed {
16447 payload,
16448 timing: FlashblockIngressTiming::new(Instant::now()),
16449 },
16450 SubscriberEvent::OpFlashblockTick => {
16451 SubscriberEvent::OpFlashblockTickTimed(FlashblockIngressTiming::new(Instant::now()))
16452 }
16453 event => event,
16454 };
16455 match event {
16456 #[cfg(feature = "raw-flashblocks-json")]
16457 SubscriberEvent::ExternalFlashblockUpdate(queued) => {
16458 let provider = queued.update.provider().clone();
16459 match self.ingest_flashblock_update_with_ingress(queued.update, queued.timing) {
16460 Ok(()) => {
16461 let _ = queued.acknowledgement.send(Ok(()));
16462 Ok(Some(SubscriberEvent::FlashblockObserved))
16463 }
16464 Err(error)
16465 if self.config.preconfirmations == PreconfirmationMode::Preferred =>
16466 {
16467 let recoverable_capacity =
16468 matches!(error, SubscriberError::ResourceExhausted(_));
16469 if !recoverable_capacity
16470 && let Some(configured) = self.external_flashblocks_provider.as_mut()
16471 && configured.endpoint == provider.endpoint
16472 {
16473 self.rejected_external_flashblock_generation = Some(
16474 self.rejected_external_flashblock_generation
16475 .map_or(provider.generation, |rejected| {
16476 rejected.max(provider.generation)
16477 }),
16478 );
16479 configured.generation = configured
16480 .generation
16481 .max(provider.generation.saturating_add(1));
16482 }
16483 self.invalidate_preconfirmation_snapshot();
16484 self.last_external_flashblock_snapshot = None;
16485 let _ = queued
16486 .acknowledgement
16487 .send(Err(FlashblockUpdateChannelError::Rejected));
16488 tracing::warn!(
16489 provider = %provider.endpoint,
16490 generation = provider.generation,
16491 error = %error,
16492 "external Flashblock update rejected; canonical delivery remains active"
16493 );
16494 Ok(Some(SubscriberEvent::FlashblockInvalidated))
16495 }
16496 Err(error) => {
16497 let _ = queued
16498 .acknowledgement
16499 .send(Err(FlashblockUpdateChannelError::Rejected));
16500 Err(error)
16501 }
16502 }
16503 }
16504 SubscriberEvent::BasePendingLogTimed {
16505 source_id,
16506 log,
16507 timing,
16508 } => {
16509 let block_number = log.block_number.ok_or_else(|| {
16510 SubscriberError::Provider(
16511 "pendingLogs item is missing its pending block number".into(),
16512 )
16513 })?;
16514 let transaction_hash = log.transaction_hash.ok_or_else(|| {
16515 SubscriberError::Provider(
16516 "pendingLogs item is missing its transaction hash".into(),
16517 )
16518 })?;
16519 let matching = self.latest_preconfirmation.as_ref().filter(|flashblock| {
16520 flashblock.block_number == block_number
16521 && flashblock.contains_transaction(&transaction_hash)
16522 });
16523 let Some(flashblock) = matching.cloned() else {
16524 if self
16525 .latest_preconfirmation
16526 .as_ref()
16527 .is_some_and(|latest| block_number < latest.block_number)
16528 {
16529 return Ok(None);
16530 }
16531 if self.unmatched_pending_logs.len() >= self.config.max_pending_records {
16532 return Err(SubscriberError::ResourceExhausted(
16533 "unmatched pendingLogs exceeded max_pending_records".into(),
16534 ));
16535 }
16536 self.unmatched_pending_logs
16537 .push_back((source_id, log, timing));
16538 return Ok(None);
16539 };
16540 let logs = self.filter_preconfirmed_logs(&flashblock, vec![log])?;
16541 Ok(Some(if logs.is_empty() {
16542 SubscriberEvent::FlashblockObserved
16543 } else {
16544 SubscriberEvent::PreconfirmedLogs {
16545 flashblock,
16546 logs,
16547 timing,
16548 }
16549 }))
16550 }
16551 SubscriberEvent::BaseFlashblockTimed {
16552 payload,
16553 timing: source_timing,
16554 } => {
16555 let (flashblock, recover_pending_snapshot) =
16556 self.accept_base_flashblock(payload)?;
16557 let mut logs = Vec::new();
16558 let mut retained = VecDeque::new();
16559 let mut timing = source_timing;
16560 while let Some((source_id, log, log_timing)) =
16561 self.unmatched_pending_logs.pop_front()
16562 {
16563 let transaction_hash = log.transaction_hash;
16564 if log.block_number == Some(flashblock.block_number)
16565 && transaction_hash
16566 .as_ref()
16567 .is_some_and(|hash| flashblock.contains_transaction(hash))
16568 {
16569 let _ = source_id;
16570 timing = timing.earliest(log_timing);
16571 logs.push(log);
16572 } else if log
16573 .block_number
16574 .is_some_and(|number| number >= flashblock.block_number)
16575 {
16576 retained.push_back((source_id, log, log_timing));
16577 } else {
16578 }
16581 }
16582 self.unmatched_pending_logs = retained;
16583
16584 let indexed_recovery = recover_pending_snapshot.then(|| {
16585 let payload_id = flashblock
16586 .payload_id
16587 .expect("indexed recovery carries a payload id");
16588 let index = flashblock.index.expect("indexed recovery carries an index");
16589 let last_diff = self
16590 .base_flashblock_transactions
16591 .as_ref()
16592 .filter(|(known_payload, known_index, _, _)| {
16593 *known_payload == payload_id && *known_index == index
16594 })
16595 .map(|(_, _, _, last_diff)| last_diff.clone())
16596 .unwrap_or_default();
16597 (payload_id, index, last_diff)
16598 });
16599 if recover_pending_snapshot {
16600 if let Some(event) = self
16601 .fetch_pending_flashblock_with_timing(indexed_recovery, timing)
16602 .await
16603 .map_err(PendingFlashblockPollError::into_subscriber)?
16604 {
16605 return Ok(Some(event));
16606 }
16607 self.invalidate_flashblock_generation();
16608 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
16609 }
16610 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
16611 Ok(Some(if logs.is_empty() {
16612 SubscriberEvent::FlashblockObserved
16613 } else {
16614 SubscriberEvent::PreconfirmedLogs {
16615 flashblock,
16616 logs,
16617 timing,
16618 }
16619 }))
16620 }
16621 SubscriberEvent::OpFlashblockTickTimed(timing) => {
16622 self.poll_op_pending_flashblock(timing).await
16623 }
16624 SubscriberEvent::CanonicalHeadTick => self.fetch_certified_canonical_head().await,
16625 SubscriberEvent::PreconfirmedLogs {
16626 flashblock,
16627 logs,
16628 timing,
16629 } => {
16630 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
16631 Ok(Some(if logs.is_empty() {
16632 SubscriberEvent::FlashblockObserved
16633 } else {
16634 SubscriberEvent::PreconfirmedLogs {
16635 flashblock,
16636 logs,
16637 timing,
16638 }
16639 }))
16640 }
16641 SubscriberEvent::FlashblockObserved => Ok(None),
16642 SubscriberEvent::BasePendingLog { .. }
16643 | SubscriberEvent::BaseFlashblock(_)
16644 | SubscriberEvent::OpFlashblockTick => unreachable!("normalized above"),
16645 event => Ok(Some(event)),
16646 }
16647 }
16648
16649 async fn fetch_certified_canonical_head(
16650 &mut self,
16651 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
16652 tokio::time::timeout(
16653 self.config.canonical_head_request_timeout,
16654 self.fetch_certified_canonical_head_inner(),
16655 )
16656 .await
16657 .map_err(|_| {
16658 SubscriberError::Provider(format!(
16659 "canonical head certification timed out after {:?}",
16660 self.config.canonical_head_request_timeout
16661 ))
16662 })?
16663 }
16664
16665 async fn fetch_certified_canonical_head_inner(
16666 &mut self,
16667 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
16668 if self.chain_id.and_then(flashblocks_adapter)
16669 == Some(FlashblocksAdapter::PendingStatePolling)
16670 {
16671 if !self.reserve_flashblock_rpc_methods(2) {
16672 return Ok(None);
16673 }
16674 self.flashblocks_rpc_metrics.pending_block_requests = self
16675 .flashblocks_rpc_metrics
16676 .pending_block_requests
16677 .saturating_add(1);
16678 let pending = self
16679 .fetch_op_pending_block()
16680 .await
16681 .map_err(PendingFlashblockPollError::into_subscriber)?
16682 .ok_or_else(|| {
16683 SubscriberError::Provider(
16684 "provider returned no OP pending block while certifying its parent".into(),
16685 )
16686 })?;
16687 let header = self
16688 .certify_op_pending_parent(&pending)
16689 .await
16690 .map_err(PendingFlashblockPollError::into_subscriber)?;
16691 let certified = BlockRef {
16692 number: header.number(),
16693 hash: header.hash(),
16694 parent_hash: Some(header.parent_hash()),
16695 timestamp: Some(header.timestamp()),
16696 };
16697 if self.last_certified_canonical_head.as_ref() == Some(&certified) {
16698 return Ok(None);
16699 }
16700 self.last_certified_canonical_head = Some(certified);
16701 return Ok(Some(SubscriberEvent::BlockHeader(header)));
16702 }
16703 self.flashblocks_rpc_metrics.canonical_head_requests = self
16704 .flashblocks_rpc_metrics
16705 .canonical_head_requests
16706 .saturating_add(1);
16707 let block = self
16708 .provider
16709 .get_block_by_number(BlockNumberOrTag::Latest)
16710 .await
16711 .map_err(provider_error)?
16712 .ok_or_else(|| {
16713 SubscriberError::Provider(
16714 "provider returned no latest block while certifying canonical head".into(),
16715 )
16716 })?;
16717 let header = block.header();
16718 if header.hash().is_zero() {
16719 return Err(SubscriberError::Provider(
16720 "provider returned a placeholder hash for the latest canonical head".into(),
16721 ));
16722 }
16723 let certified = BlockRef {
16724 number: header.number(),
16725 hash: header.hash(),
16726 parent_hash: Some(header.parent_hash()),
16727 timestamp: Some(header.timestamp()),
16728 };
16729 if self.last_certified_canonical_head.as_ref() == Some(&certified) {
16730 return Ok(None);
16731 }
16732 self.last_certified_canonical_head = Some(certified);
16733 Ok(Some(SubscriberEvent::BlockHeader(header.clone())))
16734 }
16735
16736 fn accept_base_flashblock(
16737 &mut self,
16738 payload: BaseFlashblockWirePayload,
16739 ) -> Result<(FlashblockRef, bool), SubscriberError> {
16740 let provider = self.provider_ref.clone().ok_or({
16741 SubscriberError::InvalidConfig(
16742 "Flashblocks require a stable provider ref from a pinned provider lease",
16743 )
16744 })?;
16745
16746 let (flashblock, recover_pending_snapshot) = match payload {
16747 BaseFlashblockWirePayload::Indexed(payload) => {
16748 if payload.index == 0 {
16749 let base = payload.base.clone().ok_or_else(|| {
16750 SubscriberError::Provider(
16751 "indexed newFlashblocks item zero omitted its base header".into(),
16752 )
16753 })?;
16754 self.base_flashblock_header = Some((payload.payload_id, base));
16755 }
16756
16757 let base = self
16758 .base_flashblock_header
16759 .as_ref()
16760 .filter(|(payload_id, _)| *payload_id == payload.payload_id)
16761 .map(|(_, base)| base);
16762 let block_number = base.map(|base| base.block_number).or_else(|| {
16763 payload
16764 .metadata
16765 .as_ref()
16766 .map(|metadata| metadata.block_number)
16767 });
16768 let block_number = block_number.ok_or_else(|| {
16769 SubscriberError::Provider(
16770 "indexed newFlashblocks payload omitted both base and metadata block number"
16771 .into(),
16772 )
16773 })?;
16774 let diff_transactions = flashblock_transaction_hashes(&payload.diff.transactions)?;
16775 let transaction_hashes = match self.base_flashblock_transactions.as_mut() {
16776 Some((known_payload, known_index, transactions, last_diff))
16777 if *known_payload == payload.payload_id =>
16778 {
16779 if payload.index < *known_index {
16780 return self
16781 .latest_preconfirmation
16782 .clone()
16783 .map(|flashblock| (flashblock, false))
16784 .ok_or_else(|| {
16785 SubscriberError::Provider(
16786 "regressive indexed Flashblock arrived without an active snapshot"
16787 .into(),
16788 )
16789 });
16790 }
16791 if payload.index == *known_index {
16792 if *last_diff != diff_transactions {
16793 return Err(SubscriberError::Provider(
16794 "conflicting duplicate indexed Flashblock payload".into(),
16795 ));
16796 }
16797 } else {
16798 if diff_transactions
16799 .iter()
16800 .any(|hash| transactions.contains(hash))
16801 {
16802 return Err(SubscriberError::Provider(
16803 "indexed Flashblock repeated a transaction from an earlier diff"
16804 .into(),
16805 ));
16806 }
16807 transactions.extend(diff_transactions.iter().copied());
16808 *known_index = payload.index;
16809 *last_diff = diff_transactions;
16810 }
16811 transactions.clone()
16812 }
16813 _ => {
16814 self.base_flashblock_transactions = Some((
16815 payload.payload_id,
16816 payload.index,
16817 diff_transactions.clone(),
16818 diff_transactions.clone(),
16819 ));
16820 diff_transactions
16821 }
16822 };
16823 let partial_block_hash = non_placeholder_hash(payload.diff.block_hash);
16824 let transactions_root = payload
16825 .diff
16826 .transactions_root
16827 .and_then(non_placeholder_hash);
16828 let parent_hash = base.and_then(|base| non_placeholder_hash(base.parent_hash));
16829 let state_root = non_placeholder_hash(payload.diff.state_root);
16830 let timestamp = base.map(|base| base.timestamp);
16831 let base_fee_per_gas = base.and_then(|base| base.base_fee_per_gas);
16832 let beneficiary = base.and_then(|base| base.beneficiary);
16833 let prevrandao = base
16834 .and_then(|base| base.prevrandao)
16835 .and_then(non_placeholder_hash);
16836 let gas_limit = base.and_then(|base| base.gas_limit);
16837 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
16838 provider: &provider,
16839 payload_id: Some(payload.payload_id),
16840 index: Some(payload.index),
16841 block_number,
16842 partial_block_hash,
16843 parent_hash,
16844 state_root,
16845 transactions_root,
16846 transaction_hashes: &transaction_hashes,
16847 timestamp,
16848 base_fee_per_gas,
16849 beneficiary,
16850 prevrandao,
16851 gas_limit,
16852 });
16853 let flashblock = FlashblockRef {
16854 provider,
16855 payload_id: Some(payload.payload_id),
16856 index: Some(payload.index),
16857 block_number,
16858 content_hash,
16859 partial_block_hash,
16860 parent_hash,
16861 state_root,
16862 transactions_root,
16863 transaction_hashes,
16864 timestamp,
16865 base_fee_per_gas,
16866 beneficiary,
16867 prevrandao,
16868 gas_limit,
16869 };
16870 if let Some(previous) = self.latest_preconfirmation.as_ref()
16871 && previous.same_payload(&flashblock)
16872 && previous.index == flashblock.index
16873 && previous.content_hash != flashblock.content_hash
16874 {
16875 return Err(SubscriberError::Provider(
16876 "conflicting duplicate indexed Flashblock content".into(),
16877 ));
16878 }
16879 let recover = match self.latest_preconfirmation.as_ref() {
16880 Some(previous) if previous.same_payload(&flashblock) => {
16881 if let (Some(previous), Some(current)) = (previous.index, flashblock.index)
16882 {
16883 if current < previous {
16884 return Ok((flashblock, false));
16885 }
16886 current > previous.saturating_add(1)
16887 } else {
16888 false
16889 }
16890 }
16891 Some(_) => payload.index != 0,
16892 None => payload.index != 0,
16893 };
16894 (flashblock, recover)
16895 }
16896 BaseFlashblockWirePayload::Block(payload) => {
16897 let transaction_hashes = flashblock_transaction_hashes(&payload.transactions)?;
16898 let parent_hash = non_placeholder_hash(payload.parent_hash);
16899 let state_root = non_placeholder_hash(payload.state_root);
16900 let transactions_root = payload.transactions_root.and_then(non_placeholder_hash);
16901 let partial_block_hash = non_placeholder_hash(payload.hash);
16902 let prevrandao = payload.mix_hash.and_then(non_placeholder_hash);
16903 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
16904 provider: &provider,
16905 payload_id: None,
16906 index: None,
16907 block_number: payload.number,
16908 partial_block_hash,
16909 parent_hash,
16910 state_root,
16911 transactions_root,
16912 transaction_hashes: &transaction_hashes,
16913 timestamp: Some(payload.timestamp),
16914 base_fee_per_gas: payload.base_fee_per_gas,
16915 beneficiary: payload.miner,
16916 prevrandao,
16917 gas_limit: payload.gas_limit,
16918 });
16919 let flashblock = FlashblockRef {
16920 provider,
16921 payload_id: None,
16922 index: None,
16923 block_number: payload.number,
16924 content_hash,
16925 partial_block_hash,
16926 parent_hash,
16927 state_root,
16928 transactions_root,
16929 transaction_hashes,
16930 timestamp: Some(payload.timestamp),
16931 base_fee_per_gas: payload.base_fee_per_gas,
16932 beneficiary: payload.miner,
16933 prevrandao,
16934 gas_limit: payload.gas_limit,
16935 };
16936 if let Some(previous) = self.latest_preconfirmation.as_ref()
16937 && flashblock.same_payload(previous)
16938 && flashblock.content_hash != previous.content_hash
16939 && !flashblock.is_cumulative_successor_of(previous)
16940 {
16941 return Err(SubscriberError::Provider(
16942 "cumulative Flashblock transaction membership is non-monotonic".into(),
16943 ));
16944 }
16945 (flashblock, false)
16946 }
16947 };
16948 Ok((flashblock, recover_pending_snapshot))
16949 }
16950
16951 async fn poll_op_pending_flashblock(
16952 &mut self,
16953 timing: FlashblockIngressTiming,
16954 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
16955 match self
16956 .fetch_pending_flashblock_with_timing(None, timing)
16957 .await
16958 {
16959 Ok(event) => {
16960 self.consecutive_flashblock_poll_failures = 0;
16961 Ok(event)
16962 }
16963 Err(PendingFlashblockPollError::Request(error)) => {
16964 self.flashblocks_rpc_metrics.failed_requests = self
16965 .flashblocks_rpc_metrics
16966 .failed_requests
16967 .saturating_add(1);
16968 self.consecutive_flashblock_poll_failures =
16969 self.consecutive_flashblock_poll_failures.saturating_add(1);
16970 if self.consecutive_flashblock_poll_failures
16971 >= self.config.max_consecutive_flashblock_poll_failures
16972 {
16973 return Err(error);
16974 }
16975 tracing::warn!(
16976 consecutive_failures = self.consecutive_flashblock_poll_failures,
16977 failure_limit = self.config.max_consecutive_flashblock_poll_failures,
16978 error = %error,
16979 "Optimism pending-state Flashblocks request failed; retrying on the next tick"
16980 );
16981 Ok(None)
16982 }
16983 Err(PendingFlashblockPollError::Integrity(error)) => Err(error),
16984 }
16985 }
16986
16987 #[cfg(test)]
16988 async fn fetch_pending_flashblock(
16989 &mut self,
16990 indexed_recovery: Option<(FixedBytes<8>, u64, Vec<B256>)>,
16991 ) -> Result<Option<SubscriberEvent<N>>, PendingFlashblockPollError> {
16992 self.fetch_pending_flashblock_with_timing(
16993 indexed_recovery,
16994 FlashblockIngressTiming::new(Instant::now()),
16995 )
16996 .await
16997 }
16998
16999 async fn fetch_pending_flashblock_with_timing(
17000 &mut self,
17001 indexed_recovery: Option<(FixedBytes<8>, u64, Vec<B256>)>,
17002 timing: FlashblockIngressTiming,
17003 ) -> Result<Option<SubscriberEvent<N>>, PendingFlashblockPollError> {
17004 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
17005 == Some(FlashblocksAdapter::PendingStatePolling);
17006 if samples_pending_range {
17007 let fixed_methods = 2_usize.saturating_add(self.log_stream_filters().len());
17008 if !self.reserve_flashblock_rpc_methods(fixed_methods) {
17009 return Ok(None);
17010 }
17011 }
17012 let state_provider = if samples_pending_range {
17013 self.flashblocks_state_provider
17014 .as_ref()
17015 .unwrap_or(&self.provider)
17016 } else {
17017 &self.provider
17018 };
17019 let latest = if samples_pending_range {
17020 None
17021 } else {
17022 self.flashblocks_rpc_metrics.canonical_head_requests = self
17023 .flashblocks_rpc_metrics
17024 .canonical_head_requests
17025 .saturating_add(1);
17026 Some(
17027 state_provider
17028 .get_block_number()
17029 .await
17030 .map_err(pending_flashblock_request_error)?,
17031 )
17032 };
17033 self.flashblocks_rpc_metrics.pending_block_requests = self
17034 .flashblocks_rpc_metrics
17035 .pending_block_requests
17036 .saturating_add(1);
17037 let pending_block = if samples_pending_range {
17038 self.fetch_op_pending_block().await?
17039 } else {
17040 self.provider
17041 .get_block_by_number(BlockNumberOrTag::Pending)
17042 .await
17043 .map_err(pending_flashblock_request_error)?
17044 };
17045 let Some(block) = pending_block else {
17046 if self.config.preconfirmations == PreconfirmationMode::Required {
17047 return Err(PendingFlashblockPollError::Request(
17048 SubscriberError::Provider(
17049 "Flashblocks provider returned no pending block".into(),
17050 ),
17051 ));
17052 }
17053 return Ok(None);
17054 };
17055 let latest = if samples_pending_range {
17056 self.certify_op_pending_parent(&block).await?.number()
17057 } else {
17058 latest.expect("non-OP pending recovery fetched a canonical height")
17059 };
17060 let header = block.header();
17061 if header.number() <= latest {
17062 return Ok(None);
17063 }
17064
17065 let provider = self.provider_ref.clone().ok_or({
17066 PendingFlashblockPollError::Integrity(SubscriberError::InvalidConfig(
17067 "Flashblocks require a stable provider ref from a pinned provider lease",
17068 ))
17069 })?;
17070 let parent_hash = Some(header.parent_hash());
17071 let transaction_hashes = if let Some(hashes) = block.transactions().as_hashes() {
17072 hashes.to_vec()
17073 } else if let Some(transactions) = block.transactions().as_transactions() {
17074 transactions
17075 .iter()
17076 .map(|transaction| transaction.tx_hash())
17077 .collect()
17078 } else {
17079 Vec::new()
17080 };
17081 let state_root = non_placeholder_hash(header.state_root());
17082 let transactions_root = non_placeholder_hash(header.transactions_root());
17083 let partial_block_hash = non_placeholder_hash(header.hash());
17084 let prevrandao = header.mix_hash().and_then(non_placeholder_hash);
17085 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
17086 provider: &provider,
17087 payload_id: None,
17088 index: None,
17089 block_number: header.number(),
17090 partial_block_hash,
17091 parent_hash,
17092 state_root,
17093 transactions_root,
17094 transaction_hashes: &transaction_hashes,
17095 timestamp: Some(header.timestamp()),
17096 base_fee_per_gas: header.base_fee_per_gas(),
17097 beneficiary: Some(header.beneficiary()),
17098 prevrandao,
17099 gas_limit: Some(header.gas_limit()),
17100 });
17101 let flashblock = FlashblockRef {
17102 provider,
17103 payload_id: None,
17104 index: None,
17105 block_number: header.number(),
17106 content_hash,
17107 partial_block_hash,
17108 parent_hash,
17109 state_root,
17110 transactions_root,
17111 transaction_hashes,
17112 timestamp: Some(header.timestamp()),
17113 base_fee_per_gas: header.base_fee_per_gas(),
17114 beneficiary: Some(header.beneficiary()),
17115 prevrandao,
17116 gas_limit: Some(header.gas_limit()),
17117 };
17118 if samples_pending_range
17119 && self
17120 .latest_preconfirmation
17121 .as_ref()
17122 .is_some_and(|previous| !previous.same_payload(&flashblock))
17123 {
17124 self.invalidate_preconfirmation_snapshot();
17127 }
17128 if let Some((payload_id, index, last_diff)) = indexed_recovery {
17129 self.base_flashblock_transactions = Some((
17130 payload_id,
17131 index,
17132 flashblock.transaction_hashes.clone(),
17133 last_diff,
17134 ));
17135 }
17136 let repeats_pending_snapshot = self
17137 .latest_preconfirmation
17138 .as_ref()
17139 .is_some_and(|previous| previous == &flashblock);
17140 if repeats_pending_snapshot && !samples_pending_range {
17141 return Ok(None);
17142 }
17143
17144 if let Some(previous) = self.latest_preconfirmation.as_ref()
17145 && flashblock.same_payload(previous)
17146 && !flashblock.is_cumulative_successor_of(previous)
17147 {
17148 if samples_pending_range {
17149 self.invalidate_preconfirmation_snapshot();
17154 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
17155 }
17156 return Err(PendingFlashblockPollError::Integrity(
17157 SubscriberError::Provider(
17158 "sampled cumulative Flashblock transaction membership is non-monotonic".into(),
17159 ),
17160 ));
17161 }
17162
17163 let mut logs = self.fetch_pending_logs(flashblock.block_number).await?;
17164 if samples_pending_range {
17165 let (mut receipt_logs, completed_receipts, unavailable_receipts) =
17166 self.fetch_pending_transaction_receipts(&flashblock).await?;
17167 logs.append(&mut receipt_logs);
17168 logs.retain(|log| log.block_number == Some(flashblock.block_number));
17169 for log in &logs {
17170 let transaction_hash = log.transaction_hash.ok_or_else(|| {
17171 PendingFlashblockPollError::Integrity(SubscriberError::Provider(
17172 "pre-confirmed log is missing its transaction hash".into(),
17173 ))
17174 })?;
17175 if !flashblock.contains_transaction(&transaction_hash) {
17176 self.flashblocks_rpc_metrics.raced_samples =
17177 self.flashblocks_rpc_metrics.raced_samples.saturating_add(1);
17178 return Ok(None);
17179 }
17180 }
17181 let logs = self
17182 .filter_preconfirmed_logs(&flashblock, logs)
17183 .map_err(PendingFlashblockPollError::Integrity)?;
17184 self.preconfirmed_unavailable_receipts
17185 .extend(unavailable_receipts);
17186 for transaction_hash in &completed_receipts {
17187 self.preconfirmed_unavailable_receipts
17188 .remove(transaction_hash);
17189 }
17190 self.preconfirmed_receipted_transactions
17191 .extend(completed_receipts);
17192 if repeats_pending_snapshot && logs.is_empty() {
17193 return Ok(None);
17194 }
17195 return Ok(Some(if logs.is_empty() {
17196 SubscriberEvent::FlashblockObserved
17197 } else {
17198 SubscriberEvent::PreconfirmedLogs {
17199 flashblock,
17200 logs,
17201 timing,
17202 }
17203 }));
17204 }
17205 let logs = self
17206 .filter_preconfirmed_logs(&flashblock, logs)
17207 .map_err(PendingFlashblockPollError::Integrity)?;
17208 Ok(Some(if logs.is_empty() {
17209 SubscriberEvent::FlashblockObserved
17210 } else {
17211 SubscriberEvent::PreconfirmedLogs {
17212 flashblock,
17213 logs,
17214 timing,
17215 }
17216 }))
17217 }
17218
17219 async fn fetch_pending_logs(
17220 &mut self,
17221 pending_block_number: u64,
17222 ) -> Result<Vec<Log>, PendingFlashblockPollError> {
17223 let mut logs = Vec::new();
17224 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
17225 == Some(FlashblocksAdapter::PendingStatePolling);
17226 let state_provider = if samples_pending_range {
17227 self.flashblocks_state_provider
17228 .as_ref()
17229 .unwrap_or(&self.provider)
17230 } else {
17231 &self.provider
17232 };
17233 for filter in self.log_stream_filters() {
17234 self.flashblocks_rpc_metrics.pending_log_requests = self
17235 .flashblocks_rpc_metrics
17236 .pending_log_requests
17237 .saturating_add(1);
17238 let filter = if samples_pending_range {
17239 filter
17240 .from_block(pending_block_number)
17241 .to_block(BlockNumberOrTag::Pending)
17242 } else {
17243 filter
17244 .from_block(BlockNumberOrTag::Pending)
17245 .to_block(BlockNumberOrTag::Pending)
17246 };
17247 logs.extend(
17248 state_provider
17249 .get_logs(&filter)
17250 .await
17251 .map_err(pending_flashblock_request_error)?,
17252 );
17253 }
17254 if samples_pending_range {
17255 logs.retain(|log| log.block_number == Some(pending_block_number));
17256 }
17257 Ok(logs)
17258 }
17259
17260 async fn fetch_pending_transaction_receipts(
17261 &mut self,
17262 flashblock: &FlashblockRef,
17263 ) -> Result<(Vec<Log>, Vec<B256>, Vec<B256>), PendingFlashblockPollError> {
17264 let receipt_allowance = self.pending_receipt_request_allowance();
17265 let receipt_limit = self
17266 .config
17267 .max_pending_transaction_receipts_per_tick
17268 .min(receipt_allowance);
17269 if receipt_limit == 0 {
17270 return Ok((Vec::new(), Vec::new(), Vec::new()));
17271 }
17272 let mut transaction_hashes = Vec::with_capacity(receipt_limit);
17273 for transaction_hash in &flashblock.transaction_hashes {
17274 if !self
17275 .preconfirmed_receipted_transactions
17276 .contains(transaction_hash)
17277 && !self
17278 .preconfirmed_unavailable_receipts
17279 .contains(transaction_hash)
17280 {
17281 transaction_hashes.push(*transaction_hash);
17282 if transaction_hashes.len() == receipt_limit {
17283 break;
17284 }
17285 }
17286 }
17287 if transaction_hashes.len() < receipt_limit {
17288 for transaction_hash in &flashblock.transaction_hashes {
17289 if self
17290 .preconfirmed_unavailable_receipts
17291 .contains(transaction_hash)
17292 {
17293 transaction_hashes.push(*transaction_hash);
17294 if transaction_hashes.len() == receipt_limit {
17295 break;
17296 }
17297 }
17298 }
17299 }
17300 if transaction_hashes.is_empty() {
17301 return Ok((Vec::new(), Vec::new(), Vec::new()));
17302 }
17303 let reserved = self.reserve_flashblock_rpc_methods(transaction_hashes.len());
17304 debug_assert!(reserved, "receipt allowance must remain reserved until use");
17305 if !reserved {
17306 return Ok((Vec::new(), Vec::new(), Vec::new()));
17307 }
17308 self.flashblocks_rpc_metrics.pending_receipt_requests = self
17309 .flashblocks_rpc_metrics
17310 .pending_receipt_requests
17311 .saturating_add(transaction_hashes.len() as u64);
17312 let state_provider = self
17313 .flashblocks_state_provider
17314 .as_ref()
17315 .unwrap_or(&self.provider);
17316 let client = state_provider.client();
17317 let mut batch = BatchRequest::new(client);
17318 let mut waiters = Vec::with_capacity(transaction_hashes.len());
17319 for transaction_hash in transaction_hashes {
17320 let waiter = batch
17321 .add_call::<_, serde_json::Value>("eth_getTransactionReceipt", &(transaction_hash,))
17322 .map_err(pending_flashblock_request_error)?;
17323 waiters.push((transaction_hash, waiter));
17324 }
17325 batch
17326 .send()
17327 .await
17328 .map_err(pending_flashblock_request_error)?;
17329 let mut logs = Vec::new();
17330 let mut completed = Vec::new();
17331 let mut unavailable = Vec::new();
17332 for (transaction_hash, waiter) in waiters {
17333 let value = waiter.await.map_err(pending_flashblock_request_error)?;
17334 if let Some(mut receipt_logs) =
17335 normalize_pending_transaction_receipt(transaction_hash, value)
17336 .map_err(PendingFlashblockPollError::Integrity)?
17337 {
17338 self.flashblocks_rpc_metrics.pending_receipts_completed = self
17339 .flashblocks_rpc_metrics
17340 .pending_receipts_completed
17341 .saturating_add(1);
17342 logs.append(&mut receipt_logs);
17343 completed.push(transaction_hash);
17344 } else {
17345 self.flashblocks_rpc_metrics.pending_receipts_unavailable = self
17346 .flashblocks_rpc_metrics
17347 .pending_receipts_unavailable
17348 .saturating_add(1);
17349 unavailable.push(transaction_hash);
17350 }
17351 }
17352 Ok((logs, completed, unavailable))
17353 }
17354
17355 fn pending_receipt_request_allowance(&mut self) -> usize {
17356 self.prune_flashblock_rpc_request_times();
17357 let rolling_capacity = self
17358 .config
17359 .max_flashblock_rpc_requests_per_second
17360 .saturating_sub(self.flashblock_rpc_request_times.len());
17361 rolling_capacity.min(self.pending_receipt_requests_per_tick_capacity())
17362 }
17363
17364 fn pending_receipt_requests_per_tick_capacity(&self) -> usize {
17365 let interval_nanos = self.config.flashblock_poll_interval.as_nanos().max(1);
17366 let ticks_per_second = Duration::from_secs(1).as_nanos().div_ceil(interval_nanos);
17367 let ticks_per_second = usize::try_from(ticks_per_second).unwrap_or(usize::MAX);
17368 self.pending_receipt_requests_per_second_capacity()
17369 .checked_div(ticks_per_second)
17370 .unwrap_or(0)
17371 }
17372
17373 fn pending_receipt_requests_per_second_capacity(&self) -> usize {
17374 let interval_nanos = self.config.flashblock_poll_interval.as_nanos().max(1);
17375 let ticks_per_second = Duration::from_secs(1).as_nanos().div_ceil(interval_nanos);
17376 let ticks_per_second = usize::try_from(ticks_per_second).unwrap_or(usize::MAX);
17377 let fixed_methods_per_tick = 2_usize.saturating_add(self.log_stream_filters().len());
17378 let mut reserved_methods = ticks_per_second.saturating_mul(fixed_methods_per_tick);
17379 if needs_header_block_stream(&self.interests) {
17380 let canonical_interval_nanos =
17381 self.config.canonical_head_poll_interval.as_nanos().max(1);
17382 let canonical_ticks = Duration::from_secs(1)
17383 .as_nanos()
17384 .div_ceil(canonical_interval_nanos);
17385 let canonical_ticks = usize::try_from(canonical_ticks).unwrap_or(usize::MAX);
17386 reserved_methods = reserved_methods.saturating_add(canonical_ticks.saturating_mul(2));
17387 }
17388 self.config
17389 .max_flashblock_rpc_requests_per_second
17390 .saturating_sub(reserved_methods)
17391 }
17392
17393 fn reserve_flashblock_rpc_methods(&mut self, methods: usize) -> bool {
17394 self.prune_flashblock_rpc_request_times();
17395 if self
17396 .flashblock_rpc_request_times
17397 .len()
17398 .saturating_add(methods)
17399 > self.config.max_flashblock_rpc_requests_per_second
17400 {
17401 return false;
17402 }
17403 let now = Instant::now();
17404 for _ in 0..methods {
17405 self.flashblock_rpc_request_times.push_back(now);
17406 }
17407 true
17408 }
17409
17410 fn prune_flashblock_rpc_request_times(&mut self) {
17411 let now = Instant::now();
17412 while self
17413 .flashblock_rpc_request_times
17414 .front()
17415 .is_some_and(|requested| now.duration_since(*requested) >= Duration::from_secs(1))
17416 {
17417 self.flashblock_rpc_request_times.pop_front();
17418 }
17419 }
17420
17421 fn filter_preconfirmed_logs(
17422 &mut self,
17423 flashblock: &FlashblockRef,
17424 mut logs: Vec<Log>,
17425 ) -> Result<Vec<Log>, SubscriberError> {
17426 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
17427 == Some(FlashblocksAdapter::PendingStatePolling);
17428 if self
17429 .latest_preconfirmation
17430 .as_ref()
17431 .is_some_and(|previous| !previous.same_payload(flashblock))
17432 {
17433 self.invalidate_preconfirmation_snapshot();
17438 }
17439 if self
17440 .latest_preconfirmation
17441 .as_ref()
17442 .is_none_or(|previous| !previous.same_payload(flashblock))
17443 {
17444 self.preconfirmed_seen_logs.clear();
17445 }
17446 if let Some(previous) = self.latest_preconfirmation.as_ref()
17447 && previous.same_payload(flashblock)
17448 && let (Some(previous_index), Some(current_index)) = (previous.index, flashblock.index)
17449 && current_index < previous_index
17450 {
17451 return Ok(Vec::new());
17452 }
17453 self.latest_preconfirmation = Some(flashblock.clone());
17454
17455 logs.sort_by_key(|log| (log.transaction_index.unwrap_or(u64::MAX), log.log_index));
17456 let mut filtered = Vec::new();
17457 for mut log in logs {
17458 if log.removed || log.block_number != Some(flashblock.block_number) {
17459 return Err(SubscriberError::Provider(
17460 "pre-confirmed log disagrees with its Flashblock snapshot".into(),
17461 ));
17462 }
17463 let transaction_hash = log.transaction_hash.ok_or_else(|| {
17464 SubscriberError::Provider(
17465 "pre-confirmed log is missing its transaction hash".into(),
17466 )
17467 })?;
17468 let log_index = log.log_index.ok_or_else(|| {
17469 SubscriberError::Provider("pre-confirmed log is missing its log index".into())
17470 })?;
17471 let transaction_index =
17472 flashblock
17473 .transaction_index(&transaction_hash)
17474 .ok_or_else(|| {
17475 SubscriberError::Provider(
17476 "pre-confirmed log transaction is absent from the cumulative Flashblock"
17477 .into(),
17478 )
17479 })?;
17480 if log
17481 .transaction_index
17482 .is_some_and(|reported| reported != transaction_index)
17483 {
17484 return Err(SubscriberError::Provider(
17485 "pre-confirmed log transaction index disagrees with cumulative membership"
17486 .into(),
17487 ));
17488 }
17489 let reported_hash = log.block_hash.and_then(non_placeholder_hash);
17490 if !samples_pending_range
17491 && let Some(reported) = reported_hash
17492 && reported != flashblock.content_hash
17493 && flashblock
17494 .partial_block_hash
17495 .is_some_and(|expected| reported != expected)
17496 {
17497 return Err(SubscriberError::Provider(
17498 "pre-confirmed log partial block hash disagrees with its Flashblock snapshot"
17499 .into(),
17500 ));
17501 }
17502 log.block_hash = Some(flashblock.content_hash);
17503 log.block_timestamp = flashblock.timestamp.or(log.block_timestamp);
17504 log.transaction_index = Some(transaction_index);
17505 if self
17506 .preconfirmed_seen_logs
17507 .insert((transaction_hash, log_index))
17508 && log_matches_any_interest(&log, &self.interests)
17509 {
17510 filtered.push(log);
17511 }
17512 }
17513 Ok(filtered)
17514 }
17515
17516 async fn verify_event_log_blocks(
17517 &mut self,
17518 event: &SubscriberEvent<N>,
17519 ) -> Result<(), SubscriberError> {
17520 if !self.config.verify_log_block_context {
17521 return Ok(());
17522 }
17523 match event {
17524 SubscriberEvent::Log { log, .. } => self.verify_log_block_context(log).await,
17525 SubscriberEvent::BackfilledLogs { logs, .. } | SubscriberEvent::Logs(logs) => {
17526 for log in logs {
17527 self.verify_log_block_context(log).await?;
17528 }
17529 Ok(())
17530 }
17531 #[cfg(feature = "raw-flashblocks-json")]
17532 SubscriberEvent::ExternalFlashblockUpdate(_) => Ok(()),
17533 SubscriberEvent::BlockHeader(_)
17534 | SubscriberEvent::PendingHash(_)
17535 | SubscriberEvent::PendingHashes(_)
17536 | SubscriberEvent::BasePendingLog { .. }
17537 | SubscriberEvent::BasePendingLogTimed { .. }
17538 | SubscriberEvent::BaseFlashblock { .. }
17539 | SubscriberEvent::BaseFlashblockTimed { .. }
17540 | SubscriberEvent::OpFlashblockTick
17541 | SubscriberEvent::OpFlashblockTickTimed(_)
17542 | SubscriberEvent::CanonicalHeadTick
17543 | SubscriberEvent::PreconfirmedLogs { .. }
17544 | SubscriberEvent::FlashblockInvalidated
17545 | SubscriberEvent::FlashblockObserved
17546 | SubscriberEvent::StreamTerminated(_) => Ok(()),
17547 }
17548 }
17549
17550 async fn verify_log_block_context(&mut self, log: &Log) -> Result<(), SubscriberError> {
17551 if log.removed {
17552 return Ok(());
17553 }
17554 let number = log.block_number.ok_or_else(|| {
17555 SubscriberError::Provider(
17556 "canonical log is missing its block number during context verification".into(),
17557 )
17558 })?;
17559 let hash = log.block_hash.ok_or_else(|| {
17560 SubscriberError::Provider(
17561 "canonical log is missing its block hash during context verification".into(),
17562 )
17563 })?;
17564 let key = (number, hash);
17565 if self.verified_log_blocks.contains_key(&key) {
17566 return Ok(());
17567 }
17568 let provider = self
17569 .log_verification_provider
17570 .as_ref()
17571 .unwrap_or(&self.provider);
17572 let block = provider
17573 .get_block_by_number(BlockNumberOrTag::Number(number))
17574 .await
17575 .map_err(provider_error)?
17576 .ok_or_else(|| {
17577 SubscriberError::Provider(format!(
17578 "canonical log block {number} is unavailable during context verification"
17579 ))
17580 })?;
17581 let header = block.header();
17582 let verified = BlockRef {
17583 number: header.number(),
17584 hash: header.hash(),
17585 parent_hash: Some(header.parent_hash()),
17586 timestamp: Some(header.timestamp()),
17587 };
17588 if verified.number != number
17589 || verified.hash != hash
17590 || log
17591 .block_timestamp
17592 .is_some_and(|timestamp| verified.timestamp != Some(timestamp))
17593 {
17594 return Err(SubscriberError::Provider(format!(
17595 "canonical log block {number}:{hash:?} disagrees with the provider's current canonical identity"
17596 )));
17597 }
17598 self.verified_log_blocks.insert(key, verified);
17599 self.verified_log_block_order.push_back(key);
17600 let capacity = self.config.reconnect.dedupe_window.max(1);
17601 while self.verified_log_block_order.len() > capacity {
17602 if let Some(evicted) = self.verified_log_block_order.pop_front() {
17603 self.verified_log_blocks.remove(&evicted);
17604 }
17605 }
17606 Ok(())
17607 }
17608
17609 fn enqueue_event(&mut self, event: SubscriberEvent<N>) {
17610 self.enqueue_event_with_excluded_owners(event, None);
17611 }
17612
17613 fn buffer_reconcile_event_for_owners(
17614 &mut self,
17615 event: &SubscriberEvent<N>,
17616 target_epochs: &HashSet<SubscriberOwnerEpoch>,
17617 ) {
17618 match event {
17619 SubscriberEvent::Log { log, .. } => {
17620 self.buffer_reconcile_log_for_owners(log, InputSource::Subscription, target_epochs)
17621 }
17622 SubscriberEvent::BackfilledLogs { logs, .. } => {
17623 for log in logs {
17624 self.buffer_reconcile_log_for_owners(log, InputSource::Backfill, target_epochs);
17625 }
17626 }
17627 SubscriberEvent::Logs(logs) => {
17628 for log in logs {
17629 self.buffer_reconcile_log_for_owners(log, InputSource::Poll, target_epochs);
17630 }
17631 }
17632 #[cfg(feature = "raw-flashblocks-json")]
17633 SubscriberEvent::ExternalFlashblockUpdate(_) => {}
17634 SubscriberEvent::BlockHeader(_)
17635 | SubscriberEvent::PendingHash(_)
17636 | SubscriberEvent::PendingHashes(_)
17637 | SubscriberEvent::BasePendingLog { .. }
17638 | SubscriberEvent::BasePendingLogTimed { .. }
17639 | SubscriberEvent::BaseFlashblock { .. }
17640 | SubscriberEvent::BaseFlashblockTimed { .. }
17641 | SubscriberEvent::OpFlashblockTick
17642 | SubscriberEvent::OpFlashblockTickTimed(_)
17643 | SubscriberEvent::CanonicalHeadTick
17644 | SubscriberEvent::PreconfirmedLogs { .. }
17645 | SubscriberEvent::FlashblockInvalidated
17646 | SubscriberEvent::FlashblockObserved
17647 | SubscriberEvent::StreamTerminated(_) => {}
17648 }
17649 }
17650
17651 fn buffer_reconcile_log_for_owners(
17652 &mut self,
17653 log: &Log,
17654 source: InputSource,
17655 target_epochs: &HashSet<SubscriberOwnerEpoch>,
17656 ) {
17657 let record = self.with_chain_id(log_input_record(log.clone(), source));
17658 let owners = self
17659 .staged_owners_for_record(&record)
17660 .into_iter()
17661 .filter(|owner| target_epochs.contains(owner))
17662 .collect::<Vec<_>>();
17663 if !owners.is_empty() {
17664 self.push_pending_reconcile_record(BufferedSubscriberOwnerRecord { record, owners });
17665 }
17666 }
17667
17668 fn promote_reconcile_owner_records(&mut self, target_epochs: &HashSet<SubscriberOwnerEpoch>) {
17669 let mut retained = VecDeque::new();
17670 while let Some(mut buffered) = self.pending_reconcile_owner_records.pop_front() {
17671 let mut promoted = Vec::new();
17672 buffered.owners.retain(|owner| {
17673 if target_epochs.contains(owner) {
17674 promoted.push(owner.clone());
17675 false
17676 } else {
17677 true
17678 }
17679 });
17680 if promoted.is_empty() {
17681 retained.push_back(buffered);
17682 continue;
17683 }
17684 let promoted_record = if buffered.owners.is_empty() {
17685 buffered.record
17686 } else {
17687 let record = buffered.record.clone();
17688 retained.push_back(buffered);
17689 record
17690 };
17691 self.enqueue_owner_record_for_owners_unmerged(promoted_record, promoted);
17692 }
17693 self.pending_reconcile_owner_records = retained;
17694 }
17695
17696 fn seed_reconciled_filter_anchors(
17697 &mut self,
17698 plans: &[SubscriberOwnerReconcilePlan<N>],
17699 through: u64,
17700 ) {
17701 for filter in plans.iter().flat_map(|plan| log_filters(&plan.interests)) {
17702 let Some(source_id) = self.log_source_ids.get(&filter).copied() else {
17703 continue;
17704 };
17705 let anchor = self
17706 .last_seen_log_blocks
17707 .entry(source_id)
17708 .or_insert(through);
17709 *anchor = (*anchor).max(through);
17710 }
17711 }
17712
17713 fn enqueue_event_excluding_owners(
17714 &mut self,
17715 event: SubscriberEvent<N>,
17716 excluded: &HashSet<SubscriberOwnerEpoch>,
17717 ) {
17718 self.enqueue_event_with_excluded_owners(event, Some(excluded));
17719 }
17720
17721 fn enqueue_event_with_excluded_owners(
17722 &mut self,
17723 event: SubscriberEvent<N>,
17724 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
17725 ) {
17726 match event {
17727 SubscriberEvent::Log { source_id, log } => {
17728 if log_matches_any_interest(&log, &self.interests) {
17729 let record = log_input_record(log, InputSource::Subscription);
17730 self.note_log_block(source_id, &record);
17731 self.enqueue_record_with_excluded_owners(record, excluded);
17732 }
17733 }
17734 SubscriberEvent::BackfilledLogs { source_id, logs } => {
17735 self.enqueue_backfilled_logs_with_excluded_owners(
17736 logs,
17737 Some(source_id),
17738 None,
17739 None,
17740 excluded,
17741 );
17742 }
17743 SubscriberEvent::Logs(logs) => {
17744 for log in logs {
17745 if log_matches_any_interest(&log, &self.interests) {
17746 self.enqueue_record_with_excluded_owners(
17747 log_input_record(log, InputSource::Poll),
17748 excluded,
17749 );
17750 }
17751 }
17752 }
17753 SubscriberEvent::BlockHeader(header) => {
17754 if needs_header_block_stream(&self.interests) {
17755 let record = block_header_input_record::<N>(header);
17756 self.enqueue_record_with_excluded_owners(record, excluded);
17757 }
17758 }
17759 SubscriberEvent::PendingHash(hash) => {
17760 let record = pending_hash_input_record::<N>(hash, InputSource::Subscription);
17761 self.enqueue_record_with_excluded_owners(record, excluded);
17762 }
17763 SubscriberEvent::PendingHashes(hashes) => {
17764 for hash in hashes {
17765 self.enqueue_record_with_excluded_owners(
17766 pending_hash_input_record::<N>(hash, InputSource::Poll),
17767 excluded,
17768 );
17769 }
17770 }
17771 SubscriberEvent::PreconfirmedLogs {
17772 flashblock,
17773 logs,
17774 timing,
17775 } => {
17776 for log in logs {
17777 let record = self
17778 .with_chain_id(preconfirmed_log_input_record::<N>(log, flashblock.clone()));
17779 self.push_pending_record(SubscriberInputRecord {
17780 record,
17781 scope: SubscriberInputScope::Preconfirmed,
17782 preconfirmation_timing: Some(timing),
17783 });
17784 }
17785 }
17786 SubscriberEvent::FlashblockInvalidated => {
17787 self.pending_preconfirmation_invalidation = true;
17788 }
17789 SubscriberEvent::BasePendingLog { .. }
17790 | SubscriberEvent::BasePendingLogTimed { .. }
17791 | SubscriberEvent::BaseFlashblock { .. }
17792 | SubscriberEvent::BaseFlashblockTimed { .. }
17793 | SubscriberEvent::OpFlashblockTick
17794 | SubscriberEvent::OpFlashblockTickTimed(_)
17795 | SubscriberEvent::CanonicalHeadTick
17796 | SubscriberEvent::FlashblockObserved => {}
17797 #[cfg(feature = "raw-flashblocks-json")]
17798 SubscriberEvent::ExternalFlashblockUpdate(_) => {}
17799 SubscriberEvent::StreamTerminated(_) => {}
17800 }
17801 }
17802
17803 fn enqueue_backfilled_logs(
17804 &mut self,
17805 logs: Vec<Log>,
17806 source_id: Option<usize>,
17807 owner: Option<&SubscriberOwnerEpoch>,
17808 range: Option<SubscriberBackfill>,
17809 ) {
17810 self.enqueue_backfilled_logs_with_excluded_owners(logs, source_id, owner, range, None);
17811 }
17812
17813 fn enqueue_backfilled_logs_with_excluded_owners(
17814 &mut self,
17815 logs: Vec<Log>,
17816 source_id: Option<usize>,
17817 owner: Option<&SubscriberOwnerEpoch>,
17818 range: Option<SubscriberBackfill>,
17819 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
17820 ) {
17821 for log in logs {
17822 if range.as_ref().is_some_and(|range| {
17823 log.block_number.is_some_and(|block| {
17824 block < range.start_block() || range.end_block().is_some_and(|end| block > end)
17825 })
17826 }) {
17827 continue;
17828 }
17829 let matches = match owner {
17830 Some(epoch) => self
17831 .owned_interests
17832 .iter()
17833 .find(|entry| entry.epoch.as_ref() == Some(epoch))
17834 .is_some_and(|entry| log_matches_any_interest(&log, &entry.interests)),
17835 None => log_matches_any_interest(&log, &self.interests),
17836 };
17837 if matches {
17838 let record = log_input_record(log, InputSource::Backfill);
17839 if let Some(epoch) = owner {
17840 self.enqueue_owner_record(record, epoch.clone());
17841 } else {
17842 if let Some(source_id) = source_id {
17843 self.note_log_block(source_id, &record);
17844 }
17845 self.enqueue_record_with_excluded_owners(record, excluded);
17846 }
17847 }
17848 }
17849 }
17850
17851 fn enqueue_compat_owner_backfilled_logs(
17852 &mut self,
17853 logs: Vec<Log>,
17854 owner: &HandlerId,
17855 range: SubscriberBackfill,
17856 ) {
17857 let interests = self
17858 .owned_interests
17859 .iter()
17860 .find(|entry| {
17861 &entry.owner == owner
17862 && entry.epoch.is_none()
17863 && entry.state == SubscriberOwnerState::Active
17864 })
17865 .map(|entry| entry.interests.clone());
17866 let Some(interests) = interests else {
17867 return;
17868 };
17869 for log in logs {
17870 if log.block_number.is_some_and(|block| {
17871 block < range.start_block() || range.end_block().is_some_and(|end| block > end)
17872 }) || !log_matches_any_interest(&log, &interests)
17873 {
17874 continue;
17875 }
17876 let record = log_input_record(log, InputSource::Backfill);
17877 self.enqueue_compat_owner_record(record, owner.clone());
17878 }
17879 }
17880
17881 async fn reconnect_source_stream(
17882 &mut self,
17883 source: SubscriberStreamSource,
17884 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17885 if !source.is_pubsub() {
17886 return Err(stream_terminated_error(&source));
17887 }
17888
17889 if !self.config.reconnect.enabled {
17890 return Err(SubscriberError::Provider(format!(
17891 "Alloy subscriber {} stream terminated and reconnect is disabled",
17892 source.label()
17893 )));
17894 }
17895
17896 let mut attempts = 0usize;
17897 let mut delay = self.config.reconnect.initial_delay;
17898 let mut retry_delay = self.config.reconnect.retry_delay;
17899
17900 loop {
17901 attempts = attempts.saturating_add(1);
17902 if !delay.is_zero() {
17903 tokio::time::sleep(delay).await;
17904 }
17905
17906 match self.reconnect_source_once(source.clone()).await {
17907 Ok(backfill_event) => return Ok(backfill_event),
17908 Err(error) if reconnect_attempts_exhausted(attempts, &self.config.reconnect) => {
17909 return Err(SubscriberError::Provider(format!(
17910 "Alloy subscriber {} stream terminated and reconnect failed after {attempts} attempt(s): {error}",
17911 source.label()
17912 )));
17913 }
17914 Err(error) => {
17915 tracing::warn!(
17916 stream = source.label(),
17917 attempts,
17918 error = %error,
17919 "Alloy subscriber reconnect attempt failed"
17920 );
17921 delay = retry_delay;
17922 retry_delay =
17923 next_reconnect_delay(retry_delay, self.config.reconnect.max_delay);
17924 }
17925 }
17926 }
17927 }
17928
17929 async fn reconnect_source_once(
17930 &mut self,
17931 source: SubscriberStreamSource,
17932 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17933 if matches!(
17934 &self.state,
17935 AlloySubscriberState::Active(streams) if streams.contains_source(&source)
17936 ) {
17937 let backfill_event = self.backfill_reconnected_source(&source).await?;
17941 self.pending_source_backfills
17942 .retain(|pending| !pending.same_key(&source));
17943 return Ok(backfill_event);
17944 }
17945 let stream = self.connect_source_stream(source.clone()).await?;
17946 if !matches!(self.state, AlloySubscriberState::Active(_)) {
17947 return Err(SubscriberError::Provider(
17948 "Alloy subscriber state changed before reconnect completed".to_owned(),
17949 ));
17950 }
17951 self.install_source_stream(source.clone(), stream);
17952 if self.source_requires_backfill(&source) {
17953 self.queue_source_backfill(source.clone());
17954 }
17955 let backfill_event = self.backfill_reconnected_source(&source).await?;
17956 self.pending_source_backfills
17957 .retain(|pending| !pending.same_key(&source));
17958
17959 Ok(backfill_event)
17960 }
17961
17962 async fn backfill_reconnected_source(
17963 &mut self,
17964 source: &SubscriberStreamSource,
17965 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17966 if source.is_flashblocks() {
17967 return Ok(None);
17968 }
17969 let SubscriberStreamSource::PubSubLog { id, filter } = source else {
17970 return Ok(None);
17971 };
17972 let Some(from_block) = self.last_seen_log_blocks.get(id).copied() else {
17973 return Ok(None);
17974 };
17975
17976 let latest = self
17977 .provider
17978 .get_block_number()
17979 .await
17980 .map_err(provider_error)?;
17981 if latest < from_block {
17982 return Ok(None);
17983 }
17984
17985 let logs = self
17986 .provider
17987 .get_logs(&filter.clone().from_block(from_block).to_block(latest))
17988 .await
17989 .map_err(provider_error)?;
17990 Ok(Some(SubscriberEvent::BackfilledLogs {
17991 source_id: *id,
17992 logs,
17993 }))
17994 }
17995
17996 fn note_log_block(&mut self, source_id: usize, record: &ReactiveInputRecord<N>) {
17997 if let Some(block) = record.context.block.as_ref() {
17998 self.last_seen_log_blocks.insert(source_id, block.number);
17999 }
18000 }
18001
18002 fn enqueue_record_with_excluded_owners(
18003 &mut self,
18004 record: ReactiveInputRecord<N>,
18005 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
18006 ) {
18007 let record = self.with_chain_id(record);
18008 let mut owners = self.staged_owners_for_record(&record);
18009 if let Some(excluded) = excluded {
18010 owners.retain(|owner| !excluded.contains(owner));
18011 }
18012 let canonical_duplicate = self.should_skip_recent_duplicate(&record);
18013 let owners = self.filter_recent_owner_duplicates(&record, owners);
18014 let compatibility_owners = self.compatibility_owners_for_record(&record);
18015 let (already_served, newly_served): (Vec<_>, Vec<_>) = compatibility_owners
18016 .into_iter()
18017 .partition(|owner| self.compatibility_owner_has_seen(&record, owner));
18018 if canonical_duplicate {
18019 if !owners.is_empty() {
18020 self.push_pending_record(SubscriberInputRecord {
18021 record: record.clone(),
18022 scope: SubscriberInputScope::OwnerOnly { owners },
18023 preconfirmation_timing: None,
18024 });
18025 }
18026 if !newly_served.is_empty() {
18027 for owner in &newly_served {
18028 self.remember_compatibility_owner_record(&record, owner);
18029 }
18030 self.push_pending_record(SubscriberInputRecord {
18031 record,
18032 scope: SubscriberInputScope::OwnerOnlyHandlers {
18033 owners: newly_served,
18034 },
18035 preconfirmation_timing: None,
18036 });
18037 }
18038 return;
18039 }
18040 self.remember_record(&record);
18041 for owner in already_served.iter().chain(&newly_served) {
18042 self.remember_compatibility_owner_record(&record, owner);
18043 }
18044 self.push_pending_record(SubscriberInputRecord {
18045 record,
18046 scope: if already_served.is_empty() {
18047 SubscriberInputScope::Canonical { owners }
18048 } else {
18049 SubscriberInputScope::CanonicalResidual {
18050 owners,
18051 excluded: already_served,
18052 }
18053 },
18054 preconfirmation_timing: None,
18055 });
18056 }
18057
18058 fn enqueue_compat_owner_record(&mut self, record: ReactiveInputRecord<N>, owner: HandlerId) {
18059 let record = self.with_chain_id(record);
18060 if self.compatibility_owner_has_seen(&record, &owner) {
18061 return;
18062 }
18063 self.remember_compatibility_owner_record(&record, &owner);
18064 self.push_pending_record(SubscriberInputRecord {
18065 record,
18066 scope: SubscriberInputScope::OwnerOnlyHandlers {
18067 owners: vec![owner],
18068 },
18069 preconfirmation_timing: None,
18070 });
18071 }
18072
18073 fn compatibility_owners_for_record(&self, record: &ReactiveInputRecord<N>) -> Vec<HandlerId> {
18074 self.owned_interests
18075 .iter()
18076 .filter(|entry| entry.epoch.is_none() && entry.state == SubscriberOwnerState::Active)
18077 .filter(|entry| {
18078 entry
18079 .interests
18080 .iter()
18081 .any(|interest| interest_matches(interest, &record.input))
18082 })
18083 .map(|entry| entry.owner.clone())
18084 .collect()
18085 }
18086
18087 fn compatibility_owner_has_seen(
18088 &self,
18089 record: &ReactiveInputRecord<N>,
18090 owner: &HandlerId,
18091 ) -> bool {
18092 should_dedupe_record(record)
18093 && self
18094 .recent_compat_owner_input_ref_sets
18095 .get(owner)
18096 .is_some_and(|seen| seen.contains(&record.input_ref()))
18097 }
18098
18099 fn remember_compatibility_owner_record(
18100 &mut self,
18101 record: &ReactiveInputRecord<N>,
18102 owner: &HandlerId,
18103 ) {
18104 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
18105 return;
18106 }
18107 let input_ref = record.input_ref();
18108 let seen = self
18109 .recent_compat_owner_input_ref_sets
18110 .entry(owner.clone())
18111 .or_default();
18112 if !seen.insert(input_ref) {
18113 return;
18114 }
18115 let recent = self
18116 .recent_compat_owner_input_refs
18117 .entry(owner.clone())
18118 .or_default();
18119 recent.push_back(input_ref);
18120 while recent.len() > self.config.reconnect.dedupe_window {
18121 if let Some(evicted) = recent.pop_front() {
18122 seen.remove(&evicted);
18123 }
18124 }
18125 }
18126
18127 fn enqueue_owner_record(
18128 &mut self,
18129 record: ReactiveInputRecord<N>,
18130 owner: SubscriberOwnerEpoch,
18131 ) {
18132 self.enqueue_owner_record_for_owners(record, vec![owner]);
18133 }
18134
18135 fn enqueue_owner_record_for_owners(
18136 &mut self,
18137 record: ReactiveInputRecord<N>,
18138 owners: Vec<SubscriberOwnerEpoch>,
18139 ) {
18140 self.enqueue_owner_record_for_owners_inner(record, owners, true);
18141 }
18142
18143 fn enqueue_owner_record_for_owners_unmerged(
18144 &mut self,
18145 record: ReactiveInputRecord<N>,
18146 owners: Vec<SubscriberOwnerEpoch>,
18147 ) {
18148 self.enqueue_owner_record_for_owners_inner(record, owners, false);
18149 }
18150
18151 fn enqueue_owner_record_for_owners_inner(
18152 &mut self,
18153 record: ReactiveInputRecord<N>,
18154 owners: Vec<SubscriberOwnerEpoch>,
18155 merge_pending: bool,
18156 ) {
18157 let record = self.with_chain_id(record);
18158 let owners = self.filter_recent_owner_duplicates(&record, owners);
18159 if owners.is_empty() {
18160 return;
18161 }
18162 if merge_pending
18163 && should_dedupe_record(&record)
18164 && self.config.reconnect.dedupe_window != 0
18165 {
18166 let input_ref = record.input_ref();
18167 if let Some(pending) = self
18168 .pending_records
18169 .iter_mut()
18170 .rev()
18171 .find(|pending| pending.record.input_ref() == input_ref)
18172 {
18173 let pending_owners = match &mut pending.scope {
18174 SubscriberInputScope::Canonical { owners }
18175 | SubscriberInputScope::CanonicalResidual { owners, .. }
18176 | SubscriberInputScope::OwnerOnly { owners } => Some(owners),
18177 SubscriberInputScope::OwnerOnlyHandlers { .. }
18178 | SubscriberInputScope::Preconfirmed => None,
18179 };
18180 if let Some(pending_owners) = pending_owners {
18181 for owner in owners {
18182 if !pending_owners.contains(&owner) {
18183 pending_owners.push(owner);
18184 }
18185 }
18186 return;
18187 }
18188 }
18189 }
18190 self.push_pending_record(SubscriberInputRecord {
18191 record,
18192 scope: SubscriberInputScope::OwnerOnly { owners },
18193 preconfirmation_timing: None,
18194 });
18195 }
18196
18197 fn push_pending_record(&mut self, record: SubscriberInputRecord<N>) {
18198 if self.pending_record_count() >= self.config.max_pending_records {
18199 self.note_resource_error(format!(
18200 "pending record queues reached the configured limit of {}",
18201 self.config.max_pending_records
18202 ));
18203 return;
18204 }
18205 self.pending_records.push_back(record);
18206 }
18207
18208 fn ensure_pending_record_capacity(
18209 &mut self,
18210 additional: usize,
18211 operation: &str,
18212 ) -> Result<(), SubscriberError> {
18213 let required = self.pending_record_count().saturating_add(additional);
18214 if required > self.config.max_pending_records {
18215 self.note_resource_error(format!(
18216 "{operation} require {required} pending records, above the configured limit of {}",
18217 self.config.max_pending_records
18218 ));
18219 return self.check_resource_error();
18220 }
18221 Ok(())
18222 }
18223
18224 fn push_pending_reconcile_record(&mut self, record: BufferedSubscriberOwnerRecord<N>) {
18225 if self.pending_record_count() >= self.config.max_pending_records {
18226 self.note_resource_error(format!(
18227 "pending record queues reached the configured limit of {}",
18228 self.config.max_pending_records
18229 ));
18230 return;
18231 }
18232 self.pending_reconcile_owner_records.push_back(record);
18233 }
18234
18235 fn pending_record_count(&self) -> usize {
18236 self.pending_records
18237 .len()
18238 .saturating_add(self.pending_reconcile_owner_records.len())
18239 }
18240
18241 fn note_resource_error(&mut self, message: String) {
18242 if self.resource_error.is_none() {
18243 self.resource_error = Some(message);
18244 }
18245 }
18246
18247 fn check_resource_error(&self) -> Result<(), SubscriberError> {
18248 match &self.resource_error {
18249 Some(message) => Err(SubscriberError::ResourceExhausted(message.clone())),
18250 None => Ok(()),
18251 }
18252 }
18253
18254 fn with_chain_id(&self, mut record: ReactiveInputRecord<N>) -> ReactiveInputRecord<N> {
18255 record.context.chain_id = self.chain_id;
18256 if self.config.verify_log_block_context
18257 && let ReactiveInput::Log(log) = &record.input
18258 && !log.removed
18259 && let (Some(number), Some(hash)) = (log.block_number, log.block_hash)
18260 && let Some(verified) = self.verified_log_blocks.get(&(number, hash)).copied()
18261 {
18262 record.context.block = Some(verified);
18263 record.context.chain_status = ChainStatus::Included {
18264 block: verified,
18265 confirmations: 0,
18266 };
18267 }
18268 record
18269 }
18270
18271 fn staged_owners_for_record(
18272 &self,
18273 record: &ReactiveInputRecord<N>,
18274 ) -> Vec<SubscriberOwnerEpoch> {
18275 self.owned_interests
18276 .iter()
18277 .filter(|entry| entry.state == SubscriberOwnerState::Staged)
18278 .filter(|entry| {
18279 entry
18280 .interests
18281 .iter()
18282 .any(|interest| interest_matches(interest, &record.input))
18283 })
18284 .filter_map(|entry| entry.epoch.clone())
18285 .collect()
18286 }
18287
18288 fn filter_recent_owner_duplicates(
18289 &mut self,
18290 record: &ReactiveInputRecord<N>,
18291 owners: Vec<SubscriberOwnerEpoch>,
18292 ) -> Vec<SubscriberOwnerEpoch> {
18293 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
18294 return owners;
18295 }
18296 let input_ref = record.input_ref();
18297 let window = self.config.reconnect.dedupe_window;
18298 owners
18299 .into_iter()
18300 .filter(|owner| {
18301 let seen = self
18302 .recent_owner_input_ref_sets
18303 .entry(owner.clone())
18304 .or_default();
18305 if !seen.insert(input_ref) {
18306 return false;
18307 }
18308 let recent = self
18309 .recent_owner_input_refs
18310 .entry(owner.clone())
18311 .or_default();
18312 recent.push_back(input_ref);
18313 while recent.len() > window {
18314 if let Some(evicted) = recent.pop_front() {
18315 seen.remove(&evicted);
18316 }
18317 }
18318 true
18319 })
18320 .collect()
18321 }
18322
18323 fn should_skip_recent_duplicate(&self, record: &ReactiveInputRecord<N>) -> bool {
18324 if !should_dedupe_record(record) {
18325 return false;
18326 }
18327 self.recent_input_ref_set.contains(&record.input_ref())
18328 }
18329
18330 fn remember_record(&mut self, record: &ReactiveInputRecord<N>) {
18331 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
18332 return;
18333 }
18334
18335 let input_ref = record.input_ref();
18336 if !self.recent_input_ref_set.insert(input_ref) {
18337 return;
18338 }
18339 self.recent_input_refs.push_back(input_ref);
18340
18341 while self.recent_input_refs.len() > self.config.reconnect.dedupe_window {
18342 if let Some(evicted) = self.recent_input_refs.pop_front() {
18343 self.recent_input_ref_set.remove(&evicted);
18344 }
18345 }
18346 }
18347}
18348
18349fn stream_with_termination<N, S>(
18350 stream: S,
18351 source: SubscriberStreamSource,
18352) -> BoxStream<'static, SubscriberEvent<N>>
18353where
18354 N: Network + 'static,
18355 S: futures::Stream<Item = SubscriberEvent<N>> + Send + 'static,
18356{
18357 stream
18358 .chain(stream::once(async move {
18359 SubscriberEvent::StreamTerminated(source)
18360 }))
18361 .boxed()
18362}
18363
18364fn flashblock_reconnect_future<N>(
18365 provider: RootProvider<N>,
18366 source: SubscriberStreamSource,
18367 channel_size: usize,
18368 reconnect: SubscriberReconnectConfig,
18369 first_delay: Duration,
18370 flashblock_poll_interval: Duration,
18371) -> FlashblockReconnectFuture<N>
18372where
18373 N: Network + 'static,
18374{
18375 Box::pin(async move {
18376 if !reconnect.enabled {
18377 let error = SubscriberError::Provider(format!(
18378 "Alloy subscriber {} stream terminated and reconnect is disabled",
18379 source.label()
18380 ));
18381 return (source, Err(error));
18382 }
18383
18384 let mut attempts = 0_usize;
18385 let mut delay = first_delay;
18386 let mut retry_delay = reconnect.retry_delay;
18387 loop {
18388 attempts = attempts.saturating_add(1);
18389 if !delay.is_zero() {
18390 tokio::time::sleep(delay).await;
18391 }
18392 match connect_flashblock_source_once(
18393 &provider,
18394 source.clone(),
18395 channel_size,
18396 flashblock_poll_interval,
18397 )
18398 .await
18399 {
18400 Ok(stream) => return (source, Ok(stream)),
18401 Err(error) if reconnect_attempts_exhausted(attempts, &reconnect) => {
18402 return (
18403 source.clone(),
18404 Err(SubscriberError::Provider(format!(
18405 "Alloy subscriber {} stream reconnect failed after {attempts} attempt(s): {error}",
18406 source.label()
18407 ))),
18408 );
18409 }
18410 Err(error) => {
18411 tracing::warn!(
18412 stream = source.label(),
18413 attempts,
18414 error = %error,
18415 "Flashblocks reconnect attempt failed"
18416 );
18417 delay = retry_delay;
18418 retry_delay = next_reconnect_delay(retry_delay, reconnect.max_delay);
18419 }
18420 }
18421 }
18422 })
18423}
18424
18425async fn connect_flashblock_source_once<N>(
18426 provider: &RootProvider<N>,
18427 source: SubscriberStreamSource,
18428 channel_size: usize,
18429 flashblock_poll_interval: Duration,
18430) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>
18431where
18432 N: Network + 'static,
18433{
18434 #[cfg(not(feature = "reactive-ws"))]
18435 let _ = provider;
18436
18437 match source {
18438 SubscriberStreamSource::BasePendingLog { id, filter } => {
18439 #[cfg(feature = "reactive-ws")]
18440 {
18441 let source = SubscriberStreamSource::BasePendingLog {
18442 id,
18443 filter: filter.clone(),
18444 };
18445 let params = base_pending_log_filter(&filter)?;
18446 let stream = provider
18447 .subscribe::<_, Log>(("pendingLogs", params))
18448 .channel_size(channel_size.max(1))
18449 .await
18450 .map_err(provider_error)?
18451 .into_stream()
18452 .map(move |log| SubscriberEvent::BasePendingLogTimed {
18453 source_id: id,
18454 log,
18455 timing: FlashblockIngressTiming::new(Instant::now()),
18456 });
18457 Ok(stream_with_termination(stream, source))
18458 }
18459 #[cfg(not(feature = "reactive-ws"))]
18460 {
18461 let _ = (id, filter, channel_size);
18462 Err(SubscriberError::Unsupported(
18463 "Base Flashblocks require the reactive-ws feature",
18464 ))
18465 }
18466 }
18467 SubscriberStreamSource::BaseFlashblocks => {
18468 #[cfg(feature = "reactive-ws")]
18469 {
18470 let stream = provider
18471 .subscribe::<_, BaseFlashblockWirePayload>(("newFlashblocks",))
18472 .channel_size(channel_size.max(1))
18473 .await
18474 .map_err(provider_error)?
18475 .into_stream()
18476 .map(|payload| SubscriberEvent::BaseFlashblockTimed {
18477 payload,
18478 timing: FlashblockIngressTiming::new(Instant::now()),
18479 });
18480 Ok(stream_with_termination(
18481 stream,
18482 SubscriberStreamSource::BaseFlashblocks,
18483 ))
18484 }
18485 #[cfg(not(feature = "reactive-ws"))]
18486 {
18487 let _ = channel_size;
18488 Err(SubscriberError::Unsupported(
18489 "Base Flashblocks require the reactive-ws feature",
18490 ))
18491 }
18492 }
18493 SubscriberStreamSource::OpPendingFlashblocks => {
18494 let first_tick = tokio::time::Instant::now();
18495 let mut interval = tokio::time::interval_at(first_tick, flashblock_poll_interval);
18496 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
18497 let stream = stream::unfold(interval, |mut interval| async move {
18498 interval.tick().await;
18499 Some((
18500 SubscriberEvent::OpFlashblockTickTimed(FlashblockIngressTiming::new(
18501 Instant::now(),
18502 )),
18503 interval,
18504 ))
18505 });
18506 Ok(stream_with_termination(
18507 stream,
18508 SubscriberStreamSource::OpPendingFlashblocks,
18509 ))
18510 }
18511 source => Err(SubscriberError::InvalidConfig(match source {
18512 SubscriberStreamSource::PubSubLog { .. }
18513 | SubscriberStreamSource::CanonicalHeadPolling
18514 | SubscriberStreamSource::PubSubPendingHashes
18515 | SubscriberStreamSource::PubSubBlockHeaders
18516 | SubscriberStreamSource::PollingLog { .. }
18517 | SubscriberStreamSource::PollingPendingHashes => {
18518 "Flashblocks reconnect received a canonical source"
18519 }
18520 SubscriberStreamSource::BasePendingLog { .. }
18521 | SubscriberStreamSource::BaseFlashblocks
18522 | SubscriberStreamSource::OpPendingFlashblocks => unreachable!(),
18523 #[cfg(feature = "raw-flashblocks-json")]
18524 SubscriberStreamSource::ExternalFlashblockUpdates => {
18525 "Flashblocks reconnect cannot own an application-managed source"
18526 }
18527 })),
18528 }
18529}
18530
18531fn aggregate_interests<N: Network>(
18532 base: &[ReactiveInterest<N>],
18533 owned: &[OwnedSubscriberInterests<N>],
18534) -> Vec<ReactiveInterest<N>> {
18535 base.iter()
18536 .cloned()
18537 .chain(
18538 owned
18539 .iter()
18540 .flat_map(|entry| entry.interests.iter().cloned()),
18541 )
18542 .collect()
18543}
18544
18545fn stream_terminated_error(source: &SubscriberStreamSource) -> SubscriberError {
18546 SubscriberError::Provider(format!(
18547 "Alloy subscriber {} stream terminated before the subscriber was stopped",
18548 source.label()
18549 ))
18550}
18551
18552fn reconnect_attempts_exhausted(attempts: usize, config: &SubscriberReconnectConfig) -> bool {
18553 config
18554 .max_attempts
18555 .is_some_and(|max_attempts| attempts >= max_attempts)
18556}
18557
18558fn next_reconnect_delay(current: Duration, max: Duration) -> Duration {
18559 if current.is_zero() {
18560 return current;
18561 }
18562 current.checked_mul(2).unwrap_or(max).min(max)
18563}
18564
18565fn should_dedupe_record<N: Network>(record: &ReactiveInputRecord<N>) -> bool {
18566 match &record.input {
18567 ReactiveInput::Log(log) => {
18568 is_canonical_status(&record.context.chain_status) && !log.removed
18569 }
18570 ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_) => true,
18571 ReactiveInput::FullBlock(_) | ReactiveInput::PendingTx(_) => false,
18572 }
18573}
18574
18575#[cfg(test)]
18576mod subscriber_helper_tests {
18577 use super::*;
18578 use alloy_json_rpc::{RequestPacket, ResponsePacket};
18579 use alloy_provider::ProviderBuilder;
18580 use alloy_rpc_client::RpcClient;
18581 use alloy_transport::{TransportError, TransportFut, mock::Asserter};
18582 use std::task::{Context, Poll};
18583 use tower::Service;
18584
18585 #[derive(Clone, Debug)]
18586 struct NeverRespondingTransport;
18587
18588 impl Service<RequestPacket> for NeverRespondingTransport {
18589 type Response = ResponsePacket;
18590 type Error = TransportError;
18591 type Future = TransportFut<'static>;
18592
18593 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
18594 Poll::Ready(Ok(()))
18595 }
18596
18597 fn call(&mut self, _request: RequestPacket) -> Self::Future {
18598 Box::pin(futures::future::pending())
18599 }
18600 }
18601
18602 fn indexed_flashblock(transaction_hash: B256, state_root: B256) -> BaseFlashblockWirePayload {
18603 BaseFlashblockWirePayload::Indexed(BaseFlashblockPayload {
18604 payload_id: FixedBytes::repeat_byte(0x11),
18605 index: 0,
18606 base: Some(BaseFlashblockBase {
18607 parent_hash: B256::repeat_byte(100),
18608 block_number: 101,
18609 timestamp: 1_700_000_101,
18610 gas_limit: Some(30_000_000),
18611 base_fee_per_gas: Some(7),
18612 beneficiary: Some(Address::repeat_byte(0xcb)),
18613 prevrandao: Some(B256::repeat_byte(0x77)),
18614 }),
18615 diff: BaseFlashblockDiff {
18616 state_root,
18617 block_hash: B256::ZERO,
18618 transactions: vec![serde_json::Value::String(format!("{transaction_hash:#x}"))],
18619 transactions_root: None,
18620 },
18621 metadata: None,
18622 })
18623 }
18624
18625 fn base_flashblock_event(payload: BaseFlashblockWirePayload) -> SubscriberEvent<Ethereum> {
18626 SubscriberEvent::BaseFlashblockTimed {
18627 payload,
18628 timing: FlashblockIngressTiming::new(Instant::now()),
18629 }
18630 }
18631
18632 #[test]
18633 fn duplicate_flashblock_transaction_membership_is_rejected() {
18634 let transaction = format!("{:#x}", B256::repeat_byte(0x41));
18635 let transactions = vec![
18636 serde_json::Value::String(transaction.clone()),
18637 serde_json::Value::String(transaction),
18638 ];
18639 assert!(matches!(
18640 flashblock_transaction_hashes(&transactions),
18641 Err(SubscriberError::Provider(ref message)) if message.contains("duplicate")
18642 ));
18643 }
18644
18645 #[test]
18646 fn conflicting_duplicate_indexed_flashblock_is_rejected() {
18647 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18648 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18649 provider,
18650 SubscriberMode::PubSub,
18651 SubscriberConfig::default(),
18652 )
18653 .with_provider_ref(ProviderRef::new("base-paid", 7));
18654 subscriber.chain_id = Some(8_453);
18655
18656 subscriber
18657 .accept_base_flashblock(indexed_flashblock(
18658 B256::repeat_byte(0x41),
18659 B256::repeat_byte(0xa1),
18660 ))
18661 .expect("first indexed preview");
18662 assert!(matches!(
18663 subscriber.accept_base_flashblock(indexed_flashblock(
18664 B256::repeat_byte(0x42),
18665 B256::repeat_byte(0xa2),
18666 )),
18667 Err(SubscriberError::Provider(ref message))
18668 if message.contains("conflicting duplicate")
18669 ));
18670 }
18671
18672 #[tokio::test]
18673 async fn duplicate_index_with_changed_commitment_is_rejected() {
18674 let transaction = B256::repeat_byte(0x41);
18675 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18676 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18677 provider,
18678 SubscriberMode::PubSub,
18679 SubscriberConfig::default(),
18680 )
18681 .with_provider_ref(ProviderRef::new("base-paid", 7));
18682 subscriber.chain_id = Some(8_453);
18683
18684 subscriber
18685 .normalize_flashblock_event(base_flashblock_event(indexed_flashblock(
18686 transaction,
18687 B256::repeat_byte(0xa1),
18688 )))
18689 .await
18690 .expect("first indexed preview");
18691
18692 let BaseFlashblockWirePayload::Indexed(mut conflicting) =
18693 indexed_flashblock(transaction, B256::repeat_byte(0xa1))
18694 else {
18695 unreachable!()
18696 };
18697 conflicting.diff.state_root = B256::repeat_byte(0xbb);
18698 assert!(matches!(
18699 subscriber
18700 .normalize_flashblock_event(base_flashblock_event(
18701 BaseFlashblockWirePayload::Indexed(conflicting),
18702 ))
18703 .await,
18704 Err(SubscriberError::Provider(ref message))
18705 if message.contains("conflicting duplicate indexed Flashblock content")
18706 ));
18707 }
18708
18709 #[tokio::test]
18710 async fn indexed_gap_recovery_seeds_later_cumulative_membership() {
18711 let transaction_a = B256::repeat_byte(0x41);
18712 let transaction_b = B256::repeat_byte(0x42);
18713 let transaction_c = B256::repeat_byte(0x43);
18714 let transaction_d = B256::repeat_byte(0x44);
18715 let asserter = Asserter::new();
18716 asserter.push_success(&100_u64);
18717 let pending = rpc_block(101, B256::ZERO).with_transactions(
18718 alloy_network::primitives::BlockTransactions::Hashes(vec![
18719 transaction_a,
18720 transaction_b,
18721 transaction_c,
18722 ]),
18723 );
18724 asserter.push_success(&Some(pending));
18725 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
18726 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18727 provider,
18728 SubscriberMode::PubSub,
18729 SubscriberConfig::default(),
18730 )
18731 .with_provider_ref(ProviderRef::new("base-paid", 7));
18732 subscriber.chain_id = Some(8_453);
18733
18734 subscriber
18735 .normalize_flashblock_event(base_flashblock_event(indexed_flashblock(
18736 transaction_a,
18737 B256::repeat_byte(0xa1),
18738 )))
18739 .await
18740 .expect("index zero preview");
18741 let BaseFlashblockWirePayload::Indexed(mut gap) =
18742 indexed_flashblock(transaction_c, B256::repeat_byte(0xa3))
18743 else {
18744 unreachable!()
18745 };
18746 gap.index = 2;
18747 gap.base = None;
18748 gap.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
18749 subscriber
18750 .normalize_flashblock_event(base_flashblock_event(BaseFlashblockWirePayload::Indexed(
18751 gap,
18752 )))
18753 .await
18754 .expect("the missing index is recovered from pending state");
18755
18756 let BaseFlashblockWirePayload::Indexed(mut next) =
18757 indexed_flashblock(transaction_d, B256::repeat_byte(0xa4))
18758 else {
18759 unreachable!()
18760 };
18761 next.index = 3;
18762 next.base = None;
18763 next.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
18764 let (next, recover) = subscriber
18765 .accept_base_flashblock(BaseFlashblockWirePayload::Indexed(next))
18766 .expect("the next diff extends the recovered cumulative set");
18767 assert!(!recover);
18768 assert_eq!(
18769 next.transaction_hashes,
18770 vec![transaction_a, transaction_b, transaction_c, transaction_d]
18771 );
18772 }
18773
18774 #[tokio::test]
18775 async fn unrecoverable_indexed_gap_revokes_the_generation() {
18776 let asserter = Asserter::new();
18777 asserter.push_success(&100_u64);
18778 asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(0x64))));
18779 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
18780 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18781 provider,
18782 SubscriberMode::PubSub,
18783 SubscriberConfig {
18784 preconfirmations: PreconfirmationMode::Preferred,
18785 ..SubscriberConfig::default()
18786 },
18787 )
18788 .with_provider_ref(ProviderRef::new("base-paid", 7));
18789 subscriber.chain_id = Some(8_453);
18790
18791 subscriber
18792 .normalize_flashblock_event(base_flashblock_event(indexed_flashblock(
18793 B256::repeat_byte(0x41),
18794 B256::repeat_byte(0xa1),
18795 )))
18796 .await
18797 .expect("index zero preview");
18798 let BaseFlashblockWirePayload::Indexed(mut gap) =
18799 indexed_flashblock(B256::repeat_byte(0x43), B256::repeat_byte(0xa3))
18800 else {
18801 unreachable!()
18802 };
18803 gap.index = 2;
18804 gap.base = None;
18805 gap.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
18806 let event = subscriber
18807 .normalize_flashblock_event(base_flashblock_event(BaseFlashblockWirePayload::Indexed(
18808 gap,
18809 )))
18810 .await
18811 .expect("preferred mode fails closed without pending recovery")
18812 .expect("generation invalidation is observable");
18813 assert!(matches!(event, SubscriberEvent::FlashblockInvalidated));
18814 assert!(subscriber.latest_preconfirmation.is_none());
18815 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 8);
18816 }
18817
18818 #[test]
18819 fn base_flashblock_wire_decodes_cumulative_block_shape() {
18820 let payload: BaseFlashblockWirePayload = serde_json::from_str(
18821 r#"{
18822 "hash":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
18823 "number":"0x2ef403b",
18824 "parentHash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
18825 "stateRoot":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
18826 "timestamp":"0x6a68dd59",
18827 "transactions":[]
18828 }"#,
18829 )
18830 .expect("decode current Base newFlashblocks shape");
18831 let BaseFlashblockWirePayload::Block(payload) = payload else {
18832 panic!("expected cumulative block-shaped payload")
18833 };
18834 assert_eq!(payload.number, 49_233_979);
18835 assert_eq!(payload.timestamp, 1_785_257_305);
18836 assert_eq!(payload.hash, B256::repeat_byte(0xaa));
18837 assert_eq!(payload.parent_hash, B256::repeat_byte(0xbb));
18838 assert_eq!(payload.state_root, B256::repeat_byte(0xcc));
18839 }
18840
18841 #[tokio::test]
18842 async fn zero_hash_pending_log_waits_for_the_preview_containing_its_transaction() {
18843 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18844 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18845 provider,
18846 SubscriberMode::PubSub,
18847 SubscriberConfig::default(),
18848 )
18849 .with_provider_ref(ProviderRef::new("base-paid", 7));
18850 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
18851 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
18852 local_matcher: None,
18853 route_key: None,
18854 })];
18855 subscriber.interests = subscriber.base_interests.clone();
18856
18857 let first: BaseFlashblockWirePayload = serde_json::from_str(
18858 r#"{
18859 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
18860 "number":"0x65",
18861 "parentHash":"0x6464646464646464646464646464646464646464646464646464646464646464",
18862 "stateRoot":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
18863 "transactionsRoot":"0x1111111111111111111111111111111111111111111111111111111111111111",
18864 "timestamp":"0x6553f165",
18865 "transactions":["0x4141414141414141414141414141414141414141414141414141414141414141"]
18866 }"#,
18867 )
18868 .expect("decode first cumulative preview");
18869 subscriber
18870 .normalize_flashblock_event(base_flashblock_event(first))
18871 .await
18872 .expect("first preview is accepted");
18873
18874 let mut second_log = rpc_log(false);
18875 second_log.block_hash = Some(B256::ZERO);
18876 second_log.block_number = Some(102);
18877 second_log.block_timestamp = Some(1_700_000_102);
18878 second_log.transaction_hash = Some(B256::repeat_byte(0x42));
18879 second_log.transaction_index = Some(0);
18880 second_log.log_index = Some(0);
18881
18882 let pending_log_ingress = Instant::now() - Duration::from_millis(25);
18883 let before_preview = subscriber
18884 .normalize_flashblock_event(SubscriberEvent::BasePendingLogTimed {
18885 source_id: 0,
18886 log: second_log,
18887 timing: FlashblockIngressTiming::new(pending_log_ingress),
18888 })
18889 .await
18890 .expect("a zero-hash log for the next block must be buffered");
18891 assert!(before_preview.is_none());
18892
18893 let second: BaseFlashblockWirePayload = serde_json::from_str(
18894 r#"{
18895 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
18896 "number":"0x66",
18897 "parentHash":"0x6565656565656565656565656565656565656565656565656565656565656565",
18898 "stateRoot":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
18899 "transactionsRoot":"0x2222222222222222222222222222222222222222222222222222222222222222",
18900 "timestamp":"0x6553f166",
18901 "transactions":["0x4242424242424242424242424242424242424242424242424242424242424242"]
18902 }"#,
18903 )
18904 .expect("decode second cumulative preview");
18905 let event = subscriber
18906 .normalize_flashblock_event(base_flashblock_event(second))
18907 .await
18908 .expect("second preview is accepted")
18909 .expect("the matching buffered log is released");
18910 let SubscriberEvent::PreconfirmedLogs {
18911 flashblock,
18912 logs,
18913 timing,
18914 } = event
18915 else {
18916 panic!("expected a preconfirmed log batch")
18917 };
18918 assert_eq!(timing.source_ingress(), pending_log_ingress);
18919 assert_eq!(flashblock.block_number, 102);
18920 assert_ne!(flashblock.content_hash, B256::ZERO);
18921 assert_eq!(flashblock.partial_block_hash, None);
18922 assert_eq!(logs.len(), 1);
18923 assert_eq!(logs[0].transaction_hash, Some(B256::repeat_byte(0x42)));
18924 assert_eq!(logs[0].block_hash, Some(flashblock.content_hash));
18925 }
18926
18927 #[test]
18928 fn flashblock_endpoints_certify_canonical_heads_instead_of_trusting_newheads() {
18929 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18930 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18931 provider,
18932 SubscriberMode::PubSub,
18933 SubscriberConfig {
18934 preconfirmations: PreconfirmationMode::Required,
18935 ..SubscriberConfig::default()
18936 },
18937 )
18938 .with_provider_ref(ProviderRef::new("base-paid", 7));
18939 subscriber.chain_id = Some(8_453);
18940 subscriber.interests = vec![ReactiveInterest::Blocks(BlockInterest::default())];
18941
18942 let sources = subscriber.pubsub_stream_sources();
18943 assert!(
18944 sources
18945 .iter()
18946 .any(|source| matches!(source, SubscriberStreamSource::CanonicalHeadPolling))
18947 );
18948 assert!(
18949 !sources
18950 .iter()
18951 .any(|source| matches!(source, SubscriberStreamSource::PubSubBlockHeaders))
18952 );
18953 }
18954
18955 #[test]
18956 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
18957 fn external_flashblocks_keep_normal_canonical_pubsub_sources_on_any_chain() {
18958 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18959 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18960 provider,
18961 SubscriberMode::PubSub,
18962 SubscriberConfig {
18963 preconfirmations: PreconfirmationMode::Required,
18964 ..SubscriberConfig::default()
18965 },
18966 );
18967 subscriber
18968 .configure_external_flashblock_updates(ProviderRef::new("raw-json", 4))
18969 .expect("configure external source");
18970 subscriber.chain_id = Some(1);
18971 subscriber.base_interests = vec![
18972 ReactiveInterest::Blocks(BlockInterest::default()),
18973 log_interest_matching_rpc_log(),
18974 ];
18975 subscriber.interests = subscriber.base_interests.clone();
18976
18977 let pubsub = subscriber.pubsub_stream_sources();
18978 assert!(
18979 pubsub
18980 .iter()
18981 .any(|source| matches!(source, SubscriberStreamSource::PubSubBlockHeaders))
18982 );
18983 assert!(
18984 pubsub
18985 .iter()
18986 .any(|source| matches!(source, SubscriberStreamSource::PubSubLog { .. }))
18987 );
18988 assert!(pubsub.iter().all(|source| !matches!(
18989 source,
18990 SubscriberStreamSource::BaseFlashblocks
18991 | SubscriberStreamSource::BasePendingLog { .. }
18992 | SubscriberStreamSource::OpPendingFlashblocks
18993 | SubscriberStreamSource::CanonicalHeadPolling
18994 )));
18995 assert!(
18996 subscriber
18997 .polling_stream_sources()
18998 .iter()
18999 .all(|source| { !matches!(source, SubscriberStreamSource::OpPendingFlashblocks) })
19000 );
19001 assert!(
19002 subscriber
19003 .capabilities()
19004 .supports(SubscriberCapability::Preconfirmations)
19005 );
19006 }
19007
19008 #[test]
19009 #[cfg(feature = "raw-flashblocks-json")]
19010 fn external_flashblocks_are_rejected_when_preconfirmations_are_disabled() {
19011 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19012 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19013 provider,
19014 SubscriberMode::PubSub,
19015 SubscriberConfig::default(),
19016 );
19017 subscriber
19018 .configure_external_flashblock_updates(ProviderRef::new("raw-json", 4))
19019 .expect("configure external source");
19020 subscriber.chain_id = Some(1);
19021
19022 assert!(matches!(
19023 subscriber.validate_flashblocks_setup(),
19024 Err(SubscriberError::InvalidConfig(message))
19025 if message.contains("require preconfirmations")
19026 ));
19027 }
19028
19029 #[tokio::test]
19030 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
19031 async fn external_flashblocks_configuration_is_rejected_after_registration_starts() {
19032 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19033 let mut fresh = AlloySubscriber::<_, Ethereum>::new(
19034 provider,
19035 SubscriberMode::PubSub,
19036 SubscriberConfig {
19037 preconfirmations: PreconfirmationMode::Preferred,
19038 ..SubscriberConfig::default()
19039 },
19040 );
19041 fresh
19042 .configure_external_flashblock_updates(ProviderRef::new("raw-json", 4))
19043 .expect("construction-time external source");
19044
19045 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19046 let mut started = AlloySubscriber::<_, Ethereum>::new(
19047 provider,
19048 SubscriberMode::PubSub,
19049 SubscriberConfig {
19050 preconfirmations: PreconfirmationMode::Preferred,
19051 ..SubscriberConfig::default()
19052 },
19053 )
19054 .with_provider_ref(ProviderRef::new("canonical", 3));
19055 started.chain_id = Some(8_453);
19056 started
19057 .register_interests(&[log_interest_matching_rpc_log()])
19058 .await
19059 .expect("register canonical topology");
19060
19061 assert!(matches!(
19062 started.configure_external_flashblock_updates(ProviderRef::new("raw-json", 4)),
19063 Err(SubscriberError::InvalidConfig(message))
19064 if message.contains("before subscriber registration")
19065 ));
19066 }
19067
19068 #[tokio::test]
19069 #[cfg(feature = "raw-flashblocks-json")]
19070 async fn external_flashblocks_preflight_performs_no_flashblocks_rpc() {
19071 let asserter = Asserter::new();
19072 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19073 let source = ProviderRef::new("raw-json", 4);
19074 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19075 provider,
19076 SubscriberMode::PubSub,
19077 SubscriberConfig {
19078 preconfirmations: PreconfirmationMode::Required,
19079 ..SubscriberConfig::default()
19080 },
19081 );
19082 subscriber
19083 .configure_external_flashblock_updates(source.clone())
19084 .expect("configure external source");
19085 subscriber.chain_id = Some(1);
19086 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19087 subscriber.interests = subscriber.base_interests.clone();
19088 let desired = subscriber.pubsub_stream_sources();
19089 let mut streams = SubscriberStreams::new();
19090 for source in desired {
19091 streams.push(source, stream::pending().boxed());
19092 }
19093 subscriber.state = AlloySubscriberState::Active(streams);
19094 subscriber.sources_dirty = false;
19095
19096 let preflight = subscriber
19097 .establish_flashblocks_preflight(1)
19098 .await
19099 .expect("external source preflight");
19100 assert_eq!(preflight.provider(), &source);
19101 assert_eq!(preflight.delivery(), FlashblocksDelivery::ExternalUpdates);
19102 assert_eq!(preflight.pending_log_subscriptions(), 0);
19103 assert_eq!(subscriber.flashblocks_rpc_metrics().total_requests(), 0);
19104 assert!(asserter.read_q().is_empty());
19105 }
19106
19107 #[tokio::test]
19108 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
19109 async fn bounded_external_channel_survives_subscriber_move_and_closure_keeps_canonical_stream()
19110 {
19111 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19112 let source = ProviderRef::new("raw-json", 4);
19113 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19114 provider,
19115 SubscriberMode::PubSub,
19116 SubscriberConfig {
19117 preconfirmations: PreconfirmationMode::Preferred,
19118 ..SubscriberConfig::default()
19119 },
19120 );
19121 subscriber
19122 .configure_external_flashblock_updates(source.clone())
19123 .expect("configure external source");
19124 subscriber.chain_id = Some(1);
19125 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19126 subscriber.interests = subscriber.base_interests.clone();
19127 let filter = subscriber.log_stream_filters().remove(0);
19128 let source_id = subscriber.log_source_id(&filter);
19129 let mut streams = SubscriberStreams::new();
19130 streams.push(
19131 SubscriberStreamSource::PubSubLog {
19132 id: source_id,
19133 filter,
19134 },
19135 stream::pending().boxed(),
19136 );
19137 subscriber.state = AlloySubscriberState::Active(streams);
19138 subscriber.sources_dirty = false;
19139
19140 let sender = subscriber
19141 .open_external_flashblock_update_channel(2)
19142 .expect("bounded external queue");
19143 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
19144 let update_stream = subscriber
19145 .connect_source_stream(external.clone())
19146 .await
19147 .expect("attach receiver as subscriber source");
19148 subscriber.install_source_stream(external, update_stream);
19149 subscriber.sources_dirty = false;
19150
19151 let mut adapter = RawJsonFlashblocksAdapter::new(source);
19152 let frame = br#"{
19153 "payload_id":"0x1111111111111111",
19154 "index":0,
19155 "base":{
19156 "parent_hash":"0x0606060606060606060606060606060606060606060606060606060606060606",
19157 "block_number":"0x7",
19158 "timestamp":"0x6553f107"
19159 },
19160 "diff":{
19161 "state_root":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
19162 "block_hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
19163 "transactions":["0x01"]
19164 },
19165 "metadata":{
19166 "block_number":7,
19167 "receipts":{
19168 "0x5fe7f977e71dba2ea1a68e21057beebb9be2ac30c6410aa38d4f3fbe41dcffd2":{
19169 "logs":[{
19170 "address":"0x4242424242424242424242424242424242424242",
19171 "topics":["0x0101010101010101010101010101010101010101010101010101010101010101"],
19172 "data":"0x"
19173 }]
19174 }
19175 }
19176 }
19177 }"#;
19178 let update = adapter
19179 .ingest_json(frame)
19180 .expect("valid raw update")
19181 .expect("snapshot update");
19182 let valid_update = update.clone();
19183 let sending = {
19184 let sender = sender.clone();
19185 tokio::spawn(async move { sender.send(update).await })
19186 };
19187
19188 let preview = subscriber
19189 .next_scoped_batch()
19190 .await
19191 .expect("poll preview")
19192 .expect("preview batch");
19193 assert_eq!(preview.records().len(), 1);
19194 assert!(preview.records()[0].scope().is_preconfirmed());
19195 assert_eq!(
19196 preview.records()[0].context.source,
19197 InputSource::Flashblocks
19198 );
19199 assert!(subscriber.latest_preconfirmation.is_some());
19200 assert_eq!(sending.await.expect("sender task"), Ok(()));
19201
19202 let mut invalid_update = valid_update.clone();
19203 let FlashblockUpdate::Snapshot(snapshot) = &mut invalid_update else {
19204 unreachable!("fixture is a snapshot")
19205 };
19206 snapshot.logs[0].block_hash = Some(B256::repeat_byte(0xee));
19207 let rejecting = {
19208 let sender = sender.clone();
19209 tokio::spawn(async move { sender.send(invalid_update).await })
19210 };
19211 let rejected = subscriber
19212 .next_scoped_batch()
19213 .await
19214 .expect("preferred mode keeps polling")
19215 .expect("rejected update invalidation");
19216 assert!(rejected.preconfirmation_invalidated());
19217 assert!(rejected.records().is_empty());
19218 assert!(subscriber.latest_preconfirmation.is_none());
19219 assert_eq!(
19220 rejecting.await.expect("sender task"),
19221 Err(FlashblockUpdateChannelError::Rejected)
19222 );
19223 subscriber
19224 .ingest_flashblock_update(valid_update)
19225 .expect("rejected generation is ignored thereafter");
19226 assert!(subscriber.latest_preconfirmation.is_none());
19227
19228 let _reset = adapter
19229 .reset(ProviderRef::new("raw-json", 5))
19230 .expect("advance rejected source generation");
19231 let recovered_update = adapter
19232 .ingest_json(frame)
19233 .expect("valid replacement generation")
19234 .expect("replacement snapshot update");
19235 let recovering = {
19236 let sender = sender.clone();
19237 tokio::spawn(async move { sender.send(recovered_update).await })
19238 };
19239 let recovered = subscriber
19240 .next_scoped_batch()
19241 .await
19242 .expect("poll replacement generation")
19243 .expect("replacement preview batch");
19244 assert_eq!(recovered.records().len(), 1);
19245 assert!(matches!(
19246 &recovered.records()[0].context.chain_status,
19247 ChainStatus::Preconfirmed { flashblock }
19248 if flashblock.provider == ProviderRef::new("raw-json", 5)
19249 ));
19250 assert_eq!(recovering.await.expect("sender task"), Ok(()));
19251
19252 drop(sender);
19253 let invalidation = subscriber
19254 .next_scoped_batch()
19255 .await
19256 .expect("poll channel closure")
19257 .expect("closure invalidation");
19258 assert!(invalidation.preconfirmation_invalidated());
19259 assert!(invalidation.records().is_empty());
19260 assert!(subscriber.latest_preconfirmation.is_none());
19261 assert!(matches!(
19262 &subscriber.state,
19263 AlloySubscriberState::Active(streams)
19264 if streams.entries.iter().any(|entry| matches!(
19265 entry.source,
19266 SubscriberStreamSource::PubSubLog { id, .. } if id == source_id
19267 ))
19268 ));
19269 }
19270
19271 #[tokio::test]
19272 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
19273 async fn required_external_channel_closure_fails_the_subscriber_closed() {
19274 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19275 let source = ProviderRef::new("raw-json", 4);
19276 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19277 provider,
19278 SubscriberMode::PubSub,
19279 SubscriberConfig {
19280 preconfirmations: PreconfirmationMode::Required,
19281 ..SubscriberConfig::default()
19282 },
19283 );
19284 subscriber
19285 .configure_external_flashblock_updates(source)
19286 .expect("configure external source");
19287 subscriber.chain_id = Some(1);
19288 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19289 subscriber.interests = subscriber.base_interests.clone();
19290 subscriber.state = AlloySubscriberState::Active(SubscriberStreams::new());
19291 subscriber.sources_dirty = false;
19292
19293 let sender = subscriber
19294 .open_external_flashblock_update_channel(1)
19295 .expect("bounded external queue");
19296 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
19297 let update_stream = subscriber
19298 .connect_source_stream(external.clone())
19299 .await
19300 .expect("attach receiver as subscriber source");
19301 subscriber.install_source_stream(external, update_stream);
19302 subscriber.sources_dirty = false;
19303 drop(sender);
19304
19305 assert!(matches!(
19306 subscriber.next_scoped_batch().await,
19307 Err(SubscriberError::Provider(ref message))
19308 if message.contains("required external Flashblock update channel closed")
19309 ));
19310 }
19311
19312 #[tokio::test]
19313 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
19314 async fn required_external_channel_rejects_a_queued_malformed_update() {
19315 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19316 let source = ProviderRef::new("raw-json", 4);
19317 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19318 provider,
19319 SubscriberMode::PubSub,
19320 SubscriberConfig {
19321 preconfirmations: PreconfirmationMode::Required,
19322 ..SubscriberConfig::default()
19323 },
19324 );
19325 subscriber
19326 .configure_external_flashblock_updates(source.clone())
19327 .expect("configure external source");
19328 subscriber.chain_id = Some(1);
19329 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19330 subscriber.interests = subscriber.base_interests.clone();
19331 subscriber.state = AlloySubscriberState::Active(SubscriberStreams::new());
19332 subscriber.sources_dirty = false;
19333
19334 let sender = subscriber
19335 .open_external_flashblock_update_channel(1)
19336 .expect("bounded external queue");
19337 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
19338 let update_stream = subscriber
19339 .connect_source_stream(external.clone())
19340 .await
19341 .expect("attach receiver as subscriber source");
19342 subscriber.install_source_stream(external, update_stream);
19343 subscriber.sources_dirty = false;
19344
19345 let mut adapter = RawJsonFlashblocksAdapter::new(source);
19346 let mut update = adapter
19347 .ingest_json(
19348 br#"{
19349 "payload_id":"0x1111111111111111",
19350 "index":0,
19351 "base":{
19352 "parent_hash":"0x0606060606060606060606060606060606060606060606060606060606060606",
19353 "block_number":"0x7",
19354 "timestamp":"0x6553f107"
19355 },
19356 "diff":{
19357 "state_root":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
19358 "block_hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
19359 "transactions":[]
19360 },
19361 "metadata":{"block_number":7,"receipts":{}}
19362 }"#,
19363 )
19364 .expect("valid raw frame")
19365 .expect("snapshot update");
19366 let FlashblockUpdate::Snapshot(snapshot) = &mut update else {
19367 unreachable!("fixture is a snapshot")
19368 };
19369 snapshot.flashblock.content_hash = B256::ZERO;
19370 let sending = tokio::spawn(async move { sender.send(update).await });
19371
19372 assert!(matches!(
19373 subscriber.next_scoped_batch().await,
19374 Err(SubscriberError::Provider(ref message))
19375 if message.contains("content commitment is invalid")
19376 ));
19377 assert_eq!(
19378 sending.await.expect("sender task"),
19379 Err(FlashblockUpdateChannelError::Rejected)
19380 );
19381 }
19382
19383 #[tokio::test]
19384 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
19385 async fn bounded_external_channel_reports_capacity_rejection_and_accepts_a_new_generation() {
19386 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19387 let source = ProviderRef::new("raw-json", 4);
19388 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19389 provider,
19390 SubscriberMode::PubSub,
19391 SubscriberConfig {
19392 preconfirmations: PreconfirmationMode::Preferred,
19393 max_pending_records: 1,
19394 ..SubscriberConfig::default()
19395 },
19396 );
19397 subscriber
19398 .configure_external_flashblock_updates(source.clone())
19399 .expect("configure external source");
19400 subscriber.chain_id = Some(1);
19401 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19402 subscriber.interests = subscriber.base_interests.clone();
19403 subscriber.state = AlloySubscriberState::Active(SubscriberStreams::new());
19404 subscriber.sources_dirty = false;
19405
19406 let sender = subscriber
19407 .open_external_flashblock_update_channel(1)
19408 .expect("bounded external queue");
19409 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
19410 let update_stream = subscriber
19411 .connect_source_stream(external.clone())
19412 .await
19413 .expect("attach receiver as subscriber source");
19414 subscriber.install_source_stream(external, update_stream);
19415 subscriber.sources_dirty = false;
19416
19417 let first_frame = br#"{
19418 "payload_id":"0x1111111111111111",
19419 "index":0,
19420 "base":{
19421 "parent_hash":"0x0606060606060606060606060606060606060606060606060606060606060606",
19422 "block_number":"0x7",
19423 "timestamp":"0x6553f107"
19424 },
19425 "diff":{
19426 "state_root":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
19427 "block_hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
19428 "transactions":["0x01"]
19429 },
19430 "metadata":{
19431 "block_number":7,
19432 "receipts":{
19433 "0x5fe7f977e71dba2ea1a68e21057beebb9be2ac30c6410aa38d4f3fbe41dcffd2":{
19434 "logs":[{
19435 "address":"0x4242424242424242424242424242424242424242",
19436 "topics":["0x0101010101010101010101010101010101010101010101010101010101010101"],
19437 "data":"0x"
19438 }]
19439 }
19440 }
19441 }
19442 }"#;
19443 let second_frame = br#"{
19444 "payload_id":"0x1111111111111111",
19445 "index":1,
19446 "diff":{
19447 "state_root":"0xabababababababababababababababababababababababababababababababab",
19448 "block_hash":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
19449 "transactions":["0x02"]
19450 },
19451 "metadata":{
19452 "block_number":7,
19453 "receipts":{
19454 "0xf2ee15ea639b73fa3db9b34a245bdfa015c260c598b211bf05a1ecc4b3e3b4f2":{
19455 "logs":[
19456 {"address":"0x4444444444444444444444444444444444444444","topics":[],"data":"0x"},
19457 {"address":"0x4545454545454545454545454545454545454545","topics":[],"data":"0x"}
19458 ]
19459 }
19460 }
19461 }
19462 }"#;
19463 let mut adapter = RawJsonFlashblocksAdapter::new(source);
19464 let first = adapter
19465 .ingest_json(first_frame)
19466 .expect("valid first frame")
19467 .expect("first snapshot");
19468 let first_send = {
19469 let sender = sender.clone();
19470 tokio::spawn(async move { sender.send(first).await })
19471 };
19472 let first_batch = subscriber
19473 .next_scoped_batch()
19474 .await
19475 .expect("poll first preview")
19476 .expect("first preview batch");
19477 assert_eq!(first_batch.records().len(), 1);
19478 assert_eq!(first_send.await.expect("sender task"), Ok(()));
19479
19480 let oversized = adapter
19481 .ingest_json(second_frame)
19482 .expect("valid oversized delta")
19483 .expect("oversized standardized snapshot");
19484 let rejected_send = {
19485 let sender = sender.clone();
19486 tokio::spawn(async move { sender.send(oversized).await })
19487 };
19488 let invalidation = subscriber
19489 .next_scoped_batch()
19490 .await
19491 .expect("poll capacity rejection")
19492 .expect("capacity invalidation batch");
19493 assert!(invalidation.preconfirmation_invalidated());
19494 assert_eq!(
19495 rejected_send.await.expect("sender task"),
19496 Err(FlashblockUpdateChannelError::Rejected)
19497 );
19498 assert_eq!(subscriber.rejected_external_flashblock_generation, None);
19499
19500 let _ = adapter
19501 .reset(ProviderRef::new("raw-json", 5))
19502 .expect("advance after local capacity rejection");
19503 let recovered = adapter
19504 .ingest_json(first_frame)
19505 .expect("valid recovered frame")
19506 .expect("recovered snapshot");
19507 let recovered_send = {
19508 let sender = sender.clone();
19509 tokio::spawn(async move { sender.send(recovered).await })
19510 };
19511 let recovered_batch = subscriber
19512 .next_scoped_batch()
19513 .await
19514 .expect("poll recovered generation")
19515 .expect("recovered preview batch");
19516 assert_eq!(recovered_batch.records().len(), 1);
19517 assert!(matches!(
19518 &recovered_batch.records()[0].context.chain_status,
19519 ChainStatus::Preconfirmed { flashblock }
19520 if flashblock.provider == ProviderRef::new("raw-json", 5)
19521 ));
19522 assert_eq!(recovered_send.await.expect("sender task"), Ok(()));
19523 }
19524
19525 #[tokio::test]
19526 async fn certified_canonical_heads_are_deduplicated_and_reject_placeholder_hashes() {
19527 let asserter = Asserter::new();
19528 let certified = rpc_block(101, B256::repeat_byte(0x65));
19529 asserter.push_success(&Some(certified.clone()));
19530 asserter.push_success(&Some(certified));
19531 asserter.push_success(&Some(rpc_block(102, B256::ZERO)));
19532 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
19533 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19534 provider,
19535 SubscriberMode::PubSub,
19536 SubscriberConfig::default(),
19537 );
19538
19539 assert!(matches!(
19540 subscriber
19541 .fetch_certified_canonical_head()
19542 .await
19543 .expect("first certified head"),
19544 Some(SubscriberEvent::BlockHeader(_))
19545 ));
19546 assert!(
19547 subscriber
19548 .fetch_certified_canonical_head()
19549 .await
19550 .expect("duplicate certified head")
19551 .is_none()
19552 );
19553 assert!(matches!(
19554 subscriber.fetch_certified_canonical_head().await,
19555 Err(SubscriberError::Provider(ref message))
19556 if message.contains("placeholder hash")
19557 ));
19558 }
19559
19560 #[tokio::test]
19561 async fn canonical_head_certification_times_out_a_silent_provider() {
19562 let provider =
19563 ProviderBuilder::new().connect_client(RpcClient::new(NeverRespondingTransport, true));
19564 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19565 provider,
19566 SubscriberMode::PubSub,
19567 SubscriberConfig {
19568 preconfirmations: PreconfirmationMode::Required,
19569 canonical_head_request_timeout: Duration::from_millis(10),
19570 ..SubscriberConfig::default()
19571 },
19572 );
19573 subscriber.chain_id = Some(8_453);
19574
19575 let result = tokio::time::timeout(
19576 Duration::from_millis(100),
19577 subscriber.fetch_certified_canonical_head(),
19578 )
19579 .await
19580 .expect("subscriber must bound a silent provider request");
19581 assert!(matches!(
19582 result,
19583 Err(SubscriberError::Provider(ref message))
19584 if message.contains("canonical head certification timed out")
19585 ));
19586 }
19587
19588 #[tokio::test]
19589 async fn optimism_canonical_head_is_the_exact_parent_of_pending() {
19590 let asserter = Asserter::new();
19591 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
19592 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19593 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19594 provider,
19595 SubscriberMode::PubSub,
19596 SubscriberConfig {
19597 preconfirmations: PreconfirmationMode::Required,
19598 ..SubscriberConfig::default()
19599 },
19600 );
19601 subscriber.chain_id = Some(10);
19602 subscriber.interests = vec![ReactiveInterest::Blocks(BlockInterest::default())];
19603
19604 let event = subscriber
19605 .fetch_certified_canonical_head()
19606 .await
19607 .expect("OP pending parent can be certified")
19608 .expect("the first certified parent is emitted");
19609 let SubscriberEvent::BlockHeader(header) = event else {
19610 panic!("expected a certified canonical block header")
19611 };
19612 assert_eq!(header.number(), 100);
19613 assert_eq!(header.hash, B256::repeat_byte(0x64));
19614 assert_eq!(
19615 subscriber
19616 .flashblocks_rpc_metrics()
19617 .pending_block_requests(),
19618 1
19619 );
19620 assert_eq!(
19621 subscriber
19622 .flashblocks_rpc_metrics()
19623 .canonical_head_requests(),
19624 1
19625 );
19626 assert!(asserter.read_q().is_empty());
19627 }
19628
19629 #[test]
19630 fn optimism_uses_one_bounded_pending_state_stream() {
19631 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19632 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19633 provider,
19634 SubscriberMode::PubSub,
19635 SubscriberConfig {
19636 preconfirmations: PreconfirmationMode::Required,
19637 ..SubscriberConfig::default()
19638 },
19639 )
19640 .with_provider_ref(ProviderRef::new("op-paid", 11));
19641 subscriber.chain_id = Some(10);
19642 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
19643 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
19644 local_matcher: None,
19645 route_key: None,
19646 })];
19647 subscriber.interests = subscriber.base_interests.clone();
19648
19649 let sources = subscriber.pubsub_stream_sources();
19650 assert_eq!(
19651 sources
19652 .iter()
19653 .filter(|source| matches!(source, SubscriberStreamSource::OpPendingFlashblocks))
19654 .count(),
19655 1
19656 );
19657 assert!(sources.iter().all(|source| !matches!(
19658 source,
19659 SubscriberStreamSource::BaseFlashblocks | SubscriberStreamSource::BasePendingLog { .. }
19660 )));
19661 }
19662
19663 #[test]
19664 fn optimism_default_receipt_budget_reserves_every_fixed_sampler_method() {
19665 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19666 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19667 provider,
19668 SubscriberMode::PubSub,
19669 SubscriberConfig {
19670 preconfirmations: PreconfirmationMode::Required,
19671 ..SubscriberConfig::default()
19672 },
19673 );
19674 subscriber.chain_id = Some(10);
19675 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19676 subscriber.interests = subscriber.base_interests.clone();
19677
19678 assert_eq!(
19682 subscriber.pending_receipt_requests_per_second_capacity(),
19683 28
19684 );
19685 assert_eq!(subscriber.pending_receipt_requests_per_tick_capacity(), 7);
19686
19687 subscriber
19688 .interests
19689 .push(ReactiveInterest::Blocks(BlockInterest::default()));
19690 assert_eq!(
19691 subscriber.pending_receipt_requests_per_second_capacity(),
19692 24
19693 );
19694 assert_eq!(subscriber.pending_receipt_requests_per_tick_capacity(), 6);
19695 subscriber.interests.pop();
19696
19697 for _ in 0..4 {
19698 assert!(subscriber.reserve_flashblock_rpc_methods(3));
19699 assert_eq!(subscriber.pending_receipt_request_allowance(), 7);
19700 assert!(subscriber.reserve_flashblock_rpc_methods(7));
19701 }
19702 assert!(!subscriber.reserve_flashblock_rpc_methods(1));
19703 subscriber.reset_flashblock_tracking();
19704 assert!(
19705 !subscriber.reserve_flashblock_rpc_methods(1),
19706 "a reconnect must not reset an endpoint's rolling quota window"
19707 );
19708 }
19709
19710 #[test]
19711 fn flashblocks_config_rejects_a_zero_rpc_budget() {
19712 let config = SubscriberConfig {
19713 preconfirmations: PreconfirmationMode::Required,
19714 max_flashblock_rpc_requests_per_second: 0,
19715 ..SubscriberConfig::default()
19716 };
19717
19718 assert!(matches!(
19719 validate_subscriber_config(&config),
19720 Err(SubscriberError::InvalidConfig(
19721 "SubscriberConfig::max_flashblock_rpc_requests_per_second must be greater than zero"
19722 ))
19723 ));
19724 }
19725
19726 #[test]
19727 fn flashblocks_config_rejects_a_zero_canonical_head_request_timeout() {
19728 let config = SubscriberConfig {
19729 preconfirmations: PreconfirmationMode::Required,
19730 canonical_head_request_timeout: Duration::ZERO,
19731 ..SubscriberConfig::default()
19732 };
19733
19734 assert!(matches!(
19735 validate_subscriber_config(&config),
19736 Err(SubscriberError::InvalidConfig(
19737 "SubscriberConfig::canonical_head_request_timeout must be greater than zero"
19738 ))
19739 ));
19740 }
19741
19742 #[tokio::test]
19743 async fn optimism_preflight_rejects_a_budget_without_receipt_capacity() {
19744 let asserter = Asserter::new();
19745 asserter.push_success(&serde_json::json!(["flashblocksv1"]));
19746 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19747 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19748 provider,
19749 SubscriberMode::PubSub,
19750 SubscriberConfig {
19751 preconfirmations: PreconfirmationMode::Required,
19752 max_flashblock_rpc_requests_per_second: 15,
19755 ..SubscriberConfig::default()
19756 },
19757 )
19758 .with_provider_ref(ProviderRef::new("op-paid", 12));
19759 subscriber.chain_id = Some(10);
19760 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19761 subscriber.interests = subscriber.base_interests.clone();
19762 let desired = subscriber.pubsub_stream_sources();
19763 let mut streams = SubscriberStreams::new();
19764 for source in desired {
19765 streams.push(source, stream::pending().boxed());
19766 }
19767 subscriber.state = AlloySubscriberState::Active(streams);
19768 subscriber.sources_dirty = false;
19769 assert!(matches!(
19770 subscriber.establish_flashblocks_preflight(10).await,
19771 Err(SubscriberError::InvalidConfig(message))
19772 if message.contains("leaves no capacity for OP transaction receipts")
19773 ));
19774 assert!(asserter.read_q().is_empty());
19775 }
19776
19777 #[cfg(feature = "reactive-ws")]
19778 #[tokio::test]
19779 async fn flashblocks_preflight_proves_chain_and_both_subscription_lanes() {
19780 let asserter = Asserter::new();
19781 asserter.push_success(&serde_json::json!({"flashblocks": true}));
19782 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
19783 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19784 provider,
19785 SubscriberMode::PubSub,
19786 SubscriberConfig {
19787 preconfirmations: PreconfirmationMode::Required,
19788 ..SubscriberConfig::default()
19789 },
19790 )
19791 .with_provider_ref(ProviderRef::new("base-paid", 7));
19792 subscriber.chain_id = Some(8_453);
19793 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19794 subscriber.interests = subscriber.base_interests.clone();
19795 let desired = subscriber.pubsub_stream_sources();
19796 let mut streams = SubscriberStreams::new();
19797 for source in desired {
19798 streams.push(source, stream::pending().boxed());
19799 }
19800 subscriber.state = AlloySubscriberState::Active(streams);
19801 subscriber.sources_dirty = false;
19802
19803 let preflight = subscriber
19804 .establish_flashblocks_preflight(8_453)
19805 .await
19806 .expect("preflight succeeds");
19807
19808 assert_eq!(preflight.chain_id(), 8_453);
19809 assert_eq!(preflight.provider(), &ProviderRef::new("base-paid", 7));
19810 assert_eq!(
19811 preflight.delivery(),
19812 FlashblocksDelivery::NativeSubscriptions
19813 );
19814 assert_eq!(preflight.pending_log_subscriptions(), 1);
19815 assert_eq!(preflight.pending_log_filters(), 1);
19816 assert_eq!(
19817 preflight.advertised_capabilities(),
19818 Some(&serde_json::json!({"flashblocks": true}))
19819 );
19820 }
19821
19822 #[tokio::test]
19823 async fn optimism_preflight_probes_pending_state_without_native_subscriptions() {
19824 let asserter = Asserter::new();
19825 asserter.push_success(&serde_json::json!(["flashblocksv1"]));
19826 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
19827 asserter.push_success(&Vec::<Log>::new());
19828 asserter.push_success(&serde_json::json!([]));
19829 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19830 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19831 provider,
19832 SubscriberMode::PubSub,
19833 SubscriberConfig {
19834 preconfirmations: PreconfirmationMode::Required,
19835 ..SubscriberConfig::default()
19836 },
19837 )
19838 .with_provider_ref(ProviderRef::new("op-paid", 12));
19839 subscriber.chain_id = Some(10);
19840 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19841 subscriber.interests = subscriber.base_interests.clone();
19842 let desired = subscriber.pubsub_stream_sources();
19843 let mut streams = SubscriberStreams::new();
19844 for source in desired {
19845 streams.push(source, stream::pending().boxed());
19846 }
19847 subscriber.state = AlloySubscriberState::Active(streams);
19848 subscriber.sources_dirty = false;
19849
19850 let preflight = subscriber
19851 .establish_flashblocks_preflight(10)
19852 .await
19853 .expect("Optimism pending-state preflight succeeds");
19854
19855 assert_eq!(preflight.chain_id(), 10);
19856 assert_eq!(preflight.provider(), &ProviderRef::new("op-paid", 12));
19857 assert_eq!(
19858 preflight.delivery(),
19859 FlashblocksDelivery::PendingStatePolling
19860 );
19861 assert_eq!(preflight.pending_log_subscriptions(), 0);
19862 assert_eq!(preflight.pending_log_filters(), 1);
19863 assert_eq!(
19864 preflight.advertised_capabilities(),
19865 Some(&serde_json::json!(["flashblocksv1"]))
19866 );
19867 assert!(asserter.read_q().is_empty());
19868 }
19869
19870 #[test]
19871 fn optimism_full_pending_block_normalizes_op_transaction_types_to_hashes() {
19872 let transaction_hash = B256::repeat_byte(0x7e);
19873 let mut value = serde_json::to_value(rpc_block(101, B256::ZERO))
19874 .expect("serialize pending block fixture");
19875 value["transactions"] = serde_json::json!([{
19876 "type": "0x7e",
19877 "hash": transaction_hash,
19878 "sourceHash": B256::repeat_byte(0x11),
19879 "from": Address::repeat_byte(0x22),
19880 "to": Address::repeat_byte(0x33)
19881 }]);
19882
19883 let block = normalize_op_pending_block::<Ethereum>(value)
19884 .expect("OP-specific transaction bodies are reduced to hashes");
19885
19886 assert_eq!(
19887 block.transactions().as_hashes(),
19888 Some(&[transaction_hash][..])
19889 );
19890 }
19891
19892 #[tokio::test]
19893 async fn optimism_sampler_does_not_retry_malformed_pending_content() {
19894 let asserter = Asserter::new();
19895 let mut pending = serde_json::to_value(rpc_block(101, B256::ZERO))
19896 .expect("serialize pending block fixture");
19897 pending["transactions"] = serde_json::json!([{"type": "0x7e"}]);
19898 asserter.push_success(&Some(pending));
19899 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19900 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19901 provider,
19902 SubscriberMode::PubSub,
19903 SubscriberConfig {
19904 preconfirmations: PreconfirmationMode::Required,
19905 ..SubscriberConfig::default()
19906 },
19907 )
19908 .with_provider_ref(ProviderRef::new("op-paid", 12));
19909 subscriber.chain_id = Some(10);
19910
19911 let error = match subscriber
19912 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19913 .await
19914 {
19915 Err(error) => error,
19916 Ok(_) => panic!("malformed provider content must fail immediately"),
19917 };
19918 assert!(
19919 error
19920 .to_string()
19921 .contains("transaction is missing its hash")
19922 );
19923 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
19924 assert!(asserter.read_q().is_empty());
19925 }
19926
19927 #[tokio::test]
19928 async fn optimism_sampler_certifies_the_pending_block_by_exact_parent_hash() {
19929 let asserter = Asserter::new();
19930 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
19931 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
19932 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19933 provider,
19934 SubscriberMode::PubSub,
19935 SubscriberConfig {
19936 preconfirmations: PreconfirmationMode::Required,
19937 ..SubscriberConfig::default()
19938 },
19939 )
19940 .with_provider_ref(ProviderRef::new("op-paid", 12));
19941 subscriber.chain_id = Some(10);
19942
19943 assert!(
19944 subscriber
19945 .fetch_pending_flashblock(None)
19946 .await
19947 .expect("the exact parent certifies the pending payload")
19948 .is_some()
19949 );
19950 }
19951
19952 #[tokio::test]
19953 async fn optimism_sampler_rejects_a_nonconsecutive_pending_parent() {
19954 let asserter = Asserter::new();
19955 asserter.push_success(&Some(rpc_block(101, B256::ZERO)));
19956 asserter.push_success(&Some(rpc_block(99, B256::repeat_byte(0x64))));
19957 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19958 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19959 provider,
19960 SubscriberMode::PubSub,
19961 SubscriberConfig {
19962 preconfirmations: PreconfirmationMode::Required,
19963 ..SubscriberConfig::default()
19964 },
19965 )
19966 .with_provider_ref(ProviderRef::new("op-paid", 12));
19967 subscriber.chain_id = Some(10);
19968
19969 assert!(matches!(
19970 subscriber.fetch_pending_flashblock(None).await,
19971 Err(PendingFlashblockPollError::Integrity(SubscriberError::Provider(
19972 ref message
19973 ))) if message.contains("does not extend its exact certified parent")
19974 ));
19975 assert!(asserter.read_q().is_empty());
19976 }
19977
19978 #[tokio::test]
19979 async fn optimism_sampler_rechecks_unchanged_content_without_republishing_logs() {
19980 let asserter = Asserter::new();
19981 let pending = rpc_block(101, B256::ZERO);
19982 queue_op_pending(&asserter, pending.clone());
19983 asserter.push_success(&Vec::<Log>::new());
19984 queue_op_pending(&asserter, pending);
19985 asserter.push_success(&Vec::<Log>::new());
19986 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19987 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19988 provider,
19989 SubscriberMode::PubSub,
19990 SubscriberConfig {
19991 preconfirmations: PreconfirmationMode::Required,
19992 ..SubscriberConfig::default()
19993 },
19994 )
19995 .with_provider_ref(ProviderRef::new("op-paid", 12));
19996 subscriber.chain_id = Some(10);
19997 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19998 subscriber.interests = subscriber.base_interests.clone();
19999
20000 assert!(
20001 subscriber
20002 .fetch_pending_flashblock(None)
20003 .await
20004 .expect("first cumulative pending view")
20005 .is_some()
20006 );
20007 assert!(
20008 subscriber
20009 .fetch_pending_flashblock(None)
20010 .await
20011 .expect("duplicate cumulative pending view")
20012 .is_none()
20013 );
20014
20015 assert_eq!(
20016 subscriber.flashblocks_rpc_metrics(),
20017 FlashblocksRpcMetrics {
20018 capability_requests: 0,
20019 provider_pair_chain_requests: 0,
20020 canonical_head_requests: 2,
20021 pending_block_requests: 2,
20022 pending_log_requests: 2,
20023 pending_receipt_requests: 0,
20024 pending_receipts_completed: 0,
20025 pending_receipts_unavailable: 0,
20026 failed_requests: 0,
20027 raced_samples: 0,
20028 }
20029 );
20030 assert!(asserter.read_q().is_empty());
20031 }
20032
20033 #[tokio::test]
20034 async fn optimism_sampler_rechecks_logs_for_an_unchanged_pending_view() {
20035 let asserter = Asserter::new();
20036 let transaction = B256::repeat_byte(0x42);
20037 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
20038 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
20039 );
20040 let mut log = rpc_log(false);
20041 log.block_number = Some(101);
20042 log.block_hash = Some(B256::repeat_byte(0xa2));
20043 log.transaction_hash = Some(transaction);
20044 log.transaction_index = Some(0);
20045 log.log_index = Some(0);
20046 queue_op_pending(&asserter, pending.clone());
20047 asserter.push_success(&Vec::<Log>::new());
20048 asserter.push_success(&serde_json::Value::Null);
20049 queue_op_pending(&asserter, pending);
20050 asserter.push_success(&vec![log]);
20051 asserter.push_success(&serde_json::Value::Null);
20052 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20053 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20054 provider,
20055 SubscriberMode::PubSub,
20056 SubscriberConfig {
20057 preconfirmations: PreconfirmationMode::Required,
20058 ..SubscriberConfig::default()
20059 },
20060 )
20061 .with_provider_ref(ProviderRef::new("op-paid", 12));
20062 subscriber.chain_id = Some(10);
20063 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20064 subscriber.interests = subscriber.base_interests.clone();
20065
20066 assert!(matches!(
20067 subscriber
20068 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20069 .await
20070 .expect("first pending view is coherent"),
20071 Some(SubscriberEvent::FlashblockObserved)
20072 ));
20073 assert!(matches!(
20074 subscriber
20075 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20076 .await
20077 .expect("the unchanged view is checked again for lagging logs"),
20078 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
20079 ));
20080 assert!(asserter.read_q().is_empty());
20081 }
20082
20083 #[tokio::test]
20084 async fn optimism_sampler_hydrates_exact_receipts_when_filtered_logs_are_empty() {
20085 let asserter = Asserter::new();
20086 let transaction = B256::repeat_byte(0x42);
20087 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
20088 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
20089 );
20090 let mut log = rpc_log(false);
20091 log.block_number = Some(101);
20092 log.block_hash = Some(B256::repeat_byte(0xa2));
20093 log.transaction_hash = Some(transaction);
20094 log.transaction_index = Some(0);
20095 log.log_index = Some(0);
20096 queue_op_pending(&asserter, pending);
20097 asserter.push_success(&Vec::<Log>::new());
20098 asserter.push_success(&serde_json::json!({
20099 "transactionHash": transaction,
20100 "logs": [log]
20101 }));
20102 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20103 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20104 provider,
20105 SubscriberMode::PubSub,
20106 SubscriberConfig {
20107 preconfirmations: PreconfirmationMode::Required,
20108 ..SubscriberConfig::default()
20109 },
20110 )
20111 .with_provider_ref(ProviderRef::new("op-paid", 12));
20112 subscriber.chain_id = Some(10);
20113 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20114 subscriber.interests = subscriber.base_interests.clone();
20115
20116 let event = subscriber
20117 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20118 .await
20119 .expect("pending receipt fallback succeeds");
20120 assert!(matches!(
20121 event,
20122 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
20123 ));
20124 assert_eq!(
20125 subscriber
20126 .flashblocks_rpc_metrics()
20127 .pending_receipt_requests(),
20128 1
20129 );
20130 assert!(asserter.read_q().is_empty());
20131 }
20132
20133 #[tokio::test]
20134 async fn optimism_receipt_hydration_is_bounded_and_resumes_on_the_next_tick() {
20135 let asserter = Asserter::new();
20136 let transaction_a = B256::repeat_byte(0x41);
20137 let transaction_b = B256::repeat_byte(0x42);
20138 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
20139 alloy_network::primitives::BlockTransactions::Hashes(vec![
20140 transaction_a,
20141 transaction_b,
20142 ]),
20143 );
20144 let mut log = rpc_log(false);
20145 log.block_number = Some(101);
20146 log.transaction_hash = Some(transaction_b);
20147 log.transaction_index = Some(1);
20148 queue_op_pending(&asserter, pending.clone());
20149 asserter.push_success(&Vec::<Log>::new());
20150 asserter.push_success(&serde_json::json!({
20151 "transactionHash": transaction_a,
20152 "logs": []
20153 }));
20154 queue_op_pending(&asserter, pending);
20155 asserter.push_success(&Vec::<Log>::new());
20156 asserter.push_success(&serde_json::json!({
20157 "transactionHash": transaction_b,
20158 "logs": [log]
20159 }));
20160 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20161 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20162 provider,
20163 SubscriberMode::PubSub,
20164 SubscriberConfig {
20165 preconfirmations: PreconfirmationMode::Required,
20166 max_pending_transaction_receipts_per_tick: 1,
20167 ..SubscriberConfig::default()
20168 },
20169 )
20170 .with_provider_ref(ProviderRef::new("op-paid", 12));
20171 subscriber.chain_id = Some(10);
20172 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20173 subscriber.interests = subscriber.base_interests.clone();
20174
20175 assert!(matches!(
20176 subscriber
20177 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20178 .await
20179 .expect("the first bounded receipt is hydrated"),
20180 Some(SubscriberEvent::FlashblockObserved)
20181 ));
20182 assert!(matches!(
20183 subscriber
20184 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20185 .await
20186 .expect("the remaining receipt is hydrated on the next tick"),
20187 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
20188 ));
20189 assert_eq!(
20190 subscriber
20191 .flashblocks_rpc_metrics()
20192 .pending_receipt_requests(),
20193 2
20194 );
20195 assert_eq!(subscriber.preconfirmed_receipted_transactions.len(), 2);
20196 assert!(asserter.read_q().is_empty());
20197 }
20198
20199 #[tokio::test]
20200 async fn optimism_receipt_hydration_prioritizes_unattempted_hashes_over_null_retries() {
20201 let asserter = Asserter::new();
20202 let transaction_a = B256::repeat_byte(0x41);
20203 let transaction_b = B256::repeat_byte(0x42);
20204 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
20205 alloy_network::primitives::BlockTransactions::Hashes(vec![
20206 transaction_a,
20207 transaction_b,
20208 ]),
20209 );
20210 let mut log = rpc_log(false);
20211 log.block_number = Some(101);
20212 log.transaction_hash = Some(transaction_b);
20213 log.transaction_index = Some(1);
20214 queue_op_pending(&asserter, pending.clone());
20215 asserter.push_success(&Vec::<Log>::new());
20216 asserter.push_success(&serde_json::Value::Null);
20217 queue_op_pending(&asserter, pending);
20218 asserter.push_success(&Vec::<Log>::new());
20219 asserter.push_success(&serde_json::json!({
20220 "transactionHash": transaction_b,
20221 "logs": [log]
20222 }));
20223 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20224 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20225 provider,
20226 SubscriberMode::PubSub,
20227 SubscriberConfig {
20228 preconfirmations: PreconfirmationMode::Required,
20229 max_pending_transaction_receipts_per_tick: 1,
20230 ..SubscriberConfig::default()
20231 },
20232 )
20233 .with_provider_ref(ProviderRef::new("op-paid", 12));
20234 subscriber.chain_id = Some(10);
20235 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20236 subscriber.interests = subscriber.base_interests.clone();
20237
20238 assert!(matches!(
20239 subscriber
20240 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20241 .await
20242 .expect("the first null receipt remains retryable"),
20243 Some(SubscriberEvent::FlashblockObserved)
20244 ));
20245 assert!(matches!(
20246 subscriber
20247 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20248 .await
20249 .expect("the next unattempted receipt is not starved"),
20250 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
20251 ));
20252 assert!(
20253 subscriber
20254 .preconfirmed_unavailable_receipts
20255 .contains(&transaction_a)
20256 );
20257 assert!(
20258 subscriber
20259 .preconfirmed_receipted_transactions
20260 .contains(&transaction_b)
20261 );
20262 assert!(asserter.read_q().is_empty());
20263 }
20264
20265 #[tokio::test]
20266 async fn optimism_receipt_batch_commits_dedupe_only_after_every_response_succeeds() {
20267 let asserter = Asserter::new();
20268 let transaction_a = B256::repeat_byte(0x41);
20269 let transaction_b = B256::repeat_byte(0x42);
20270 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
20271 alloy_network::primitives::BlockTransactions::Hashes(vec![
20272 transaction_a,
20273 transaction_b,
20274 ]),
20275 );
20276 let mut log = rpc_log(false);
20277 log.block_number = Some(101);
20278 log.transaction_hash = Some(transaction_b);
20279 log.transaction_index = Some(1);
20280 queue_op_pending(&asserter, pending.clone());
20281 asserter.push_success(&Vec::<Log>::new());
20282 asserter.push_success(&serde_json::json!({
20283 "transactionHash": transaction_a,
20284 "logs": []
20285 }));
20286 asserter.push_failure_msg("receipt temporarily unavailable");
20287 queue_op_pending(&asserter, pending);
20288 asserter.push_success(&Vec::<Log>::new());
20289 asserter.push_success(&serde_json::json!({
20290 "transactionHash": transaction_a,
20291 "logs": []
20292 }));
20293 asserter.push_success(&serde_json::json!({
20294 "transactionHash": transaction_b,
20295 "logs": [log]
20296 }));
20297 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20298 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20299 provider,
20300 SubscriberMode::PubSub,
20301 SubscriberConfig {
20302 preconfirmations: PreconfirmationMode::Required,
20303 max_pending_transaction_receipts_per_tick: 2,
20304 max_consecutive_flashblock_poll_failures: 2,
20305 ..SubscriberConfig::default()
20306 },
20307 )
20308 .with_provider_ref(ProviderRef::new("op-paid", 12));
20309 subscriber.chain_id = Some(10);
20310 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20311 subscriber.interests = subscriber.base_interests.clone();
20312
20313 assert!(
20314 subscriber
20315 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20316 .await
20317 .expect("one failed receipt response remains retryable")
20318 .is_none()
20319 );
20320 assert!(subscriber.preconfirmed_receipted_transactions.is_empty());
20321 assert!(matches!(
20322 subscriber
20323 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20324 .await
20325 .expect("the complete batch is retried transactionally"),
20326 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
20327 ));
20328 assert_eq!(subscriber.preconfirmed_receipted_transactions.len(), 2);
20329 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 1);
20330 assert_eq!(
20331 subscriber
20332 .flashblocks_rpc_metrics()
20333 .pending_receipt_requests(),
20334 4
20335 );
20336 assert!(asserter.read_q().is_empty());
20337 }
20338
20339 #[tokio::test]
20340 async fn optimism_sampler_rejects_a_receipt_for_a_different_transaction() {
20341 let asserter = Asserter::new();
20342 let sampled_transaction = B256::repeat_byte(0x41);
20343 let advanced_transaction = B256::repeat_byte(0x42);
20344 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
20345 alloy_network::primitives::BlockTransactions::Hashes(vec![sampled_transaction]),
20346 );
20347 let mut log = rpc_log(false);
20348 log.block_number = Some(101);
20349 log.transaction_hash = Some(advanced_transaction);
20350 queue_op_pending(&asserter, pending);
20351 asserter.push_success(&Vec::<Log>::new());
20352 asserter.push_success(&serde_json::json!({
20353 "transactionHash": advanced_transaction,
20354 "logs": [log]
20355 }));
20356 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20357 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20358 provider,
20359 SubscriberMode::PubSub,
20360 SubscriberConfig {
20361 preconfirmations: PreconfirmationMode::Required,
20362 ..SubscriberConfig::default()
20363 },
20364 )
20365 .with_provider_ref(ProviderRef::new("op-paid", 12));
20366 subscriber.chain_id = Some(10);
20367 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20368 subscriber.interests = subscriber.base_interests.clone();
20369
20370 let error = match subscriber
20371 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20372 .await
20373 {
20374 Err(error) => error,
20375 Ok(_) => panic!("a receipt for another transaction must fail closed"),
20376 };
20377 assert!(
20378 error
20379 .to_string()
20380 .contains("hash disagrees with its request")
20381 );
20382 assert!(asserter.read_q().is_empty());
20383 }
20384
20385 #[tokio::test]
20386 async fn optimism_sampler_revokes_then_recovers_from_a_regressive_pending_view() {
20387 let asserter = Asserter::new();
20388 let transaction_a = B256::repeat_byte(0x41);
20389 let transaction_b = B256::repeat_byte(0x42);
20390 let first = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
20391 alloy_network::primitives::BlockTransactions::Hashes(vec![
20392 transaction_a,
20393 transaction_b,
20394 ]),
20395 );
20396 let regressive = rpc_block(101, B256::repeat_byte(0xa2)).with_transactions(
20397 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction_a]),
20398 );
20399 queue_op_pending(&asserter, first);
20400 asserter.push_success(&Vec::<Log>::new());
20401 asserter.push_success(&serde_json::Value::Null);
20402 asserter.push_success(&serde_json::Value::Null);
20403 queue_op_pending(&asserter, regressive.clone());
20404 queue_op_pending(&asserter, regressive);
20405 asserter.push_success(&Vec::<Log>::new());
20406 asserter.push_success(&serde_json::Value::Null);
20407 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20408 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20409 provider,
20410 SubscriberMode::PubSub,
20411 SubscriberConfig {
20412 preconfirmations: PreconfirmationMode::Required,
20413 ..SubscriberConfig::default()
20414 },
20415 )
20416 .with_provider_ref(ProviderRef::new("op-paid", 12));
20417 subscriber.chain_id = Some(10);
20418 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20419 subscriber.interests = subscriber.base_interests.clone();
20420
20421 assert!(
20422 subscriber
20423 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20424 .await
20425 .expect("first pending view is coherent")
20426 .is_some()
20427 );
20428 assert!(matches!(
20429 subscriber
20430 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20431 .await
20432 .expect("regression revokes instead of terminating the stream"),
20433 Some(SubscriberEvent::FlashblockInvalidated)
20434 ));
20435 assert!(subscriber.latest_preconfirmation.is_none());
20436 assert!(subscriber.pending_preconfirmation_invalidation);
20437 assert!(
20438 subscriber
20439 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20440 .await
20441 .expect("a later coherent view establishes a fresh snapshot")
20442 .is_some()
20443 );
20444 assert!(subscriber.latest_preconfirmation.is_some());
20445 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 12);
20446 assert!(asserter.read_q().is_empty());
20447 }
20448
20449 #[tokio::test]
20450 async fn optimism_new_quiet_payload_revokes_the_previous_snapshot() {
20451 let asserter = Asserter::new();
20452 let first = rpc_block(101, B256::repeat_byte(0xa1));
20453 let second = rpc_block(102, B256::repeat_byte(0xa2));
20454 queue_op_pending(&asserter, first);
20455 asserter.push_success(&Vec::<Log>::new());
20456 queue_op_pending(&asserter, second);
20457 asserter.push_success(&Vec::<Log>::new());
20458 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20459 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20460 provider,
20461 SubscriberMode::PubSub,
20462 SubscriberConfig {
20463 preconfirmations: PreconfirmationMode::Required,
20464 ..SubscriberConfig::default()
20465 },
20466 )
20467 .with_provider_ref(ProviderRef::new("op-paid", 12));
20468 subscriber.chain_id = Some(10);
20469 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20470 subscriber.interests = subscriber.base_interests.clone();
20471
20472 subscriber
20473 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20474 .await
20475 .expect("first quiet payload is observed");
20476 assert!(!subscriber.pending_preconfirmation_invalidation);
20477 subscriber
20478 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20479 .await
20480 .expect("replacement quiet payload is observed");
20481 assert!(subscriber.pending_preconfirmation_invalidation);
20482 assert_eq!(
20483 subscriber
20484 .latest_preconfirmation
20485 .as_ref()
20486 .map(|flashblock| flashblock.block_number),
20487 Some(102)
20488 );
20489 assert!(asserter.read_q().is_empty());
20490 }
20491
20492 #[tokio::test]
20493 async fn optimism_sampler_rejects_malformed_pending_receipts() {
20494 let asserter = Asserter::new();
20495 let transaction = B256::repeat_byte(0x42);
20496 let pending = rpc_block(101, B256::ZERO).with_transactions(
20497 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
20498 );
20499 queue_op_pending(&asserter, pending);
20500 asserter.push_success(&Vec::<Log>::new());
20501 asserter.push_success(&serde_json::json!({"transactionHash": transaction}));
20502 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20503 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20504 provider,
20505 SubscriberMode::PubSub,
20506 SubscriberConfig {
20507 preconfirmations: PreconfirmationMode::Required,
20508 ..SubscriberConfig::default()
20509 },
20510 )
20511 .with_provider_ref(ProviderRef::new("op-paid", 12));
20512 subscriber.chain_id = Some(10);
20513 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20514 subscriber.interests = subscriber.base_interests.clone();
20515
20516 let error = match subscriber
20517 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20518 .await
20519 {
20520 Err(error) => error,
20521 Ok(_) => panic!("malformed receipt content must fail closed"),
20522 };
20523 assert!(error.to_string().contains("missing its log array"));
20524 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
20525 assert!(asserter.read_q().is_empty());
20526 }
20527
20528 #[tokio::test]
20529 async fn optimism_sampler_retries_when_logs_advance_past_the_sampled_block() {
20530 let asserter = Asserter::new();
20531 let transaction_a = B256::repeat_byte(0x41);
20532 let transaction_b = B256::repeat_byte(0x42);
20533 let first = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
20534 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction_a]),
20535 );
20536 let second = rpc_block(101, B256::repeat_byte(0xa2)).with_transactions(
20537 alloy_network::primitives::BlockTransactions::Hashes(vec![
20538 transaction_a,
20539 transaction_b,
20540 ]),
20541 );
20542 let mut log = rpc_log(false);
20543 log.block_number = Some(101);
20544 log.block_hash = Some(B256::repeat_byte(0xa2));
20545 log.transaction_hash = Some(transaction_b);
20546 log.transaction_index = Some(1);
20547 log.log_index = Some(0);
20548 queue_op_pending(&asserter, first);
20549 asserter.push_success(&vec![log.clone()]);
20550 asserter.push_success(&serde_json::Value::Null);
20551 queue_op_pending(&asserter, second);
20552 asserter.push_success(&vec![log.clone()]);
20553 asserter.push_success(&serde_json::Value::Null);
20554 asserter.push_success(&serde_json::json!({
20555 "transactionHash": transaction_b,
20556 "logs": [log]
20557 }));
20558 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20559 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20560 provider,
20561 SubscriberMode::PubSub,
20562 SubscriberConfig {
20563 preconfirmations: PreconfirmationMode::Required,
20564 ..SubscriberConfig::default()
20565 },
20566 )
20567 .with_provider_ref(ProviderRef::new("op-paid", 12));
20568 subscriber.chain_id = Some(10);
20569 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20570 subscriber.interests = subscriber.base_interests.clone();
20571
20572 assert!(
20573 subscriber
20574 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20575 .await
20576 .expect("a cross-request race remains retryable")
20577 .is_none()
20578 );
20579 assert!(
20580 subscriber
20581 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20582 .await
20583 .expect("the next coherent cumulative view is delivered")
20584 .is_some()
20585 );
20586 assert_eq!(subscriber.flashblocks_rpc_metrics().raced_samples(), 1);
20587 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
20588 assert!(asserter.read_q().is_empty());
20589 }
20590
20591 #[tokio::test]
20592 async fn optimism_sampler_uses_the_paired_pending_state_provider() {
20593 let stream_asserter = Asserter::new();
20594 let stream_provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
20595 let state_asserter = Asserter::new();
20596 queue_op_pending(&state_asserter, rpc_block(101, B256::ZERO));
20597 state_asserter.push_success(&Vec::<Log>::new());
20598 let state_provider = ProviderBuilder::new().connect_mocked_client(state_asserter.clone());
20599 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20600 stream_provider,
20601 SubscriberMode::PubSub,
20602 SubscriberConfig {
20603 preconfirmations: PreconfirmationMode::Required,
20604 ..SubscriberConfig::default()
20605 },
20606 )
20607 .with_provider_ref(ProviderRef::new("op-paid", 12))
20608 .with_flashblocks_state_provider(state_provider);
20609 subscriber.chain_id = Some(10);
20610 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20611 subscriber.interests = subscriber.base_interests.clone();
20612
20613 assert!(
20614 subscriber
20615 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20616 .await
20617 .expect("paired pending-state reads succeed")
20618 .is_some()
20619 );
20620 assert!(state_asserter.read_q().is_empty());
20621 assert!(stream_asserter.read_q().is_empty());
20622 }
20623
20624 #[tokio::test]
20625 async fn optimism_sampler_retries_an_isolated_provider_request_failure() {
20626 let asserter = Asserter::new();
20627 let pending = rpc_block(101, B256::ZERO);
20628 queue_op_pending(&asserter, pending.clone());
20629 asserter.push_failure_msg("temporarily unavailable");
20630 queue_op_pending(&asserter, pending);
20631 asserter.push_success(&Vec::<Log>::new());
20632 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20633 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20634 provider,
20635 SubscriberMode::PubSub,
20636 SubscriberConfig {
20637 preconfirmations: PreconfirmationMode::Required,
20638 max_consecutive_flashblock_poll_failures: 2,
20639 ..SubscriberConfig::default()
20640 },
20641 )
20642 .with_provider_ref(ProviderRef::new("op-paid", 12));
20643 subscriber.chain_id = Some(10);
20644 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20645 subscriber.interests = subscriber.base_interests.clone();
20646
20647 assert!(
20648 subscriber
20649 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20650 .await
20651 .expect("one request failure stays retryable")
20652 .is_none()
20653 );
20654 assert!(
20655 subscriber
20656 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20657 .await
20658 .expect("the next cumulative view retries the missing logs")
20659 .is_some()
20660 );
20661 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 1);
20662 assert!(asserter.read_q().is_empty());
20663 }
20664
20665 #[tokio::test]
20666 async fn optimism_sampler_surfaces_sustained_provider_request_failures() {
20667 let asserter = Asserter::new();
20668 asserter.push_failure_msg("temporarily unavailable");
20669 asserter.push_failure_msg("still unavailable");
20670 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20671 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20672 provider,
20673 SubscriberMode::PubSub,
20674 SubscriberConfig {
20675 preconfirmations: PreconfirmationMode::Required,
20676 max_consecutive_flashblock_poll_failures: 2,
20677 ..SubscriberConfig::default()
20678 },
20679 )
20680 .with_provider_ref(ProviderRef::new("op-paid", 12));
20681 subscriber.chain_id = Some(10);
20682
20683 assert!(
20684 subscriber
20685 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20686 .await
20687 .expect("the first request failure stays retryable")
20688 .is_none()
20689 );
20690 let error = match subscriber
20691 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20692 .await
20693 {
20694 Err(error) => error,
20695 Ok(_) => panic!("the configured consecutive-failure limit must fail closed"),
20696 };
20697 assert!(error.to_string().contains("still unavailable"));
20698 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 2);
20699 assert!(asserter.read_q().is_empty());
20700 }
20701
20702 #[tokio::test]
20703 async fn flashblocks_preflight_rejects_a_mismatched_chain_before_subscribing() {
20704 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20705 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20706 provider,
20707 SubscriberMode::PubSub,
20708 SubscriberConfig {
20709 preconfirmations: PreconfirmationMode::Required,
20710 ..SubscriberConfig::default()
20711 },
20712 )
20713 .with_provider_ref(ProviderRef::new("wrong-chain", 1));
20714 subscriber.chain_id = Some(10);
20715 subscriber.interests = vec![log_interest_matching_rpc_log()];
20716
20717 assert!(matches!(
20718 subscriber.establish_flashblocks_preflight(8_453).await,
20719 Err(SubscriberError::ChainMismatch {
20720 expected: 8_453,
20721 actual: 10
20722 })
20723 ));
20724 }
20725
20726 #[tokio::test]
20727 async fn optimism_preflight_rejects_a_mismatched_paired_provider() {
20728 let stream_asserter = Asserter::new();
20729 let stream_provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
20730 let state_asserter = Asserter::new();
20731 state_asserter.push_success(&serde_json::json!(["flashblocksv1"]));
20732 state_asserter.push_success(&8_453_u64);
20733 let state_provider = ProviderBuilder::new().connect_mocked_client(state_asserter.clone());
20734 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20735 stream_provider,
20736 SubscriberMode::PubSub,
20737 SubscriberConfig {
20738 preconfirmations: PreconfirmationMode::Required,
20739 ..SubscriberConfig::default()
20740 },
20741 )
20742 .with_provider_ref(ProviderRef::new("op-paid", 12))
20743 .with_flashblocks_state_provider(state_provider);
20744 subscriber.chain_id = Some(10);
20745 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20746 subscriber.interests = subscriber.base_interests.clone();
20747 let desired = subscriber.pubsub_stream_sources();
20748 let mut streams = SubscriberStreams::new();
20749 for source in desired {
20750 streams.push(source, stream::pending().boxed());
20751 }
20752 subscriber.state = AlloySubscriberState::Active(streams);
20753 subscriber.sources_dirty = false;
20754 assert!(matches!(
20755 subscriber.establish_flashblocks_preflight(10).await,
20756 Err(SubscriberError::ChainMismatch {
20757 expected: 10,
20758 actual: 8_453
20759 })
20760 ));
20761 assert!(state_asserter.read_q().is_empty());
20762 assert!(stream_asserter.read_q().is_empty());
20763 }
20764
20765 #[test]
20766 fn unproven_parent_replacement_rewind_discards_every_unauthenticated_identity() {
20767 let parent = BlockRef {
20768 number: 79,
20769 hash: B256::repeat_byte(0x79),
20770 parent_hash: Some(B256::repeat_byte(0x78)),
20771 timestamp: Some(1_700_000_079),
20772 };
20773 let old_tip = BlockRef {
20774 number: 80,
20775 hash: B256::repeat_byte(0x80),
20776 parent_hash: Some(parent.hash),
20777 timestamp: Some(1_700_000_080),
20778 };
20779 let replacement = BlockRef {
20780 hash: B256::repeat_byte(0xe0),
20781 parent_hash: Some(B256::repeat_byte(0xdf)),
20782 ..old_tip
20783 };
20784 let mut state =
20785 CanonicalSequenceState::new(vec![parent, old_tip], Some(old_tip), Some(parent), None);
20786
20787 let rewind = apply_sequence_canonical_block(&mut state, &replacement, false)
20788 .expect("replacement metadata is structurally valid")
20789 .expect("unknown parent is an observable rewind");
20790
20791 assert_eq!(rewind.common_ancestor, None);
20792 assert_eq!(rewind.dropped, vec![parent, old_tip]);
20793 assert_eq!(state.retained_canonical_history(), &[replacement]);
20794 assert_eq!(state.coverage_head(), Some(&replacement));
20795 assert_eq!(state.safe_head(), None);
20796 assert_eq!(state.finalized_head(), None);
20797 }
20798
20799 #[test]
20800 fn handler_ids_are_non_empty_across_construction_and_deserialization() {
20801 assert_eq!(HandlerId::try_new("").unwrap_err(), HandlerIdError);
20802 let valid = HandlerId::try_new("owner-1").expect("non-empty id");
20803 let encoded = serde_json::to_string(&valid).expect("serialize id");
20804 assert_eq!(
20805 serde_json::from_str::<HandlerId>(&encoded).expect("deserialize valid id"),
20806 valid
20807 );
20808 assert!(serde_json::from_str::<HandlerId>(r#"""#).is_err());
20809 }
20810
20811 fn rpc_log(removed: bool) -> Log {
20812 Log {
20813 inner: alloy_primitives::Log::new_unchecked(
20814 Address::repeat_byte(0x42),
20815 vec![B256::repeat_byte(0x01)],
20816 Bytes::new(),
20817 ),
20818 block_hash: Some(B256::repeat_byte(0x02)),
20819 block_number: Some(7),
20820 block_timestamp: Some(1_700_000_000),
20821 transaction_hash: Some(B256::repeat_byte(0x03)),
20822 transaction_index: Some(4),
20823 log_index: Some(5),
20824 removed,
20825 }
20826 }
20827
20828 fn rpc_transaction(chain_id: Option<u64>) -> alloy_rpc_types_eth::Transaction {
20829 use alloy_consensus::SignableTransaction as _;
20830
20831 let envelope: alloy_consensus::TxEnvelope = alloy_consensus::TxLegacy {
20832 chain_id,
20833 ..Default::default()
20834 }
20835 .into_signed(alloy_primitives::Signature::test_signature())
20836 .into();
20837 alloy_rpc_types_eth::Transaction {
20838 inner: alloy_consensus::transaction::Recovered::new_unchecked(envelope, Address::ZERO),
20839 block_hash: None,
20840 block_number: None,
20841 transaction_index: None,
20842 effective_gas_price: None,
20843 }
20844 }
20845
20846 #[cfg(feature = "reactive-ws")]
20847 fn rpc_log_at(block_number: u64, transaction_index: u64, log_index: u64) -> Log {
20848 Log {
20849 inner: alloy_primitives::Log::new_unchecked(
20850 Address::repeat_byte(0x42),
20851 vec![B256::repeat_byte(0x01)],
20852 Bytes::new(),
20853 ),
20854 block_hash: Some(B256::repeat_byte(block_number as u8)),
20855 block_number: Some(block_number),
20856 block_timestamp: Some(1_700_000_000 + block_number),
20857 transaction_hash: Some(B256::repeat_byte(0x20 + transaction_index as u8)),
20858 transaction_index: Some(transaction_index),
20859 log_index: Some(log_index),
20860 removed: false,
20861 }
20862 }
20863
20864 #[cfg(any(
20865 feature = "raw-flashblocks-json",
20866 feature = "reactive-polling",
20867 feature = "reactive-ws"
20868 ))]
20869 fn rpc_block(number: u64, hash: B256) -> alloy_rpc_types_eth::Block {
20870 alloy_rpc_types_eth::Block::empty(alloy_rpc_types_eth::Header {
20871 hash,
20872 inner: alloy_consensus::Header {
20873 number,
20874 parent_hash: B256::repeat_byte(number.saturating_sub(1) as u8),
20875 timestamp: 1_700_000_000 + number,
20876 ..Default::default()
20877 },
20878 total_difficulty: None,
20879 size: None,
20880 })
20881 }
20882
20883 fn queue_op_pending(asserter: &Asserter, pending: alloy_rpc_types_eth::Block) {
20884 let parent = rpc_block(
20885 pending.header().number().saturating_sub(1),
20886 pending.header().parent_hash(),
20887 );
20888 asserter.push_success(&Some(pending));
20889 asserter.push_success(&Some(parent));
20890 }
20891
20892 #[tokio::test(flavor = "multi_thread")]
20893 #[cfg(feature = "reactive-ws")]
20894 async fn verified_log_context_fetches_and_caches_exact_parent_identity() {
20895 let stream_asserter = Asserter::new();
20896 let provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
20897 let verification_asserter = Asserter::new();
20898 verification_asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(7))));
20899 let verification_provider =
20900 ProviderBuilder::new().connect_mocked_client(verification_asserter.clone());
20901 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20902 provider,
20903 SubscriberMode::PubSub,
20904 SubscriberConfig {
20905 verify_log_block_context: true,
20906 ..SubscriberConfig::default()
20907 },
20908 )
20909 .with_log_verification_provider(verification_provider);
20910 let log = rpc_log_at(7, 0, 0);
20911
20912 subscriber
20913 .verify_log_block_context(&log)
20914 .await
20915 .expect("verify live log block");
20916 subscriber
20917 .verify_log_block_context(&log)
20918 .await
20919 .expect("reuse verified block cache");
20920 let record = subscriber.with_chain_id(log_input_record(log, InputSource::Subscription));
20921
20922 assert_eq!(
20923 record.context.block.expect("verified block").parent_hash,
20924 Some(B256::repeat_byte(6))
20925 );
20926 assert!(
20927 verification_asserter.read_q().is_empty(),
20928 "one provider lookup should verify every log in the same block"
20929 );
20930 assert!(
20931 stream_asserter.read_q().is_empty(),
20932 "verification must not use the high-volume stream provider"
20933 );
20934 }
20935
20936 #[tokio::test(flavor = "multi_thread")]
20937 async fn stream_with_termination_yields_terminal_source_marker() {
20938 let mut stream = stream_with_termination::<Ethereum, _>(
20939 stream::iter([SubscriberEvent::<Ethereum>::PendingHash(B256::repeat_byte(
20940 0xaa,
20941 ))]),
20942 SubscriberStreamSource::PubSubPendingHashes,
20943 );
20944
20945 assert!(matches!(
20946 stream.next().await,
20947 Some(SubscriberEvent::PendingHash(hash)) if hash == B256::repeat_byte(0xaa)
20948 ));
20949 assert!(matches!(
20950 stream.next().await,
20951 Some(SubscriberEvent::StreamTerminated(source)) if source.is_pubsub()
20952 ));
20953 assert!(stream.next().await.is_none());
20954 }
20955
20956 #[test]
20957 fn reconnect_delay_doubles_until_capped() {
20958 assert_eq!(
20959 next_reconnect_delay(Duration::from_millis(250), Duration::from_secs(1)),
20960 Duration::from_millis(500)
20961 );
20962 assert_eq!(
20963 next_reconnect_delay(Duration::from_millis(750), Duration::from_secs(1)),
20964 Duration::from_secs(1)
20965 );
20966 assert_eq!(
20967 next_reconnect_delay(Duration::ZERO, Duration::from_secs(1)),
20968 Duration::ZERO
20969 );
20970 }
20971
20972 #[test]
20973 fn canonical_logs_are_deduped_but_removed_logs_are_not() {
20974 let included = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
20975 let removed = log_input_record::<Ethereum>(rpc_log(true), InputSource::Subscription);
20976
20977 assert!(should_dedupe_record(&included));
20978 assert!(!should_dedupe_record(&removed));
20979 }
20980
20981 #[test]
20982 fn owner_reconcile_dedupe_rejects_conflicts_and_preserves_compatible_enrichment() {
20983 let set_context_timestamp = |record: &mut ReactiveInputRecord<Ethereum>,
20984 timestamp: Option<u64>| {
20985 record.context.block.as_mut().expect("block").timestamp = timestamp;
20986 match &mut record.context.chain_status {
20987 ChainStatus::Included { block, .. }
20988 | ChainStatus::Safe { block }
20989 | ChainStatus::Finalized { block }
20990 | ChainStatus::Reorged {
20991 dropped_from: block,
20992 } => block.timestamp = timestamp,
20993 ChainStatus::Pending | ChainStatus::Preconfirmed { .. } => {
20994 panic!("log record is canonical")
20995 }
20996 }
20997 };
20998
20999 let mut payload_only = log_input_record::<Ethereum>(rpc_log(false), InputSource::Backfill);
21000 let payload_timestamp = match &payload_only.input {
21001 ReactiveInput::Log(log) => log.block_timestamp.expect("timestamp"),
21002 _ => unreachable!(),
21003 };
21004 set_context_timestamp(&mut payload_only, None);
21005 let mut context_only = payload_only.clone();
21006 if let ReactiveInput::Log(log) = &mut context_only.input {
21007 log.block_timestamp = None;
21008 }
21009 set_context_timestamp(&mut context_only, Some(payload_timestamp + 1));
21010 assert!(matches!(
21011 dedupe_records(vec![payload_only, context_only]),
21012 Err(ReactiveError::InvalidInputRecord { .. })
21013 ));
21014
21015 let mut partial = log_input_record::<Ethereum>(rpc_log(false), InputSource::Backfill);
21016 if let ReactiveInput::Log(log) = &mut partial.input {
21017 log.block_timestamp = None;
21018 }
21019 set_context_timestamp(&mut partial, None);
21020 let complete = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
21021 let deduped =
21022 dedupe_records(vec![partial, complete]).expect("compatible metadata enriches");
21023 assert_eq!(deduped.len(), 1);
21024 deduped[0]
21025 .validated_identity()
21026 .expect("merged record remains coherent");
21027 let resolved = resolve_record_block_payload_metadata(
21028 &deduped[0],
21029 *canonical_record_block(&deduped[0]).expect("canonical"),
21030 )
21031 .expect("effective block");
21032 assert_eq!(resolved.timestamp, Some(payload_timestamp));
21033 }
21034
21035 #[test]
21036 fn full_block_bodies_are_never_suppressed_from_header_hash_alone() {
21037 use alloy_rpc_types_eth::{Block, Header};
21038
21039 let block_ref = BlockRef {
21040 number: 7,
21041 hash: B256::repeat_byte(0x77),
21042 parent_hash: Some(B256::repeat_byte(0x66)),
21043 timestamp: Some(1_700_000_007),
21044 };
21045 let block = Block::empty(Header {
21046 hash: block_ref.hash,
21047 inner: alloy_consensus::Header {
21048 number: block_ref.number,
21049 parent_hash: block_ref.parent_hash.expect("parent"),
21050 timestamp: block_ref.timestamp.expect("timestamp"),
21051 ..Default::default()
21052 },
21053 total_difficulty: None,
21054 size: None,
21055 });
21056 let record = ReactiveInputRecord::<Ethereum>::new(
21057 ReactiveInput::FullBlock(block),
21058 ReactiveContext {
21059 chain_id: Some(1),
21060 source: InputSource::Subscription,
21061 chain_status: ChainStatus::Included {
21062 block: block_ref,
21063 confirmations: 0,
21064 },
21065 block: Some(block_ref),
21066 transaction_index: None,
21067 log_index: None,
21068 },
21069 );
21070
21071 assert!(!record.is_payload_deduplicable());
21072 assert!(!record.same_deduplicable_payload(&record));
21073 let retained = dedupe_scoped_records(vec![
21074 (
21075 record.clone(),
21076 DeliveryAudience::All,
21077 DeliveryScope::Canonical,
21078 ),
21079 (record, DeliveryAudience::All, DeliveryScope::Canonical),
21080 ])
21081 .expect("non-deduplicable bodies are preserved, not treated as conflicts");
21082 assert_eq!(retained.len(), 2);
21083 }
21084
21085 #[test]
21086 fn hydrated_transaction_wrappers_reject_inclusion_and_chain_identity_conflicts() {
21087 let pending_context = ReactiveContext {
21088 chain_id: Some(1),
21089 source: InputSource::Batch,
21090 chain_status: ChainStatus::Pending,
21091 block: None,
21092 transaction_index: None,
21093 log_index: None,
21094 };
21095 let mut included_pending = rpc_transaction(Some(1));
21096 included_pending.block_hash = Some(B256::repeat_byte(0xaa));
21097 assert!(matches!(
21098 ReactiveInputRecord::<Ethereum>::new(
21099 ReactiveInput::PendingTx(included_pending),
21100 pending_context.clone(),
21101 )
21102 .validated_identity(),
21103 Err(ReactiveError::InvalidInputRecord { .. })
21104 ));
21105 assert!(matches!(
21106 ReactiveInputRecord::<Ethereum>::new(
21107 ReactiveInput::PendingTx(rpc_transaction(Some(2))),
21108 pending_context,
21109 )
21110 .validated_identity(),
21111 Err(ReactiveError::InvalidInputRecord { .. })
21112 ));
21113
21114 let block_ref = BlockRef {
21115 number: 8,
21116 hash: B256::repeat_byte(0x88),
21117 parent_hash: Some(B256::repeat_byte(0x77)),
21118 timestamp: Some(1_700_000_008),
21119 };
21120 let header = alloy_rpc_types_eth::Header {
21121 hash: block_ref.hash,
21122 inner: alloy_consensus::Header {
21123 number: block_ref.number,
21124 parent_hash: block_ref.parent_hash.expect("parent"),
21125 timestamp: block_ref.timestamp.expect("timestamp"),
21126 ..Default::default()
21127 },
21128 total_difficulty: None,
21129 size: None,
21130 };
21131 let context = ReactiveContext {
21132 chain_id: Some(1),
21133 source: InputSource::Batch,
21134 chain_status: ChainStatus::Included {
21135 block: block_ref,
21136 confirmations: 0,
21137 },
21138 block: Some(block_ref),
21139 transaction_index: None,
21140 log_index: None,
21141 };
21142 for transaction in [
21143 alloy_rpc_types_eth::Transaction {
21144 block_hash: Some(B256::repeat_byte(0xff)),
21145 ..rpc_transaction(Some(1))
21146 },
21147 alloy_rpc_types_eth::Transaction {
21148 block_hash: Some(block_ref.hash),
21149 block_number: Some(block_ref.number),
21150 transaction_index: Some(1),
21151 ..rpc_transaction(Some(1))
21152 },
21153 rpc_transaction(Some(2)),
21154 ] {
21155 let block = alloy_rpc_types_eth::Block::new(
21156 header.clone(),
21157 alloy_network::primitives::BlockTransactions::Full(vec![transaction]),
21158 );
21159 assert!(matches!(
21160 ReactiveInputRecord::<Ethereum>::new(
21161 ReactiveInput::FullBlock(block),
21162 context.clone(),
21163 )
21164 .validated_identity(),
21165 Err(ReactiveError::InvalidInputRecord { .. })
21166 ));
21167 }
21168 }
21169
21170 #[test]
21171 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
21172 fn compatibility_owner_backfill_and_live_overlap_split_exact_audiences() {
21173 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21174 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21175 provider,
21176 SubscriberMode::Auto,
21177 SubscriberConfig::default(),
21178 );
21179 let owner = HandlerId::new("compat-owner");
21180 subscriber
21181 .add_interest_owner(
21182 owner.clone(),
21183 &[ReactiveInterest::Logs(LogInterest {
21184 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21185 local_matcher: None,
21186 route_key: None,
21187 })],
21188 )
21189 .unwrap();
21190 let log = rpc_log(false);
21191
21192 subscriber.enqueue_compat_owner_record(
21193 log_input_record(log.clone(), InputSource::Backfill),
21194 owner.clone(),
21195 );
21196 subscriber.enqueue_event(SubscriberEvent::Log { source_id: 0, log });
21197
21198 let batch = subscriber
21199 .drain_next_scoped_batch()
21200 .expect("owner catch-up and residual live copies");
21201 assert_eq!(batch.records.len(), 2);
21202 assert_eq!(
21203 batch.records[0].scope,
21204 SubscriberInputScope::OwnerOnlyHandlers {
21205 owners: vec![owner.clone()]
21206 }
21207 );
21208 assert_eq!(
21209 batch.records[1].scope,
21210 SubscriberInputScope::CanonicalResidual {
21211 owners: Vec::new(),
21212 excluded: vec![owner.clone()]
21213 }
21214 );
21215
21216 let reactive = batch.into_reactive_batch();
21217 assert_eq!(
21218 reactive.record_audience(0),
21219 Some(&DeliveryAudience::Owners(vec![owner.clone()]))
21220 );
21221 assert_eq!(
21222 reactive.record_delivery_scope(0),
21223 Some(DeliveryScope::OwnerCatchup)
21224 );
21225 assert_eq!(
21226 reactive.record_audience(1),
21227 Some(&DeliveryAudience::AllExcept(vec![owner]))
21228 );
21229 assert_eq!(
21230 reactive.record_delivery_scope(1),
21231 Some(DeliveryScope::Canonical)
21232 );
21233 }
21234
21235 #[test]
21236 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
21237 fn active_owner_replacement_commits_atomically_to_one_new_epoch() {
21238 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21239 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21240 provider,
21241 SubscriberMode::Auto,
21242 SubscriberConfig::default(),
21243 );
21244 let owner = HandlerId::new("replace-owner");
21245 let original = ReactiveInterest::Logs(LogInterest {
21246 provider_filter: Filter::new().address(Address::repeat_byte(0x41)),
21247 local_matcher: None,
21248 route_key: None,
21249 });
21250 let replacement_interest = ReactiveInterest::Logs(LogInterest {
21251 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21252 local_matcher: None,
21253 route_key: None,
21254 });
21255 let active = subscriber
21256 .stage_interest_owner(owner.clone(), &[original], SubscriberOwnerStart::Live)
21257 .unwrap();
21258 assert!(subscriber.activate_interest_owner(&active));
21259 let replacement = subscriber
21260 .stage_interest_owner_replacement(
21261 owner,
21262 &[replacement_interest],
21263 SubscriberOwnerStart::Live,
21264 )
21265 .unwrap();
21266
21267 assert!(subscriber.commit_interest_owner_replacement(&active, &replacement));
21268 assert_eq!(subscriber.interest_owner_state(&active), None);
21269 assert_eq!(
21270 subscriber.interest_owner_state(&replacement),
21271 Some(SubscriberOwnerState::Active)
21272 );
21273 assert_eq!(subscriber.registered_interests().len(), 1);
21274 }
21275
21276 #[test]
21277 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
21278 fn compatibility_and_epoch_owner_lifecycles_cannot_mix() {
21279 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21280 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21281 provider,
21282 SubscriberMode::Auto,
21283 SubscriberConfig::default(),
21284 );
21285 let owner = HandlerId::new("one-lifecycle");
21286 let interest = ReactiveInterest::Logs(LogInterest {
21287 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21288 local_matcher: None,
21289 route_key: None,
21290 });
21291 let epoch = subscriber
21292 .stage_interest_owner(
21293 owner.clone(),
21294 std::slice::from_ref(&interest),
21295 SubscriberOwnerStart::Live,
21296 )
21297 .expect("stage epoch owner");
21298
21299 assert!(matches!(
21300 subscriber.add_interest_owner(owner.clone(), std::slice::from_ref(&interest)),
21301 Err(SubscriberError::InvalidConfig(_))
21302 ));
21303 assert_eq!(
21304 subscriber.interest_owner_state(&epoch),
21305 Some(SubscriberOwnerState::Staged)
21306 );
21307 assert!(subscriber.abort_interest_owner(&epoch));
21308 subscriber
21309 .add_interest_owner(owner.clone(), std::slice::from_ref(&interest))
21310 .expect("compatibility owner after epoch abort");
21311 assert!(matches!(
21312 subscriber.stage_interest_owner_replacement(
21313 owner,
21314 std::slice::from_ref(&interest),
21315 SubscriberOwnerStart::Live,
21316 ),
21317 Err(SubscriberOwnerError::AlreadyRegistered(_))
21318 ));
21319 }
21320
21321 #[test]
21322 fn pending_record_overflow_is_sticky_and_fail_closed() {
21323 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21324 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21325 provider,
21326 SubscriberMode::Polling,
21327 SubscriberConfig {
21328 max_pending_records: 1,
21329 ..SubscriberConfig::default()
21330 },
21331 );
21332 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
21333 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21334 local_matcher: None,
21335 route_key: None,
21336 })];
21337 subscriber.enqueue_event(SubscriberEvent::Log {
21338 source_id: 0,
21339 log: rpc_log(false),
21340 });
21341 let mut second = rpc_log(false);
21342 second.log_index = Some(6);
21343 second.transaction_hash = Some(B256::repeat_byte(0x04));
21344 subscriber.enqueue_event(SubscriberEvent::Log {
21345 source_id: 0,
21346 log: second,
21347 });
21348
21349 assert_eq!(subscriber.pending_records.len(), 1);
21350 assert!(matches!(
21351 subscriber.check_resource_error(),
21352 Err(SubscriberError::ResourceExhausted(_))
21353 ));
21354 subscriber.reset_delivery_state();
21355 assert!(subscriber.check_resource_error().is_ok());
21356 }
21357
21358 #[test]
21359 fn historical_log_payload_bytes_are_bounded_independently_of_log_count() {
21360 let baseline = rpc_log(false);
21361 let fixed_bytes =
21362 validate_backfill_resource_limits(std::slice::from_ref(&baseline), 1, usize::MAX)
21363 .expect("measure fixed log accounting");
21364 let mut large = baseline;
21365 large.inner = alloy_primitives::Log::new_unchecked(
21366 Address::repeat_byte(0x42),
21367 vec![B256::repeat_byte(0x01)],
21368 Bytes::from(vec![0u8; 256]),
21369 );
21370
21371 assert!(matches!(
21372 validate_backfill_resource_limits(&[large], 1, fixed_bytes + 255),
21373 Err(SubscriberError::ResourceExhausted(_))
21374 ));
21375 }
21376
21377 #[tokio::test(flavor = "multi_thread")]
21378 #[cfg(feature = "reactive-polling")]
21379 async fn reconcile_capacity_failure_does_not_publish_progress_or_partial_history() {
21380 use alloy_rpc_types_eth::{Block, Header};
21381
21382 let asserter = Asserter::new();
21383 let baseline = BlockRef {
21384 number: 6,
21385 hash: B256::repeat_byte(6),
21386 parent_hash: Some(B256::repeat_byte(5)),
21387 timestamp: Some(1_700_000_006),
21388 };
21389 let through = BlockRef {
21390 number: 7,
21391 hash: B256::repeat_byte(7),
21392 parent_hash: Some(baseline.hash),
21393 timestamp: Some(1_700_000_007),
21394 };
21395 let rpc_block = || -> Block {
21396 Block::empty(Header {
21397 hash: through.hash,
21398 inner: alloy_consensus::Header {
21399 number: through.number,
21400 parent_hash: through.parent_hash.expect("parent"),
21401 timestamp: through.timestamp.expect("timestamp"),
21402 ..Default::default()
21403 },
21404 total_difficulty: None,
21405 size: None,
21406 })
21407 };
21408 let mut historical = rpc_log(false);
21409 historical.block_hash = Some(through.hash);
21410 historical.block_timestamp = through.timestamp;
21411 asserter.push_success(&Some(rpc_block()));
21412 asserter.push_success(&vec![historical]);
21413 asserter.push_success(&Some(rpc_block()));
21414 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
21415 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21416 provider,
21417 SubscriberMode::Polling,
21418 SubscriberConfig {
21419 max_pending_records: 1,
21420 ..SubscriberConfig::default()
21421 },
21422 );
21423 subscriber.chain_id = Some(1);
21424 let interest = ReactiveInterest::Logs(LogInterest {
21425 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21426 local_matcher: None,
21427 route_key: None,
21428 });
21429 let epoch = subscriber
21430 .stage_interest_owner(
21431 HandlerId::new("capacity-owner"),
21432 std::slice::from_ref(&interest),
21433 SubscriberOwnerStart::PostBlock(baseline),
21434 )
21435 .expect("stage owner");
21436 subscriber.sources_dirty = false;
21439 subscriber.state = AlloySubscriberState::Empty;
21440 subscriber.push_pending_record(SubscriberInputRecord {
21441 record: log_input_record(rpc_log(false), InputSource::Poll),
21442 scope: SubscriberInputScope::Canonical { owners: Vec::new() },
21443 preconfirmation_timing: None,
21444 });
21445
21446 let error = subscriber
21447 .reconcile_interest_owner(&epoch, through)
21448 .await
21449 .expect_err("historical delivery cannot displace the queued live record");
21450 assert!(matches!(
21451 error,
21452 SubscriberOwnerError::Subscriber(SubscriberError::ResourceExhausted(_))
21453 ));
21454 assert!(subscriber.interest_owner_progress(&epoch).is_none());
21455 assert_eq!(subscriber.pending_records.len(), 1);
21456 assert!(matches!(
21457 subscriber.pending_records[0].scope,
21458 SubscriberInputScope::Canonical { .. }
21459 ));
21460 }
21461
21462 #[test]
21463 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
21464 fn lazy_backfill_queue_capacity_failure_is_atomic() {
21465 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21466 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21467 provider,
21468 SubscriberMode::Auto,
21469 SubscriberConfig {
21470 max_pending_backfills: 1,
21471 ..SubscriberConfig::default()
21472 },
21473 );
21474 let interest = |address| {
21475 ReactiveInterest::Logs(LogInterest {
21476 provider_filter: Filter::new().address(address),
21477 local_matcher: None,
21478 route_key: None,
21479 })
21480 };
21481 subscriber
21482 .add_interest_owner_with_backfill(
21483 HandlerId::new("owner-a"),
21484 &[interest(Address::repeat_byte(0x41))],
21485 SubscriberBackfill::from_block(10),
21486 )
21487 .expect("first queued backfill");
21488
21489 let error = subscriber
21490 .add_interest_owner_with_backfill(
21491 HandlerId::new("owner-b"),
21492 &[interest(Address::repeat_byte(0x42))],
21493 SubscriberBackfill::from_block(10),
21494 )
21495 .expect_err("second backfill must exceed capacity");
21496
21497 assert!(matches!(error, SubscriberError::ResourceExhausted(_)));
21498 assert!(
21499 subscriber
21500 .owner_interests(&HandlerId::new("owner-b"))
21501 .is_none()
21502 );
21503 assert_eq!(subscriber.pending_backfills.len(), 1);
21504 }
21505
21506 #[test]
21507 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
21508 fn exact_owner_replacement_is_atomic_and_removes_crash_stale_owners() {
21509 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21510 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21511 provider,
21512 SubscriberMode::Auto,
21513 SubscriberConfig {
21514 max_pending_backfills: 1,
21515 ..SubscriberConfig::default()
21516 },
21517 );
21518 let interest = |address| {
21519 ReactiveInterest::Logs(LogInterest {
21520 provider_filter: Filter::new().address(address),
21521 local_matcher: None,
21522 route_key: None,
21523 })
21524 };
21525 subscriber
21526 .add_interest_owner(
21527 HandlerId::new("crash-stale"),
21528 &[interest(Address::repeat_byte(0xee))],
21529 )
21530 .expect("seed stale owner");
21531 subscriber.base_interests = vec![interest(Address::repeat_byte(0xdd))];
21532 subscriber.rebuild_registered_interests();
21533 subscriber.push_pending_record(SubscriberInputRecord {
21534 record: log_input_record(rpc_log(false), InputSource::Poll),
21535 scope: SubscriberInputScope::Canonical { owners: Vec::new() },
21536 preconfirmation_timing: None,
21537 });
21538 let baseline = BlockRef {
21539 number: 100,
21540 hash: B256::repeat_byte(100),
21541 parent_hash: Some(B256::repeat_byte(99)),
21542 timestamp: Some(1_700_000_100),
21543 };
21544 let backfill = SubscriberBackfill::after_canonical_block(baseline).expect("C + 1");
21545
21546 let error = subscriber
21547 .replace_interest_owners_with_global_backfill(
21548 vec![
21549 (
21550 HandlerId::new("pool-a"),
21551 vec![interest(Address::repeat_byte(0xa1))],
21552 ),
21553 (
21554 HandlerId::new("pool-b"),
21555 vec![ReactiveInterest::Logs(LogInterest {
21556 provider_filter: Filter::new()
21559 .address(Address::repeat_byte(0xb2))
21560 .from_block(7),
21561 local_matcher: None,
21562 route_key: None,
21563 })],
21564 ),
21565 ],
21566 backfill,
21567 )
21568 .expect_err("two backfills exceed atomic capacity");
21569 assert!(matches!(error, SubscriberError::ResourceExhausted(_)));
21570 assert!(
21571 subscriber
21572 .owner_interests(&HandlerId::new("crash-stale"))
21573 .is_some(),
21574 "failed replacement must preserve the prior topology"
21575 );
21576 assert!(
21577 subscriber
21578 .owner_interests(&HandlerId::new("pool-a"))
21579 .is_none()
21580 );
21581 assert_eq!(subscriber.base_interests.len(), 1);
21582 assert_eq!(subscriber.pending_records.len(), 1);
21583
21584 subscriber
21585 .replace_interest_owners_with_global_backfill(
21586 vec![(
21587 HandlerId::new("pool-a"),
21588 vec![interest(Address::repeat_byte(0xa1))],
21589 )],
21590 backfill,
21591 )
21592 .expect("replacement within capacity");
21593 assert!(
21594 subscriber
21595 .owner_interests(&HandlerId::new("crash-stale"))
21596 .is_none(),
21597 "successful exact replacement removes stale owners"
21598 );
21599 assert!(
21600 subscriber.base_interests.is_empty(),
21601 "successful exact replacement removes stale unowned interests"
21602 );
21603 assert!(
21604 subscriber.drain_next_scoped_batch().is_none(),
21605 "stale canonical delivery must not escape before C + 1 recovery"
21606 );
21607 assert!(
21608 subscriber
21609 .owner_interests(&HandlerId::new("pool-a"))
21610 .is_some()
21611 );
21612 assert_eq!(subscriber.pending_backfills.len(), 1);
21613 assert_eq!(subscriber.pending_backfills[0].backfill, backfill);
21614 assert!(
21615 subscriber.pending_backfills[0].owner.is_none(),
21616 "startup history must be global canonical catch-up, not owner-only"
21617 );
21618 }
21619
21620 #[test]
21621 fn exclusive_canonical_backfill_rejects_block_number_overflow() {
21622 let baseline = BlockRef {
21623 number: u64::MAX,
21624 hash: B256::repeat_byte(0xff),
21625 parent_hash: None,
21626 timestamp: None,
21627 };
21628 assert!(matches!(
21629 SubscriberBackfill::after_canonical_block(baseline),
21630 Err(SubscriberError::InvalidConfig(_))
21631 ));
21632 }
21633
21634 #[tokio::test(flavor = "multi_thread")]
21635 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
21636 async fn exclusive_canonical_backfill_validates_the_retained_baseline_hash() {
21637 let asserter = Asserter::new();
21638 asserter.push_success(&101u64);
21639 asserter.push_success(&Some(rpc_block(101, B256::repeat_byte(101))));
21640 asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(0xee))));
21641 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
21642 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21643 provider,
21644 SubscriberMode::Auto,
21645 SubscriberConfig::default(),
21646 );
21647 let baseline = BlockRef {
21648 number: 100,
21649 hash: B256::repeat_byte(0xaa),
21650 parent_hash: None,
21651 timestamp: None,
21652 };
21653 let backfill = SubscriberBackfill::after_canonical_block(baseline).expect("C + 1");
21654 subscriber
21655 .add_interest_owner_with_backfill(
21656 HandlerId::new("pool"),
21657 &[ReactiveInterest::Logs(LogInterest {
21658 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
21659 local_matcher: None,
21660 route_key: None,
21661 })],
21662 backfill,
21663 )
21664 .expect("queue post-baseline backfill");
21665
21666 let error = subscriber
21667 .drain_pending_backfills()
21668 .await
21669 .expect_err("provider branch differs at retained baseline");
21670 assert!(matches!(error, SubscriberError::InvalidBackfill(_)));
21671 assert_eq!(subscriber.pending_backfills.len(), 1);
21672 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 101);
21673 assert!(subscriber.pending_records.is_empty());
21674 }
21675
21676 #[tokio::test(flavor = "multi_thread")]
21677 #[cfg(feature = "reactive-ws")]
21678 async fn coordinated_multifilter_windows_are_globally_sorted_for_owner_and_canonical_delivery()
21679 {
21680 let asserter = Asserter::new();
21681 let retained = BlockRef {
21682 number: 10,
21683 hash: B256::repeat_byte(10),
21684 parent_hash: Some(B256::repeat_byte(9)),
21685 timestamp: Some(1_700_000_010),
21686 };
21687 let activation = BlockRef {
21688 number: 12,
21689 hash: B256::repeat_byte(12),
21690 parent_hash: Some(B256::repeat_byte(11)),
21691 timestamp: Some(1_700_000_012),
21692 };
21693
21694 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
21698 asserter.push_success(&vec![rpc_log_at(10, 2, 2)]);
21699 asserter.push_success(&vec![rpc_log_at(10, 1, 1)]);
21700 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
21701 asserter.push_success(&activation.number);
21702 asserter.push_success(&Some(rpc_block(activation.number, activation.hash)));
21703 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
21704 asserter.push_success(&vec![rpc_log_at(12, 2, 2)]);
21705 asserter.push_success(&vec![rpc_log_at(11, 1, 1)]);
21706 asserter.push_success(&Some(rpc_block(activation.number, activation.hash)));
21707 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
21708 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21709 provider,
21710 SubscriberMode::Auto,
21711 SubscriberConfig::default(),
21712 );
21713 let interests = (0..257)
21714 .map(|start| {
21715 ReactiveInterest::Logs(LogInterest {
21716 provider_filter: Filter::new()
21717 .address(Address::repeat_byte(0x42))
21718 .event_signature(B256::repeat_byte(0x01))
21719 .from_block(start),
21720 local_matcher: None,
21721 route_key: None,
21722 })
21723 })
21724 .collect::<Vec<_>>();
21725 subscriber
21726 .add_interest_owner_with_canonical_catchup(
21727 HandlerId::new("many-filters"),
21728 &interests,
21729 retained,
21730 )
21731 .expect("queue coordinated windows");
21732 assert_eq!(subscriber.pending_backfills.len(), 2);
21733 assert_eq!(subscriber.pending_backfills[0].filters.len(), 257);
21734 assert_eq!(subscriber.pending_backfills[1].filters.len(), 257);
21735
21736 subscriber
21737 .drain_pending_backfills()
21738 .await
21739 .expect("owner filter group");
21740 let owner = subscriber
21741 .drain_next_scoped_batch()
21742 .expect("owner ordered batch");
21743 assert_eq!(owner.records.len(), 2);
21744 assert_eq!(owner.records[0].record.context.transaction_index, Some(1));
21745 assert_eq!(owner.records[1].record.context.transaction_index, Some(2));
21746 assert!(
21747 owner.records.iter().all(|record| matches!(
21748 record.scope,
21749 SubscriberInputScope::OwnerOnlyHandlers { .. }
21750 ))
21751 );
21752
21753 subscriber
21754 .drain_pending_backfills()
21755 .await
21756 .expect("global filter group");
21757 let global = subscriber
21758 .drain_next_scoped_batch()
21759 .expect("global ordered batch");
21760 assert_eq!(global.records.len(), 2);
21761 assert_eq!(
21762 global.records[0].record.context.block.map(|b| b.number),
21763 Some(11)
21764 );
21765 assert_eq!(
21766 global.records[1].record.context.block.map(|b| b.number),
21767 Some(12)
21768 );
21769 assert!(
21770 global
21771 .records
21772 .iter()
21773 .all(|record| record.scope.is_canonical())
21774 );
21775 assert!(matches!(
21776 global.chain_controls.as_slice(),
21777 [ChainControl::Barrier {
21778 block: Some(block),
21779 ..
21780 }] if block == &activation
21781 ));
21782 }
21783
21784 #[tokio::test(flavor = "multi_thread")]
21785 #[cfg(feature = "reactive-ws")]
21786 async fn aborting_staged_epoch_purges_only_its_buffered_delivery() {
21787 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21788 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21789 provider,
21790 SubscriberMode::PubSub,
21791 SubscriberConfig::default(),
21792 );
21793 subscriber.chain_id = Some(1);
21794 let interest = ReactiveInterest::Logs(LogInterest {
21795 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21796 local_matcher: None,
21797 route_key: None,
21798 });
21799 let owner_a = subscriber
21800 .stage_interest_owner(
21801 HandlerId::new("owner-a"),
21802 std::slice::from_ref(&interest),
21803 SubscriberOwnerStart::Live,
21804 )
21805 .unwrap();
21806 let owner_b = subscriber
21807 .stage_interest_owner(
21808 HandlerId::new("owner-b"),
21809 &[interest],
21810 SubscriberOwnerStart::Live,
21811 )
21812 .unwrap();
21813
21814 subscriber.enqueue_event(SubscriberEvent::Log {
21815 source_id: 0,
21816 log: rpc_log(false),
21817 });
21818 assert!(subscriber.abort_interest_owner(&owner_a));
21819
21820 let batch = subscriber
21821 .next_scoped_batch()
21822 .await
21823 .unwrap()
21824 .expect("shared canonical delivery remains queued");
21825 assert_eq!(batch.records.len(), 1);
21826 assert_eq!(
21827 batch.records[0].scope,
21828 SubscriberInputScope::Canonical {
21829 owners: vec![owner_b]
21830 }
21831 );
21832 }
21833
21834 #[tokio::test(flavor = "multi_thread")]
21835 #[cfg(feature = "reactive-ws")]
21836 async fn owner_backfill_dedupe_never_suppresses_canonical_delivery() {
21837 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21838 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21839 provider,
21840 SubscriberMode::PubSub,
21841 SubscriberConfig::default(),
21842 );
21843 subscriber.chain_id = Some(1);
21844 let epoch = subscriber
21845 .stage_interest_owner(
21846 HandlerId::new("owner"),
21847 &[ReactiveInterest::Logs(LogInterest {
21848 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21849 local_matcher: None,
21850 route_key: None,
21851 })],
21852 SubscriberOwnerStart::Live,
21853 )
21854 .unwrap();
21855 let log = rpc_log(false);
21856
21857 subscriber.enqueue_owner_record(
21858 log_input_record(log.clone(), InputSource::Backfill),
21859 epoch.clone(),
21860 );
21861 subscriber.enqueue_event(SubscriberEvent::Log { source_id: 0, log });
21862
21863 let batch = subscriber
21864 .next_scoped_batch()
21865 .await
21866 .unwrap()
21867 .expect("owner backfill and canonical live delivery");
21868 assert_eq!(batch.records.len(), 2);
21869 assert_eq!(
21870 batch.records[0].scope,
21871 SubscriberInputScope::OwnerOnly {
21872 owners: vec![epoch]
21873 }
21874 );
21875 assert_eq!(
21876 batch.records[1].scope,
21877 SubscriberInputScope::Canonical { owners: Vec::new() },
21878 "owner replay dedupe must not suppress the global live record"
21879 );
21880 }
21881
21882 #[tokio::test(flavor = "multi_thread")]
21883 #[cfg(feature = "reactive-polling")]
21884 async fn reconcile_fetch_drains_live_burst_beyond_output_batch_capacity() {
21885 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21886 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21887 provider,
21888 SubscriberMode::Polling,
21889 SubscriberConfig {
21890 max_batch_size: 2,
21891 ..SubscriberConfig::default()
21892 },
21893 );
21894 let interest = ReactiveInterest::Logs(LogInterest {
21895 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21896 local_matcher: None,
21897 route_key: None,
21898 });
21899 let epoch = subscriber
21900 .stage_interest_owner(
21901 HandlerId::new("owner"),
21902 std::slice::from_ref(&interest),
21903 SubscriberOwnerStart::Live,
21904 )
21905 .unwrap();
21906 subscriber.sources_dirty = false;
21907
21908 let mut duplicate = rpc_log(false);
21909 duplicate.transaction_hash = Some(B256::repeat_byte(1));
21910 duplicate.log_index = Some(0);
21911 let events = (0u8..10).map(|index| {
21912 let mut log = rpc_log(false);
21913 log.transaction_hash = Some(B256::repeat_byte(index.saturating_add(1)));
21914 log.log_index = Some(index as u64);
21915 SubscriberEvent::Log { source_id: 0, log }
21916 });
21917 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
21918 let mut streams = SubscriberStreams::new();
21919 streams.push(
21920 SubscriberStreamSource::PollingLog { filter },
21921 stream::iter(events).boxed(),
21922 );
21923 subscriber.state = AlloySubscriberState::Active(streams);
21924
21925 let mut polls = 0usize;
21926 let fetched_duplicate = duplicate.clone();
21927 let fetch = poll_fn(move |cx| {
21928 polls += 1;
21929 if polls > 10 {
21930 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>(fetched_duplicate.clone()))
21931 } else {
21932 cx.waker().wake_by_ref();
21933 std::task::Poll::Pending
21934 }
21935 });
21936 let target_epochs = HashSet::from([epoch.clone()]);
21937 let fetched_duplicate = subscriber
21938 .drive_reconcile_fetch(fetch, &target_epochs)
21939 .await
21940 .unwrap();
21941 subscriber.enqueue_owner_record_for_owners_unmerged(
21942 log_input_record(fetched_duplicate, InputSource::Backfill),
21943 vec![epoch.clone()],
21944 );
21945 subscriber.promote_reconcile_owner_records(&target_epochs);
21946
21947 assert_eq!(subscriber.pending_records.len(), 20);
21948 assert!(subscriber.pending_records.iter().take(10).all(|record| {
21949 record.scope == SubscriberInputScope::Canonical { owners: Vec::new() }
21950 }));
21951 assert!(subscriber.pending_records.iter().skip(10).all(|record| {
21952 record.scope
21953 == SubscriberInputScope::OwnerOnly {
21954 owners: vec![epoch.clone()],
21955 }
21956 }));
21957 }
21958
21959 #[tokio::test(flavor = "multi_thread")]
21960 #[cfg(feature = "reactive-polling")]
21961 async fn reconcile_fetch_waits_for_provider_when_live_topology_is_empty() {
21962 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21963 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21964 provider,
21965 SubscriberMode::Polling,
21966 SubscriberConfig::default(),
21967 );
21968 subscriber.chain_id = Some(1);
21969 subscriber.sources_dirty = false;
21970 let mut first_poll = true;
21971 let fetch = poll_fn(move |cx| {
21972 if first_poll {
21973 first_poll = false;
21974 cx.waker().wake_by_ref();
21975 std::task::Poll::Pending
21976 } else {
21977 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>("certified"))
21978 }
21979 });
21980
21981 let result = subscriber
21982 .drive_reconcile_fetch(fetch, &HashSet::new())
21983 .await
21984 .expect("an empty live topology must not be mistaken for termination");
21985 assert_eq!(result, "certified");
21986 }
21987
21988 #[tokio::test(flavor = "multi_thread")]
21989 #[cfg(all(feature = "reactive-polling", feature = "reactive-ws"))]
21990 async fn successful_owner_reconcile_seeds_its_live_filter_reconnect_anchor() {
21991 use alloy_rpc_types_eth::{Block, Header};
21992
21993 let asserter = Asserter::new();
21994 let baseline = BlockRef {
21995 number: 100,
21996 hash: B256::repeat_byte(0x64),
21997 parent_hash: Some(B256::repeat_byte(0x63)),
21998 timestamp: Some(1_700_000_100),
21999 };
22000 let through = BlockRef {
22001 number: 101,
22002 hash: B256::repeat_byte(0x65),
22003 parent_hash: Some(baseline.hash),
22004 timestamp: Some(1_700_000_101),
22005 };
22006 let rpc_block = || -> Block {
22007 Block::empty(Header {
22008 hash: through.hash,
22009 inner: alloy_consensus::Header {
22010 number: through.number,
22011 parent_hash: through.parent_hash.unwrap(),
22012 timestamp: through.timestamp.unwrap(),
22013 ..Default::default()
22014 },
22015 total_difficulty: None,
22016 size: None,
22017 })
22018 };
22019 asserter.push_success(&Some(rpc_block()));
22020 asserter.push_success(&Vec::<Log>::new());
22021 asserter.push_success(&Some(rpc_block()));
22022 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
22023 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22024 provider,
22025 SubscriberMode::PubSub,
22026 SubscriberConfig::default(),
22027 );
22028 subscriber.chain_id = Some(1);
22029 let interest = ReactiveInterest::Logs(LogInterest {
22030 provider_filter: Filter::new().address(Address::repeat_byte(0xac)),
22031 local_matcher: None,
22032 route_key: None,
22033 });
22034 let epoch = subscriber
22035 .stage_interest_owner(
22036 HandlerId::new("reconnect-anchor"),
22037 std::slice::from_ref(&interest),
22038 SubscriberOwnerStart::PostBlock(baseline),
22039 )
22040 .unwrap();
22041 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
22042 let source = SubscriberStreamSource::PubSubLog {
22043 id: subscriber.log_source_id(&filter),
22044 filter: filter.clone(),
22045 };
22046 let mut streams = SubscriberStreams::new();
22047 streams.push(source, stream::pending().boxed());
22048 subscriber.state = AlloySubscriberState::Active(streams);
22049 subscriber.sources_dirty = false;
22050
22051 subscriber
22052 .reconcile_interest_owner(&epoch, through)
22053 .await
22054 .unwrap();
22055 assert_eq!(subscriber.log_anchor(&filter), Some(through.number));
22056 assert!(asserter.read_q().is_empty());
22057 }
22058
22059 #[tokio::test(flavor = "multi_thread")]
22060 #[cfg(feature = "reactive-polling")]
22061 async fn cancelled_reconcile_retains_hidden_owner_live_delivery_for_retry() {
22062 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22063 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22064 provider,
22065 SubscriberMode::Polling,
22066 SubscriberConfig::default(),
22067 );
22068 let interest = ReactiveInterest::Logs(LogInterest {
22069 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
22070 local_matcher: None,
22071 route_key: None,
22072 });
22073 let epoch = subscriber
22074 .stage_interest_owner(
22075 HandlerId::new("owner"),
22076 std::slice::from_ref(&interest),
22077 SubscriberOwnerStart::PostBlock(BlockRef {
22078 number: 100,
22079 hash: B256::repeat_byte(0x64),
22080 parent_hash: None,
22081 timestamp: None,
22082 }),
22083 )
22084 .unwrap();
22085 subscriber.sources_dirty = false;
22086
22087 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
22088 let event = SubscriberEvent::Log {
22089 source_id: 0,
22090 log: rpc_log(false),
22091 };
22092 let mut streams = SubscriberStreams::new();
22093 streams.push(
22094 SubscriberStreamSource::PollingLog { filter },
22095 stream::once(async move { event })
22096 .chain(stream::pending())
22097 .boxed(),
22098 );
22099 subscriber.state = AlloySubscriberState::Active(streams);
22100
22101 let targets = HashSet::from([epoch.clone()]);
22102 {
22103 let fetch = futures::future::pending::<Result<(), SubscriberOwnerError>>();
22104 let drive = subscriber.drive_reconcile_fetch(fetch, &targets);
22105 futures::pin_mut!(drive);
22106 poll_fn(|cx| {
22107 assert!(drive.as_mut().poll(cx).is_pending());
22108 std::task::Poll::Ready(())
22109 })
22110 .await;
22111 }
22112
22113 assert_eq!(subscriber.pending_records.len(), 1);
22114 assert_eq!(
22115 subscriber.pending_records[0].scope,
22116 SubscriberInputScope::Canonical { owners: Vec::new() },
22117 "canonical delivery commits immediately at a cancellation-safe boundary"
22118 );
22119 assert_eq!(subscriber.pending_reconcile_owner_records.len(), 1);
22120
22121 subscriber
22122 .drive_reconcile_fetch(futures::future::ready(Ok(())), &targets)
22123 .await
22124 .unwrap();
22125 subscriber.promote_reconcile_owner_records(&targets);
22126 assert!(subscriber.pending_reconcile_owner_records.is_empty());
22127 assert_eq!(subscriber.pending_records.len(), 2);
22128 assert_eq!(
22129 subscriber.pending_records[0].scope,
22130 SubscriberInputScope::Canonical { owners: Vec::new() },
22131 "canonical delivery remains target-excluded"
22132 );
22133 assert_eq!(
22134 subscriber.pending_records[1].scope,
22135 SubscriberInputScope::OwnerOnly {
22136 owners: vec![epoch]
22137 },
22138 "retry commit appends hidden owner delivery after historical catch-up"
22139 );
22140 }
22141
22142 #[tokio::test(flavor = "multi_thread")]
22143 #[cfg(feature = "reactive-ws")]
22144 async fn control_cancellation_preserves_terminated_source_reconcile_intent() {
22145 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22146 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22147 provider,
22148 SubscriberMode::PubSub,
22149 SubscriberConfig {
22150 reconnect: SubscriberReconnectConfig {
22151 initial_delay: Duration::from_secs(60),
22152 ..SubscriberReconnectConfig::default()
22153 },
22154 ..SubscriberConfig::default()
22155 },
22156 );
22157 subscriber.chain_id = Some(1);
22158 let interest = ReactiveInterest::Logs(LogInterest {
22159 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
22160 local_matcher: None,
22161 route_key: None,
22162 });
22163 let epoch = subscriber
22164 .stage_interest_owner(
22165 HandlerId::new("owner"),
22166 std::slice::from_ref(&interest),
22167 SubscriberOwnerStart::PostBlock(BlockRef {
22168 number: 7,
22169 hash: B256::repeat_byte(0x07),
22170 parent_hash: Some(B256::repeat_byte(0x06)),
22171 timestamp: Some(1_700_000_007),
22172 }),
22173 )
22174 .unwrap();
22175 subscriber.sources_dirty = false;
22176 subscriber.stream_revision = 1;
22177 let entry = subscriber
22178 .owned_interests
22179 .iter_mut()
22180 .find(|entry| entry.epoch.as_ref() == Some(&epoch))
22181 .unwrap();
22182 entry.progress = Some(SubscriberOwnerProgress {
22183 owner: epoch.clone(),
22184 through: entry.baseline.unwrap(),
22185 });
22186 entry.progress_stream_revision = Some(1);
22187
22188 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
22189 let source = SubscriberStreamSource::PubSubLog {
22190 id: subscriber.log_source_id(&filter),
22191 filter,
22192 };
22193 let mut streams = SubscriberStreams::new();
22194 streams.push(
22195 source.clone(),
22196 stream::iter([SubscriberEvent::StreamTerminated(source)]).boxed(),
22197 );
22198 subscriber.state = AlloySubscriberState::Active(streams);
22199 let prior_revision = subscriber.stream_revision;
22200
22201 let mut first_poll = true;
22202 let control = poll_fn(move |cx| {
22203 if first_poll {
22204 first_poll = false;
22205 cx.waker().wake_by_ref();
22206 std::task::Poll::Pending
22207 } else {
22208 std::task::Poll::Ready("stop")
22209 }
22210 });
22211 futures::pin_mut!(control);
22212 let outcome = subscriber
22213 .next_scoped_batch_or(control.as_mut())
22214 .await
22215 .unwrap();
22216
22217 assert!(matches!(outcome, SubscriberDriverPoll::Control("stop")));
22218 assert!(subscriber.sources_dirty);
22219 assert!(subscriber.stream_revision > prior_revision);
22220 assert!(
22221 !subscriber.activate_interest_owner(&epoch),
22222 "progress certified against the terminated stream revision is stale"
22223 );
22224 }
22225
22226 #[tokio::test]
22227 #[cfg(feature = "reactive-ws")]
22228 async fn pubsub_sources_assign_stable_log_ids_before_shared_streams() {
22229 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22230 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22231 provider,
22232 SubscriberMode::PubSub,
22233 SubscriberConfig::default(),
22234 );
22235 subscriber.chain_id = Some(1);
22236 subscriber
22237 .register_interests(&[
22238 ReactiveInterest::Logs(LogInterest {
22239 provider_filter: Filter::new().address(Address::repeat_byte(0x01)),
22240 local_matcher: None,
22241 route_key: None,
22242 }),
22243 ReactiveInterest::Logs(LogInterest {
22244 provider_filter: Filter::new().address(Address::repeat_byte(0x02)),
22245 local_matcher: None,
22246 route_key: None,
22247 }),
22248 ReactiveInterest::PendingTransactions(PendingTxInterest::default()),
22249 ])
22250 .await
22251 .expect("register base interests");
22252
22253 let sources = subscriber.stream_sources().expect("stream sources");
22257 assert_eq!(sources.len(), 2);
22258 assert!(matches!(
22259 &sources[0],
22260 SubscriberStreamSource::PubSubLog { id: 0, .. }
22261 ));
22262 assert!(matches!(
22263 sources[1],
22264 SubscriberStreamSource::PubSubPendingHashes
22265 ));
22266
22267 let again = subscriber.stream_sources().expect("stream sources again");
22269 assert!(again[0].same_key(&sources[0]));
22270 }
22271
22272 #[tokio::test(flavor = "multi_thread")]
22273 #[cfg(feature = "reactive-ws")]
22274 async fn pubsub_stream_termination_attempts_reconnect_before_error() {
22275 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22276 let mut subscriber = AlloySubscriber::new(
22277 provider,
22278 SubscriberMode::PubSub,
22279 SubscriberConfig {
22280 reconnect: SubscriberReconnectConfig {
22281 initial_delay: Duration::ZERO,
22282 retry_delay: Duration::ZERO,
22283 max_delay: Duration::ZERO,
22284 max_attempts: Some(1),
22285 ..SubscriberReconnectConfig::default()
22286 },
22287 ..SubscriberConfig::default()
22288 },
22289 );
22290 subscriber.chain_id = Some(1);
22291 subscriber.interests = vec![ReactiveInterest::PendingTransactions(
22292 PendingTxInterest::default(),
22293 )];
22294
22295 let mut streams = SubscriberStreams::new();
22296 let source = SubscriberStreamSource::PubSubPendingHashes;
22297 streams.push(
22298 source,
22299 stream::once(async {
22300 SubscriberEvent::<Ethereum>::StreamTerminated(
22301 SubscriberStreamSource::PubSubPendingHashes,
22302 )
22303 })
22304 .boxed(),
22305 );
22306 subscriber.state = AlloySubscriberState::Active(streams);
22307
22308 let result = subscriber.next_batch().await;
22309 assert!(
22310 matches!(result, Err(SubscriberError::Provider(ref message)) if message.contains("reconnect failed after 1 attempt")),
22311 "terminated pubsub streams should attempt reconnect before surfacing failure: {result:?}"
22312 );
22313 }
22314
22315 #[tokio::test]
22316 #[cfg(feature = "reactive-ws")]
22317 async fn flashblock_stream_termination_invalidates_before_reconnect_io() {
22318 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22319 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22320 provider,
22321 SubscriberMode::PubSub,
22322 SubscriberConfig {
22323 preconfirmations: PreconfirmationMode::Required,
22324 ..SubscriberConfig::default()
22325 },
22326 )
22327 .with_provider_ref(ProviderRef::new("base-paid", 7));
22328 subscriber.chain_id = Some(8_453);
22329 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
22330 subscriber.interests = subscriber.base_interests.clone();
22331 subscriber.sources_dirty = false;
22332
22333 let preview: BaseFlashblockWirePayload = serde_json::from_str(
22334 r#"{
22335 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
22336 "number":"0x65",
22337 "parentHash":"0x6464646464646464646464646464646464646464646464646464646464646464",
22338 "stateRoot":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
22339 "transactionsRoot":"0x1111111111111111111111111111111111111111111111111111111111111111",
22340 "timestamp":"0x6553f165",
22341 "transactions":["0x4141414141414141414141414141414141414141414141414141414141414141"]
22342 }"#,
22343 )
22344 .unwrap();
22345 let (preview, _) = subscriber.accept_base_flashblock(preview).unwrap();
22346 subscriber.latest_preconfirmation = Some(preview);
22347
22348 let mut streams = SubscriberStreams::new();
22349 streams.push(
22350 SubscriberStreamSource::BaseFlashblocks,
22351 stream::once(async {
22352 SubscriberEvent::<Ethereum>::StreamTerminated(
22353 SubscriberStreamSource::BaseFlashblocks,
22354 )
22355 })
22356 .boxed(),
22357 );
22358 subscriber.state = AlloySubscriberState::Active(streams);
22359
22360 let batch = subscriber
22361 .next_scoped_batch()
22362 .await
22363 .expect("termination handling succeeds")
22364 .expect("invalidation is delivered");
22365 assert!(batch.preconfirmation_invalidated());
22366 assert!(subscriber.latest_preconfirmation.is_none());
22367 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 8);
22368 assert_eq!(subscriber.pending_flashblock_reconnects.len(), 2);
22369 assert!(
22370 subscriber
22371 .pending_flashblock_reconnect_sources
22372 .iter()
22373 .any(|source| matches!(source, SubscriberStreamSource::BaseFlashblocks))
22374 );
22375 assert!(
22376 subscriber
22377 .pending_flashblock_reconnect_sources
22378 .iter()
22379 .any(|source| matches!(source, SubscriberStreamSource::BasePendingLog { .. }))
22380 );
22381 let AlloySubscriberState::Active(streams) = &subscriber.state else {
22382 panic!("subscriber remains active while reconnect is pending")
22383 };
22384 assert!(
22385 streams
22386 .entries
22387 .iter()
22388 .all(|entry| !entry.source.is_flashblocks())
22389 );
22390 }
22391
22392 #[tokio::test]
22393 #[cfg(feature = "reactive-ws")]
22394 async fn preferred_initial_flashblock_rejection_retains_canonical_streams() {
22395 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22396 let filter = Filter::new().address(Address::repeat_byte(0x42));
22397 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22398 provider,
22399 SubscriberMode::PubSub,
22400 SubscriberConfig {
22401 preconfirmations: PreconfirmationMode::Preferred,
22402 reconnect: SubscriberReconnectConfig {
22403 enabled: false,
22404 ..SubscriberReconnectConfig::default()
22405 },
22406 ..SubscriberConfig::default()
22407 },
22408 )
22409 .with_provider_ref(ProviderRef::new("base-paid", 1));
22410 subscriber.chain_id = Some(8_453);
22411 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
22412 provider_filter: filter.clone(),
22413 local_matcher: None,
22414 route_key: None,
22415 })];
22416 subscriber.interests = subscriber.base_interests.clone();
22417 subscriber.log_source_ids.insert(filter.clone(), 0);
22418 subscriber.next_log_source_id = 1;
22419
22420 let canonical_source = SubscriberStreamSource::PubSubLog {
22421 id: 0,
22422 filter: filter.clone(),
22423 };
22424 let mut streams = SubscriberStreams::new();
22425 streams.push(
22426 canonical_source.clone(),
22427 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
22428 );
22429 subscriber.state = AlloySubscriberState::Active(streams);
22430 subscriber.sources_dirty = true;
22431
22432 subscriber
22433 .ensure_streams()
22434 .await
22435 .expect("preferred Flashblocks setup degrades to canonical-only");
22436 let AlloySubscriberState::Active(streams) = &subscriber.state else {
22437 panic!("canonical stream remains active")
22438 };
22439 assert!(streams.contains_source(&canonical_source));
22440 assert!(
22441 streams
22442 .entries
22443 .iter()
22444 .all(|entry| !entry.source.is_flashblocks())
22445 );
22446 assert!(subscriber.pending_flashblock_reconnects.is_empty());
22447 assert!(!subscriber.sources_dirty);
22448 }
22449
22450 #[tokio::test]
22451 #[cfg(feature = "reactive-ws")]
22452 async fn required_initial_flashblock_rejection_remains_fail_closed() {
22453 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22454 let filter = Filter::new().address(Address::repeat_byte(0x42));
22455 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22456 provider,
22457 SubscriberMode::PubSub,
22458 SubscriberConfig {
22459 preconfirmations: PreconfirmationMode::Required,
22460 reconnect: SubscriberReconnectConfig {
22461 enabled: false,
22462 ..SubscriberReconnectConfig::default()
22463 },
22464 ..SubscriberConfig::default()
22465 },
22466 )
22467 .with_provider_ref(ProviderRef::new("base-paid", 1));
22468 subscriber.chain_id = Some(8_453);
22469 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
22470 provider_filter: filter.clone(),
22471 local_matcher: None,
22472 route_key: None,
22473 })];
22474 subscriber.interests = subscriber.base_interests.clone();
22475 subscriber.log_source_ids.insert(filter.clone(), 0);
22476 subscriber.next_log_source_id = 1;
22477
22478 let canonical_source = SubscriberStreamSource::PubSubLog {
22479 id: 0,
22480 filter: filter.clone(),
22481 };
22482 let mut streams = SubscriberStreams::new();
22483 streams.push(
22484 canonical_source.clone(),
22485 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
22486 );
22487 subscriber.state = AlloySubscriberState::Active(streams);
22488 subscriber.sources_dirty = true;
22489
22490 let error = subscriber
22491 .ensure_streams()
22492 .await
22493 .expect_err("required Flashblocks setup must fail closed");
22494 assert!(matches!(error, SubscriberError::Provider(_)));
22495 let AlloySubscriberState::Active(streams) = &subscriber.state else {
22496 panic!("the already-connected canonical stream is retained")
22497 };
22498 assert!(streams.contains_source(&canonical_source));
22499 }
22500
22501 #[tokio::test]
22502 #[cfg(feature = "reactive-ws")]
22503 async fn preferred_flashblock_termination_preserves_canonical_delivery() {
22504 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22505 let filter = Filter::new().address(Address::repeat_byte(0x42));
22506 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22507 provider,
22508 SubscriberMode::PubSub,
22509 SubscriberConfig {
22510 preconfirmations: PreconfirmationMode::Preferred,
22511 reconnect: SubscriberReconnectConfig {
22512 enabled: false,
22513 ..SubscriberReconnectConfig::default()
22514 },
22515 ..SubscriberConfig::default()
22516 },
22517 )
22518 .with_provider_ref(ProviderRef::new("base-paid", 1));
22519 subscriber.chain_id = Some(8_453);
22520 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
22521 provider_filter: filter.clone(),
22522 local_matcher: None,
22523 route_key: None,
22524 })];
22525 subscriber.interests = subscriber.base_interests.clone();
22526 subscriber.log_source_ids.insert(filter.clone(), 0);
22527 subscriber.next_log_source_id = 1;
22528 subscriber.sources_dirty = false;
22529
22530 let mut streams = SubscriberStreams::new();
22531 streams.push(
22532 SubscriberStreamSource::BaseFlashblocks,
22533 stream::once(async {
22534 SubscriberEvent::<Ethereum>::StreamTerminated(
22535 SubscriberStreamSource::BaseFlashblocks,
22536 )
22537 })
22538 .boxed(),
22539 );
22540 streams.push(
22541 SubscriberStreamSource::PubSubLog {
22542 id: 0,
22543 filter: filter.clone(),
22544 },
22545 stream::once(async {
22546 SubscriberEvent::<Ethereum>::Log {
22547 source_id: 0,
22548 log: rpc_log(false),
22549 }
22550 })
22551 .boxed(),
22552 );
22553 subscriber.state = AlloySubscriberState::Active(streams);
22554
22555 let invalidation = subscriber
22556 .next_scoped_batch()
22557 .await
22558 .expect("preferred termination does not fail")
22559 .expect("invalidation is delivered");
22560 assert!(invalidation.preconfirmation_invalidated());
22561
22562 let canonical = subscriber
22563 .next_scoped_batch()
22564 .await
22565 .expect("canonical stream remains healthy")
22566 .expect("canonical log is delivered");
22567 assert!(!canonical.preconfirmation_invalidated());
22568 assert_eq!(canonical.records().len(), 1);
22569 assert_eq!(
22570 canonical.records()[0].record.context.source,
22571 InputSource::Subscription
22572 );
22573 }
22574
22575 #[tokio::test]
22576 #[cfg(feature = "reactive-ws")]
22577 async fn preferred_flashblock_reconnect_exhaustion_preserves_canonical_delivery() {
22578 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22579 let filter = Filter::new().address(Address::repeat_byte(0x42));
22580 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22581 provider,
22582 SubscriberMode::PubSub,
22583 SubscriberConfig {
22584 preconfirmations: PreconfirmationMode::Preferred,
22585 reconnect: SubscriberReconnectConfig {
22586 enabled: false,
22587 ..SubscriberReconnectConfig::default()
22588 },
22589 ..SubscriberConfig::default()
22590 },
22591 )
22592 .with_provider_ref(ProviderRef::new("base-paid", 1));
22593 subscriber.chain_id = Some(8_453);
22594 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
22595 provider_filter: filter.clone(),
22596 local_matcher: None,
22597 route_key: None,
22598 })];
22599 subscriber.interests = subscriber.base_interests.clone();
22600 subscriber.log_source_ids.insert(filter.clone(), 0);
22601 subscriber.next_log_source_id = 1;
22602 subscriber.sources_dirty = false;
22603
22604 let canonical_source = SubscriberStreamSource::PubSubLog { id: 0, filter };
22605 let mut streams = SubscriberStreams::new();
22606 streams.push(
22607 canonical_source,
22608 stream::once(async {
22609 tokio::time::sleep(Duration::from_millis(1)).await;
22610 SubscriberEvent::<Ethereum>::Log {
22611 source_id: 0,
22612 log: rpc_log(false),
22613 }
22614 })
22615 .boxed(),
22616 );
22617 subscriber.state = AlloySubscriberState::Active(streams);
22618
22619 let source = SubscriberStreamSource::BaseFlashblocks;
22620 subscriber
22621 .pending_flashblock_reconnect_sources
22622 .push(source.clone());
22623 subscriber
22624 .pending_flashblock_reconnects
22625 .push(Box::pin(async move {
22626 (
22627 source,
22628 Err(SubscriberError::Provider(
22629 "test reconnect window exhausted".to_owned(),
22630 )),
22631 )
22632 }));
22633
22634 let canonical = subscriber
22635 .next_scoped_batch()
22636 .await
22637 .expect("preferred reconnect exhaustion does not fail")
22638 .expect("canonical log is delivered");
22639 assert_eq!(canonical.records().len(), 1);
22640 assert!(subscriber.pending_flashblock_reconnects.is_empty());
22641 }
22642
22643 #[test]
22644 fn backfilled_logs_skip_recent_subscription_duplicates() {
22645 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22646 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22647 provider,
22648 SubscriberMode::PubSub,
22649 SubscriberConfig::default(),
22650 );
22651 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
22652 provider_filter: Filter::new()
22653 .address(Address::repeat_byte(0x42))
22654 .event_signature(B256::repeat_byte(0x01)),
22655 local_matcher: None,
22656 route_key: None,
22657 })];
22658
22659 let log = rpc_log(false);
22660 subscriber.enqueue_event(SubscriberEvent::Log {
22661 source_id: 0,
22662 log: log.clone(),
22663 });
22664 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
22665 source_id: 0,
22666 logs: vec![log],
22667 });
22668
22669 assert_eq!(subscriber.pending_records.len(), 1);
22670 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
22671 assert_eq!(
22672 subscriber.pending_records[0].context.source,
22673 InputSource::Subscription
22674 );
22675 }
22676
22677 #[test]
22678 fn backfilled_logs_surface_with_backfill_source() {
22679 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22684 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22685 provider,
22686 SubscriberMode::PubSub,
22687 SubscriberConfig::default(),
22688 );
22689 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
22690 provider_filter: Filter::new()
22691 .address(Address::repeat_byte(0x42))
22692 .event_signature(B256::repeat_byte(0x01)),
22693 local_matcher: None,
22694 route_key: None,
22695 })];
22696
22697 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
22698 source_id: 0,
22699 logs: vec![rpc_log(false)],
22700 });
22701
22702 assert_eq!(subscriber.pending_records.len(), 1);
22703 assert_eq!(
22704 subscriber.pending_records[0].context.source,
22705 InputSource::Backfill
22706 );
22707 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
22708 }
22709
22710 #[test]
22711 #[cfg(feature = "reactive-ws")]
22712 fn owner_removal_preserves_delivery_and_dedupe_state() {
22713 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22714 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22715 provider,
22716 SubscriberMode::PubSub,
22717 SubscriberConfig::default(),
22718 );
22719 subscriber
22720 .add_interest_owner(
22721 HandlerId::new("pool-a"),
22722 &[ReactiveInterest::Logs(LogInterest {
22723 provider_filter: Filter::new()
22724 .address(Address::repeat_byte(0x42))
22725 .event_signature(B256::repeat_byte(0x01)),
22726 local_matcher: None,
22727 route_key: None,
22728 })],
22729 )
22730 .expect("register pool-a owner");
22731 subscriber
22732 .add_interest_owner(
22733 HandlerId::new("pool-b"),
22734 &[ReactiveInterest::Logs(LogInterest {
22735 provider_filter: Filter::new()
22736 .address(Address::repeat_byte(0x24))
22737 .event_signature(B256::repeat_byte(0x02)),
22738 local_matcher: None,
22739 route_key: None,
22740 })],
22741 )
22742 .expect("register pool-b owner");
22743
22744 let sources = subscriber.stream_sources().expect("stream sources");
22747 subscriber.enqueue_event(SubscriberEvent::Log {
22748 source_id: 0,
22749 log: rpc_log(false),
22750 });
22751 let mut streams = SubscriberStreams::new();
22752 streams.push(
22753 sources[0].clone(),
22754 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
22755 );
22756 subscriber.state = AlloySubscriberState::Active(streams);
22757 assert_eq!(subscriber.pending_records.len(), 1);
22758 assert_eq!(subscriber.recent_input_refs.len(), 1);
22759 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
22760
22761 let removed = subscriber
22762 .remove_interest_owner(&HandlerId::new("pool-b"))
22763 .expect("pool-b should be removed");
22764
22765 assert_eq!(removed.len(), 1);
22766 assert_eq!(subscriber.pending_records.len(), 1);
22767 assert_eq!(subscriber.recent_input_refs.len(), 1);
22768 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
22769 assert!(
22770 subscriber
22771 .owner_interests(&HandlerId::new("pool-a"))
22772 .is_some()
22773 );
22774 assert!(
22775 subscriber
22776 .owner_interests(&HandlerId::new("pool-b"))
22777 .is_none()
22778 );
22779 assert_eq!(subscriber.registered_interests().len(), 1);
22780 }
22781
22782 #[test]
22783 #[cfg(feature = "reactive-ws")]
22784 fn owner_log_sources_fan_in_across_owners() {
22785 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22786 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22787 provider,
22788 SubscriberMode::PubSub,
22789 SubscriberConfig::default(),
22790 );
22791 subscriber
22792 .add_interest_owner(
22793 HandlerId::new("pool-a"),
22794 &[ReactiveInterest::Logs(LogInterest {
22795 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
22796 local_matcher: None,
22797 route_key: None,
22798 })],
22799 )
22800 .expect("register pool-a owner");
22801
22802 let initial_sources = subscriber.stream_sources().expect("initial sources");
22803 assert_eq!(initial_sources.len(), 1);
22804 let pool_a_source = initial_sources[0].clone();
22805 assert!(matches!(
22806 &pool_a_source,
22807 SubscriberStreamSource::PubSubLog { id: 0, .. }
22808 ));
22809
22810 subscriber
22811 .add_interest_owner(
22812 HandlerId::new("pool-b"),
22813 &[ReactiveInterest::Logs(LogInterest {
22814 provider_filter: Filter::new().address(Address::repeat_byte(0xb2)),
22815 local_matcher: None,
22816 route_key: None,
22817 })],
22818 )
22819 .expect("register pool-b owner");
22820
22821 let expanded_sources = subscriber.stream_sources().expect("expanded sources");
22822 assert_eq!(
22823 expanded_sources.len(),
22824 1,
22825 "compatible owner filters should share one provider subscription"
22826 );
22827 assert!(
22828 !expanded_sources[0].same_key(&pool_a_source),
22829 "the provider-facing superset changes while owner routing remains exact"
22830 );
22831
22832 subscriber
22833 .remove_interest_owner(&HandlerId::new("pool-b"))
22834 .expect("pool-b should be removed");
22835 let trimmed_sources = subscriber.stream_sources().expect("trimmed sources");
22836 assert_eq!(trimmed_sources.len(), 1);
22837 assert!(trimmed_sources[0].same_key(&pool_a_source));
22838 }
22839
22840 #[test]
22841 #[cfg(feature = "reactive-ws")]
22842 fn provider_log_fan_in_respects_address_ceiling() {
22843 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22844 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22845 provider,
22846 SubscriberMode::PubSub,
22847 SubscriberConfig {
22848 max_log_addresses_per_subscription: 2,
22849 ..SubscriberConfig::default()
22850 },
22851 );
22852 for index in 0..5 {
22853 subscriber
22854 .add_interest_owner(
22855 HandlerId::new(format!("pool-{index}")),
22856 &[log_interest_for(index + 1)],
22857 )
22858 .expect("register pool owner");
22859 }
22860
22861 let sources = subscriber.stream_sources().expect("stream sources");
22862 assert_eq!(sources.len(), 3);
22863 let mut address_counts: Vec<_> = sources
22864 .iter()
22865 .map(|source| match source {
22866 SubscriberStreamSource::PubSubLog { filter, .. } => filter.address.iter().count(),
22867 _ => panic!("expected log source"),
22868 })
22869 .collect();
22870 address_counts.sort_unstable();
22871 assert_eq!(address_counts, vec![1, 2, 2]);
22872 }
22873
22874 #[tokio::test(flavor = "multi_thread")]
22875 #[cfg(feature = "reactive-ws")]
22876 async fn owner_backfill_seeds_reconnect_anchor_before_live_log() {
22877 let asserter = Asserter::new();
22878 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
22879 asserter.push_success(&vec![rpc_log(false)]);
22880 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
22881 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
22882 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22883 provider,
22884 SubscriberMode::PubSub,
22885 SubscriberConfig::default(),
22886 );
22887 subscriber
22888 .add_interest_owner_with_backfill(
22889 HandlerId::new("pool-a"),
22890 &[ReactiveInterest::Logs(LogInterest {
22891 provider_filter: Filter::new()
22892 .address(Address::repeat_byte(0x42))
22893 .event_signature(B256::repeat_byte(0x01)),
22894 local_matcher: None,
22895 route_key: None,
22896 })],
22897 SubscriberBackfill::range(1, 7),
22898 )
22899 .expect("register pool-a with backfill");
22900
22901 subscriber
22902 .drain_pending_backfills()
22903 .await
22904 .expect("owner backfill should drain");
22905
22906 assert_eq!(subscriber.pending_records.len(), 1);
22907 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
22908 }
22909
22910 #[tokio::test(flavor = "multi_thread")]
22911 async fn subscriber_streams_poll_ready_sources_round_robin() {
22912 let first_hash = B256::repeat_byte(0x01);
22913 let second_hash = B256::repeat_byte(0x02);
22914 let mut streams = SubscriberStreams::new();
22915 streams.push(
22916 SubscriberStreamSource::PubSubPendingHashes,
22917 stream::iter([
22918 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
22919 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
22920 ])
22921 .boxed(),
22922 );
22923 streams.push(
22924 SubscriberStreamSource::PubSubBlockHeaders,
22925 stream::once(async move { SubscriberEvent::<Ethereum>::PendingHash(second_hash) })
22926 .boxed(),
22927 );
22928
22929 assert!(matches!(
22930 streams.next().await,
22931 Some(SubscriberEvent::PendingHash(hash)) if hash == first_hash
22932 ));
22933 assert!(matches!(
22934 streams.next().await,
22935 Some(SubscriberEvent::PendingHash(hash)) if hash == second_hash
22936 ));
22937 }
22938
22939 #[tokio::test(flavor = "multi_thread")]
22940 #[cfg(feature = "reactive-ws")]
22941 async fn owner_updates_ensure_streams_without_full_reset() {
22942 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22943 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22944 provider,
22945 SubscriberMode::PubSub,
22946 SubscriberConfig::default(),
22947 );
22948 subscriber.chain_id = Some(1);
22949 subscriber
22950 .register_interests(&[ReactiveInterest::PendingTransactions(
22951 PendingTxInterest::default(),
22952 )])
22953 .await
22954 .expect("register base pending interest");
22955 subscriber
22956 .add_interest_owner(
22957 HandlerId::new("headers"),
22958 &[ReactiveInterest::Blocks(BlockInterest::default())],
22959 )
22960 .expect("register header owner");
22961
22962 let mut streams = SubscriberStreams::new();
22963 streams.push(
22964 SubscriberStreamSource::PubSubPendingHashes,
22965 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
22966 );
22967 streams.push(
22968 SubscriberStreamSource::PubSubBlockHeaders,
22969 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
22970 );
22971 subscriber.state = AlloySubscriberState::Active(streams);
22972
22973 subscriber
22974 .remove_interest_owner(&HandlerId::new("headers"))
22975 .expect("header owner should be removed");
22976 assert!(matches!(
22977 &subscriber.state,
22978 AlloySubscriberState::Active(streams) if streams.len() == 2
22979 ));
22980
22981 subscriber
22982 .ensure_streams()
22983 .await
22984 .expect("pure removal reconciliation should not touch provider");
22985
22986 assert!(matches!(
22987 &subscriber.state,
22988 AlloySubscriberState::Active(streams)
22989 if streams.len() == 1
22990 && streams.contains_source(&SubscriberStreamSource::PubSubPendingHashes)
22991 && !streams.contains_source(&SubscriberStreamSource::PubSubBlockHeaders)
22992 ));
22993
22994 subscriber
22995 .add_interest_owner(
22996 HandlerId::new("headers"),
22997 &[ReactiveInterest::Blocks(BlockInterest::default())],
22998 )
22999 .expect("re-add header owner");
23000 assert!(matches!(
23001 &subscriber.state,
23002 AlloySubscriberState::Active(streams) if streams.len() == 1
23003 ));
23004 }
23005
23006 #[tokio::test(flavor = "multi_thread")]
23007 #[cfg(feature = "reactive-polling")]
23008 async fn ensure_streams_retains_each_successful_connection_across_later_failure() {
23009 let asserter = Asserter::new();
23010 asserter.push_success(&U256::from(1));
23011 asserter.push_failure_msg("second filter connection failed");
23012 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
23013 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23014 provider,
23015 SubscriberMode::Polling,
23016 SubscriberConfig {
23017 max_log_addresses_per_subscription: 1,
23018 ..SubscriberConfig::default()
23019 },
23020 );
23021 subscriber.chain_id = Some(1);
23022 subscriber
23023 .register_interests(&[log_interest_for(0x41), log_interest_for(0x42)])
23024 .await
23025 .expect("register two independently connected filters");
23026
23027 let error = subscriber
23028 .ensure_streams()
23029 .await
23030 .expect_err("second provider connection is forced to fail");
23031 assert!(matches!(error, SubscriberError::Provider(_)));
23032 assert!(subscriber.sources_dirty);
23033 let retained_streams = match &subscriber.state {
23034 AlloySubscriberState::Active(streams) => Some(streams.len()),
23035 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => None,
23036 };
23037 assert_eq!(
23038 retained_streams,
23039 Some(1),
23040 "first connection must survive later error {error:?}; revision {}",
23041 subscriber.stream_revision
23042 );
23043
23044 asserter.push_success(&U256::from(2));
23045 subscriber
23046 .ensure_streams()
23047 .await
23048 .expect("retry connects only the missing source");
23049 assert!(!subscriber.sources_dirty);
23050 assert!(matches!(
23051 &subscriber.state,
23052 AlloySubscriberState::Active(streams) if streams.len() == 2
23053 ));
23054 assert!(asserter.read_q().is_empty());
23055 }
23056
23057 #[tokio::test(flavor = "multi_thread")]
23058 #[cfg(feature = "reactive-ws")]
23059 async fn cancelled_post_install_backfill_is_retried_without_reconnecting() {
23060 let asserter = Asserter::new();
23061 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
23062 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23063 provider,
23064 SubscriberMode::PubSub,
23065 SubscriberConfig::default(),
23066 );
23067 subscriber.chain_id = Some(1);
23068 subscriber
23069 .register_interests(&[log_interest_for(0x43)])
23070 .await
23071 .expect("register log source");
23072 let source = subscriber
23073 .stream_sources()
23074 .expect("one desired source")
23075 .pop()
23076 .expect("log source");
23077 let SubscriberStreamSource::PubSubLog { id, .. } = source else {
23078 panic!("expected pubsub log source")
23079 };
23080 subscriber.last_seen_log_blocks.insert(id, 6);
23081
23082 {
23083 let source = SubscriberStreamSource::PubSubLog {
23084 id,
23085 filter: subscriber
23086 .log_stream_filters()
23087 .pop()
23088 .expect("provider filter"),
23089 };
23090 let interrupted = async {
23091 subscriber.install_source_stream(
23092 source.clone(),
23093 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
23094 );
23095 subscriber.queue_source_backfill(source);
23096 subscriber.sources_dirty = true;
23097 futures::future::pending::<()>().await;
23098 };
23099 futures::pin_mut!(interrupted);
23100 poll_fn(|cx| {
23101 assert!(interrupted.as_mut().poll(cx).is_pending());
23102 std::task::Poll::Ready(())
23103 })
23104 .await;
23105 }
23106
23107 assert_eq!(subscriber.pending_source_backfills.len(), 1);
23108 assert!(matches!(
23109 &subscriber.state,
23110 AlloySubscriberState::Active(streams) if streams.len() == 1
23111 ));
23112
23113 asserter.push_success(&7u64);
23114 asserter.push_success(&Vec::<Log>::new());
23115 subscriber
23116 .ensure_streams()
23117 .await
23118 .expect("retry completes only the pending historical window");
23119
23120 assert!(subscriber.pending_source_backfills.is_empty());
23121 assert!(!subscriber.sources_dirty);
23122 assert!(matches!(
23123 &subscriber.state,
23124 AlloySubscriberState::Active(streams) if streams.len() == 1
23125 ));
23126 assert!(asserter.read_q().is_empty());
23127 }
23128
23129 #[cfg(any(
23131 feature = "raw-flashblocks-json",
23132 feature = "reactive-polling",
23133 feature = "reactive-ws"
23134 ))]
23135 fn log_interest_matching_rpc_log() -> ReactiveInterest<Ethereum> {
23136 ReactiveInterest::Logs(LogInterest {
23137 provider_filter: Filter::new()
23138 .address(Address::repeat_byte(0x42))
23139 .event_signature(B256::repeat_byte(0x01)),
23140 local_matcher: None,
23141 route_key: None,
23142 })
23143 }
23144
23145 #[cfg(any(feature = "reactive-ws", feature = "reactive-polling"))]
23146 fn log_interest_for(address: u8) -> ReactiveInterest<Ethereum> {
23147 ReactiveInterest::Logs(LogInterest {
23148 provider_filter: Filter::new().address(Address::repeat_byte(address)),
23149 local_matcher: None,
23150 route_key: None,
23151 })
23152 }
23153
23154 #[tokio::test(flavor = "multi_thread")]
23157 #[cfg(feature = "reactive-ws")]
23158 async fn drain_backfill_retains_queue_entry_on_provider_error() {
23159 let asserter = Asserter::new();
23160 asserter.push_failure_msg("rate limited");
23161 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
23162 asserter.push_success(&vec![rpc_log(false)]);
23163 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
23164 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
23165 let mut subscriber = AlloySubscriber::new(
23166 provider,
23167 SubscriberMode::PubSub,
23168 SubscriberConfig::default(),
23169 );
23170 subscriber
23171 .add_interest_owner_with_backfill(
23172 HandlerId::new("pool"),
23173 &[log_interest_matching_rpc_log()],
23174 SubscriberBackfill::range(1, 7),
23175 )
23176 .expect("register owner with backfill");
23177 assert_eq!(subscriber.pending_backfills.len(), 1);
23178
23179 let first = subscriber.drain_pending_backfills().await;
23180 assert!(first.is_err(), "provider failure should surface");
23181 assert_eq!(
23182 subscriber.pending_backfills.len(),
23183 1,
23184 "failed fetch must leave the backfill queued for retry"
23185 );
23186 assert!(subscriber.pending_records.is_empty());
23187
23188 subscriber
23189 .drain_pending_backfills()
23190 .await
23191 .expect("retry should succeed");
23192 assert!(subscriber.pending_backfills.is_empty());
23193 assert_eq!(subscriber.pending_records.len(), 1);
23194 }
23195
23196 #[tokio::test(flavor = "multi_thread")]
23199 #[cfg(feature = "reactive-ws")]
23200 async fn drain_backfill_seeds_anchor_on_empty_window() {
23201 let asserter = Asserter::new();
23202 asserter.push_success(&Some(rpc_block(42, B256::repeat_byte(42))));
23203 asserter.push_success(&Vec::<Log>::new());
23204 asserter.push_success(&Some(rpc_block(42, B256::repeat_byte(42))));
23205 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
23206 let mut subscriber = AlloySubscriber::new(
23207 provider,
23208 SubscriberMode::PubSub,
23209 SubscriberConfig::default(),
23210 );
23211 subscriber
23212 .add_interest_owner_with_backfill(
23213 HandlerId::new("pool"),
23214 &[log_interest_matching_rpc_log()],
23215 SubscriberBackfill::range(1, 42),
23216 )
23217 .expect("register owner with backfill");
23218
23219 subscriber
23220 .drain_pending_backfills()
23221 .await
23222 .expect("empty backfill should drain");
23223
23224 assert!(subscriber.pending_records.is_empty());
23225 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
23226 .pop()
23227 .unwrap();
23228 assert_eq!(
23229 subscriber.log_anchor(&filter),
23230 Some(42),
23231 "empty window must still seed the anchor at its upper bound"
23232 );
23233 }
23234
23235 #[tokio::test(flavor = "multi_thread")]
23238 #[cfg(feature = "reactive-ws")]
23239 async fn drain_backfill_open_ended_resolves_head_and_seeds_anchor() {
23240 let asserter = Asserter::new();
23241 asserter.push_success(&100u64); asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(100))));
23243 asserter.push_success(&Vec::<Log>::new()); asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(100))));
23245 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
23246 let mut subscriber = AlloySubscriber::new(
23247 provider,
23248 SubscriberMode::PubSub,
23249 SubscriberConfig::default(),
23250 );
23251 subscriber
23252 .add_interest_owner_with_backfill(
23253 HandlerId::new("pool"),
23254 &[log_interest_matching_rpc_log()],
23255 SubscriberBackfill::from_block(10),
23256 )
23257 .expect("register owner with open-ended backfill");
23258
23259 subscriber
23260 .drain_pending_backfills()
23261 .await
23262 .expect("open-ended backfill should drain");
23263
23264 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
23265 .pop()
23266 .unwrap();
23267 assert_eq!(subscriber.log_anchor(&filter), Some(100));
23268 }
23269
23270 #[test]
23273 #[cfg(feature = "reactive-ws")]
23274 fn duplicate_filters_across_owners_map_to_single_source() {
23275 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23276 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23277 provider,
23278 SubscriberMode::PubSub,
23279 SubscriberConfig::default(),
23280 );
23281 subscriber
23282 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
23283 .expect("register pool-a");
23284 subscriber
23285 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xaa)])
23286 .expect("register pool-b with identical filter");
23287
23288 assert_eq!(
23289 subscriber.log_stream_filters().len(),
23290 1,
23291 "identical filters across owners must collapse to one"
23292 );
23293 let sources = subscriber.stream_sources().expect("stream sources");
23294 assert_eq!(sources.len(), 1);
23295 }
23296
23297 #[test]
23300 #[cfg(feature = "reactive-ws")]
23301 fn owner_removal_prunes_source_ids_and_anchors() {
23302 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23303 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23304 provider,
23305 SubscriberMode::PubSub,
23306 SubscriberConfig::default(),
23307 );
23308 subscriber
23309 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
23310 .expect("register pool-a");
23311 subscriber
23312 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xbb)])
23313 .expect("register pool-b");
23314
23315 let _ = subscriber.stream_sources().expect("stream sources");
23317 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
23318 let filter_b = log_filters(&[log_interest_for(0xbb)]).pop().unwrap();
23319 let id_a = subscriber.log_source_id(&filter_a);
23320 let id_b = subscriber.log_source_id(&filter_b);
23321 subscriber.last_seen_log_blocks.insert(id_a, 10);
23322 subscriber.last_seen_log_blocks.insert(id_b, 20);
23323 assert_eq!(
23324 subscriber.log_source_ids.len(),
23325 3,
23326 "one provider fan-in id plus two explicitly seeded logical ids"
23327 );
23328
23329 subscriber
23330 .remove_interest_owner(&HandlerId::new("pool-b"))
23331 .expect("remove pool-b");
23332
23333 assert_eq!(
23334 subscriber.log_source_ids.len(),
23335 1,
23336 "pool-b's filter id should be retired"
23337 );
23338 assert!(subscriber.log_source_ids.contains_key(&filter_a));
23339 assert_eq!(subscriber.last_seen_log_blocks.get(&id_a), Some(&10));
23340 assert_eq!(
23341 subscriber.last_seen_log_blocks.get(&id_b),
23342 None,
23343 "pool-b's anchor should be pruned"
23344 );
23345 }
23346
23347 #[test]
23352 #[cfg(feature = "reactive-ws")]
23353 fn owner_filter_growth_queues_continuity_backfill_from_prior_anchor() {
23354 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23355 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23356 provider,
23357 SubscriberMode::PubSub,
23358 SubscriberConfig::default(),
23359 );
23360 subscriber
23361 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
23362 .expect("register amm with pool A");
23363
23364 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
23367 let id_a = subscriber.log_source_id(&filter_a);
23368 subscriber.last_seen_log_blocks.insert(id_a, 50);
23369
23370 subscriber
23373 .add_interest_owner(
23374 HandlerId::new("amm"),
23375 &[log_interest_for(0xaa), log_interest_for(0xbb)],
23376 )
23377 .expect("grow amm to pools A+B");
23378
23379 assert_eq!(
23380 subscriber.pending_backfills.len(),
23381 1,
23382 "the changed merged filter should queue exactly one continuity backfill"
23383 );
23384 let queued = &subscriber.pending_backfills[0];
23385 assert_eq!(queued.owner, Some(HandlerId::new("amm")));
23386 assert_eq!(queued.backfill.start_block(), 50);
23387 assert_eq!(
23388 queued.backfill.end_block(),
23389 None,
23390 "continuity backfill runs open-ended to the current head"
23391 );
23392 }
23393
23394 #[test]
23397 #[cfg(feature = "reactive-ws")]
23398 fn unchanged_owner_filter_does_not_queue_continuity_backfill() {
23399 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23400 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23401 provider,
23402 SubscriberMode::PubSub,
23403 SubscriberConfig::default(),
23404 );
23405 subscriber
23406 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
23407 .expect("register amm");
23408 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
23409 let id_a = subscriber.log_source_id(&filter_a);
23410 subscriber.last_seen_log_blocks.insert(id_a, 50);
23411
23412 subscriber
23413 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
23414 .expect("re-register identical interests");
23415
23416 assert!(
23417 subscriber.pending_backfills.is_empty(),
23418 "an unchanged filter shape must not queue continuity backfill"
23419 );
23420 }
23421
23422 #[test]
23426 #[cfg(feature = "reactive-ws")]
23427 fn explicit_open_ended_backfill_below_anchor_suppresses_continuity() {
23428 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23429 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23430 provider,
23431 SubscriberMode::PubSub,
23432 SubscriberConfig::default(),
23433 );
23434 subscriber
23435 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
23436 .expect("register amm");
23437 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
23438 let id_a = subscriber.log_source_id(&filter_a);
23439 subscriber.last_seen_log_blocks.insert(id_a, 50);
23440
23441 subscriber
23443 .add_interest_owner_with_backfill(
23444 HandlerId::new("amm"),
23445 &[log_interest_for(0xaa), log_interest_for(0xbb)],
23446 SubscriberBackfill::from_block(10),
23447 )
23448 .expect("grow amm with explicit deep backfill");
23449
23450 assert_eq!(
23451 subscriber.pending_backfills.len(),
23452 1,
23453 "only the explicit backfill should be queued; continuity is subsumed"
23454 );
23455 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 10);
23456 }
23457
23458 #[tokio::test(flavor = "multi_thread")]
23461 #[cfg(feature = "reactive-ws")]
23462 async fn ensure_streams_is_noop_when_not_dirty() {
23463 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23464 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23465 provider,
23466 SubscriberMode::PubSub,
23467 SubscriberConfig::default(),
23468 );
23469 subscriber
23471 .add_interest_owner(
23472 HandlerId::new("headers"),
23473 &[ReactiveInterest::Blocks(BlockInterest::default())],
23474 )
23475 .expect("register header owner");
23476 subscriber.state = AlloySubscriberState::Empty;
23478 subscriber.sources_dirty = false;
23479
23480 subscriber
23481 .ensure_streams()
23482 .await
23483 .expect("clean reconcile must be a no-op");
23484
23485 assert!(
23486 matches!(subscriber.state, AlloySubscriberState::Empty),
23487 "not-dirty ensure_streams must not connect new sources"
23488 );
23489 }
23490}
23491
23492fn resolve_subscriber_transport(
23493 mode: SubscriberMode,
23494) -> Result<SubscriberTransport, SubscriberError> {
23495 match mode {
23496 SubscriberMode::PubSub => {
23497 #[cfg(feature = "reactive-ws")]
23498 {
23499 Ok(SubscriberTransport::PubSub)
23500 }
23501 #[cfg(not(feature = "reactive-ws"))]
23502 {
23503 Err(SubscriberError::Unsupported(
23504 "AlloySubscriber pubsub mode requires the reactive-ws feature",
23505 ))
23506 }
23507 }
23508 SubscriberMode::Polling => {
23509 #[cfg(feature = "reactive-polling")]
23510 {
23511 Ok(SubscriberTransport::Polling)
23512 }
23513 #[cfg(not(feature = "reactive-polling"))]
23514 {
23515 Err(SubscriberError::Unsupported(
23516 "AlloySubscriber polling mode requires the reactive-polling feature",
23517 ))
23518 }
23519 }
23520 SubscriberMode::Auto => resolve_auto_subscriber_transport(),
23521 }
23522}
23523
23524fn resolve_auto_subscriber_transport() -> Result<SubscriberTransport, SubscriberError> {
23525 #[cfg(feature = "reactive-ws")]
23526 {
23527 Ok(SubscriberTransport::PubSub)
23528 }
23529
23530 #[cfg(all(not(feature = "reactive-ws"), feature = "reactive-polling"))]
23531 {
23532 Ok(SubscriberTransport::Polling)
23533 }
23534
23535 #[cfg(not(any(feature = "reactive-ws", feature = "reactive-polling")))]
23536 {
23537 Err(SubscriberError::Unsupported(
23538 "AlloySubscriber requires either reactive-ws or reactive-polling",
23539 ))
23540 }
23541}
23542
23543fn validate_subscriber_config(config: &SubscriberConfig) -> Result<(), SubscriberError> {
23544 if config.preconfirmations != PreconfirmationMode::Disabled
23545 && config.canonical_head_poll_interval.is_zero()
23546 {
23547 return Err(SubscriberError::InvalidConfig(
23548 "SubscriberConfig::canonical_head_poll_interval must be greater than zero",
23549 ));
23550 }
23551 if config.preconfirmations != PreconfirmationMode::Disabled
23552 && config.canonical_head_request_timeout.is_zero()
23553 {
23554 return Err(SubscriberError::InvalidConfig(
23555 "SubscriberConfig::canonical_head_request_timeout must be greater than zero",
23556 ));
23557 }
23558 if config.preconfirmations != PreconfirmationMode::Disabled
23559 && config.flashblock_poll_interval.is_zero()
23560 {
23561 return Err(SubscriberError::InvalidConfig(
23562 "SubscriberConfig::flashblock_poll_interval must be greater than zero",
23563 ));
23564 }
23565 if config.preconfirmations != PreconfirmationMode::Disabled
23566 && config.max_consecutive_flashblock_poll_failures == 0
23567 {
23568 return Err(SubscriberError::InvalidConfig(
23569 "SubscriberConfig::max_consecutive_flashblock_poll_failures must be greater than zero",
23570 ));
23571 }
23572 if config.preconfirmations != PreconfirmationMode::Disabled
23573 && config.max_pending_transaction_receipts_per_tick == 0
23574 {
23575 return Err(SubscriberError::InvalidConfig(
23576 "SubscriberConfig::max_pending_transaction_receipts_per_tick must be greater than zero",
23577 ));
23578 }
23579 if config.preconfirmations != PreconfirmationMode::Disabled
23580 && config.max_flashblock_rpc_requests_per_second == 0
23581 {
23582 return Err(SubscriberError::InvalidConfig(
23583 "SubscriberConfig::max_flashblock_rpc_requests_per_second must be greater than zero",
23584 ));
23585 }
23586 if config.max_batch_size == 0 {
23587 return Err(SubscriberError::InvalidConfig(
23588 "SubscriberConfig::max_batch_size must be greater than zero",
23589 ));
23590 }
23591 if config.max_log_addresses_per_subscription == 0 {
23592 return Err(SubscriberError::InvalidConfig(
23593 "SubscriberConfig::max_log_addresses_per_subscription must be greater than zero",
23594 ));
23595 }
23596 if config.max_pending_records == 0 {
23597 return Err(SubscriberError::InvalidConfig(
23598 "SubscriberConfig::max_pending_records must be greater than zero",
23599 ));
23600 }
23601 if config.max_pending_backfills == 0 {
23602 return Err(SubscriberError::InvalidConfig(
23603 "SubscriberConfig::max_pending_backfills must be greater than zero",
23604 ));
23605 }
23606 if config.max_backfill_log_bytes == 0 {
23607 return Err(SubscriberError::InvalidConfig(
23608 "SubscriberConfig::max_backfill_log_bytes must be greater than zero",
23609 ));
23610 }
23611 if config.max_reconcile_requests_in_flight == 0 {
23612 return Err(SubscriberError::InvalidConfig(
23613 "SubscriberConfig::max_reconcile_requests_in_flight must be greater than zero",
23614 ));
23615 }
23616 if config.reconnect.enabled {
23617 if config.reconnect.retry_delay > config.reconnect.max_delay {
23618 return Err(SubscriberError::InvalidConfig(
23619 "SubscriberReconnectConfig::retry_delay must be less than or equal to max_delay",
23620 ));
23621 }
23622 if matches!(config.reconnect.max_attempts, Some(0)) {
23623 return Err(SubscriberError::InvalidConfig(
23624 "SubscriberReconnectConfig::max_attempts must be greater than zero when set",
23625 ));
23626 }
23627 }
23628 Ok(())
23629}
23630
23631fn validate_supported_interests<N: Network>(
23632 mode: SubscriberMode,
23633 config: &SubscriberConfig,
23634 interests: &[ReactiveInterest<N>],
23635) -> Result<(), SubscriberError> {
23636 let transport = resolve_subscriber_transport(mode)?;
23637
23638 for interest in interests {
23639 match interest {
23640 ReactiveInterest::Logs(_) => {}
23641 ReactiveInterest::PendingTransactions(interest)
23642 if !config.hydrate_pending_transactions && interest.matches_hash_only() => {}
23643 ReactiveInterest::PendingTransactions(_) => {
23644 return Err(SubscriberError::Unsupported(
23645 "AlloySubscriber currently supports pending transaction hash interests only (full pending-tx hydration is unimplemented)",
23646 ));
23647 }
23648 ReactiveInterest::Blocks(interest) => match (transport, interest.mode) {
23649 (SubscriberTransport::PubSub, BlockInterestMode::Header) => {}
23650 (_, BlockInterestMode::FullBlock) => {
23651 return Err(SubscriberError::Unsupported(
23652 "AlloySubscriber full block streams are not implemented in this transport slice",
23653 ));
23654 }
23655 (SubscriberTransport::Polling, BlockInterestMode::Header) => {
23656 return Err(SubscriberError::Unsupported(
23657 "AlloySubscriber polling block streams are not implemented in this transport slice",
23658 ));
23659 }
23660 },
23661 }
23662 }
23663
23664 Ok(())
23665}
23666
23667fn log_filters<N: Network>(interests: &[ReactiveInterest<N>]) -> Vec<Filter> {
23668 let mut filters = Vec::new();
23669 for interest in interests {
23670 if let ReactiveInterest::Logs(interest) = interest {
23671 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
23672 }
23673 }
23674 filters
23675}
23676
23677fn needs_header_block_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
23678 interests.iter().any(|interest| {
23679 matches!(
23680 interest,
23681 ReactiveInterest::Blocks(BlockInterest {
23682 mode: BlockInterestMode::Header,
23683 })
23684 )
23685 })
23686}
23687
23688fn needs_pending_hash_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
23689 interests.iter().any(|interest| {
23690 matches!(
23691 interest,
23692 ReactiveInterest::PendingTransactions(interest) if interest.matches_hash_only()
23693 )
23694 })
23695}
23696
23697fn log_matches_any_interest<N: Network>(log: &Log, interests: &[ReactiveInterest<N>]) -> bool {
23698 interests.iter().any(|interest| {
23699 matches!(
23700 interest,
23701 ReactiveInterest::Logs(interest) if interest.matches(log)
23702 )
23703 })
23704}
23705
23706fn validate_owner_backfill_logs(
23707 logs: &[Log],
23708 from_block: u64,
23709 through: &BlockRef,
23710) -> Result<(), SubscriberOwnerError> {
23711 for log in logs {
23712 if log.removed {
23713 return Err(SubscriberOwnerError::InvalidBackfillLog(
23714 "removed log in canonical catch-up",
23715 ));
23716 }
23717 let number = log
23718 .block_number
23719 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
23720 "log missing block number",
23721 ))?;
23722 let hash = log
23723 .block_hash
23724 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
23725 "log missing block hash",
23726 ))?;
23727 log.transaction_hash
23728 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
23729 "log missing transaction hash",
23730 ))?;
23731 log.transaction_index
23732 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
23733 "log missing transaction index",
23734 ))?;
23735 log.log_index
23736 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
23737 "log missing log index",
23738 ))?;
23739 if number < from_block || number > through.number {
23740 return Err(SubscriberOwnerError::InvalidBackfillLog(
23741 "log outside requested block range",
23742 ));
23743 }
23744 if number == through.number && hash != through.hash {
23745 return Err(SubscriberOwnerError::InvalidBackfillLog(
23746 "target-block log hash mismatch",
23747 ));
23748 }
23749 }
23750 Ok(())
23751}
23752
23753fn validate_backfill_resource_limits(
23754 logs: &[Log],
23755 max_logs: usize,
23756 max_log_bytes: usize,
23757) -> Result<usize, SubscriberError> {
23758 if logs.len() > max_logs {
23759 return Err(SubscriberError::ResourceExhausted(format!(
23760 "historical response returned {} logs, above the configured limit of {max_logs}",
23761 logs.len()
23762 )));
23763 }
23764 let bytes = logs.iter().fold(0usize, |total, log| {
23765 let fixed = 20usize + (32 * 3) + (8 * 4) + 1;
23769 total
23770 .saturating_add(fixed)
23771 .saturating_add(log.topics().len().saturating_mul(32))
23772 .saturating_add(log.inner.data.data.len())
23773 });
23774 if bytes > max_log_bytes {
23775 return Err(SubscriberError::ResourceExhausted(format!(
23776 "historical response retained approximately {bytes} log bytes, above the configured limit of {max_log_bytes}"
23777 )));
23778 }
23779 Ok(bytes)
23780}
23781
23782async fn fetch_provider_block_ref<P, N>(
23783 provider: &P,
23784 number: u64,
23785) -> Result<BlockRef, SubscriberError>
23786where
23787 P: Provider<N> + Send + Sync,
23788 N: Network,
23789{
23790 let block = provider
23791 .get_block_by_number(BlockNumberOrTag::Number(number))
23792 .await
23793 .map_err(provider_error)?
23794 .ok_or_else(|| {
23795 SubscriberError::InvalidBackfill(format!(
23796 "canonical target block {number} is unavailable"
23797 ))
23798 })?;
23799 let header = block.header();
23800 Ok(BlockRef {
23801 number: header.number(),
23802 hash: header.hash(),
23803 parent_hash: Some(header.parent_hash()),
23804 timestamp: Some(header.timestamp()),
23805 })
23806}
23807
23808fn block_ref_satisfies_expected(actual: &BlockRef, expected: &BlockRef) -> bool {
23809 actual.number == expected.number
23810 && actual.hash == expected.hash
23811 && optional_metadata_compatible(actual.parent_hash.as_ref(), expected.parent_hash.as_ref())
23812 && optional_metadata_compatible(actual.timestamp.as_ref(), expected.timestamp.as_ref())
23813}
23814
23815fn validate_owner_backfill_log_set(logs: &[Log]) -> Result<(), SubscriberOwnerError> {
23816 let mut positions = HashMap::new();
23817 let mut block_hashes = HashMap::new();
23818 let mut transaction_hashes = HashMap::new();
23819 let mut transaction_positions = HashMap::new();
23820 let mut ordering = BTreeMap::<u64, Vec<(u64, u64)>>::new();
23821 for log in logs {
23822 let number = log
23823 .block_number
23824 .expect("individual owner catch-up logs are validated before set validation");
23825 let block_hash = log
23826 .block_hash
23827 .expect("individual owner catch-up logs are validated before set validation");
23828 let transaction_hash = log
23829 .transaction_hash
23830 .expect("individual owner catch-up logs are validated before set validation");
23831 let transaction_index = log
23832 .transaction_index
23833 .expect("individual owner catch-up logs are validated before set validation");
23834 let log_index = log
23835 .log_index
23836 .expect("individual owner catch-up logs are validated before set validation");
23837 if block_hashes
23838 .insert(number, block_hash)
23839 .is_some_and(|prior| prior != block_hash)
23840 {
23841 return Err(SubscriberOwnerError::InvalidBackfillLog(
23842 "conflicting block identity in canonical catch-up",
23843 ));
23844 }
23845 if let Some(previous) = positions.insert((number, log_index), log)
23846 && previous != log
23847 {
23848 return Err(SubscriberOwnerError::InvalidBackfillLog(
23849 "conflicting logs at one canonical block position",
23850 ));
23851 }
23852 let conflicting_transaction = transaction_hashes
23853 .insert((number, transaction_index), transaction_hash)
23854 .is_some_and(|prior| prior != transaction_hash)
23855 || transaction_positions
23856 .insert((number, transaction_hash), transaction_index)
23857 .is_some_and(|prior| prior != transaction_index);
23858 if conflicting_transaction {
23859 return Err(SubscriberOwnerError::InvalidBackfillLog(
23860 "conflicting transaction identity at one canonical block position",
23861 ));
23862 }
23863 ordering
23864 .entry(number)
23865 .or_default()
23866 .push((log_index, transaction_index));
23867 }
23868 for positions in ordering.values_mut() {
23869 positions.sort_unstable();
23870 if positions.windows(2).any(|pair| pair[0].1 > pair[1].1) {
23871 return Err(SubscriberOwnerError::InvalidBackfillLog(
23872 "transaction and log positions disagree on canonical order",
23873 ));
23874 }
23875 }
23876 Ok(())
23877}
23878
23879fn merged_owner_reconcile_filters<N: Network>(
23880 plans: &[SubscriberOwnerReconcilePlan<N>],
23881 through: u64,
23882) -> Vec<SubscriberOwnerReconcileFilter> {
23883 let mut by_start = BTreeMap::<u64, Vec<Filter>>::new();
23884 for plan in plans.iter().filter(|plan| plan.from_block <= through) {
23885 let filters = by_start.entry(plan.from_block).or_default();
23886 filters.extend(
23887 log_filters(&plan.interests)
23888 .into_iter()
23889 .map(|filter| filter.from_block(plan.from_block).to_block(through)),
23890 );
23891 }
23892
23893 let mut chunks = Vec::new();
23894 for (from_block, filters) in by_start {
23895 for filters in filters.chunks(OWNER_RECONCILE_FILTERS_PER_CHUNK) {
23896 let mut merged = Vec::new();
23897 for filter in filters {
23898 merge_log_subscription_filter(&mut merged, filter);
23899 }
23900 chunks.extend(
23901 merged
23902 .into_iter()
23903 .map(|filter| SubscriberOwnerReconcileFilter { filter, from_block }),
23904 );
23905 }
23906 }
23907 chunks
23908}
23909
23910fn merged_lazy_backfill_filters(
23911 filters: &[Filter],
23912 from_block: u64,
23913 through: u64,
23914) -> Vec<SubscriberOwnerReconcileFilter> {
23915 let mut requests = Vec::new();
23916 for filters in filters.chunks(OWNER_RECONCILE_FILTERS_PER_CHUNK) {
23917 let mut merged = Vec::new();
23918 for filter in filters {
23919 merge_log_subscription_filter(
23920 &mut merged,
23921 &filter.clone().from_block(from_block).to_block(through),
23922 );
23923 }
23924 requests.extend(
23925 merged
23926 .into_iter()
23927 .map(|filter| SubscriberOwnerReconcileFilter { filter, from_block }),
23928 );
23929 }
23930 requests
23931}
23932
23933fn lazy_backfill_error(error: SubscriberOwnerError) -> SubscriberError {
23934 match error {
23935 SubscriberOwnerError::Subscriber(error) => error,
23936 error => SubscriberError::InvalidBackfill(error.to_string()),
23937 }
23938}
23939
23940fn global_backfill_barrier(backfill: SubscriberBackfill, certified: BlockRef) -> ChainControl {
23941 let mut id = b"alloy-global-backfill-v1".to_vec();
23942 id.extend_from_slice(&backfill.start_block().to_be_bytes());
23943 id.extend_from_slice(&certified.number.to_be_bytes());
23944 id.extend_from_slice(certified.hash.as_slice());
23945 ChainControl::Barrier {
23946 id,
23947 block: Some(certified),
23948 }
23949}
23950
23951async fn fetch_owner_catchup<P, N>(
23952 provider: P,
23953 filters: Vec<SubscriberOwnerReconcileFilter>,
23954 retained: Vec<BlockRef>,
23955 through: BlockRef,
23956 options: SubscriberOwnerCatchupOptions,
23957) -> Result<SubscriberOwnerCatchup, SubscriberOwnerError>
23958where
23959 P: Provider<N> + Send + Sync,
23960 N: Network,
23961{
23962 if !options.target_preverified {
23963 let _ = verify_provider_reconcile_target::<P, N>(&provider, &through).await?;
23964 }
23965 let mut certified_positions = HashSet::new();
23966 for position in retained {
23967 let target_certifies_position = position == through
23968 || (position.number.checked_add(1) == Some(through.number)
23969 && through.parent_hash == Some(position.hash));
23970 if !target_certifies_position && certified_positions.insert(position) {
23971 let _ = verify_provider_reconcile_target::<P, N>(&provider, &position).await?;
23972 }
23973 }
23974 let mut logs = Vec::new();
23975 let mut total_log_bytes = 0usize;
23976 let requests = stream::iter(filters.into_iter().map(|filter| {
23977 let provider = &provider;
23978 async move {
23979 let logs = provider
23980 .get_logs(&filter.filter)
23981 .await
23982 .map_err(provider_error)?;
23983 Ok::<_, SubscriberOwnerError>((filter.from_block, logs))
23984 }
23985 }))
23986 .buffer_unordered(options.max_requests_in_flight);
23987 futures::pin_mut!(requests);
23988 while let Some(result) = requests.next().await {
23989 let (from_block, fetched) = result?;
23990 let fetched_bytes =
23991 validate_backfill_resource_limits(&fetched, options.max_logs, options.max_log_bytes)?;
23992 validate_owner_backfill_logs(&fetched, from_block, &through)?;
23993 if logs.len().saturating_add(fetched.len()) > options.max_logs {
23994 return Err(SubscriberError::ResourceExhausted(format!(
23995 "bulk reconcile returned more than {} logs",
23996 options.max_logs
23997 ))
23998 .into());
23999 }
24000 total_log_bytes = total_log_bytes.saturating_add(fetched_bytes);
24001 if total_log_bytes > options.max_log_bytes {
24002 return Err(SubscriberError::ResourceExhausted(format!(
24003 "bulk reconcile retained approximately {total_log_bytes} log bytes, above the configured limit of {}",
24004 options.max_log_bytes
24005 ))
24006 .into());
24007 }
24008 logs.extend(fetched);
24009 }
24010 validate_owner_backfill_log_set(&logs)?;
24011 let certified = verify_provider_reconcile_target::<P, N>(&provider, &through).await?;
24012 Ok(SubscriberOwnerCatchup { logs, certified })
24013}
24014
24015async fn verify_provider_reconcile_target<P, N>(
24016 provider: &P,
24017 expected: &BlockRef,
24018) -> Result<BlockRef, SubscriberOwnerError>
24019where
24020 P: Provider<N> + Send + Sync,
24021 N: Network,
24022{
24023 let block = provider
24024 .get_block_by_number(BlockNumberOrTag::Number(expected.number))
24025 .await
24026 .map_err(provider_error)?
24027 .ok_or(SubscriberOwnerError::BlockUnavailable(expected.number))?;
24028 let header = block.header();
24029 let actual = BlockRef {
24030 number: header.number(),
24031 hash: header.hash(),
24032 parent_hash: Some(header.parent_hash()),
24033 timestamp: Some(header.timestamp()),
24034 };
24035 let exact_parent = expected
24036 .parent_hash
24037 .is_none_or(|parent| Some(parent) == actual.parent_hash);
24038 let exact_timestamp = expected
24039 .timestamp
24040 .is_none_or(|timestamp| Some(timestamp) == actual.timestamp);
24041 if actual.number != expected.number
24042 || actual.hash != expected.hash
24043 || !exact_parent
24044 || !exact_timestamp
24045 {
24046 return Err(SubscriberOwnerError::BlockMismatch {
24047 expected_number: expected.number,
24048 expected_hash: expected.hash,
24049 actual_number: actual.number,
24050 actual_hash: actual.hash,
24051 });
24052 }
24053 Ok(actual)
24054}
24055
24056fn log_input_record<N: Network>(log: Log, source: InputSource) -> ReactiveInputRecord<N> {
24057 let context = log_reactive_context(&log);
24058 ReactiveInputRecord::new(
24059 ReactiveInput::Log(log),
24060 ReactiveContext { source, ..context },
24061 )
24062}
24063
24064fn preconfirmed_log_input_record<N: Network>(
24065 log: Log,
24066 flashblock: FlashblockRef,
24067) -> ReactiveInputRecord<N> {
24068 let block = flashblock.block_ref();
24069 let provider = flashblock.provider.clone();
24070 ReactiveInputRecord::new(
24071 ReactiveInput::Log(log.clone()),
24072 ReactiveContext {
24073 chain_id: None,
24074 source: InputSource::Flashblocks,
24075 chain_status: ChainStatus::Preconfirmed {
24076 flashblock: Arc::new(flashblock),
24077 },
24078 block: Some(block),
24079 transaction_index: log.transaction_index,
24080 log_index: log.log_index,
24081 },
24082 )
24083 .with_provider(provider)
24084}
24085
24086fn log_reactive_context(log: &Log) -> ReactiveContext {
24087 let block = match (log.block_hash, log.block_number) {
24088 (Some(hash), Some(number)) => Some(BlockRef {
24089 number,
24090 hash,
24091 parent_hash: None,
24092 timestamp: log.block_timestamp,
24093 }),
24094 _ => None,
24095 };
24096
24097 let chain_status = match (&block, log.removed) {
24098 (Some(block), true) => ChainStatus::Reorged {
24099 dropped_from: *block,
24100 },
24101 (Some(block), false) => ChainStatus::Included {
24102 block: *block,
24103 confirmations: 0,
24104 },
24105 (None, _) => ChainStatus::Pending,
24106 };
24107
24108 ReactiveContext {
24109 chain_id: None,
24110 source: InputSource::Poll,
24111 chain_status,
24112 block,
24113 transaction_index: log.transaction_index,
24114 log_index: log.log_index,
24115 }
24116}
24117
24118fn block_header_input_record<N>(header: N::HeaderResponse) -> ReactiveInputRecord<N>
24119where
24120 N: Network,
24121{
24122 let block = BlockRef {
24123 number: header.number(),
24124 hash: HeaderResponseTrait::hash(&header),
24125 parent_hash: Some(header.parent_hash()),
24126 timestamp: Some(header.timestamp()),
24127 };
24128 ReactiveInputRecord::new(
24129 ReactiveInput::BlockHeader(header),
24130 ReactiveContext {
24131 chain_id: None,
24132 source: InputSource::Subscription,
24133 chain_status: ChainStatus::Included {
24134 block,
24135 confirmations: 0,
24136 },
24137 block: Some(block),
24138 transaction_index: None,
24139 log_index: None,
24140 },
24141 )
24142}
24143
24144fn pending_hash_input_record<N: Network>(
24145 hash: B256,
24146 source: InputSource,
24147) -> ReactiveInputRecord<N> {
24148 ReactiveInputRecord::new(
24149 ReactiveInput::PendingTxHash(hash),
24150 ReactiveContext {
24151 chain_id: None,
24152 source,
24153 chain_status: ChainStatus::Pending,
24154 block: None,
24155 transaction_index: None,
24156 log_index: None,
24157 },
24158 )
24159}
24160
24161#[cfg(feature = "reactive-ws")]
24162fn base_pending_log_filter(filter: &Filter) -> Result<serde_json::Value, SubscriberError> {
24163 let encoded = serde_json::to_value(filter)
24164 .map_err(|error| SubscriberError::Provider(error.to_string()))?;
24165 let serde_json::Value::Object(mut fields) = encoded else {
24166 return Err(SubscriberError::Provider(
24167 "Alloy log filter did not serialize as an object".into(),
24168 ));
24169 };
24170 fields.retain(|key, _| key == "address" || key == "topics");
24171 Ok(serde_json::Value::Object(fields))
24172}
24173
24174fn provider_error(error: impl fmt::Display) -> SubscriberError {
24175 SubscriberError::Provider(error.to_string())
24176}
24177
24178#[derive(Debug, thiserror::Error)]
24180#[non_exhaustive]
24181pub enum SubscriberError {
24182 #[error("{0}")]
24184 InvalidConfig(&'static str),
24185 #[error("{0}")]
24187 Unsupported(&'static str),
24188 #[error("subscriber chain mismatch: expected {expected}, got {actual}")]
24190 ChainMismatch {
24191 expected: u64,
24193 actual: u64,
24195 },
24196 #[error("provider error: {0}")]
24198 Provider(String),
24199 #[error("invalid canonical backfill: {0}")]
24202 InvalidBackfill(String),
24203 #[error("subscriber resource limit exceeded: {0}")]
24205 ResourceExhausted(String),
24206}