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#[cfg(feature = "reactive-ws")]
53use tokio::sync::broadcast;
54
55use crate::{
56 cache::{
57 AccountProof, BlockStateDiff, DurableCheckpointBlock, DurableCheckpointError,
58 DurableCheckpointIdentity, DurableCheckpointMetadata, DurableCheckpointStore, EvmCache,
59 EvmCacheStateSnapshot, LoadedDurableCheckpoint,
60 },
61 errors::{BlockContextError, StorageFetchResult},
62 events::{EventDecoder, StateView},
63 freshness::FreshnessRegistry,
64 state_update::{AccountPatch, PurgeScope, StateDiff, StateUpdate},
65};
66
67#[cfg(all(test, not(feature = "ingress-timing")))]
68mod ingress_feature_tests;
69#[cfg(feature = "ingress-timing")]
70mod ingress_timing;
71#[cfg(feature = "ingress-timing")]
72pub use ingress_timing::{CanonicalIngressTiming, TransportReceipt, TransportReceiptLookup};
73
74#[cfg(feature = "raw-flashblocks-json")]
75mod raw_json_flashblocks;
76#[cfg(feature = "raw-flashblocks-json")]
77pub use raw_json_flashblocks::{
78 BufferedRawJsonFlashblocksAdapter, FlashblockInvalidation, FlashblockInvalidationReason,
79 FlashblockSnapshot, FlashblockUpdate, FlashblockUpdateAcknowledgement,
80 FlashblockUpdateChannelError, FlashblockUpdateSender, RawJsonFlashblocksAdapter,
81 RawJsonFlashblocksError, RawJsonFlashblocksLimits, TimedFlashblockUpdate,
82};
83
84#[derive(Clone, Debug, PartialEq, Eq)]
86pub enum ReactiveInput<N: Network = Ethereum> {
87 Log(Log),
89 BlockHeader(N::HeaderResponse),
91 FullBlock(N::BlockResponse),
93 PendingTxHash(B256),
95 PendingTx(N::TransactionResponse),
97}
98
99#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
101pub struct ReactiveContext {
102 pub chain_id: Option<u64>,
104 pub source: InputSource,
106 pub chain_status: ChainStatus,
108 pub block: Option<BlockRef>,
110 pub transaction_index: Option<u64>,
112 pub log_index: Option<u64>,
114}
115
116#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
118pub struct BlockRef {
119 pub number: u64,
121 pub hash: B256,
123 pub parent_hash: Option<B256>,
125 pub timestamp: Option<u64>,
127}
128
129#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
136pub struct ProviderRef {
137 pub endpoint: EndpointId,
139 pub generation: u64,
141}
142
143impl ProviderRef {
144 pub fn new(endpoint: impl Into<EndpointId>, generation: u64) -> Self {
146 Self {
147 endpoint: endpoint.into(),
148 generation,
149 }
150 }
151}
152
153#[derive(Clone, Copy, Debug, PartialEq, Eq)]
159pub struct FlashblockIngressTiming {
160 source_ingress: Instant,
161}
162
163impl FlashblockIngressTiming {
164 pub const fn new(source_ingress: Instant) -> Self {
166 Self { source_ingress }
167 }
168
169 pub const fn source_ingress(self) -> Instant {
171 self.source_ingress
172 }
173
174 pub fn earliest(self, other: Self) -> Self {
176 Self::new(self.source_ingress.min(other.source_ingress))
177 }
178}
179
180#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
182pub struct FlashblockRef {
183 pub provider: ProviderRef,
185 pub payload_id: Option<FixedBytes<8>>,
189 pub index: Option<u64>,
191 pub block_number: u64,
193 pub content_hash: B256,
199 pub partial_block_hash: Option<B256>,
201 pub parent_hash: Option<B256>,
203 pub state_root: Option<B256>,
205 pub transactions_root: Option<B256>,
207 pub transaction_hashes: Vec<B256>,
209 pub timestamp: Option<u64>,
211 pub base_fee_per_gas: Option<u64>,
213 pub beneficiary: Option<Address>,
215 pub prevrandao: Option<B256>,
217 pub gas_limit: Option<u64>,
219}
220
221impl FlashblockRef {
222 pub const fn block_ref(&self) -> BlockRef {
227 BlockRef {
228 number: self.block_number,
229 hash: self.content_hash,
230 parent_hash: self.parent_hash,
231 timestamp: self.timestamp,
232 }
233 }
234
235 pub fn contains_transaction(&self, transaction_hash: &B256) -> bool {
237 self.transaction_hashes.contains(transaction_hash)
238 }
239
240 fn transaction_index(&self, transaction_hash: &B256) -> Option<u64> {
241 self.transaction_hashes
242 .iter()
243 .position(|candidate| candidate == transaction_hash)
244 .and_then(|index| u64::try_from(index).ok())
245 }
246
247 fn same_payload(&self, other: &Self) -> bool {
248 self.provider == other.provider
249 && match (self.payload_id, other.payload_id) {
250 (Some(left), Some(right)) => left == right,
251 _ => {
252 self.block_number == other.block_number && self.parent_hash == other.parent_hash
253 }
254 }
255 }
256
257 #[cfg(feature = "raw-flashblocks-json")]
265 pub fn same_base_identity(&self, other: &Self) -> bool {
266 self.block_number == other.block_number
267 && self.parent_hash == other.parent_hash
268 && self.timestamp == other.timestamp
269 && self.base_fee_per_gas == other.base_fee_per_gas
270 && self.beneficiary == other.beneficiary
271 && self.prevrandao == other.prevrandao
272 && self.gas_limit == other.gas_limit
273 }
274
275 fn is_cumulative_successor_of(&self, previous: &Self) -> bool {
276 self.same_payload(previous)
277 && self.transaction_hashes.len() >= previous.transaction_hashes.len()
278 && self
279 .transaction_hashes
280 .starts_with(&previous.transaction_hashes)
281 && match (previous.index, self.index) {
282 (Some(previous), Some(current)) => current >= previous,
283 _ => true,
284 }
285 }
286}
287
288#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
290pub enum PreconfirmationMode {
291 #[default]
293 Disabled,
294 Preferred,
297 Required,
299}
300
301#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
303pub struct BaseFlashblockPayload {
304 pub payload_id: FixedBytes<8>,
306 pub index: u64,
308 pub base: Option<BaseFlashblockBase>,
310 pub diff: BaseFlashblockDiff,
312 #[serde(default)]
314 pub metadata: Option<BaseFlashblockMetadata>,
315}
316
317#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
319pub struct BaseFlashblockBase {
320 pub parent_hash: B256,
322 #[serde(deserialize_with = "deserialize_rpc_u64")]
324 pub block_number: u64,
325 #[serde(deserialize_with = "deserialize_rpc_u64")]
327 pub timestamp: u64,
328 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
330 pub gas_limit: Option<u64>,
331 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
333 pub base_fee_per_gas: Option<u64>,
334 #[serde(default, alias = "fee_recipient", alias = "feeRecipient")]
336 pub beneficiary: Option<Address>,
337 #[serde(
339 default,
340 alias = "prev_randao",
341 alias = "prevRandao",
342 alias = "mixHash"
343 )]
344 pub prevrandao: Option<B256>,
345}
346
347#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
349pub struct BaseFlashblockDiff {
350 pub state_root: B256,
352 pub block_hash: B256,
354 #[serde(default)]
356 pub transactions: Vec<serde_json::Value>,
357 #[serde(default)]
359 pub transactions_root: Option<B256>,
360}
361
362#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
365pub struct BaseFlashblockMetadata {
366 #[serde(deserialize_with = "deserialize_rpc_u64")]
368 pub block_number: u64,
369}
370
371#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
374#[serde(rename_all = "camelCase")]
375struct BaseFlashblockBlockPayload {
376 hash: B256,
377 #[serde(deserialize_with = "deserialize_rpc_u64")]
378 number: u64,
379 parent_hash: B256,
380 state_root: B256,
381 #[serde(default)]
382 transactions_root: Option<B256>,
383 #[serde(default)]
384 transactions: Vec<serde_json::Value>,
385 #[serde(deserialize_with = "deserialize_rpc_u64")]
386 timestamp: u64,
387 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
388 base_fee_per_gas: Option<u64>,
389 #[serde(default, alias = "beneficiary", alias = "feeRecipient")]
390 miner: Option<Address>,
391 #[serde(default, alias = "prevRandao")]
392 mix_hash: Option<B256>,
393 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
394 gas_limit: Option<u64>,
395}
396
397#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
401#[serde(untagged)]
402enum BaseFlashblockWirePayload {
403 Indexed(BaseFlashblockPayload),
404 Block(BaseFlashblockBlockPayload),
405}
406
407fn deserialize_rpc_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
408where
409 D: serde::Deserializer<'de>,
410{
411 #[derive(serde::Deserialize)]
412 #[serde(untagged)]
413 enum RpcU64 {
414 Number(u64),
415 String(String),
416 }
417
418 match <RpcU64 as serde::Deserialize>::deserialize(deserializer)? {
419 RpcU64::Number(number) => Ok(number),
420 RpcU64::String(value) => {
421 let value = value.strip_prefix("0x").unwrap_or(&value);
422 u64::from_str_radix(value, 16).map_err(serde::de::Error::custom)
423 }
424 }
425}
426
427fn deserialize_optional_rpc_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
428where
429 D: serde::Deserializer<'de>,
430{
431 #[derive(serde::Deserialize)]
432 #[serde(untagged)]
433 enum RpcU64 {
434 Number(u64),
435 String(String),
436 }
437
438 let Some(value) = <Option<RpcU64> as serde::Deserialize>::deserialize(deserializer)? else {
439 return Ok(None);
440 };
441 match value {
442 RpcU64::Number(number) => Ok(Some(number)),
443 RpcU64::String(value) => {
444 let value = value.strip_prefix("0x").unwrap_or(&value);
445 u64::from_str_radix(value, 16)
446 .map(Some)
447 .map_err(serde::de::Error::custom)
448 }
449 }
450}
451
452fn non_placeholder_hash(hash: B256) -> Option<B256> {
453 (!hash.is_zero()).then_some(hash)
454}
455
456fn flashblock_transaction_hashes(
457 transactions: &[serde_json::Value],
458) -> Result<Vec<B256>, SubscriberError> {
459 let hashes: Vec<B256> = transactions
460 .iter()
461 .map(|transaction| {
462 let value = match transaction {
463 serde_json::Value::String(value) => value.as_str(),
464 serde_json::Value::Object(object) => object
465 .get("hash")
466 .or_else(|| object.get("transactionHash"))
467 .and_then(serde_json::Value::as_str)
468 .ok_or_else(|| {
469 SubscriberError::Provider(
470 "Flashblock transaction object is missing its hash".into(),
471 )
472 })?,
473 _ => {
474 return Err(SubscriberError::Provider(
475 "Flashblock transaction must be a hash, raw transaction, or object".into(),
476 ));
477 }
478 };
479 if value.len() == 66 {
480 return value.parse::<B256>().map_err(|error| {
481 SubscriberError::Provider(format!(
482 "Flashblock transaction hash is invalid: {error}"
483 ))
484 });
485 }
486 let encoded = value.strip_prefix("0x").unwrap_or(value);
487 let raw = alloy_primitives::hex::decode(encoded).map_err(|error| {
488 SubscriberError::Provider(format!(
489 "Flashblock raw transaction is invalid hex: {error}"
490 ))
491 })?;
492 Ok(alloy_primitives::keccak256(raw))
493 })
494 .collect::<Result<_, _>>()?;
495 let mut unique = HashSet::with_capacity(hashes.len());
496 if hashes.iter().any(|hash| !unique.insert(*hash)) {
497 return Err(SubscriberError::Provider(
498 "Flashblock cumulative transaction membership contains a duplicate hash".into(),
499 ));
500 }
501 Ok(hashes)
502}
503
504struct FlashblockContentCommitment<'a> {
505 provider: &'a ProviderRef,
506 payload_id: Option<FixedBytes<8>>,
507 index: Option<u64>,
508 block_number: u64,
509 partial_block_hash: Option<B256>,
510 parent_hash: Option<B256>,
511 state_root: Option<B256>,
512 transactions_root: Option<B256>,
513 transaction_hashes: &'a [B256],
514 timestamp: Option<u64>,
515 base_fee_per_gas: Option<u64>,
516 beneficiary: Option<Address>,
517 prevrandao: Option<B256>,
518 gas_limit: Option<u64>,
519}
520
521fn flashblock_content_hash(content: FlashblockContentCommitment<'_>) -> B256 {
522 let mut commitment = Keccak256::new();
523 commitment.update(b"evm-fork-cache/flashblock-content/v1");
524 let endpoint = content.provider.endpoint.as_str().as_bytes();
525 commitment.update((endpoint.len() as u64).to_be_bytes());
526 commitment.update(endpoint);
527 commitment.update(content.provider.generation.to_be_bytes());
528 commitment.update(content.block_number.to_be_bytes());
529 commit_optional_bytes(
530 &mut commitment,
531 content.payload_id.as_ref().map(FixedBytes::as_slice),
532 );
533 commit_optional_u64(&mut commitment, content.index);
534 commit_optional_bytes(
535 &mut commitment,
536 content
537 .partial_block_hash
538 .as_ref()
539 .map(FixedBytes::as_slice),
540 );
541 commit_optional_bytes(
542 &mut commitment,
543 content.parent_hash.as_ref().map(FixedBytes::as_slice),
544 );
545 commit_optional_bytes(
546 &mut commitment,
547 content.state_root.as_ref().map(FixedBytes::as_slice),
548 );
549 commit_optional_bytes(
550 &mut commitment,
551 content.transactions_root.as_ref().map(FixedBytes::as_slice),
552 );
553 commitment.update((content.transaction_hashes.len() as u64).to_be_bytes());
554 for transaction_hash in content.transaction_hashes {
555 commitment.update(transaction_hash);
556 }
557 commit_optional_u64(&mut commitment, content.timestamp);
558 commit_optional_u64(&mut commitment, content.base_fee_per_gas);
559 commit_optional_bytes(
560 &mut commitment,
561 content
562 .beneficiary
563 .as_ref()
564 .map(|address| address.as_slice()),
565 );
566 commit_optional_bytes(
567 &mut commitment,
568 content.prevrandao.as_ref().map(FixedBytes::as_slice),
569 );
570 commit_optional_u64(&mut commitment, content.gas_limit);
571 let hash = commitment.finalize();
572 if hash.is_zero() {
573 B256::with_last_byte(1)
574 } else {
575 hash
576 }
577}
578
579#[cfg(feature = "raw-flashblocks-json")]
580fn validate_standard_flashblock_snapshot(
581 snapshot: &FlashblockSnapshot,
582) -> Result<(), SubscriberError> {
583 let flashblock = &snapshot.flashblock;
584 if flashblock.payload_id.is_none() || flashblock.index.is_none() {
585 return Err(SubscriberError::Provider(
586 "external Flashblock snapshot is missing its indexed payload identity".into(),
587 ));
588 }
589 let expected_content_hash = flashblock_content_hash(FlashblockContentCommitment {
590 provider: &flashblock.provider,
591 payload_id: flashblock.payload_id,
592 index: flashblock.index,
593 block_number: flashblock.block_number,
594 partial_block_hash: flashblock.partial_block_hash,
595 parent_hash: flashblock.parent_hash,
596 state_root: flashblock.state_root,
597 transactions_root: flashblock.transactions_root,
598 transaction_hashes: &flashblock.transaction_hashes,
599 timestamp: flashblock.timestamp,
600 base_fee_per_gas: flashblock.base_fee_per_gas,
601 beneficiary: flashblock.beneficiary,
602 prevrandao: flashblock.prevrandao,
603 gas_limit: flashblock.gas_limit,
604 });
605 if flashblock.content_hash != expected_content_hash {
606 return Err(SubscriberError::Provider(
607 "external Flashblock content commitment is invalid".into(),
608 ));
609 }
610
611 let mut transactions = HashSet::with_capacity(flashblock.transaction_hashes.len());
612 if flashblock
613 .transaction_hashes
614 .iter()
615 .any(|hash| !transactions.insert(*hash))
616 {
617 return Err(SubscriberError::Provider(
618 "external Flashblock cumulative transaction membership contains a duplicate hash"
619 .into(),
620 ));
621 }
622
623 let mut log_ids = HashSet::with_capacity(snapshot.logs.len());
624 for log in &snapshot.logs {
625 if log.removed || log.block_number != Some(flashblock.block_number) {
626 return Err(SubscriberError::Provider(
627 "external pre-confirmed log disagrees with its Flashblock block identity".into(),
628 ));
629 }
630 if log.block_hash != Some(flashblock.content_hash) {
631 return Err(SubscriberError::Provider(
632 "external pre-confirmed log is not bound to its Flashblock content commitment"
633 .into(),
634 ));
635 }
636 let transaction_hash = log.transaction_hash.ok_or_else(|| {
637 SubscriberError::Provider(
638 "external pre-confirmed log is missing its transaction hash".into(),
639 )
640 })?;
641 let expected_transaction_index = flashblock
642 .transaction_index(&transaction_hash)
643 .ok_or_else(|| {
644 SubscriberError::Provider(
645 "external pre-confirmed log transaction is absent from the cumulative Flashblock"
646 .into(),
647 )
648 })?;
649 if log.transaction_index != Some(expected_transaction_index) {
650 return Err(SubscriberError::Provider(
651 "external pre-confirmed log transaction index disagrees with cumulative membership"
652 .into(),
653 ));
654 }
655 let log_index = log.log_index.ok_or_else(|| {
656 SubscriberError::Provider("external pre-confirmed log is missing its log index".into())
657 })?;
658 if !log_ids.insert((transaction_hash, log_index)) {
659 return Err(SubscriberError::Provider(
660 "external Flashblock snapshot contains a duplicate log identity".into(),
661 ));
662 }
663 }
664 Ok(())
665}
666
667fn commit_optional_bytes(commitment: &mut Keccak256, value: Option<&[u8]>) {
668 match value {
669 Some(value) => {
670 commitment.update([1]);
671 commitment.update((value.len() as u64).to_be_bytes());
672 commitment.update(value);
673 }
674 None => commitment.update([0]),
675 }
676}
677
678fn commit_optional_u64(commitment: &mut Keccak256, value: Option<u64>) {
679 match value {
680 Some(value) => {
681 commitment.update([1]);
682 commitment.update(value.to_be_bytes());
683 }
684 None => commitment.update([0]),
685 }
686}
687
688#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
691pub struct ReactiveCanonicalBaseline {
692 pub chain_id: u64,
694 pub block: BlockRef,
696}
697
698impl ReactiveCanonicalBaseline {
699 pub const fn new(chain_id: u64, block: BlockRef) -> Self {
701 Self { chain_id, block }
702 }
703}
704
705#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
714#[non_exhaustive]
715pub enum ChainControl {
716 Reorg {
718 common_ancestor: BlockRef,
720 old_tip: BlockRef,
722 new_tip: BlockRef,
724 },
725 Safe(BlockRef),
727 Finalized(BlockRef),
729 CanonicalProgress(BlockRef),
736 LogCoverage(BlockRef),
760 Barrier {
762 id: Vec<u8>,
764 block: Option<BlockRef>,
766 },
767}
768
769#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
788pub struct CanonicalSequenceState {
789 retained_canonical_history: Vec<BlockRef>,
790 coverage_head: Option<BlockRef>,
791 safe_head: Option<BlockRef>,
792 finalized_head: Option<BlockRef>,
793 log_coverage_head: Option<BlockRef>,
794}
795
796impl CanonicalSequenceState {
797 pub fn new(
803 retained_canonical_history: Vec<BlockRef>,
804 coverage_head: Option<BlockRef>,
805 safe_head: Option<BlockRef>,
806 finalized_head: Option<BlockRef>,
807 ) -> Self {
808 Self {
809 retained_canonical_history,
810 coverage_head,
811 safe_head,
812 finalized_head,
813 log_coverage_head: None,
814 }
815 }
816
817 #[must_use]
823 pub fn with_log_coverage_head(mut self, log_coverage_head: Option<BlockRef>) -> Self {
824 self.log_coverage_head = log_coverage_head;
825 self
826 }
827
828 pub fn retained_canonical_history(&self) -> &[BlockRef] {
830 &self.retained_canonical_history
831 }
832
833 pub const fn coverage_head(&self) -> Option<&BlockRef> {
835 self.coverage_head.as_ref()
836 }
837
838 pub const fn safe_head(&self) -> Option<&BlockRef> {
840 self.safe_head.as_ref()
841 }
842
843 pub const fn finalized_head(&self) -> Option<&BlockRef> {
845 self.finalized_head.as_ref()
846 }
847
848 pub const fn log_coverage_head(&self) -> Option<&BlockRef> {
854 self.log_coverage_head.as_ref()
855 }
856
857 pub fn retain_recent_history(&mut self, max_entries: usize) {
866 let remove = self
867 .retained_canonical_history
868 .len()
869 .saturating_sub(max_entries);
870 self.retained_canonical_history.drain(..remove);
871 }
872
873 pub fn validate(&self) -> Result<(), ReactiveError> {
887 validate_canonical_sequence_snapshot(self)
888 }
889}
890
891#[derive(Clone, Debug, PartialEq, Eq)]
893#[non_exhaustive]
894pub enum CanonicalSequenceMutation {
895 Rewind {
897 common_ancestor: Option<BlockRef>,
903 dropped: Vec<BlockRef>,
905 },
906 Canonical(BlockRef),
908 Safe(BlockRef),
910 Finalized(BlockRef),
912 LogCoverage(BlockRef),
914}
915
916#[derive(Clone, Debug, PartialEq, Eq)]
918pub struct CanonicalSequenceValidation {
919 pre_record_state: CanonicalSequenceState,
920 next_state: CanonicalSequenceState,
921 mutations: Vec<CanonicalSequenceMutation>,
922 normalized_chain_controls: Vec<ChainControl>,
923}
924
925impl CanonicalSequenceValidation {
926 pub const fn pre_record_state(&self) -> &CanonicalSequenceState {
928 &self.pre_record_state
929 }
930
931 pub const fn next_state(&self) -> &CanonicalSequenceState {
933 &self.next_state
934 }
935
936 pub fn mutations(&self) -> &[CanonicalSequenceMutation] {
938 &self.mutations
939 }
940
941 pub fn normalized_chain_controls(&self) -> &[ChainControl] {
951 &self.normalized_chain_controls
952 }
953}
954
955#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
957#[non_exhaustive]
958pub enum ChainStatus {
959 Pending,
961 Preconfirmed {
966 flashblock: Arc<FlashblockRef>,
970 },
971 Included {
973 block: BlockRef,
975 confirmations: u64,
977 },
978 Safe {
980 block: BlockRef,
982 },
983 Finalized {
985 block: BlockRef,
987 },
988 Reorged {
990 dropped_from: BlockRef,
992 },
993}
994
995#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
997#[non_exhaustive]
998pub enum InputSource {
999 Batch,
1001 Subscription,
1003 Poll,
1005 Backfill,
1007 Flashblocks,
1009 Synthetic,
1011}
1012
1013#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1015pub enum InputRef {
1016 Log {
1018 chain_id: Option<u64>,
1020 block_hash: B256,
1022 transaction_hash: B256,
1024 log_index: u64,
1026 },
1027 PendingTx {
1029 chain_id: Option<u64>,
1031 hash: B256,
1033 },
1034 Block {
1036 chain_id: Option<u64>,
1038 hash: B256,
1040 number: u64,
1042 },
1043}
1044
1045#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1052#[non_exhaustive]
1053pub enum ReactiveInputKind {
1054 CanonicalLog,
1056 ReorgSignalLog,
1058 BlockHeader,
1060 FullBlock,
1062 PendingTxHash,
1064 PendingTx,
1066}
1067
1068#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1075pub struct ReactiveInputIdentity {
1076 input_ref: InputRef,
1077 kind: ReactiveInputKind,
1078}
1079
1080impl ReactiveInputIdentity {
1081 pub fn try_from_parts(
1095 input_ref: InputRef,
1096 kind: ReactiveInputKind,
1097 ) -> Result<Self, ReactiveInputIdentityError> {
1098 let compatible = matches!(
1099 (input_ref, kind),
1100 (
1101 InputRef::Log { .. },
1102 ReactiveInputKind::CanonicalLog | ReactiveInputKind::ReorgSignalLog
1103 ) | (
1104 InputRef::Block { .. },
1105 ReactiveInputKind::BlockHeader | ReactiveInputKind::FullBlock
1106 ) | (
1107 InputRef::PendingTx { .. },
1108 ReactiveInputKind::PendingTxHash | ReactiveInputKind::PendingTx
1109 )
1110 );
1111 if !compatible {
1112 return Err(ReactiveInputIdentityError { input_ref, kind });
1113 }
1114 Ok(Self { input_ref, kind })
1115 }
1116
1117 pub const fn input_ref(&self) -> InputRef {
1119 self.input_ref
1120 }
1121
1122 pub const fn kind(&self) -> ReactiveInputKind {
1124 self.kind
1125 }
1126}
1127
1128#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
1131#[error("reactive input kind {kind:?} is incompatible with input reference {input_ref:?}")]
1132pub struct ReactiveInputIdentityError {
1133 input_ref: InputRef,
1134 kind: ReactiveInputKind,
1135}
1136
1137impl ReactiveInputIdentityError {
1138 pub const fn input_ref(&self) -> InputRef {
1140 self.input_ref
1141 }
1142
1143 pub const fn kind(&self) -> ReactiveInputKind {
1145 self.kind
1146 }
1147}
1148
1149#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1151pub enum StateEffectQuality {
1152 ExactFromInput,
1154 AppliedWithPendingResync,
1156 ResyncedAuthoritatively,
1158 RequiresRepair,
1160 NoStateEffect,
1162}
1163
1164#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize)]
1166pub struct HandlerId(String);
1167
1168impl HandlerId {
1169 pub fn new(id: impl Into<String>) -> Self {
1176 Self::try_new(id).expect("handler id must not be empty")
1177 }
1178
1179 pub fn try_new(id: impl Into<String>) -> Result<Self, HandlerIdError> {
1186 let id = id.into();
1187 if id.is_empty() {
1188 return Err(HandlerIdError);
1189 }
1190 Ok(Self(id))
1191 }
1192
1193 pub fn as_str(&self) -> &str {
1195 &self.0
1196 }
1197}
1198
1199impl<'de> serde::Deserialize<'de> for HandlerId {
1200 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1201 where
1202 D: serde::Deserializer<'de>,
1203 {
1204 let id = <String as serde::Deserialize>::deserialize(deserializer)?;
1205 Self::try_new(id).map_err(serde::de::Error::custom)
1206 }
1207}
1208
1209#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
1212#[error("handler id must not be empty")]
1213pub struct HandlerIdError;
1214
1215impl fmt::Display for HandlerId {
1216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1217 self.0.fmt(f)
1218 }
1219}
1220
1221#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1223pub struct ReportTag {
1224 pub key: String,
1226 pub value: String,
1228}
1229
1230impl ReportTag {
1231 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1233 Self {
1234 key: key.into(),
1235 value: value.into(),
1236 }
1237 }
1238}
1239
1240#[derive(Clone)]
1242pub struct HookSignal {
1243 pub namespace: Cow<'static, str>,
1245 pub kind: Cow<'static, str>,
1247 pub labels: Vec<ReportTag>,
1249 pub payload: Option<Arc<dyn Any + Send + Sync>>,
1251}
1252
1253impl fmt::Debug for HookSignal {
1254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1255 f.debug_struct("HookSignal")
1256 .field("namespace", &self.namespace)
1257 .field("kind", &self.kind)
1258 .field("labels", &self.labels)
1259 .field("payload", &self.payload.as_ref().map(|_| "<payload>"))
1260 .finish()
1261 }
1262}
1263
1264#[derive(Clone, Debug)]
1266pub enum ReactiveEffect {
1267 StateUpdate(StateUpdate),
1269 Resync(ResyncRequest),
1271 Invalidate(InvalidationRequest),
1273 Hook(HookSignal),
1275 Speculative(SpeculativeRequest),
1277}
1278
1279#[derive(Clone, Debug)]
1281pub struct HandlerOutcome {
1282 pub effects: Vec<ReactiveEffect>,
1284 pub quality: StateEffectQuality,
1286 pub tags: Vec<ReportTag>,
1288}
1289
1290impl HandlerOutcome {
1291 pub fn empty(quality: StateEffectQuality) -> Self {
1293 Self {
1294 effects: Vec::new(),
1295 quality,
1296 tags: Vec::new(),
1297 }
1298 }
1299}
1300
1301#[derive(Clone, Debug)]
1303pub struct ReactiveInputRecord<N: Network = Ethereum> {
1304 pub input: ReactiveInput<N>,
1306 pub context: ReactiveContext,
1308 pub provider: Option<ProviderRef>,
1311}
1312
1313impl<N: Network> ReactiveInputRecord<N> {
1314 pub fn new(input: ReactiveInput<N>, context: ReactiveContext) -> Self {
1316 Self {
1317 input,
1318 context,
1319 provider: None,
1320 }
1321 }
1322
1323 #[must_use]
1325 pub fn with_provider(mut self, provider: ProviderRef) -> Self {
1326 self.provider = Some(provider);
1327 self
1328 }
1329
1330 pub fn input_ref(&self) -> InputRef {
1332 input_ref(&self.input, &self.context)
1333 }
1334
1335 pub fn validated_identity(&self) -> Result<ReactiveInputIdentity, ReactiveError> {
1352 validate_input_record(self)?;
1353 let kind = match &self.input {
1354 ReactiveInput::Log(log)
1355 if log.removed
1356 || matches!(self.context.chain_status, ChainStatus::Reorged { .. }) =>
1357 {
1358 ReactiveInputKind::ReorgSignalLog
1359 }
1360 ReactiveInput::Log(_) => ReactiveInputKind::CanonicalLog,
1361 ReactiveInput::BlockHeader(_) => ReactiveInputKind::BlockHeader,
1362 ReactiveInput::FullBlock(_) => ReactiveInputKind::FullBlock,
1363 ReactiveInput::PendingTxHash(_) => ReactiveInputKind::PendingTxHash,
1364 ReactiveInput::PendingTx(_) => ReactiveInputKind::PendingTx,
1365 };
1366 ReactiveInputIdentity::try_from_parts(self.input_ref(), kind).map_err(|error| {
1367 ReactiveError::InvalidInputRecord {
1368 message: error.to_string(),
1369 }
1370 })
1371 }
1372
1373 pub fn same_deduplicable_payload(&self, other: &Self) -> bool {
1387 match (&self.input, &other.input) {
1388 (ReactiveInput::Log(left), ReactiveInput::Log(right)) => {
1389 left.inner == right.inner
1390 && left.block_hash == right.block_hash
1391 && left.block_number == right.block_number
1392 && optional_metadata_compatible(
1393 left.block_timestamp.as_ref(),
1394 right.block_timestamp.as_ref(),
1395 )
1396 && left.transaction_hash == right.transaction_hash
1397 && left.transaction_index == right.transaction_index
1398 && left.log_index == right.log_index
1399 && left.removed == right.removed
1400 }
1401 (ReactiveInput::BlockHeader(left), ReactiveInput::BlockHeader(right)) => {
1402 left.hash() == right.hash()
1403 }
1404 (ReactiveInput::FullBlock(_), ReactiveInput::FullBlock(_)) => false,
1405 (ReactiveInput::PendingTxHash(left), ReactiveInput::PendingTxHash(right)) => {
1406 left == right
1407 }
1408 (ReactiveInput::PendingTx(_), ReactiveInput::PendingTx(_)) => false,
1409 _ => false,
1410 }
1411 }
1412
1413 pub fn is_payload_deduplicable(&self) -> bool {
1419 matches!(
1420 &self.input,
1421 ReactiveInput::Log(_) | ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_)
1422 )
1423 }
1424
1425 pub fn merge_compatible_duplicate(&mut self, other: &Self) -> Result<bool, ReactiveError> {
1440 let identity = self.validated_identity()?;
1441 let other_identity = other.validated_identity()?;
1442 if identity != other_identity
1443 || !self.is_payload_deduplicable()
1444 || !other.is_payload_deduplicable()
1445 {
1446 return Ok(false);
1447 }
1448 if !self.same_deduplicable_payload(other) || !self.dedupe_context_is_compatible(other) {
1449 return Err(ReactiveError::InvalidInputRecord {
1450 message: format!(
1451 "conflicting payload or semantic context for identity {identity:?}"
1452 ),
1453 });
1454 }
1455 let mut merged = self.clone();
1456 merge_deduplicable_record(&mut merged, other);
1457 merged.validated_identity()?;
1458 *self = merged;
1459 Ok(true)
1460 }
1461
1462 pub fn dedupe_context_is_compatible(&self, other: &Self) -> bool {
1470 let left = &self.context;
1471 let right = &other.context;
1472 left.chain_id == right.chain_id
1473 && optional_block_refs_are_compatible(left.block.as_ref(), right.block.as_ref())
1474 && left.transaction_index == right.transaction_index
1475 && left.log_index == right.log_index
1476 && chain_statuses_are_dedupe_compatible(&left.chain_status, &right.chain_status)
1477 }
1478}
1479
1480fn chain_statuses_are_dedupe_compatible(left: &ChainStatus, right: &ChainStatus) -> bool {
1481 match (left, right) {
1482 (ChainStatus::Pending, ChainStatus::Pending)
1483 | (ChainStatus::Reorged { .. }, ChainStatus::Reorged { .. }) => true,
1484 (
1485 ChainStatus::Preconfirmed { flashblock: left },
1486 ChainStatus::Preconfirmed { flashblock: right },
1487 ) => left == right,
1488 (
1489 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. },
1490 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. },
1491 ) => true,
1492 _ => false,
1493 }
1494}
1495
1496fn optional_metadata_compatible<T: PartialEq>(left: Option<&T>, right: Option<&T>) -> bool {
1497 left.zip(right).is_none_or(|(left, right)| left == right)
1498}
1499
1500fn optional_block_refs_are_compatible(left: Option<&BlockRef>, right: Option<&BlockRef>) -> bool {
1501 match (left, right) {
1502 (None, None) => true,
1503 (Some(left), Some(right)) => {
1504 left.number == right.number
1505 && left.hash == right.hash
1506 && optional_metadata_compatible(
1507 left.parent_hash.as_ref(),
1508 right.parent_hash.as_ref(),
1509 )
1510 && optional_metadata_compatible(left.timestamp.as_ref(), right.timestamp.as_ref())
1511 }
1512 _ => false,
1513 }
1514}
1515
1516fn merge_deduplicable_record<N: Network>(
1517 retained: &mut ReactiveInputRecord<N>,
1518 incoming: &ReactiveInputRecord<N>,
1519) {
1520 if let (ReactiveInput::Log(retained), ReactiveInput::Log(incoming)) =
1521 (&mut retained.input, &incoming.input)
1522 && retained.block_timestamp.is_none()
1523 {
1524 retained.block_timestamp = incoming.block_timestamp;
1525 }
1526 if let (Some(retained), Some(incoming)) =
1527 (&mut retained.context.block, incoming.context.block.as_ref())
1528 {
1529 enrich_block_ref(retained, incoming);
1530 }
1531 retained.context.chain_status = merged_chain_status(
1532 &retained.context.chain_status,
1533 &incoming.context.chain_status,
1534 );
1535 if input_source_rank(incoming.context.source) > input_source_rank(retained.context.source) {
1536 retained.context.source = incoming.context.source;
1537 }
1538 if retained.provider.is_none() {
1539 retained.provider = incoming.provider.clone();
1540 }
1541}
1542
1543fn enrich_block_ref(retained: &mut BlockRef, incoming: &BlockRef) {
1544 if retained.parent_hash.is_none() {
1545 retained.parent_hash = incoming.parent_hash;
1546 }
1547 if retained.timestamp.is_none() {
1548 retained.timestamp = incoming.timestamp;
1549 }
1550}
1551
1552fn merged_chain_status(retained: &ChainStatus, incoming: &ChainStatus) -> ChainStatus {
1553 let merged_block = |left: &BlockRef, right: &BlockRef| {
1554 let mut block = *left;
1555 enrich_block_ref(&mut block, right);
1556 block
1557 };
1558 match (retained, incoming) {
1559 (ChainStatus::Pending, ChainStatus::Pending) => ChainStatus::Pending,
1560 (
1561 ChainStatus::Preconfirmed { flashblock: left },
1562 ChainStatus::Preconfirmed { flashblock: right },
1563 ) => {
1564 debug_assert_eq!(left, right, "compatible pre-confirmed records agree");
1565 ChainStatus::Preconfirmed {
1566 flashblock: left.clone(),
1567 }
1568 }
1569 (
1570 ChainStatus::Reorged { dropped_from: left },
1571 ChainStatus::Reorged {
1572 dropped_from: right,
1573 },
1574 ) => ChainStatus::Reorged {
1575 dropped_from: merged_block(left, right),
1576 },
1577 (left, right) => {
1578 let (left_block, left_rank, left_confirmations) = canonical_status_parts(left)
1579 .expect("compatible duplicate has a canonical lifecycle");
1580 let (right_block, right_rank, right_confirmations) = canonical_status_parts(right)
1581 .expect("compatible duplicate has a canonical lifecycle");
1582 let block = merged_block(left_block, right_block);
1583 let rank = left_rank.max(right_rank);
1584 match rank {
1585 3 => ChainStatus::Finalized { block },
1586 2 => ChainStatus::Safe { block },
1587 _ => ChainStatus::Included {
1588 block,
1589 confirmations: left_confirmations.max(right_confirmations),
1590 },
1591 }
1592 }
1593 }
1594}
1595
1596fn canonical_status_parts(status: &ChainStatus) -> Option<(&BlockRef, u8, u64)> {
1597 match status {
1598 ChainStatus::Included {
1599 block,
1600 confirmations,
1601 } => Some((block, 1, *confirmations)),
1602 ChainStatus::Safe { block } => Some((block, 2, 0)),
1603 ChainStatus::Finalized { block } => Some((block, 3, 0)),
1604 ChainStatus::Pending | ChainStatus::Preconfirmed { .. } | ChainStatus::Reorged { .. } => {
1605 None
1606 }
1607 }
1608}
1609
1610fn input_source_rank(source: InputSource) -> u8 {
1611 match source {
1612 InputSource::Backfill => 0,
1613 InputSource::Poll => 1,
1614 InputSource::Subscription => 2,
1615 InputSource::Flashblocks => 3,
1616 InputSource::Batch => 4,
1617 InputSource::Synthetic => 5,
1618 }
1619}
1620
1621#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1631pub struct SubscriberDeliveryToken(Vec<u8>);
1632
1633impl SubscriberDeliveryToken {
1634 pub fn new(bytes: Vec<u8>) -> Self {
1636 Self(bytes)
1637 }
1638
1639 pub fn as_bytes(&self) -> &[u8] {
1641 &self.0
1642 }
1643
1644 pub fn into_bytes(self) -> Vec<u8> {
1646 self.0
1647 }
1648}
1649
1650#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1656pub struct SubscriberCheckpoint(Vec<u8>);
1657
1658impl SubscriberCheckpoint {
1659 pub fn new(bytes: Vec<u8>) -> Self {
1661 Self(bytes)
1662 }
1663
1664 pub fn as_bytes(&self) -> &[u8] {
1666 &self.0
1667 }
1668
1669 pub fn into_bytes(self) -> Vec<u8> {
1671 self.0
1672 }
1673}
1674
1675#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1685pub struct SubscriberPayloadCommitment(B256);
1686
1687impl SubscriberPayloadCommitment {
1688 pub const fn new(commitment: B256) -> Self {
1690 Self(commitment)
1691 }
1692
1693 pub const fn digest(&self) -> B256 {
1695 self.0
1696 }
1697}
1698
1699#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1705#[non_exhaustive]
1706pub struct SubscriberResumePosition {
1707 pub chain_id: u64,
1709 pub coverage_head: BlockRef,
1711 pub canonical_history: Vec<BlockRef>,
1713 pub delivery_token: Option<SubscriberDeliveryToken>,
1717 pub subscriber_checkpoint: Option<SubscriberCheckpoint>,
1719}
1720
1721impl SubscriberResumePosition {
1722 pub fn new(
1724 chain_id: u64,
1725 coverage_head: BlockRef,
1726 canonical_history: Vec<BlockRef>,
1727 delivery_token: Option<SubscriberDeliveryToken>,
1728 subscriber_checkpoint: Option<SubscriberCheckpoint>,
1729 ) -> Self {
1730 Self {
1731 chain_id,
1732 coverage_head,
1733 canonical_history,
1734 delivery_token,
1735 subscriber_checkpoint,
1736 }
1737 }
1738}
1739
1740#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1748#[non_exhaustive]
1749pub enum DeliveryAudience {
1750 #[default]
1752 All,
1753 Owners(Vec<HandlerId>),
1755 AllExcept(Vec<HandlerId>),
1760}
1761
1762#[derive(
1770 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
1771)]
1772#[non_exhaustive]
1773pub enum DeliveryScope {
1774 #[default]
1776 Canonical,
1777 CanonicalProgress,
1779 OwnerCatchup,
1781 Preconfirmed,
1784}
1785
1786impl DeliveryScope {
1787 const fn advances_canonical_state(self) -> bool {
1788 matches!(self, Self::Canonical | Self::CanonicalProgress)
1789 }
1790}
1791
1792#[derive(Clone, Debug)]
1794pub struct ReactiveInputDelivery<N: Network = Ethereum> {
1795 record: ReactiveInputRecord<N>,
1796 audience: DeliveryAudience,
1797 scope: DeliveryScope,
1798}
1799
1800impl<N: Network> ReactiveInputDelivery<N> {
1801 pub fn new(
1803 record: ReactiveInputRecord<N>,
1804 audience: DeliveryAudience,
1805 scope: DeliveryScope,
1806 ) -> Self {
1807 Self {
1808 record,
1809 audience,
1810 scope,
1811 }
1812 }
1813
1814 pub const fn record(&self) -> &ReactiveInputRecord<N> {
1816 &self.record
1817 }
1818
1819 pub const fn audience(&self) -> &DeliveryAudience {
1821 &self.audience
1822 }
1823
1824 pub const fn scope(&self) -> DeliveryScope {
1826 self.scope
1827 }
1828
1829 pub fn into_parts(self) -> (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope) {
1831 (self.record, self.audience, self.scope)
1832 }
1833}
1834
1835#[derive(Clone, Debug)]
1840#[non_exhaustive]
1841pub struct ReactiveInputBatchParts<N: Network = Ethereum> {
1842 pub chain_id: Option<u64>,
1844 pub deliveries: Vec<ReactiveInputDelivery<N>>,
1846 pub delivery_token: Option<SubscriberDeliveryToken>,
1848 pub subscriber_checkpoint: Option<SubscriberCheckpoint>,
1850 pub payload_commitment: Option<SubscriberPayloadCommitment>,
1852 pub chain_controls: Vec<ChainControl>,
1854 pub preconfirmation_timing: Option<FlashblockIngressTiming>,
1856}
1857
1858#[derive(Clone, Debug)]
1860pub struct ReactiveInputBatch<N: Network = Ethereum> {
1861 records: Vec<ReactiveInputRecord<N>>,
1862 chain_id: Option<u64>,
1863 delivery_token: Option<SubscriberDeliveryToken>,
1864 subscriber_checkpoint: Option<SubscriberCheckpoint>,
1865 payload_commitment: Option<SubscriberPayloadCommitment>,
1866 audience: DeliveryAudience,
1867 record_audiences: Option<Vec<DeliveryAudience>>,
1868 delivery_scope: DeliveryScope,
1869 record_delivery_scopes: Option<Vec<DeliveryScope>>,
1870 chain_controls: Vec<ChainControl>,
1871 preconfirmation_timing: Option<FlashblockIngressTiming>,
1872}
1873
1874type RuntimeInputDelivery<N> = (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope);
1875
1876impl<N: Network> ReactiveInputBatch<N> {
1877 pub fn new(records: Vec<ReactiveInputRecord<N>>) -> Self {
1879 let chain_id = common_record_chain_id(&records);
1880 Self {
1881 records,
1882 chain_id,
1883 delivery_token: None,
1884 subscriber_checkpoint: None,
1885 payload_commitment: None,
1886 audience: DeliveryAudience::All,
1887 record_audiences: None,
1888 delivery_scope: DeliveryScope::Canonical,
1889 record_delivery_scopes: None,
1890 chain_controls: Vec::new(),
1891 preconfirmation_timing: None,
1892 }
1893 }
1894
1895 pub fn with_chain_id(mut self, chain_id: u64) -> Self {
1898 self.chain_id = Some(chain_id);
1899 self
1900 }
1901
1902 pub const fn chain_id(&self) -> Option<u64> {
1905 self.chain_id
1906 }
1907
1908 pub fn with_delivery_token(mut self, token: SubscriberDeliveryToken) -> Self {
1910 self.delivery_token = Some(token);
1911 self
1912 }
1913
1914 pub fn delivery_token(&self) -> Option<&SubscriberDeliveryToken> {
1916 self.delivery_token.as_ref()
1917 }
1918
1919 pub fn with_subscriber_checkpoint(mut self, checkpoint: SubscriberCheckpoint) -> Self {
1921 self.subscriber_checkpoint = Some(checkpoint);
1922 self
1923 }
1924
1925 pub fn subscriber_checkpoint(&self) -> Option<&SubscriberCheckpoint> {
1927 self.subscriber_checkpoint.as_ref()
1928 }
1929
1930 pub fn with_payload_commitment(mut self, commitment: SubscriberPayloadCommitment) -> Self {
1933 self.payload_commitment = Some(commitment);
1934 self
1935 }
1936
1937 pub const fn payload_commitment(&self) -> Option<&SubscriberPayloadCommitment> {
1939 self.payload_commitment.as_ref()
1940 }
1941
1942 pub fn with_audience(mut self, audience: DeliveryAudience) -> Self {
1944 self.audience = audience;
1945 self.record_audiences = None;
1946 self
1947 }
1948
1949 pub const fn audience(&self) -> &DeliveryAudience {
1951 &self.audience
1952 }
1953
1954 pub fn from_scoped_records(
1956 records: impl IntoIterator<Item = (ReactiveInputRecord<N>, DeliveryAudience)>,
1957 ) -> Self {
1958 let (records, record_audiences): (Vec<_>, Vec<_>) = records.into_iter().unzip();
1959 let chain_id = common_record_chain_id(&records);
1960 Self {
1961 records,
1962 chain_id,
1963 delivery_token: None,
1964 subscriber_checkpoint: None,
1965 payload_commitment: None,
1966 audience: DeliveryAudience::All,
1967 record_audiences: Some(record_audiences),
1968 delivery_scope: DeliveryScope::Canonical,
1969 record_delivery_scopes: None,
1970 chain_controls: Vec::new(),
1971 preconfirmation_timing: None,
1972 }
1973 }
1974
1975 pub fn from_deliveries(deliveries: impl IntoIterator<Item = ReactiveInputDelivery<N>>) -> Self {
1977 Self::from_scoped_records_with_delivery_scope(
1978 deliveries
1979 .into_iter()
1980 .map(ReactiveInputDelivery::into_parts),
1981 )
1982 }
1983
1984 pub fn record_audience(&self, index: usize) -> Option<&DeliveryAudience> {
1986 if index >= self.records.len() {
1987 return None;
1988 }
1989 Some(
1990 self.record_audiences
1991 .as_ref()
1992 .and_then(|audiences| audiences.get(index))
1993 .unwrap_or(&self.audience),
1994 )
1995 }
1996
1997 pub fn with_delivery_scope(mut self, scope: DeliveryScope) -> Self {
1999 self.delivery_scope = scope;
2000 self.record_delivery_scopes = None;
2001 self
2002 }
2003
2004 pub fn record_delivery_scope(&self, index: usize) -> Option<DeliveryScope> {
2006 if index >= self.records.len() {
2007 return None;
2008 }
2009 Some(
2010 self.record_delivery_scopes
2011 .as_ref()
2012 .and_then(|scopes| scopes.get(index))
2013 .copied()
2014 .unwrap_or(self.delivery_scope),
2015 )
2016 }
2017
2018 fn from_scoped_records_with_delivery_scope(
2019 records: impl IntoIterator<Item = (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
2020 ) -> Self {
2021 let mut input_records = Vec::new();
2022 let mut audiences = Vec::new();
2023 let mut scopes = Vec::new();
2024 for (record, audience, scope) in records {
2025 input_records.push(record);
2026 audiences.push(audience);
2027 scopes.push(scope);
2028 }
2029 let chain_id = common_record_chain_id(&input_records);
2030 Self {
2031 records: input_records,
2032 chain_id,
2033 delivery_token: None,
2034 subscriber_checkpoint: None,
2035 payload_commitment: None,
2036 audience: DeliveryAudience::All,
2037 record_audiences: Some(audiences),
2038 delivery_scope: DeliveryScope::Canonical,
2039 record_delivery_scopes: Some(scopes),
2040 chain_controls: Vec::new(),
2041 preconfirmation_timing: None,
2042 }
2043 }
2044
2045 pub fn with_preconfirmation_timing(mut self, timing: FlashblockIngressTiming) -> Self {
2047 self.preconfirmation_timing = Some(timing);
2048 self
2049 }
2050
2051 pub const fn preconfirmation_timing(&self) -> Option<FlashblockIngressTiming> {
2053 self.preconfirmation_timing
2054 }
2055
2056 pub fn with_chain_controls(mut self, controls: impl IntoIterator<Item = ChainControl>) -> Self {
2063 self.chain_controls = controls.into_iter().collect();
2064 self
2065 }
2066
2067 pub fn chain_controls(&self) -> &[ChainControl] {
2069 &self.chain_controls
2070 }
2071
2072 pub fn records(&self) -> &[ReactiveInputRecord<N>] {
2074 &self.records
2075 }
2076
2077 pub fn into_records(self) -> Vec<ReactiveInputRecord<N>> {
2084 self.records
2085 }
2086
2087 pub fn into_parts(self) -> ReactiveInputBatchParts<N> {
2089 let chain_id = self.chain_id;
2090 let delivery_token = self.delivery_token;
2091 let subscriber_checkpoint = self.subscriber_checkpoint;
2092 let payload_commitment = self.payload_commitment;
2093 let chain_controls = self.chain_controls;
2094 let preconfirmation_timing = self.preconfirmation_timing;
2095 let audiences = self
2096 .record_audiences
2097 .unwrap_or_else(|| vec![self.audience; self.records.len()]);
2098 let scopes = self
2099 .record_delivery_scopes
2100 .unwrap_or_else(|| vec![self.delivery_scope; self.records.len()]);
2101 let deliveries = self
2102 .records
2103 .into_iter()
2104 .zip(audiences)
2105 .zip(scopes)
2106 .map(|((record, audience), scope)| ReactiveInputDelivery::new(record, audience, scope))
2107 .collect();
2108 ReactiveInputBatchParts {
2109 chain_id,
2110 deliveries,
2111 delivery_token,
2112 subscriber_checkpoint,
2113 payload_commitment,
2114 chain_controls,
2115 preconfirmation_timing,
2116 }
2117 }
2118
2119 fn into_runtime_parts(self) -> (Vec<RuntimeInputDelivery<N>>, Vec<ChainControl>, Option<u64>) {
2120 let audiences = self
2121 .record_audiences
2122 .unwrap_or_else(|| vec![self.audience; self.records.len()]);
2123 let scopes = self
2124 .record_delivery_scopes
2125 .unwrap_or_else(|| vec![self.delivery_scope; self.records.len()]);
2126 let records = self
2127 .records
2128 .into_iter()
2129 .zip(audiences)
2130 .zip(scopes)
2131 .map(|((record, audience), scope)| (record, audience, scope))
2132 .collect();
2133 (records, self.chain_controls, self.chain_id)
2134 }
2135
2136 fn take_delivery_token(&mut self) -> Option<SubscriberDeliveryToken> {
2137 self.delivery_token.take()
2138 }
2139
2140 fn take_subscriber_checkpoint(&mut self) -> Option<SubscriberCheckpoint> {
2141 self.subscriber_checkpoint.take()
2142 }
2143}
2144
2145fn common_record_chain_id<N: Network>(records: &[ReactiveInputRecord<N>]) -> Option<u64> {
2146 let chain_id = records.first()?.context.chain_id?;
2147 records
2148 .iter()
2149 .all(|record| record.context.chain_id == Some(chain_id))
2150 .then_some(chain_id)
2151}
2152
2153pub trait ReactiveHandler<N: Network = Ethereum>: Send + Sync {
2155 fn id(&self) -> HandlerId;
2157
2158 fn interests(&self) -> Vec<ReactiveInterest<N>>;
2160
2161 fn log_route_index(&self) -> Option<LogRouteIndex> {
2168 None
2169 }
2170
2171 fn handle(
2173 &self,
2174 ctx: &ReactiveContext,
2175 input: &ReactiveInput<N>,
2176 state: &dyn StateView,
2177 ) -> Result<HandlerOutcome, HandlerError>;
2178}
2179
2180pub trait ReactiveHook<N: Network = Ethereum>: Send + Sync {
2190 fn on_report(&self, report: Arc<ReactiveReport<N>>);
2192}
2193
2194#[allow(clippy::large_enum_variant)]
2196#[derive(Clone)]
2197pub enum ReactiveInterest<N: Network = Ethereum> {
2198 Logs(LogInterest),
2200 Blocks(BlockInterest),
2202 PendingTransactions(PendingTxInterest<N>),
2204}
2205
2206impl<N: Network> fmt::Debug for ReactiveInterest<N> {
2207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2208 match self {
2209 Self::Logs(interest) => f.debug_tuple("Logs").field(interest).finish(),
2210 Self::Blocks(interest) => f.debug_tuple("Blocks").field(interest).finish(),
2211 Self::PendingTransactions(interest) => f
2212 .debug_tuple("PendingTransactions")
2213 .field(interest)
2214 .finish(),
2215 }
2216 }
2217}
2218
2219#[derive(Clone)]
2221pub struct LogInterest {
2222 pub provider_filter: Filter,
2224 pub local_matcher: Option<Arc<dyn LogMatcher>>,
2226 pub route_key: Option<RouteKeySpec>,
2228}
2229
2230impl LogInterest {
2231 pub fn matches(&self, log: &Log) -> bool {
2233 self.provider_filter.rpc_matches(log)
2234 && self
2235 .local_matcher
2236 .as_ref()
2237 .is_none_or(|matcher| matcher.matches(log))
2238 }
2239
2240 pub fn route_key(&self, log: &Log) -> Option<RouteKey> {
2242 self.route_key.as_ref().and_then(|spec| spec.extract(log))
2243 }
2244}
2245
2246impl fmt::Debug for LogInterest {
2247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2248 f.debug_struct("LogInterest")
2249 .field("provider_filter", &self.provider_filter)
2250 .field(
2251 "local_matcher",
2252 &self.local_matcher.as_ref().map(|_| "<matcher>"),
2253 )
2254 .field("route_key", &self.route_key)
2255 .finish()
2256 }
2257}
2258
2259pub trait LogMatcher: Send + Sync {
2261 fn matches(&self, log: &Log) -> bool;
2263}
2264
2265#[derive(Clone)]
2267pub enum RouteKeySpec {
2268 EmitterAddress,
2270 Topic {
2272 index: usize,
2274 },
2275 DataSlice {
2277 offset: usize,
2279 len: usize,
2281 },
2282 Custom(Arc<dyn RouteKeyExtractor>),
2284}
2285
2286impl RouteKeySpec {
2287 pub fn extract(&self, log: &Log) -> Option<RouteKey> {
2289 match self {
2290 Self::EmitterAddress => Some(RouteKey::Address(log.address())),
2291 Self::Topic { index } => log.topics().get(*index).copied().map(RouteKey::Bytes32),
2292 Self::DataSlice { offset, len } => {
2293 let data = log.inner.data.data.as_ref();
2294 let end = offset.checked_add(*len)?;
2295 data.get(*offset..end)
2296 .map(|bytes| RouteKey::Bytes(bytes.to_vec()))
2297 }
2298 Self::Custom(extractor) => extractor.extract(log),
2299 }
2300 }
2301}
2302
2303impl fmt::Debug for RouteKeySpec {
2304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2305 match self {
2306 Self::EmitterAddress => f.write_str("EmitterAddress"),
2307 Self::Topic { index } => f.debug_struct("Topic").field("index", index).finish(),
2308 Self::DataSlice { offset, len } => f
2309 .debug_struct("DataSlice")
2310 .field("offset", offset)
2311 .field("len", len)
2312 .finish(),
2313 Self::Custom(_) => f.write_str("Custom(<extractor>)"),
2314 }
2315 }
2316}
2317
2318pub trait RouteKeyExtractor: Send + Sync {
2320 fn extract(&self, log: &Log) -> Option<RouteKey>;
2322}
2323
2324#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2326pub enum RouteKey {
2327 Address(Address),
2329 Bytes32(B256),
2331 Bytes(Vec<u8>),
2333}
2334
2335#[non_exhaustive]
2337#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2338pub enum LogRouteKey {
2339 Emitter(Address),
2341 Topic {
2343 index: usize,
2345 value: B256,
2347 },
2348 DataSlice {
2350 offset: usize,
2352 value: Vec<u8>,
2354 },
2355}
2356
2357#[derive(Clone, Debug, PartialEq, Eq)]
2359pub struct LogRouteIndex {
2360 keys: Vec<LogRouteKey>,
2361}
2362
2363impl LogRouteIndex {
2364 pub fn new(primary: LogRouteKey, additional: impl IntoIterator<Item = LogRouteKey>) -> Self {
2366 let mut keys = vec![primary];
2367 for key in additional {
2368 if !keys.contains(&key) {
2369 keys.push(key);
2370 }
2371 }
2372 Self { keys }
2373 }
2374
2375 pub fn single(key: LogRouteKey) -> Self {
2377 Self { keys: vec![key] }
2378 }
2379
2380 pub fn keys(&self) -> &[LogRouteKey] {
2382 &self.keys
2383 }
2384}
2385
2386#[derive(Clone, Debug, PartialEq, Eq)]
2388pub struct ReactiveLogRoute {
2389 pub handler_id: HandlerId,
2391 pub route_key: Option<RouteKey>,
2393}
2394
2395#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2397pub struct BlockInterest {
2398 pub mode: BlockInterestMode,
2400}
2401
2402impl Default for BlockInterest {
2403 fn default() -> Self {
2404 Self {
2405 mode: BlockInterestMode::Header,
2406 }
2407 }
2408}
2409
2410#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2412pub enum BlockInterestMode {
2413 Header,
2415 FullBlock,
2417}
2418
2419#[derive(Clone)]
2421pub struct PendingTxInterest<N: Network = Ethereum> {
2422 pub full_transactions: bool,
2424 pub from: AddressMatcher,
2426 pub to: AddressMatcher,
2428 pub selectors: SelectorMatcher,
2430 pub local_matcher: Option<Arc<dyn PendingTxMatcher<N>>>,
2432}
2433
2434impl<N: Network> Default for PendingTxInterest<N> {
2435 fn default() -> Self {
2436 Self {
2437 full_transactions: false,
2438 from: AddressMatcher::Any,
2439 to: AddressMatcher::Any,
2440 selectors: SelectorMatcher::Any,
2441 local_matcher: None,
2442 }
2443 }
2444}
2445
2446impl<N: Network> PendingTxInterest<N> {
2447 fn matches_hash_only(&self) -> bool {
2448 !self.full_transactions
2449 && self.from.is_any()
2450 && self.to.is_any()
2451 && self.selectors.is_any()
2452 && self.local_matcher.is_none()
2453 }
2454
2455 fn matches_tx(&self, tx: &N::TransactionResponse) -> bool {
2456 self.from.matches(tx.from())
2457 && self.to.matches_option(tx.to())
2458 && self.selectors.matches(tx.input())
2459 && self
2460 .local_matcher
2461 .as_ref()
2462 .is_none_or(|matcher| matcher.matches(tx))
2463 }
2464}
2465
2466impl<N: Network> fmt::Debug for PendingTxInterest<N> {
2467 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2468 f.debug_struct("PendingTxInterest")
2469 .field("full_transactions", &self.full_transactions)
2470 .field("from", &self.from)
2471 .field("to", &self.to)
2472 .field("selectors", &self.selectors)
2473 .field(
2474 "local_matcher",
2475 &self.local_matcher.as_ref().map(|_| "<matcher>"),
2476 )
2477 .finish()
2478 }
2479}
2480
2481#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2483pub enum AddressMatcher {
2484 Any,
2486 Exact(Address),
2488 AnyOf(Vec<Address>),
2490}
2491
2492impl AddressMatcher {
2493 pub fn is_any(&self) -> bool {
2495 matches!(self, Self::Any)
2496 }
2497
2498 pub fn matches(&self, address: Address) -> bool {
2500 match self {
2501 Self::Any => true,
2502 Self::Exact(expected) => *expected == address,
2503 Self::AnyOf(addresses) => addresses.contains(&address),
2504 }
2505 }
2506
2507 pub fn matches_option(&self, address: Option<Address>) -> bool {
2509 match (self, address) {
2510 (Self::Any, _) => true,
2511 (_, Some(address)) => self.matches(address),
2512 _ => false,
2513 }
2514 }
2515}
2516
2517#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2519pub enum SelectorMatcher {
2520 Any,
2522 AnyOf(Vec<[u8; 4]>),
2524}
2525
2526impl SelectorMatcher {
2527 pub fn is_any(&self) -> bool {
2529 matches!(self, Self::Any)
2530 }
2531
2532 pub fn matches(&self, input: &Bytes) -> bool {
2534 match self {
2535 Self::Any => true,
2536 Self::AnyOf(selectors) => input
2537 .get(..4)
2538 .and_then(|bytes| bytes.try_into().ok())
2539 .is_some_and(|selector| selectors.contains(&selector)),
2540 }
2541 }
2542}
2543
2544pub trait PendingTxMatcher<N: Network = Ethereum>: Send + Sync {
2546 fn matches(&self, tx: &N::TransactionResponse) -> bool;
2548}
2549
2550#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
2568#[non_exhaustive]
2569pub enum TrackingPolicy {
2570 Slots {
2575 slots: Vec<U256>,
2577 },
2578 WholeAccount,
2583 Scalars,
2588}
2589
2590#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2603pub enum RootGateCadence {
2604 EveryNBlocks(NonZeroU64),
2608 Disabled,
2610}
2611
2612impl RootGateCadence {
2613 #[must_use]
2624 pub fn every_n_blocks(n: u64) -> Self {
2625 Self::EveryNBlocks(NonZeroU64::new(n.max(1)).expect("clamped to at least 1"))
2626 }
2627}
2628
2629impl Default for RootGateCadence {
2630 fn default() -> Self {
2634 Self::every_n_blocks(16)
2635 }
2636}
2637
2638#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
2645struct TrackedRoot {
2646 last_root: B256,
2647 last_block: u64,
2648 balance: U256,
2649 nonce: u64,
2650 code_hash: B256,
2651}
2652
2653#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2655pub struct ResyncRequest {
2656 pub id: ResyncId,
2658 pub reason: ResyncReason,
2660 pub block: ResyncBlock,
2662 pub targets: Vec<ResyncTarget>,
2664 pub priority: ResyncPriority,
2666}
2667
2668#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2670pub struct ResyncId(String);
2671
2672impl ResyncId {
2673 pub fn new(id: impl Into<String>) -> Self {
2675 Self(id.into())
2676 }
2677}
2678
2679#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2681#[non_exhaustive]
2682pub enum ResyncReason {
2683 HandlerRequested,
2685 SkippedStateEffect,
2687 MissedBlockRange,
2694 RootMoved,
2704 Custom(String),
2706}
2707
2708#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2710pub enum ResyncBlock {
2711 Latest,
2713 Pending,
2715 Safe,
2717 Finalized,
2719 Number(u64),
2721 Hash {
2723 number: u64,
2725 hash: B256,
2727 require_canonical: bool,
2729 },
2730}
2731
2732#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2734pub enum ResyncTarget {
2735 StorageSlot {
2737 address: Address,
2739 slot: U256,
2741 },
2742 StorageSlots {
2744 address: Address,
2746 slots: Vec<U256>,
2748 },
2749 Account {
2751 address: Address,
2753 fields: AccountFieldMask,
2755 },
2756}
2757
2758#[derive(
2760 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
2761)]
2762pub struct AccountFieldMask {
2763 pub balance: bool,
2765 pub nonce: bool,
2767 pub code: bool,
2769}
2770
2771#[derive(
2773 Clone,
2774 Copy,
2775 Debug,
2776 Default,
2777 PartialEq,
2778 Eq,
2779 Hash,
2780 PartialOrd,
2781 Ord,
2782 serde::Serialize,
2783 serde::Deserialize,
2784)]
2785pub enum ResyncPriority {
2786 Low,
2788 #[default]
2790 Normal,
2791 High,
2793}
2794
2795#[derive(Clone, Debug, PartialEq, Eq)]
2797pub struct InvalidationRequest {
2798 pub scope: PurgeScope,
2800 pub address: Address,
2802 pub reason: InvalidationReason,
2804}
2805
2806#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2808pub enum InvalidationReason {
2809 HandlerRequested,
2811 Reorg,
2813 Custom(String),
2815}
2816
2817#[derive(Clone, Debug, PartialEq, Eq)]
2819pub struct SpeculativeRequest {
2820 pub id: SpeculativeId,
2822 pub input_ref: InputRef,
2824 pub labels: Vec<ReportTag>,
2826}
2827
2828#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2830pub struct SpeculativeId(String);
2831
2832impl SpeculativeId {
2833 pub fn new(id: impl Into<String>) -> Self {
2835 Self(id.into())
2836 }
2837}
2838
2839#[derive(Clone, Debug, PartialEq, Eq)]
2841pub struct ReactiveConfig {
2842 pub hook_backpressure: HookBackpressure,
2847 pub journal_depth: usize,
2864}
2865
2866impl Default for ReactiveConfig {
2867 fn default() -> Self {
2868 Self {
2869 hook_backpressure: HookBackpressure::Block,
2870 journal_depth: 64,
2871 }
2872 }
2873}
2874
2875#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2877pub enum HookBackpressure {
2878 Block,
2880 DropNewest,
2882 DropOldest,
2884 Error,
2886}
2887
2888#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2896#[non_exhaustive]
2897pub enum CacheHealth {
2898 #[default]
2900 Healthy,
2901 Degraded {
2905 since_block: u64,
2907 },
2908 Unhealthy {
2911 since_block: u64,
2913 },
2914}
2915
2916#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2923#[non_exhaustive]
2924pub struct CacheMetricsSnapshot {
2925 pub deep_reorgs: u64,
2928 pub reorgs_recovered: u64,
2930 pub resync_requests: u64,
2932 pub resync_failures: u64,
2934 pub missed_ranges: u64,
2936 pub coverage_gaps: u64,
2938 pub pending_contamination: u64,
2940 pub stale_verdicts: u64,
2942}
2943
2944#[derive(Debug, Default)]
2950struct CacheMetrics {
2951 deep_reorgs: AtomicU64,
2952 reorgs_recovered: AtomicU64,
2953 resync_requests: AtomicU64,
2954 resync_failures: AtomicU64,
2955 missed_ranges: AtomicU64,
2956 coverage_gaps: AtomicU64,
2957 pending_contamination: AtomicU64,
2958 stale_verdicts: AtomicU64,
2959}
2960
2961impl CacheMetrics {
2962 fn snapshot(&self) -> CacheMetricsSnapshot {
2963 CacheMetricsSnapshot {
2964 deep_reorgs: self.deep_reorgs.load(Ordering::Relaxed),
2965 reorgs_recovered: self.reorgs_recovered.load(Ordering::Relaxed),
2966 resync_requests: self.resync_requests.load(Ordering::Relaxed),
2967 resync_failures: self.resync_failures.load(Ordering::Relaxed),
2968 missed_ranges: self.missed_ranges.load(Ordering::Relaxed),
2969 coverage_gaps: self.coverage_gaps.load(Ordering::Relaxed),
2970 pending_contamination: self.pending_contamination.load(Ordering::Relaxed),
2971 stale_verdicts: self.stale_verdicts.load(Ordering::Relaxed),
2972 }
2973 }
2974
2975 fn restore(&self, snapshot: CacheMetricsSnapshot) {
2976 self.deep_reorgs
2977 .store(snapshot.deep_reorgs, Ordering::Relaxed);
2978 self.reorgs_recovered
2979 .store(snapshot.reorgs_recovered, Ordering::Relaxed);
2980 self.resync_requests
2981 .store(snapshot.resync_requests, Ordering::Relaxed);
2982 self.resync_failures
2983 .store(snapshot.resync_failures, Ordering::Relaxed);
2984 self.missed_ranges
2985 .store(snapshot.missed_ranges, Ordering::Relaxed);
2986 self.coverage_gaps
2987 .store(snapshot.coverage_gaps, Ordering::Relaxed);
2988 self.pending_contamination
2989 .store(snapshot.pending_contamination, Ordering::Relaxed);
2990 self.stale_verdicts
2991 .store(snapshot.stale_verdicts, Ordering::Relaxed);
2992 }
2993}
2994
2995#[derive(Clone, Debug)]
2997#[non_exhaustive]
2998pub enum ReactiveReport<N: Network = Ethereum> {
2999 Input(InputReport<N>),
3001 Decoded(DecodedReport<N>),
3003 Applied(AppliedReport<N>),
3005 Resynced(ResyncReport),
3007 BlockCommitted(BlockReport<N>),
3009 Reorg(ReorgReport<N>),
3011 ChainControl(ChainControlReport),
3013 MissedBlockRange(MissedRangeReport<N>),
3016 Health(HealthReport<N>),
3018 CoverageGap(CoverageGapReport<N>),
3021 Error(ReactiveErrorReport<N>),
3023}
3024
3025#[derive(Clone, Debug, PartialEq, Eq)]
3027pub struct ChainControlReport {
3028 pub control: ChainControl,
3030}
3031
3032#[derive(Clone, Debug)]
3034pub struct InputReport<N: Network = Ethereum> {
3035 pub input_ref: InputRef,
3037 pub context: ReactiveContext,
3039 pub provider: Option<ProviderRef>,
3041 pub _network: PhantomData<N>,
3043}
3044
3045#[derive(Clone, Debug)]
3047pub struct DecodedReport<N: Network = Ethereum> {
3048 pub input_ref: InputRef,
3050 pub handler_ids: Vec<HandlerId>,
3052 pub _network: PhantomData<N>,
3054}
3055
3056#[derive(Clone, Debug)]
3058pub struct AppliedReport<N: Network = Ethereum> {
3059 pub input_ref: InputRef,
3061 pub handler_id: HandlerId,
3063 pub quality: StateEffectQuality,
3065 pub tags: Vec<ReportTag>,
3067 pub diff: StateDiff,
3069 pub state_updates: Vec<StateUpdate>,
3071 pub invalidations: Vec<InvalidationRequest>,
3073 pub resyncs: Vec<ResyncRequest>,
3075 pub speculative: Vec<SpeculativeRequest>,
3077 pub hook_signals: Vec<HookSignal>,
3079 pub _network: PhantomData<N>,
3081}
3082
3083#[derive(Clone, Debug, Default, PartialEq, Eq)]
3087pub struct ResyncReport {
3088 pub requested: Vec<ResyncRequest>,
3090 pub state_updates: Vec<StateUpdate>,
3092 pub diff: StateDiff,
3094 pub failed: Vec<ResyncFailure>,
3096}
3097
3098#[derive(Clone, Debug, PartialEq, Eq)]
3100pub struct ResyncFailure {
3101 pub request_id: ResyncId,
3103 pub block: ResyncBlock,
3105 pub target: ResyncTarget,
3107 pub kind: ResyncFailureKind,
3109 pub message: String,
3111}
3112
3113#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
3115#[non_exhaustive]
3116pub enum ResyncFailureKind {
3117 MissingStorageFetcher,
3119 StorageFetchFailed,
3121 StorageFetchOmitted,
3123 MissingAccountFetcher,
3125 AccountFetchFailed,
3127 AccountFetchOmitted,
3129}
3130
3131#[derive(Clone, Debug)]
3133pub struct BlockReport<N: Network = Ethereum> {
3134 pub block: Option<BlockRef>,
3136 pub inputs: Vec<InputRef>,
3138 pub _network: PhantomData<N>,
3140}
3141
3142#[derive(Clone, Debug)]
3158pub struct ReorgReport<N: Network = Ethereum> {
3159 pub dropped: Option<BlockRef>,
3161 pub dropped_blocks: Vec<BlockRef>,
3163 pub dropped_inputs: Vec<InputRef>,
3165 pub rollback_updates: Vec<StateUpdate>,
3167 pub rollback_diff: StateDiff,
3169 pub purge_updates: Vec<StateUpdate>,
3171 pub purge_diff: StateDiff,
3173 pub canceled_resyncs: Vec<ResyncRequest>,
3175 pub reason: ReorgReason,
3177 pub _network: PhantomData<N>,
3179}
3180
3181#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
3183pub enum ReorgReason {
3184 RemovedLog,
3186 ReorgedInput,
3188 ParentMismatch,
3190 Explicit,
3192}
3193
3194#[derive(Clone, Debug)]
3202pub struct MissedRangeReport<N: Network = Ethereum> {
3203 pub from: u64,
3205 pub to: u64,
3207 pub block: u64,
3209 pub _network: PhantomData<N>,
3211}
3212
3213#[derive(Clone, Debug)]
3216pub struct HealthReport<N: Network = Ethereum> {
3217 pub from: CacheHealth,
3219 pub to: CacheHealth,
3221 pub block: Option<u64>,
3223 pub _network: PhantomData<N>,
3225}
3226
3227#[derive(Clone, Debug)]
3240pub struct CoverageGapReport<N: Network = Ethereum> {
3241 pub address: Address,
3243 pub block: u64,
3245 pub _network: PhantomData<N>,
3247}
3248
3249#[derive(Clone, Debug)]
3252pub struct ReactiveErrorReport<N: Network = Ethereum> {
3253 pub input_ref: Option<InputRef>,
3255 pub message: String,
3257 pub _network: PhantomData<N>,
3259}
3260
3261#[derive(Clone, Debug)]
3264pub struct ReactiveBatchReport<N: Network = Ethereum> {
3265 pub applied: Vec<AppliedReport<N>>,
3267 pub resyncs: Vec<ResyncRequest>,
3269 pub speculative: Vec<SpeculativeRequest>,
3271 pub reports: Vec<Arc<ReactiveReport<N>>>,
3273}
3274
3275impl<N: Network> Default for ReactiveBatchReport<N> {
3276 fn default() -> Self {
3277 Self {
3278 applied: Vec::new(),
3279 resyncs: Vec::new(),
3280 speculative: Vec::new(),
3281 reports: Vec::new(),
3282 }
3283 }
3284}
3285
3286#[derive(Clone, Debug, PartialEq, Eq)]
3288pub struct HandlerError {
3289 message: String,
3290}
3291
3292impl HandlerError {
3293 pub fn new(message: impl Into<String>) -> Self {
3295 Self {
3296 message: message.into(),
3297 }
3298 }
3299}
3300
3301impl fmt::Display for HandlerError {
3302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3303 self.message.fmt(f)
3304 }
3305}
3306
3307impl std::error::Error for HandlerError {}
3308
3309impl From<String> for HandlerError {
3310 fn from(message: String) -> Self {
3311 Self::new(message)
3312 }
3313}
3314
3315impl From<&str> for HandlerError {
3316 fn from(message: &str) -> Self {
3317 Self::new(message)
3318 }
3319}
3320
3321#[derive(Debug, thiserror::Error)]
3323#[non_exhaustive]
3324pub enum ReactiveError {
3325 #[error("handler `{handler_id}` failed: {source}")]
3327 HandlerFailed {
3328 handler_id: HandlerId,
3330 source: HandlerError,
3332 },
3333 #[error(
3335 "conflicting effects for input {input_ref:?} on target {target:?}: `{first}` vs `{second}`"
3336 )]
3337 ConflictingEffects {
3338 input_ref: Box<InputRef>,
3340 target: Box<EffectTarget>,
3342 first: HandlerId,
3344 second: HandlerId,
3346 },
3347 #[error(
3349 "pending input {input_ref:?} emitted invalid canonical effect `{effect_kind}` from `{handler_id}`"
3350 )]
3351 InvalidPendingEffect {
3352 input_ref: Box<InputRef>,
3354 handler_id: HandlerId,
3356 effect_kind: &'static str,
3358 },
3359 #[error("invalid reactive input record: {message}")]
3362 InvalidInputRecord {
3363 message: String,
3365 },
3366 #[error("invalid chain control: {message}")]
3368 InvalidChainControl {
3369 message: String,
3371 },
3372 #[error(
3375 "owner catch-up block {number} {hash} is outside the retained canonical rollback journal"
3376 )]
3377 OwnerCatchupOutsideJournal {
3378 number: u64,
3380 hash: B256,
3382 },
3383 #[error(transparent)]
3385 Register(#[from] RegisterError),
3386}
3387
3388#[derive(Debug, thiserror::Error)]
3390#[non_exhaustive]
3391pub enum RegisterError {
3392 #[error("handler id `{0}` is already registered")]
3394 DuplicateHandler(HandlerId),
3395}
3396
3397#[derive(Debug, thiserror::Error)]
3400#[non_exhaustive]
3401pub enum ReactiveEngineRegisterError {
3402 #[error(transparent)]
3404 Register(#[from] RegisterError),
3405 #[error(transparent)]
3407 Subscriber(#[from] SubscriberError),
3408 #[error(
3411 "owner backfill {start_block}..={end_block:?} must target exactly one hash-certified block in the retained rollback journal"
3412 )]
3413 BackfillOutsideJournal {
3414 start_block: u64,
3416 end_block: Option<u64>,
3418 retained_anchor: Option<BlockRef>,
3420 },
3421}
3422
3423#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
3426#[non_exhaustive]
3427pub enum ReactiveBaselineError {
3428 #[error("cannot adopt a canonical baseline after reactive processing has started")]
3430 ActiveRuntime,
3431 #[error(
3434 "canonical baseline conflicts with existing block {existing_number} {existing_hash} (requested {requested_number} {requested_hash})"
3435 )]
3436 ConflictingBaseline {
3437 existing_number: u64,
3439 existing_hash: B256,
3441 requested_number: u64,
3443 requested_hash: B256,
3445 },
3446 #[error("baseline chain id {baseline_chain_id} does not match cache chain id {cache_chain_id}")]
3448 CacheChainMismatch {
3449 baseline_chain_id: u64,
3451 cache_chain_id: u64,
3453 },
3454 #[error("cache block selector is not canonically hash-pinned to baseline {number} {hash}")]
3456 CacheBlockMismatch {
3457 number: u64,
3459 hash: B256,
3461 },
3462}
3463
3464#[derive(Debug, thiserror::Error)]
3467#[non_exhaustive]
3468pub enum ReactiveEngineError {
3469 #[error(transparent)]
3471 Subscriber(#[from] SubscriberError),
3472 #[error(transparent)]
3474 Runtime(ReactiveError),
3475 #[error(transparent)]
3477 Baseline(#[from] ReactiveBaselineError),
3478 #[error("runtime ingestion succeeded but subscriber acknowledgement failed: {0}")]
3481 Acknowledgement(#[source] SubscriberError),
3482 #[error("runtime ingestion succeeded but durable checkpoint commit failed: {0}")]
3486 Checkpoint(#[source] DurableCheckpointError),
3487 #[error("cannot durably checkpoint reactive state before observing a canonical block")]
3489 MissingCheckpointBlock,
3490 #[error("pre-confirmed Flashblock batches cannot be durably checkpointed")]
3493 PreconfirmationNotCheckpointable,
3494 #[error("failed to encode durable reactive runtime state: {0}")]
3496 RuntimeCheckpoint(String),
3497 #[error("cannot use ordinary ingestion while a durable checkpoint commit is pending")]
3500 PendingCheckpointCommit,
3501 #[error("cannot use checkpointed ingestion while an ordinary acknowledgement is pending")]
3504 PendingAcknowledgementCommit,
3505 #[error(
3509 "raw engine ingestion cannot consume delivery tokens or subscriber checkpoints; use a combined next_ingest helper"
3510 )]
3511 UncommittedDeliveryMetadata,
3512 #[error(
3514 "subscriber chain id {subscriber_chain_id} does not match cache chain id {cache_chain_id}"
3515 )]
3516 SubscriberChainMismatch {
3517 subscriber_chain_id: u64,
3519 cache_chain_id: u64,
3521 },
3522 #[error("subscriber does not advertise durable replay support")]
3524 SubscriberNotDurable,
3525 #[error(
3528 "committed delivery token has no delivery witness; replay cannot be acknowledged safely"
3529 )]
3530 MissingReplayWitness,
3531 #[error("replayed delivery token does not match its committed delivery witness")]
3534 ReplayDeliveryMismatch,
3535 #[error("failed to encode durable delivery witness: {0}")]
3537 DeliveryWitness(String),
3538 #[error(
3541 "tokened block-header, full-block, or hydrated-transaction delivery requires an exact payload commitment"
3542 )]
3543 MissingPayloadCommitment,
3544 #[error(
3547 "cache changed while durable checkpoint commit was pending (staged generation {staged_generation}, current generation {current_generation})"
3548 )]
3549 PendingCheckpointCacheChanged {
3550 staged_generation: u64,
3552 current_generation: u64,
3554 },
3555 #[error(
3558 "reorg after block {common_ancestor} exceeds the retained rollback journal (oldest retained block {oldest_journaled:?}, configured depth {journal_depth})"
3559 )]
3560 CheckpointReorgOutsideJournal {
3561 common_ancestor: u64,
3563 oldest_journaled: Option<u64>,
3565 journal_depth: usize,
3567 },
3568 #[error(
3571 "owner catch-up block {number} {hash} is outside the retained canonical rollback journal"
3572 )]
3573 OwnerCatchupOutsideJournal {
3574 number: u64,
3576 hash: B256,
3578 },
3579}
3580
3581impl From<ReactiveError> for ReactiveEngineError {
3582 fn from(error: ReactiveError) -> Self {
3583 match error {
3584 ReactiveError::OwnerCatchupOutsideJournal { number, hash } => {
3585 Self::OwnerCatchupOutsideJournal { number, hash }
3586 }
3587 error => Self::Runtime(error),
3588 }
3589 }
3590}
3591
3592#[derive(Debug, thiserror::Error)]
3594#[non_exhaustive]
3595pub enum ReactiveCheckpointRestoreError {
3596 #[error("cannot restore a durable checkpoint into a runtime with canonical journal state")]
3598 ActiveRuntime,
3599 #[error("invalid durable reactive runtime state: {0}")]
3601 InvalidRuntimeCheckpoint(String),
3602 #[error(transparent)]
3604 Checkpoint(#[from] DurableCheckpointError),
3605 #[error("subscriber rejected durable resume position: {0}")]
3607 Subscriber(#[source] SubscriberError),
3608 #[error(
3610 "subscriber chain id {subscriber_chain_id} does not match checkpoint chain id {checkpoint_chain_id}"
3611 )]
3612 SubscriberChainMismatch {
3613 subscriber_chain_id: u64,
3615 checkpoint_chain_id: u64,
3617 },
3618 #[error("subscriber does not advertise durable replay support")]
3620 SubscriberNotDurable,
3621}
3622
3623#[derive(Clone, Debug)]
3625#[non_exhaustive]
3626pub enum CheckpointedIngest<N: Network = Ethereum> {
3627 Applied(ReactiveBatchReport<N>),
3629 ReplayAcknowledged,
3632}
3633
3634#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3636pub enum EffectTarget {
3637 StorageSlot {
3639 address: Address,
3641 slot: U256,
3643 },
3644 AccountBalance {
3646 address: Address,
3648 },
3649 AccountNonce {
3651 address: Address,
3653 },
3654 AccountCode {
3656 address: Address,
3658 },
3659 MaskedStorageSlot {
3661 address: Address,
3663 slot: U256,
3665 mask: U256,
3667 },
3668}
3669
3670#[derive(Clone, Debug, PartialEq, Eq)]
3671enum AbsoluteValue {
3672 U256(U256),
3673 U64(u64),
3674 Bytes(Bytes),
3675}
3676
3677pub struct ReactiveRuntime<N: Network = Ethereum> {
3679 registry: ReactiveRegistry<N>,
3680 hooks: Vec<Arc<dyn ReactiveHook<N>>>,
3681 config: ReactiveConfig,
3682 journal: VecDeque<BlockJournal<N>>,
3683 coverage_head: Option<BlockRef>,
3684 log_coverage_head: Option<BlockRef>,
3687 pending_resyncs: Vec<ResyncRequest>,
3688 health: CacheHealth,
3689 safe_head: Option<BlockRef>,
3690 finalized_head: Option<BlockRef>,
3691 metrics: CacheMetrics,
3692 freshness: Option<FreshnessRegistry>,
3701 tracking: HashMap<Address, TrackingPolicy>,
3705 tracked_roots: HashMap<Address, TrackedRoot>,
3708 root_gate_cadence: RootGateCadence,
3710 last_gate_block: Option<u64>,
3714 touched_since_gate: HashSet<Address>,
3720 preconfirmed_branch: Option<PreconfirmedBranch>,
3723}
3724
3725#[derive(Clone)]
3726struct PreconfirmedBranch {
3727 flashblock: FlashblockRef,
3728 canonical_cache: EvmCacheStateSnapshot,
3729}
3730
3731#[derive(Clone, Debug)]
3732struct BlockJournal<N: Network = Ethereum> {
3733 block: BlockRef,
3734 inputs: Vec<InputRef>,
3735 applied: Vec<AppliedReport<N>>,
3736 handler_ids: Vec<HandlerId>,
3737 resynced: Vec<ResyncReport>,
3738 rollback_diffs: Vec<StateDiff>,
3739}
3740
3741const DURABLE_RUNTIME_CHECKPOINT_VERSION: u32 = 4;
3743
3744#[derive(serde::Serialize, serde::Deserialize)]
3745struct DurableRuntimeCheckpoint {
3746 version: u32,
3747 safe_head: Option<BlockRef>,
3748 finalized_head: Option<BlockRef>,
3749 health: CacheHealth,
3750 pending_resyncs: Vec<ResyncRequest>,
3751 coverage_head: Option<BlockRef>,
3752 log_coverage_head: Option<BlockRef>,
3753 journal: Vec<DurableBlockJournal>,
3754 freshness: Option<FreshnessRegistry>,
3755 tracking: HashMap<Address, TrackingPolicy>,
3756 tracked_roots: HashMap<Address, TrackedRoot>,
3757 root_gate_cadence: RootGateCadence,
3758 last_gate_block: Option<u64>,
3759 touched_since_gate: HashSet<Address>,
3760 metrics: CacheMetricsSnapshot,
3761}
3762
3763#[derive(serde::Serialize, serde::Deserialize)]
3764struct DurableBlockJournal {
3765 block: BlockRef,
3766 handler_ids: Vec<HandlerId>,
3767 rollback_diffs: Vec<StateDiff>,
3768}
3769
3770struct DurableRuntimeRestorePlan {
3771 checkpoint: Option<DurableRuntimeCheckpoint>,
3772 fallback_history: Vec<BlockRef>,
3773}
3774
3775impl DurableRuntimeRestorePlan {
3776 fn canonical_history(&self) -> Vec<BlockRef> {
3777 self.checkpoint.as_ref().map_or_else(
3778 || self.fallback_history.clone(),
3779 |checkpoint| checkpoint.journal.iter().map(|entry| entry.block).collect(),
3780 )
3781 }
3782}
3783
3784#[derive(Clone)]
3785struct ReactiveRuntimeState<N: Network> {
3786 journal: VecDeque<BlockJournal<N>>,
3787 coverage_head: Option<BlockRef>,
3788 log_coverage_head: Option<BlockRef>,
3789 pending_resyncs: Vec<ResyncRequest>,
3790 health: CacheHealth,
3791 safe_head: Option<BlockRef>,
3792 finalized_head: Option<BlockRef>,
3793 freshness: Option<FreshnessRegistry>,
3794 tracking: HashMap<Address, TrackingPolicy>,
3795 tracked_roots: HashMap<Address, TrackedRoot>,
3796 root_gate_cadence: RootGateCadence,
3797 last_gate_block: Option<u64>,
3798 touched_since_gate: HashSet<Address>,
3799 metrics: CacheMetricsSnapshot,
3800}
3801
3802#[derive(Clone)]
3803struct ChainControlState {
3804 journal_invalidated_from: Option<u64>,
3805 resolved_canonical_blocks: HashMap<(u64, B256), BlockRef>,
3806}
3807
3808#[derive(Default)]
3817struct BatchDroppedCanonical {
3818 identities: HashSet<(u64, B256)>,
3819 implicit_spans: Vec<(u64, u64)>,
3820}
3821
3822impl BatchDroppedCanonical {
3823 fn covers_implicit_number(&self, number: u64) -> bool {
3824 self.implicit_spans
3825 .iter()
3826 .any(|(from, through)| number >= *from && number <= *through)
3827 }
3828
3829 fn contains(&self, block: &BlockRef) -> bool {
3830 self.identities.contains(&(block.number, block.hash))
3831 }
3832
3833 fn record_identity(&mut self, block: &BlockRef) {
3834 self.identities.insert((block.number, block.hash));
3835 }
3836
3837 fn record_explicit(&mut self, _common_ancestor: &BlockRef, old_tip: &BlockRef) {
3838 self.identities.insert((old_tip.number, old_tip.hash));
3839 }
3840
3841 fn record_drained(&mut self, blocks: &[BlockRef]) {
3842 let Some(from) = blocks.iter().map(|block| block.number).min() else {
3843 return;
3844 };
3845 let through = blocks
3846 .iter()
3847 .map(|block| block.number)
3848 .max()
3849 .expect("a non-empty drained set has a maximum");
3850 self.implicit_spans.push((from, through));
3851 self.identities
3852 .extend(blocks.iter().map(|block| (block.number, block.hash)));
3853 }
3854}
3855
3856pub struct ReactiveRegistry<N: Network = Ethereum> {
3864 handlers: BTreeMap<u128, RegisteredHandler<N>>,
3865 handler_positions: HashMap<HandlerId, u128>,
3866 next_handler_position: u128,
3867 indexed_log_handlers: HashMap<LogRouteKey, BTreeSet<u128>>,
3868 fallback_log_handlers: BTreeSet<u128>,
3869 data_slice_shapes: HashMap<(usize, usize), usize>,
3870}
3871
3872struct RegisteredHandler<N: Network = Ethereum> {
3873 id: HandlerId,
3874 handler: Arc<dyn ReactiveHandler<N>>,
3875 interests: Vec<ReactiveInterest<N>>,
3876 has_log_interests: bool,
3877 log_route_index: Option<LogRouteIndex>,
3878}
3879
3880impl<N: Network> Default for ReactiveRegistry<N> {
3881 fn default() -> Self {
3882 Self::new()
3883 }
3884}
3885
3886impl<N: Network> ReactiveRegistry<N> {
3887 pub fn new() -> Self {
3889 Self {
3890 handlers: BTreeMap::new(),
3891 handler_positions: HashMap::new(),
3892 next_handler_position: 0,
3893 indexed_log_handlers: HashMap::new(),
3894 fallback_log_handlers: BTreeSet::new(),
3895 data_slice_shapes: HashMap::new(),
3896 }
3897 }
3898
3899 pub fn register_handler(
3909 &mut self,
3910 handler: Arc<dyn ReactiveHandler<N>>,
3911 ) -> Result<(), RegisterError> {
3912 let id = handler.id();
3913 if self.handler_positions.contains_key(&id) {
3914 return Err(RegisterError::DuplicateHandler(id));
3915 }
3916 let interests = handler.interests();
3917 self.insert_handler_prepared(id, handler, interests);
3918 Ok(())
3919 }
3920
3921 fn insert_handler_prepared(
3922 &mut self,
3923 id: HandlerId,
3924 handler: Arc<dyn ReactiveHandler<N>>,
3925 interests: Vec<ReactiveInterest<N>>,
3926 ) {
3927 debug_assert!(!self.handler_positions.contains_key(&id));
3928 let has_log_interests = interests
3929 .iter()
3930 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)));
3931 let log_route_index = handler.log_route_index();
3932 if self.next_handler_position == u128::MAX {
3933 self.compact_handler_positions();
3934 }
3935 let position = self.next_handler_position;
3936 self.next_handler_position += 1;
3937 self.handler_positions.insert(id.clone(), position);
3938 if let Some(index) = &log_route_index {
3939 for key in index.keys() {
3940 if let LogRouteKey::DataSlice { offset, value } = key {
3941 *self
3942 .data_slice_shapes
3943 .entry((*offset, value.len()))
3944 .or_default() += 1;
3945 }
3946 self.indexed_log_handlers
3947 .entry(key.clone())
3948 .or_default()
3949 .insert(position);
3950 }
3951 } else if has_log_interests {
3952 self.fallback_log_handlers.insert(position);
3953 }
3954 self.handlers.insert(
3955 position,
3956 RegisteredHandler {
3957 id,
3958 handler,
3959 interests,
3960 has_log_interests,
3961 log_route_index,
3962 },
3963 );
3964 }
3965
3966 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
3972 let position = self.handler_positions.remove(id)?;
3973 let registered = self.handlers.remove(&position)?;
3974 if let Some(index) = ®istered.log_route_index {
3975 for key in index.keys() {
3976 let remove_bucket = self
3977 .indexed_log_handlers
3978 .get_mut(key)
3979 .is_some_and(|owners| {
3980 owners.remove(&position);
3981 owners.is_empty()
3982 });
3983 if remove_bucket {
3984 self.indexed_log_handlers.remove(key);
3985 }
3986 if let LogRouteKey::DataSlice { offset, value } = key {
3987 let shape = (*offset, value.len());
3988 let remove_shape =
3989 self.data_slice_shapes.get_mut(&shape).is_some_and(|count| {
3990 *count -= 1;
3991 *count == 0
3992 });
3993 if remove_shape {
3994 self.data_slice_shapes.remove(&shape);
3995 }
3996 }
3997 }
3998 } else {
3999 self.fallback_log_handlers.remove(&position);
4000 }
4001 Some(registered.handler)
4002 }
4003
4004 pub fn contains_handler(&self, id: &HandlerId) -> bool {
4006 self.handler_positions.contains_key(id)
4007 }
4008
4009 pub fn handler_ids(&self) -> Vec<HandlerId> {
4011 self.handlers
4012 .values()
4013 .map(|handler| handler.id.clone())
4014 .collect()
4015 }
4016
4017 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
4019 self.handler_positions
4020 .get(id)
4021 .and_then(|position| self.handlers.get(position))
4022 .map(|registered| registered.interests.as_slice())
4023 }
4024
4025 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
4027 self.handlers
4028 .values()
4029 .flat_map(|handler| handler.interests.clone())
4030 .collect()
4031 }
4032
4033 pub fn log_subscription_filters(&self) -> Vec<Filter> {
4040 let mut filters = Vec::new();
4041 for interest in self.log_interests() {
4042 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
4043 }
4044 filters
4045 }
4046
4047 pub fn route_log(&self, log: &Log) -> Vec<ReactiveLogRoute> {
4053 self.log_handler_candidates(log)
4054 .into_iter()
4055 .filter_map(|handler| handler.route_log(log))
4056 .collect()
4057 }
4058
4059 fn log_handler_candidates(&self, log: &Log) -> Vec<&RegisteredHandler<N>> {
4060 let mut indexed_positions = Vec::new();
4061 if let Some(indexed) = self
4062 .indexed_log_handlers
4063 .get(&LogRouteKey::Emitter(log.address()))
4064 {
4065 indexed_positions.extend(indexed.iter().copied());
4066 }
4067 for (index, value) in log.topics().iter().copied().enumerate() {
4068 if let Some(indexed) = self
4069 .indexed_log_handlers
4070 .get(&LogRouteKey::Topic { index, value })
4071 {
4072 indexed_positions.extend(indexed.iter().copied());
4073 }
4074 }
4075 let data = log.inner.data.data.as_ref();
4076 for &(offset, len) in self.data_slice_shapes.keys() {
4077 let Some(end) = offset.checked_add(len) else {
4078 continue;
4079 };
4080 let Some(value) = data.get(offset..end) else {
4081 continue;
4082 };
4083 if let Some(indexed) = self.indexed_log_handlers.get(&LogRouteKey::DataSlice {
4084 offset,
4085 value: value.to_vec(),
4086 }) {
4087 indexed_positions.extend(indexed.iter().copied());
4088 }
4089 }
4090 if indexed_positions.is_empty() {
4091 if self.fallback_log_handlers.is_empty() {
4092 return Vec::new();
4093 }
4094 if !self.indexed_log_handlers.is_empty() {
4095 return self
4096 .fallback_log_handlers
4097 .iter()
4098 .filter_map(|position| self.handlers.get(position))
4099 .collect();
4100 }
4101 return self
4102 .handlers
4103 .values()
4104 .filter(|handler| handler.has_log_interests && handler.log_route_index.is_none())
4105 .collect();
4106 }
4107
4108 indexed_positions.extend(self.fallback_log_handlers.iter().copied());
4109 indexed_positions.sort_unstable();
4110 indexed_positions.dedup();
4111 indexed_positions
4112 .into_iter()
4113 .filter_map(|position| self.handlers.get(&position))
4114 .collect()
4115 }
4116
4117 fn handlers(&self) -> impl Iterator<Item = &RegisteredHandler<N>> {
4118 self.handlers.values()
4119 }
4120
4121 fn log_interests(&self) -> impl Iterator<Item = &LogInterest> {
4122 self.handlers.values().flat_map(|handler| {
4123 handler
4124 .interests
4125 .iter()
4126 .filter_map(|interest| match interest {
4127 ReactiveInterest::Logs(interest) => Some(interest),
4128 ReactiveInterest::Blocks(_) | ReactiveInterest::PendingTransactions(_) => None,
4129 })
4130 })
4131 }
4132
4133 fn compact_handler_positions(&mut self) {
4134 let handlers = std::mem::take(&mut self.handlers);
4135 self.handler_positions.clear();
4136 self.indexed_log_handlers.clear();
4137 self.fallback_log_handlers.clear();
4138 self.data_slice_shapes.clear();
4139
4140 for (position, (_, handler)) in handlers.into_iter().enumerate() {
4141 let position = position as u128;
4142 self.handler_positions.insert(handler.id.clone(), position);
4143 if let Some(index) = &handler.log_route_index {
4144 for key in index.keys() {
4145 if let LogRouteKey::DataSlice { offset, value } = key {
4146 *self
4147 .data_slice_shapes
4148 .entry((*offset, value.len()))
4149 .or_default() += 1;
4150 }
4151 self.indexed_log_handlers
4152 .entry(key.clone())
4153 .or_default()
4154 .insert(position);
4155 }
4156 } else if handler.has_log_interests {
4157 self.fallback_log_handlers.insert(position);
4158 }
4159 self.handlers.insert(position, handler);
4160 }
4161 self.next_handler_position = self.handlers.len() as u128;
4162 }
4163}
4164
4165impl<N: Network> ReactiveRuntime<N> {
4166 pub fn new(config: ReactiveConfig) -> Self {
4168 Self {
4169 registry: ReactiveRegistry::new(),
4170 hooks: Vec::new(),
4171 config,
4172 journal: VecDeque::new(),
4173 coverage_head: None,
4174 log_coverage_head: None,
4175 pending_resyncs: Vec::new(),
4176 health: CacheHealth::Healthy,
4177 safe_head: None,
4178 finalized_head: None,
4179 metrics: CacheMetrics::default(),
4180 freshness: None,
4181 tracking: HashMap::new(),
4182 tracked_roots: HashMap::new(),
4183 root_gate_cadence: RootGateCadence::default(),
4184 last_gate_block: None,
4185 touched_since_gate: HashSet::new(),
4186 preconfirmed_branch: None,
4187 }
4188 }
4189
4190 fn checkpoint_state(&self) -> ReactiveRuntimeState<N> {
4191 ReactiveRuntimeState {
4192 journal: self.journal.clone(),
4193 coverage_head: self.coverage_head,
4194 log_coverage_head: self.log_coverage_head,
4195 pending_resyncs: self.pending_resyncs.clone(),
4196 health: self.health,
4197 safe_head: self.safe_head,
4198 finalized_head: self.finalized_head,
4199 freshness: self.freshness.clone(),
4200 tracking: self.tracking.clone(),
4201 tracked_roots: self.tracked_roots.clone(),
4202 root_gate_cadence: self.root_gate_cadence,
4203 last_gate_block: self.last_gate_block,
4204 touched_since_gate: self.touched_since_gate.clone(),
4205 metrics: self.metrics.snapshot(),
4206 }
4207 }
4208
4209 fn is_pristine_for_checkpoint_restore(&self) -> bool {
4210 self.preconfirmed_branch.is_none()
4211 && self.journal.is_empty()
4212 && self.coverage_head.is_none()
4213 && self.pending_resyncs.is_empty()
4214 && self.health == CacheHealth::Healthy
4215 && self.safe_head.is_none()
4216 && self.finalized_head.is_none()
4217 && self.tracked_roots.is_empty()
4218 && self.last_gate_block.is_none()
4219 && self.touched_since_gate.is_empty()
4220 && self.metrics.snapshot() == CacheMetricsSnapshot::default()
4221 }
4222
4223 fn adopted_baseline_only(&self) -> Option<BlockRef> {
4224 let baseline = self.coverage_head?;
4225 let journal_is_baseline_only = if self.config.journal_depth == 0 {
4226 self.journal.is_empty()
4227 } else {
4228 self.journal.len() == 1
4229 && self.journal.front().is_some_and(|entry| {
4230 entry.block == baseline
4231 && entry.inputs.is_empty()
4232 && entry.applied.is_empty()
4233 && entry.handler_ids.is_empty()
4234 && entry.resynced.is_empty()
4235 && entry.rollback_diffs.is_empty()
4236 })
4237 };
4238 (self.preconfirmed_branch.is_none()
4239 && journal_is_baseline_only
4240 && self.pending_resyncs.is_empty()
4241 && self.health == CacheHealth::Healthy
4242 && self.safe_head.is_none()
4243 && self.finalized_head.is_none()
4244 && self.tracked_roots.is_empty()
4245 && self.last_gate_block.is_none()
4246 && self.touched_since_gate.is_empty()
4247 && self.metrics.snapshot() == CacheMetricsSnapshot::default())
4248 .then_some(baseline)
4249 }
4250
4251 fn restore_state(&mut self, state: ReactiveRuntimeState<N>) {
4252 self.journal = state.journal;
4253 self.coverage_head = state.coverage_head;
4254 self.log_coverage_head = state.log_coverage_head;
4255 self.pending_resyncs = state.pending_resyncs;
4256 self.health = state.health;
4257 self.safe_head = state.safe_head;
4258 self.finalized_head = state.finalized_head;
4259 self.freshness = state.freshness;
4260 self.tracking = state.tracking;
4261 self.tracked_roots = state.tracked_roots;
4262 self.root_gate_cadence = state.root_gate_cadence;
4263 self.last_gate_block = state.last_gate_block;
4264 self.touched_since_gate = state.touched_since_gate;
4265 self.metrics.restore(state.metrics);
4266 }
4267
4268 fn restore_transaction_state(&mut self, state: ReactiveRuntimeState<N>) {
4269 let metrics = self.metrics.snapshot();
4274 self.restore_state(state);
4275 self.metrics.restore(metrics);
4276 }
4277
4278 fn durable_checkpoint_bytes(&self) -> Result<Vec<u8>, ReactiveEngineError> {
4279 let checkpoint = DurableRuntimeCheckpoint {
4280 version: DURABLE_RUNTIME_CHECKPOINT_VERSION,
4281 safe_head: self.safe_head,
4282 finalized_head: self.finalized_head,
4283 health: self.health,
4284 pending_resyncs: self.pending_resyncs.clone(),
4285 coverage_head: self.coverage_head,
4286 log_coverage_head: self.log_coverage_head,
4287 journal: self
4288 .journal
4289 .iter()
4290 .map(|entry| DurableBlockJournal {
4291 block: entry.block,
4292 handler_ids: entry.handler_ids.clone(),
4293 rollback_diffs: entry.rollback_diffs.clone(),
4294 })
4295 .collect(),
4296 freshness: self.freshness.clone(),
4297 tracking: self.tracking.clone(),
4298 tracked_roots: self.tracked_roots.clone(),
4299 root_gate_cadence: self.root_gate_cadence,
4300 last_gate_block: self.last_gate_block,
4301 touched_since_gate: self.touched_since_gate.clone(),
4302 metrics: self.metrics.snapshot(),
4303 };
4304 bincode::serialize(&checkpoint)
4305 .map_err(|error| ReactiveEngineError::RuntimeCheckpoint(error.to_string()))
4306 }
4307
4308 fn plan_durable_checkpoint_restore(
4309 &self,
4310 bytes: &[u8],
4311 expected_coverage: &BlockRef,
4312 ) -> Result<DurableRuntimeRestorePlan, ReactiveCheckpointRestoreError> {
4313 let mut cursor = std::io::Cursor::new(bytes);
4314 let mut checkpoint: DurableRuntimeCheckpoint = bincode::DefaultOptions::new()
4315 .with_fixint_encoding()
4316 .with_limit(bytes.len() as u64)
4317 .deserialize_from(&mut cursor)
4318 .map_err(|error| {
4319 ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(error.to_string())
4320 })?;
4321 if cursor.position() != bytes.len() as u64 {
4322 return Err(ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(
4323 "runtime checkpoint has trailing bytes".to_owned(),
4324 ));
4325 }
4326 if checkpoint.version != DURABLE_RUNTIME_CHECKPOINT_VERSION {
4327 return Err(ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(
4328 format!(
4329 "unsupported runtime checkpoint version {}",
4330 checkpoint.version
4331 ),
4332 ));
4333 }
4334 self.validate_durable_runtime_checkpoint(&checkpoint, expected_coverage)?;
4335
4336 let retained = self.config.journal_depth.min(checkpoint.journal.len());
4337 let discard = checkpoint.journal.len() - retained;
4338 checkpoint.journal.drain(..discard);
4339 Ok(DurableRuntimeRestorePlan {
4340 checkpoint: Some(checkpoint),
4341 fallback_history: Vec::new(),
4342 })
4343 }
4344
4345 fn apply_durable_checkpoint_restore(&mut self, plan: DurableRuntimeRestorePlan) {
4346 let Some(checkpoint) = plan.checkpoint else {
4347 self.journal = plan
4348 .fallback_history
4349 .into_iter()
4350 .map(|block| BlockJournal {
4351 block,
4352 inputs: Vec::new(),
4353 applied: Vec::new(),
4354 handler_ids: Vec::new(),
4355 resynced: Vec::new(),
4356 rollback_diffs: Vec::new(),
4357 })
4358 .collect();
4359 return;
4360 };
4361 self.safe_head = checkpoint.safe_head;
4362 self.finalized_head = checkpoint.finalized_head;
4363 self.health = checkpoint.health;
4364 self.pending_resyncs = checkpoint.pending_resyncs;
4365 self.coverage_head = checkpoint.coverage_head;
4366 self.log_coverage_head = checkpoint.log_coverage_head;
4367 self.journal = checkpoint
4368 .journal
4369 .into_iter()
4370 .map(|entry| BlockJournal {
4371 block: entry.block,
4372 inputs: Vec::new(),
4373 applied: Vec::new(),
4374 handler_ids: entry.handler_ids,
4375 resynced: Vec::new(),
4376 rollback_diffs: entry.rollback_diffs,
4377 })
4378 .collect();
4379 self.freshness = checkpoint.freshness;
4380 self.tracking = checkpoint.tracking;
4381 self.tracked_roots = checkpoint.tracked_roots;
4382 self.root_gate_cadence = checkpoint.root_gate_cadence;
4383 self.last_gate_block = checkpoint.last_gate_block;
4384 self.touched_since_gate = checkpoint.touched_since_gate;
4385 self.metrics.restore(checkpoint.metrics);
4386 }
4387
4388 fn validate_durable_runtime_checkpoint(
4389 &self,
4390 checkpoint: &DurableRuntimeCheckpoint,
4391 expected_coverage: &BlockRef,
4392 ) -> Result<(), ReactiveCheckpointRestoreError> {
4393 let invalid =
4394 |message: String| ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(message);
4395 let Some(coverage) = checkpoint.coverage_head.as_ref() else {
4396 return Err(invalid(
4397 "runtime checkpoint is missing its canonical coverage head".into(),
4398 ));
4399 };
4400 if !optional_block_refs_are_compatible(Some(coverage), Some(expected_coverage)) {
4401 return Err(invalid(format!(
4402 "runtime coverage {}:{:?} conflicts with checkpoint metadata {}:{:?}",
4403 coverage.number, coverage.hash, expected_coverage.number, expected_coverage.hash
4404 )));
4405 }
4406 for (label, head) in [
4407 ("safe", checkpoint.safe_head.as_ref()),
4408 ("finalized", checkpoint.finalized_head.as_ref()),
4409 ] {
4410 let Some(head) = head else { continue };
4411 if head.number > coverage.number
4412 || (head.number == coverage.number && head.hash != coverage.hash)
4413 {
4414 return Err(invalid(format!(
4415 "{label} head {}:{:?} lies beyond or conflicts with canonical coverage {}:{:?}",
4416 head.number, head.hash, coverage.number, coverage.hash
4417 )));
4418 }
4419 if head.number.checked_add(1) == Some(coverage.number)
4420 && coverage
4421 .parent_hash
4422 .is_some_and(|parent| parent != head.hash)
4423 {
4424 return Err(invalid(format!(
4425 "canonical coverage does not descend from adjacent {label} head"
4426 )));
4427 }
4428 }
4429 if let (Some(finalized), Some(safe)) = (
4430 checkpoint.finalized_head.as_ref(),
4431 checkpoint.safe_head.as_ref(),
4432 ) {
4433 if finalized.number > safe.number
4434 || (finalized.number == safe.number && finalized.hash != safe.hash)
4435 {
4436 return Err(invalid(
4437 "finalized head is above or conflicts with the safe head".into(),
4438 ));
4439 }
4440 if finalized.number.checked_add(1) == Some(safe.number)
4441 && safe.parent_hash != Some(finalized.hash)
4442 {
4443 return Err(invalid(
4444 "adjacent safe head does not descend from finalized head".into(),
4445 ));
4446 }
4447 }
4448
4449 let mut previous: Option<&DurableBlockJournal> = None;
4450 for entry in &checkpoint.journal {
4451 if entry.block.number > coverage.number
4452 || (entry.block.number == coverage.number && entry.block.hash != coverage.hash)
4453 {
4454 return Err(invalid(format!(
4455 "journal block {}:{:?} lies beyond or conflicts with canonical coverage",
4456 entry.block.number, entry.block.hash
4457 )));
4458 }
4459 if let Some(previous) = previous {
4460 if entry.block.number <= previous.block.number {
4461 return Err(invalid(
4462 "runtime journal block numbers are not strictly increasing".into(),
4463 ));
4464 }
4465 if previous.block.number.checked_add(1) == Some(entry.block.number)
4466 && entry.block.parent_hash.is_some()
4467 && entry.block.parent_hash != Some(previous.block.hash)
4468 {
4469 return Err(invalid(
4470 "adjacent runtime journal blocks are not parent-linked".into(),
4471 ));
4472 }
4473 }
4474 for (label, head) in [
4475 ("safe", checkpoint.safe_head.as_ref()),
4476 ("finalized", checkpoint.finalized_head.as_ref()),
4477 ] {
4478 if let Some(head) = head
4479 && head.number == entry.block.number
4480 && !optional_block_refs_are_compatible(Some(head), Some(&entry.block))
4481 {
4482 return Err(invalid(format!(
4483 "{label} head conflicts with the retained journal at block {}",
4484 head.number
4485 )));
4486 }
4487 }
4488 let mut handler_ids = HashSet::new();
4489 if entry
4490 .handler_ids
4491 .iter()
4492 .any(|handler_id| !handler_ids.insert(handler_id))
4493 {
4494 return Err(invalid(
4495 "runtime journal contains duplicate handler generation ids".into(),
4496 ));
4497 }
4498 previous = Some(entry);
4499 }
4500 if let Some(tail) = checkpoint.journal.last()
4501 && tail.block.number == coverage.number
4502 && !optional_block_refs_are_compatible(Some(&tail.block), Some(coverage))
4503 {
4504 return Err(invalid(format!(
4505 "runtime journal tail conflicts with canonical coverage at block {}",
4506 coverage.number
4507 )));
4508 }
4509 if let Some(tail) = checkpoint.journal.last()
4510 && tail.block.number.checked_add(1) == Some(coverage.number)
4511 && coverage
4512 .parent_hash
4513 .is_some_and(|parent_hash| parent_hash != tail.block.hash)
4514 {
4515 return Err(invalid(format!(
4516 "canonical coverage does not descend from adjacent runtime journal tail at block {}",
4517 tail.block.number
4518 )));
4519 }
4520
4521 if let Some(last_gate_block) = checkpoint.last_gate_block {
4522 if last_gate_block > coverage.number {
4523 return Err(invalid(
4524 "root-gate cursor lies beyond canonical coverage".into(),
4525 ));
4526 }
4527 } else if !checkpoint.tracked_roots.is_empty() {
4528 return Err(invalid(
4529 "root-gate baselines exist without a completed gate cursor".into(),
4530 ));
4531 }
4532 for (address, baseline) in &checkpoint.tracked_roots {
4533 let Some(policy) = checkpoint.tracking.get(address) else {
4534 return Err(invalid(
4535 "root-gate baseline has no corresponding tracking policy".into(),
4536 ));
4537 };
4538 if matches!(policy, TrackingPolicy::Slots { .. }) {
4539 return Err(invalid(
4540 "slot-only tracking cannot carry an account root baseline".into(),
4541 ));
4542 }
4543 if baseline.last_block > coverage.number
4544 || checkpoint
4545 .last_gate_block
4546 .is_some_and(|last_gate| baseline.last_block > last_gate)
4547 {
4548 return Err(invalid(
4549 "root-gate baseline lies beyond the committed gate window".into(),
4550 ));
4551 }
4552 }
4553 Ok(())
4554 }
4555
4556 pub fn track_account(&mut self, address: Address, policy: TrackingPolicy) {
4577 self.tracking.insert(address, policy);
4578 self.tracked_roots.remove(&address);
4579 }
4580
4581 pub fn untrack_account(&mut self, address: Address) -> bool {
4585 self.tracked_roots.remove(&address);
4586 self.tracking.remove(&address).is_some()
4587 }
4588
4589 pub fn set_root_gate_cadence(&mut self, cadence: RootGateCadence) {
4597 self.root_gate_cadence = cadence;
4598 self.last_gate_block = None;
4599 self.touched_since_gate.clear();
4600 }
4601
4602 pub const fn log_coverage_head(&self) -> Option<&BlockRef> {
4610 self.log_coverage_head.as_ref()
4611 }
4612
4613 pub fn root_gate_cadence(&self) -> RootGateCadence {
4615 self.root_gate_cadence
4616 }
4617
4618 pub fn enable_freshness_stamping(&mut self) {
4630 if self.freshness.is_none() {
4631 self.freshness = Some(FreshnessRegistry::new());
4632 }
4633 }
4634
4635 pub fn freshness(&self) -> Option<&FreshnessRegistry> {
4640 self.freshness.as_ref()
4641 }
4642
4643 pub fn freshness_mut(&mut self) -> Option<&mut FreshnessRegistry> {
4648 self.freshness.as_mut()
4649 }
4650
4651 pub fn health(&self) -> CacheHealth {
4653 self.health
4654 }
4655
4656 pub fn metrics(&self) -> CacheMetricsSnapshot {
4658 self.metrics.snapshot()
4659 }
4660
4661 pub fn reset_health(&mut self) {
4671 self.health = CacheHealth::Healthy;
4672 }
4673
4674 fn escalate_trust(&mut self, block: u64) -> Option<Arc<ReactiveReport<N>>> {
4688 let to = match self.health {
4689 CacheHealth::Healthy => CacheHealth::Degraded { since_block: block },
4690 CacheHealth::Degraded { .. } => CacheHealth::Unhealthy { since_block: block },
4691 CacheHealth::Unhealthy { .. } => return None,
4692 };
4693 self.transition_health(to, Some(block))
4694 }
4695
4696 fn transition_health(
4704 &mut self,
4705 to: CacheHealth,
4706 block: Option<u64>,
4707 ) -> Option<Arc<ReactiveReport<N>>> {
4708 if to == self.health {
4709 return None;
4710 }
4711 let from = self.health;
4712 self.health = to;
4713 Some(Arc::new(ReactiveReport::Health(HealthReport {
4714 from,
4715 to,
4716 block,
4717 _network: PhantomData,
4718 })))
4719 }
4720
4721 pub fn register_handler(
4728 &mut self,
4729 handler: Arc<dyn ReactiveHandler<N>>,
4730 ) -> Result<(), RegisterError> {
4731 self.registry.register_handler(handler)
4732 }
4733
4734 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
4742 self.registry.unregister_handler(id)
4743 }
4744
4745 pub fn contains_handler(&self, id: &HandlerId) -> bool {
4747 self.registry.contains_handler(id)
4748 }
4749
4750 pub fn handler_ids(&self) -> Vec<HandlerId> {
4752 self.registry.handler_ids()
4753 }
4754
4755 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
4757 self.registry.handler_interests(id)
4758 }
4759
4760 pub fn last_canonical_block(&self) -> Option<BlockRef> {
4770 self.coverage_head
4771 }
4772
4773 pub fn adopt_canonical_baseline(
4790 &mut self,
4791 baseline: BlockRef,
4792 ) -> Result<(), ReactiveBaselineError> {
4793 self.validate_canonical_baseline_adoption(baseline)?;
4794 if self.adopted_baseline_only().is_some() {
4795 return Ok(());
4796 }
4797
4798 self.coverage_head = Some(baseline);
4799 if self.config.journal_depth > 0 {
4800 self.journal.push_back(BlockJournal {
4801 block: baseline,
4802 inputs: Vec::new(),
4803 applied: Vec::new(),
4804 handler_ids: Vec::new(),
4805 resynced: Vec::new(),
4806 rollback_diffs: Vec::new(),
4807 });
4808 }
4809 Ok(())
4810 }
4811
4812 fn validate_canonical_baseline_adoption(
4813 &self,
4814 baseline: BlockRef,
4815 ) -> Result<(), ReactiveBaselineError> {
4816 if let Some(existing) = self.adopted_baseline_only() {
4817 return if existing == baseline {
4818 Ok(())
4819 } else {
4820 Err(ReactiveBaselineError::ConflictingBaseline {
4821 existing_number: existing.number,
4822 existing_hash: existing.hash,
4823 requested_number: baseline.number,
4824 requested_hash: baseline.hash,
4825 })
4826 };
4827 }
4828 if !self.is_pristine_for_checkpoint_restore() {
4829 return Err(ReactiveBaselineError::ActiveRuntime);
4830 }
4831 Ok(())
4832 }
4833
4834 pub const fn safe_head(&self) -> Option<&BlockRef> {
4836 self.safe_head.as_ref()
4837 }
4838
4839 pub const fn finalized_head(&self) -> Option<&BlockRef> {
4841 self.finalized_head.as_ref()
4842 }
4843
4844 pub fn has_journaled_handler_effects(&self, handler_id: &HandlerId) -> bool {
4852 self.journal
4853 .iter()
4854 .any(|entry| entry.handler_ids.contains(handler_id))
4855 }
4856
4857 pub fn journaled_handler_ids(&self) -> HashSet<HandlerId> {
4864 self.journal
4865 .iter()
4866 .flat_map(|entry| entry.handler_ids.iter().cloned())
4867 .collect()
4868 }
4869
4870 pub fn pending_resyncs(&self) -> &[ResyncRequest] {
4881 &self.pending_resyncs
4882 }
4883
4884 pub fn cancel_pending_resync(&mut self, id: &ResyncId) -> Vec<ResyncRequest> {
4893 self.cancel_pending_resyncs_by_id(std::slice::from_ref(id))
4894 }
4895
4896 pub fn cancel_pending_resyncs_by_id(&mut self, ids: &[ResyncId]) -> Vec<ResyncRequest> {
4903 if ids.is_empty() {
4904 return Vec::new();
4905 }
4906 let ids: HashSet<&ResyncId> = ids.iter().collect();
4907 let mut cancelled = Vec::new();
4908 self.pending_resyncs.retain(|request| {
4909 if ids.contains(&request.id) {
4910 cancelled.push(request.clone());
4911 false
4912 } else {
4913 true
4914 }
4915 });
4916 cancelled
4917 }
4918
4919 pub fn cancel_pending_resyncs(&mut self, address: Address) -> Vec<ResyncRequest> {
4934 let mut cancelled = Vec::new();
4935 self.pending_resyncs.retain_mut(|request| {
4936 let (matching, remaining): (Vec<_>, Vec<_>) = request
4937 .targets
4938 .drain(..)
4939 .partition(|target| resync_target_address(target) == address);
4940 request.targets = remaining;
4941 if !matching.is_empty() {
4942 cancelled.push(ResyncRequest {
4943 id: request.id.clone(),
4944 reason: request.reason.clone(),
4945 block: request.block.clone(),
4946 targets: matching,
4947 priority: request.priority,
4948 });
4949 }
4950 !request.targets.is_empty()
4951 });
4952 cancelled
4953 }
4954
4955 pub fn register_hook(&mut self, hook: Arc<dyn ReactiveHook<N>>) -> Result<(), RegisterError> {
4962 self.hooks.push(hook);
4963 Ok(())
4964 }
4965
4966 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
4968 self.registry.interests()
4969 }
4970
4971 pub fn ingest_batch(
4989 &mut self,
4990 cache: &mut EvmCache,
4991 batch: ReactiveInputBatch<N>,
4992 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4993 let preconfirmation = batch_preconfirmation(&batch)?;
4994 if let Some(flashblock) = preconfirmation.as_ref() {
4995 self.prepare_preconfirmed_branch(cache, flashblock)?;
4996 } else {
4997 self.discard_preconfirmed_branch(cache);
4998 }
4999 let cache_state = EvmCacheStateSnapshot::capture(cache);
5000 let runtime_state = self.checkpoint_state();
5001 let batch_report = match self.ingest_batch_direct(cache, batch) {
5002 Ok(report) => report,
5003 Err(error) => {
5004 cache_state.restore(cache);
5005 self.restore_transaction_state(runtime_state);
5006 return Err(error);
5007 }
5008 };
5009 if let Some(flashblock) = preconfirmation {
5010 self.restore_transaction_state(runtime_state);
5011 if let Some(branch) = self.preconfirmed_branch.as_mut() {
5012 branch.flashblock = flashblock;
5013 }
5014 }
5015 self.dispatch_reports(&batch_report.reports);
5016 let _ = &self.config;
5017 Ok(batch_report)
5018 }
5019
5020 pub fn ingest_batch_with_resync(
5037 &mut self,
5038 cache: &mut EvmCache,
5039 batch: ReactiveInputBatch<N>,
5040 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
5041 let preconfirmation = batch_preconfirmation(&batch)?;
5042 if let Some(flashblock) = preconfirmation.as_ref() {
5043 self.prepare_preconfirmed_branch(cache, flashblock)?;
5044 } else {
5045 self.discard_preconfirmed_branch(cache);
5046 }
5047 let cache_state = EvmCacheStateSnapshot::capture(cache);
5048 let runtime_state = self.checkpoint_state();
5049 let batch_report = match self.ingest_batch_with_resync_direct(cache, batch) {
5050 Ok(report) => report,
5051 Err(error) => {
5052 cache_state.restore(cache);
5053 self.restore_transaction_state(runtime_state);
5054 return Err(error);
5055 }
5056 };
5057
5058 if let Some(flashblock) = preconfirmation {
5059 self.restore_transaction_state(runtime_state);
5060 if let Some(branch) = self.preconfirmed_branch.as_mut() {
5061 branch.flashblock = flashblock;
5062 }
5063 }
5064
5065 self.dispatch_reports(&batch_report.reports);
5066 let _ = &self.config;
5067 Ok(batch_report)
5068 }
5069
5070 pub fn active_preconfirmation(&self) -> Option<&FlashblockRef> {
5073 self.preconfirmed_branch
5074 .as_ref()
5075 .map(|branch| &branch.flashblock)
5076 }
5077
5078 pub fn discard_preconfirmation(&mut self, cache: &mut EvmCache) {
5081 self.discard_preconfirmed_branch(cache);
5082 }
5083
5084 fn discard_preconfirmed_branch(&mut self, cache: &mut EvmCache) {
5085 if let Some(branch) = self.preconfirmed_branch.take() {
5086 branch.canonical_cache.restore(cache);
5087 }
5088 }
5089
5090 fn prepare_preconfirmed_branch(
5091 &mut self,
5092 cache: &mut EvmCache,
5093 incoming: &FlashblockRef,
5094 ) -> Result<(), ReactiveError> {
5095 let Some(canonical) = self.coverage_head else {
5096 self.discard_preconfirmed_branch(cache);
5097 return Err(ReactiveError::InvalidInputRecord {
5098 message: "pre-confirmed state requires an exact canonical coverage baseline".into(),
5099 });
5100 };
5101 if canonical.number.checked_add(1) != Some(incoming.block_number) {
5102 self.discard_preconfirmed_branch(cache);
5103 return Err(ReactiveError::InvalidInputRecord {
5104 message: format!(
5105 "pre-confirmed block {} is not the exact successor of canonical block {}",
5106 incoming.block_number, canonical.number
5107 ),
5108 });
5109 }
5110 if incoming.parent_hash != Some(canonical.hash) {
5111 self.discard_preconfirmed_branch(cache);
5112 return Err(ReactiveError::InvalidInputRecord {
5113 message: "pre-confirmed block parent does not match the canonical coverage hash"
5114 .into(),
5115 });
5116 }
5117 if let Some(active) = self.preconfirmed_branch.as_ref()
5118 && active.flashblock.same_payload(incoming)
5119 {
5120 if let (Some(active_index), Some(incoming_index)) =
5121 (active.flashblock.index, incoming.index)
5122 && incoming_index < active_index
5123 {
5124 return Err(ReactiveError::InvalidInputRecord {
5125 message: format!(
5126 "Flashblock index regressed from {active_index} to {incoming_index}"
5127 ),
5128 });
5129 }
5130 if active.flashblock.index.is_some()
5131 && active.flashblock.index == incoming.index
5132 && active.flashblock.content_hash != incoming.content_hash
5133 {
5134 self.discard_preconfirmed_branch(cache);
5135 return Err(ReactiveError::InvalidInputRecord {
5136 message: "same Flashblock payload/index carried conflicting cumulative content"
5137 .into(),
5138 });
5139 }
5140 install_preconfirmed_cache_context(cache, incoming);
5141 return Ok(());
5142 }
5143
5144 self.discard_preconfirmed_branch(cache);
5145 self.preconfirmed_branch = Some(PreconfirmedBranch {
5146 flashblock: incoming.clone(),
5147 canonical_cache: EvmCacheStateSnapshot::capture(cache),
5148 });
5149 install_preconfirmed_cache_context(cache, incoming);
5150 Ok(())
5151 }
5152
5153 fn ingest_batch_with_resync_direct(
5154 &mut self,
5155 cache: &mut EvmCache,
5156 batch: ReactiveInputBatch<N>,
5157 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
5158 let mut batch_report = self.ingest_batch_direct(cache, batch)?;
5159 if !batch_report.resyncs.is_empty() {
5160 let resync_report = execute_resync_requests(cache, &batch_report.resyncs);
5161 let unique_requests = resync_report
5165 .requested
5166 .iter()
5167 .map(|request| &request.id)
5168 .collect::<HashSet<_>>()
5169 .len();
5170 self.metrics
5171 .resync_requests
5172 .fetch_add(unique_requests as u64, Ordering::Relaxed);
5173 self.metrics
5174 .resync_failures
5175 .fetch_add(resync_report.failed.len() as u64, Ordering::Relaxed);
5176 self.remove_pending_resyncs(batch_report.resyncs.iter().map(|request| &request.id));
5177 self.record_journal_resync(&resync_report);
5178 batch_report
5179 .reports
5180 .push(Arc::new(ReactiveReport::Resynced(resync_report)));
5181 }
5182 Ok(batch_report)
5183 }
5184
5185 fn ingest_batch_direct(
5186 &mut self,
5187 cache: &mut EvmCache,
5188 batch: ReactiveInputBatch<N>,
5189 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
5190 let (records, chain_controls, batch_chain_id) = batch.into_runtime_parts();
5191 if let Some(chain_id) = batch_chain_id
5192 && chain_id != cache.chain_id()
5193 {
5194 return Err(ReactiveError::InvalidInputRecord {
5195 message: format!(
5196 "batch chain id {chain_id} does not match cache chain id {}",
5197 cache.chain_id()
5198 ),
5199 });
5200 }
5201 if !chain_controls.is_empty() && batch_chain_id.is_none() {
5202 return Err(ReactiveError::InvalidChainControl {
5203 message: "chain-control batches require an authoritative batch chain id".into(),
5204 });
5205 }
5206 for (record, _, _) in &records {
5207 record.validated_identity()?;
5208 if let Some(chain_id) = record.context.chain_id
5209 && chain_id != cache.chain_id()
5210 {
5211 return Err(ReactiveError::InvalidInputRecord {
5212 message: format!(
5213 "input chain id {chain_id} does not match cache chain id {}",
5214 cache.chain_id()
5215 ),
5216 });
5217 }
5218 }
5219 let records = sort_scoped_records(dedupe_scoped_records(records)?);
5220
5221 let mut batch_report = ReactiveBatchReport::default();
5222 let mut reports_to_dispatch = Vec::new();
5223 let control_split = validate_control_phase_order(&chain_controls)?;
5224 let (pre_record_controls, post_record_controls) = chain_controls.split_at(control_split);
5225 let pre_record_state =
5226 self.validate_ingest_sequence(pre_record_controls, post_record_controls, &records)?;
5227 self.validate_owner_catchup_against_journal(&pre_record_state, &records)?;
5228 let mut batch_dropped = BatchDroppedCanonical::default();
5229 for control in pre_record_controls {
5230 if let ChainControl::Reorg {
5231 common_ancestor,
5232 old_tip,
5233 ..
5234 } = control
5235 {
5236 batch_dropped.record_explicit(common_ancestor, old_tip);
5237 let drained = self
5238 .journal
5239 .iter()
5240 .filter(|entry| entry.block.number > common_ancestor.number)
5241 .map(|entry| entry.block)
5242 .collect::<Vec<_>>();
5243 batch_dropped.record_drained(&drained);
5244 }
5245 }
5246 let certified_progress_through = post_record_controls
5247 .iter()
5248 .filter_map(canonical_coverage_control_block)
5249 .map(|block| block.number)
5250 .max();
5251 for control in pre_record_controls.iter().cloned() {
5252 self.apply_chain_control(cache, control, &mut batch_report, &mut reports_to_dispatch);
5253 }
5254 let mut touched_addrs: HashSet<Address> = HashSet::new();
5259 let mut canonical_batch_block: Option<u64> = None;
5260
5261 for (record, audience, delivery_scope) in records {
5262 let raw_canonical_block = canonical_record_block(&record).copied();
5263 let canonical_block = raw_canonical_block.map(|block| {
5264 pre_record_state
5265 .resolved_canonical_blocks
5266 .get(&(block.number, block.hash))
5267 .copied()
5268 .unwrap_or(block)
5269 });
5270 let input_ref = record.input_ref();
5271 reports_to_dispatch.push(Arc::new(ReactiveReport::Input(InputReport {
5272 input_ref,
5273 context: record.context.clone(),
5274 provider: record.provider.clone(),
5275 _network: PhantomData,
5276 })));
5277
5278 let recovered_reorg = if delivery_scope.advances_canonical_state() {
5279 if let Some(block) = canonical_block.as_ref() {
5280 let gap_is_certified = delivery_scope == DeliveryScope::CanonicalProgress
5281 && certified_progress_through
5282 .is_some_and(|through| block.number <= through);
5283 let parentless_replacement_is_proven = raw_canonical_block.is_some_and(|raw| {
5284 raw.parent_hash.is_none()
5285 && batch_dropped.covers_implicit_number(raw.number)
5286 });
5287 self.recover_for_canonical_input(
5288 cache,
5289 block,
5290 gap_is_certified,
5291 parentless_replacement_is_proven,
5292 &mut reports_to_dispatch,
5293 )
5294 } else {
5295 None
5296 }
5297 } else {
5298 None
5299 };
5300 let recovered_reorg_for_input = recovered_reorg.is_some();
5301 if let Some(reorg_report) = recovered_reorg {
5302 self.metrics
5303 .reorgs_recovered
5304 .fetch_add(1, Ordering::Relaxed);
5305 remove_canceled_resyncs_from_batch(
5306 &mut batch_report.resyncs,
5307 &reorg_report.canceled_resyncs,
5308 );
5309 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5310 }
5311
5312 if reorg_signal_block(&record).is_some() {
5318 if delivery_scope.advances_canonical_state()
5319 && let Some(reorg_report) = self.recover_for_reorged_input(
5320 cache,
5321 &record,
5322 &mut batch_dropped,
5323 &mut reports_to_dispatch,
5324 )
5325 {
5326 self.metrics
5327 .reorgs_recovered
5328 .fetch_add(1, Ordering::Relaxed);
5329 remove_canceled_resyncs_from_batch(
5330 &mut batch_report.resyncs,
5331 &reorg_report.canceled_resyncs,
5332 );
5333 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5334 }
5335 continue;
5336 }
5337
5338 if delivery_scope == DeliveryScope::OwnerCatchup {
5345 self.validate_owner_catchup_record_against_current_journal(&record)?;
5346 }
5347
5348 if delivery_scope.advances_canonical_state()
5349 && let Some(block) = canonical_block.as_ref()
5350 {
5351 canonical_batch_block = Some(block.number);
5354 self.record_journal_input(block, input_ref);
5355 }
5356
5357 if delivery_scope.advances_canonical_state()
5363 && let Some(block) = canonical_block.as_ref()
5364 {
5365 match advance_block_for_canonical_record(cache, &record) {
5366 Some(Ok(())) => {
5367 cache.advance_compact_block(block.number, block.hash, block.timestamp, true)
5368 }
5369 Some(Err(err)) => {
5370 cache.advance_compact_block(
5371 block.number,
5372 block.hash,
5373 block.timestamp,
5374 false,
5375 );
5376 reports_to_dispatch.push(Arc::new(ReactiveReport::Error(
5377 ReactiveErrorReport {
5378 input_ref: Some(input_ref),
5379 message: err.to_string(),
5380 _network: PhantomData,
5381 },
5382 )));
5383 }
5384 None => cache.advance_compact_block(
5385 block.number,
5386 block.hash,
5387 block.timestamp,
5388 !recovered_reorg_for_input,
5389 ),
5390 }
5391 }
5392
5393 let executions = self.execute_handlers(cache, &record, input_ref, &audience)?;
5394 if executions.is_empty() {
5395 continue;
5396 }
5397
5398 reports_to_dispatch.push(Arc::new(ReactiveReport::Decoded(DecodedReport {
5399 input_ref,
5400 handler_ids: executions
5401 .iter()
5402 .map(|execution| execution.handler_id.clone())
5403 .collect(),
5404 _network: PhantomData,
5405 })));
5406
5407 detect_conflicts(input_ref, &executions)?;
5408
5409 let canonical_block_number = delivery_scope
5415 .advances_canonical_state()
5416 .then_some(canonical_block)
5417 .flatten()
5418 .map(|block| block.number);
5419
5420 for execution in executions {
5421 let diff = if execution.state_updates.is_empty() {
5422 StateDiff::default()
5423 } else {
5424 cache.apply_updates(&execution.state_updates)
5425 };
5426
5427 batch_report
5428 .resyncs
5429 .extend(execution.resyncs.iter().cloned());
5430 self.pending_resyncs
5431 .extend(execution.resyncs.iter().cloned());
5432 batch_report
5433 .speculative
5434 .extend(execution.speculative.iter().cloned());
5435
5436 let applied = AppliedReport {
5437 input_ref,
5438 handler_id: execution.handler_id,
5439 quality: execution.quality,
5440 tags: execution.tags,
5441 diff,
5442 state_updates: execution.state_updates,
5443 invalidations: execution.invalidations,
5444 resyncs: execution.resyncs,
5445 speculative: execution.speculative,
5446 hook_signals: execution.hook_signals,
5447 _network: PhantomData,
5448 };
5449 if let (Some(number), Some(registry)) =
5458 (canonical_block_number, self.freshness.as_mut())
5459 {
5460 for change in &applied.diff.slots {
5461 registry.valid_through_slot(change.address, change.slot, number);
5462 }
5463 }
5464
5465 if delivery_scope.advances_canonical_state() {
5473 collect_diff_addresses(&applied.diff, &mut touched_addrs);
5474 }
5475
5476 let report = Arc::new(ReactiveReport::Applied(applied.clone()));
5477 reports_to_dispatch.push(report);
5478 if let Some(block) = canonical_block.as_ref() {
5479 if delivery_scope.advances_canonical_state() {
5480 self.record_journal_applied(block, applied.clone());
5481 } else {
5482 self.record_journal_applied_if_present(block, applied.clone());
5483 }
5484 }
5485 batch_report.applied.push(applied);
5486 }
5487 }
5488
5489 for control in post_record_controls.iter().cloned() {
5494 if let Some(block) = canonical_coverage_control_block(&control) {
5495 canonical_batch_block = Some(
5496 canonical_batch_block.map_or(block.number, |current| current.max(block.number)),
5497 );
5498 }
5499 self.apply_chain_control(cache, control, &mut batch_report, &mut reports_to_dispatch);
5500 }
5501
5502 if self.root_gate_runnable(cache) {
5512 self.touched_since_gate
5513 .extend(touched_addrs.iter().copied());
5514 if self.root_gate_due(canonical_batch_block) {
5515 let accumulated = std::mem::take(&mut self.touched_since_gate);
5516 self.run_root_gate(
5517 cache,
5518 canonical_batch_block,
5519 &accumulated,
5520 &mut batch_report.resyncs,
5521 &mut reports_to_dispatch,
5522 );
5523 self.last_gate_block = canonical_batch_block;
5524 }
5525 } else {
5526 self.touched_since_gate.clear();
5533 }
5534
5535 batch_report.reports = reports_to_dispatch;
5536 Ok(batch_report)
5537 }
5538
5539 fn validate_owner_catchup_against_journal(
5553 &self,
5554 control_state: &ChainControlState,
5555 records: &[(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)],
5556 ) -> Result<(), ReactiveError> {
5557 for (record, _, delivery_scope) in records {
5558 if *delivery_scope != DeliveryScope::OwnerCatchup {
5559 continue;
5560 }
5561 if reorg_signal_block(record).is_some() {
5566 continue;
5567 }
5568 let context_block = canonical_record_block(record).ok_or_else(|| {
5569 ReactiveError::InvalidChainControl {
5570 message: "owner catch-up input has no canonical block identity".into(),
5571 }
5572 })?;
5573 let block = resolve_record_block_payload_metadata(record, *context_block)?;
5574 let invalidated_by_control = control_state
5575 .journal_invalidated_from
5576 .is_some_and(|from| block.number >= from);
5577 let rollbackable = !invalidated_by_control
5578 && self.journal.iter().any(|entry| {
5579 optional_block_refs_are_compatible(Some(&entry.block), Some(&block))
5580 });
5581 if !rollbackable {
5582 return Err(ReactiveError::OwnerCatchupOutsideJournal {
5583 number: block.number,
5584 hash: block.hash,
5585 });
5586 }
5587 }
5588 Ok(())
5589 }
5590
5591 fn validate_owner_catchup_record_against_current_journal(
5592 &self,
5593 record: &ReactiveInputRecord<N>,
5594 ) -> Result<(), ReactiveError> {
5595 let context_block =
5596 canonical_record_block(record).ok_or_else(|| ReactiveError::InvalidChainControl {
5597 message: "owner catch-up input has no canonical block identity".into(),
5598 })?;
5599 let block = resolve_record_block_payload_metadata(record, *context_block)?;
5600 if self
5601 .journal
5602 .iter()
5603 .any(|entry| optional_block_refs_are_compatible(Some(&entry.block), Some(&block)))
5604 {
5605 return Ok(());
5606 }
5607 Err(ReactiveError::OwnerCatchupOutsideJournal {
5608 number: block.number,
5609 hash: block.hash,
5610 })
5611 }
5612
5613 fn root_gate_runnable(&self, cache: &EvmCache) -> bool {
5618 if matches!(self.root_gate_cadence, RootGateCadence::Disabled) {
5619 return false;
5620 }
5621 let has_gated_targets = self
5622 .tracking
5623 .values()
5624 .any(|policy| !matches!(policy, TrackingPolicy::Slots { .. }));
5625 has_gated_targets && cache.account_proof_fetcher().is_some()
5626 }
5627
5628 fn root_gate_due(&self, canonical_block: Option<u64>) -> bool {
5632 let Some(block) = canonical_block else {
5633 return false;
5634 };
5635 match self.root_gate_cadence {
5636 RootGateCadence::Disabled => false,
5637 RootGateCadence::EveryNBlocks(n) => match self.last_gate_block {
5638 None => true,
5639 Some(last) => block >= last.saturating_add(n.get()),
5640 },
5641 }
5642 }
5643
5644 fn run_root_gate(
5670 &mut self,
5671 cache: &EvmCache,
5672 canonical_block: Option<u64>,
5673 touched: &HashSet<Address>,
5674 resyncs: &mut Vec<ResyncRequest>,
5675 reports: &mut Vec<Arc<ReactiveReport<N>>>,
5676 ) {
5677 if self.tracking.is_empty() {
5678 return;
5679 }
5680 let Some(block) = canonical_block else {
5681 return;
5682 };
5683 let Some(fetcher) = cache.account_proof_fetcher().cloned() else {
5684 return;
5685 };
5686
5687 let mut targets: Vec<(Address, bool)> = self
5690 .tracking
5691 .iter()
5692 .filter_map(|(address, policy)| match policy {
5693 TrackingPolicy::Slots { .. } => None,
5694 TrackingPolicy::WholeAccount => Some((*address, true)),
5695 TrackingPolicy::Scalars => Some((*address, false)),
5696 })
5697 .collect();
5698 if targets.is_empty() {
5699 return;
5700 }
5701 targets.sort_by_key(|(address, _)| *address);
5702
5703 let block_id = BlockId::number(block);
5704 let mut probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
5709 targets
5710 .iter()
5711 .map(|&(address, _)| (address, vec![]))
5712 .collect(),
5713 block_id,
5714 )
5715 .into_iter()
5716 .collect();
5717 for (address, whole_account) in targets {
5718 let Some(Ok(proof)) = probes.remove(&address) else {
5719 continue;
5722 };
5723
5724 let baseline = self.tracked_roots.get(&address).cloned();
5725 let Some(baseline) = baseline else {
5726 self.adopt_root(address, block, &proof);
5728 continue;
5729 };
5730
5731 if block <= baseline.last_block {
5736 continue;
5737 }
5738
5739 if whole_account {
5740 if proof.storage_hash == baseline.last_root {
5741 continue;
5743 }
5744 if !touched.contains(&address) {
5746 reports.push(Arc::new(ReactiveReport::CoverageGap(CoverageGapReport {
5748 address,
5749 block,
5750 _network: PhantomData,
5751 })));
5752 self.metrics.coverage_gaps.fetch_add(1, Ordering::Relaxed);
5753 resyncs.push(root_moved_account_resync(
5754 address,
5755 block,
5756 AccountFieldMask {
5757 balance: true,
5758 nonce: true,
5759 code: true,
5760 },
5761 ));
5762 }
5763 self.adopt_root(address, block, &proof);
5765 } else {
5766 let balance_moved = proof.balance != baseline.balance;
5769 let nonce_moved = proof.nonce != baseline.nonce;
5770 let code_moved = proof.code_hash != baseline.code_hash;
5771 if (balance_moved || nonce_moved || code_moved) && !touched.contains(&address) {
5772 resyncs.push(root_moved_account_resync(
5773 address,
5774 block,
5775 AccountFieldMask {
5776 balance: balance_moved,
5777 nonce: nonce_moved,
5778 code: code_moved,
5779 },
5780 ));
5781 }
5782 self.adopt_root(address, block, &proof);
5783 }
5784 }
5785 }
5786
5787 fn adopt_root(&mut self, address: Address, block: u64, proof: &AccountProof) {
5789 self.tracked_roots.insert(
5790 address,
5791 TrackedRoot {
5792 last_root: proof.storage_hash,
5793 last_block: block,
5794 balance: proof.balance,
5795 nonce: proof.nonce,
5796 code_hash: proof.code_hash,
5797 },
5798 );
5799 }
5800
5801 fn execute_handlers(
5802 &self,
5803 cache: &EvmCache,
5804 record: &ReactiveInputRecord<N>,
5805 input_ref: InputRef,
5806 audience: &DeliveryAudience,
5807 ) -> Result<Vec<HandlerExecution>, ReactiveError> {
5808 let mut executions = Vec::new();
5809 let candidates: Vec<_> = match &record.input {
5810 ReactiveInput::Log(log) => self.registry.log_handler_candidates(log),
5811 ReactiveInput::BlockHeader(_)
5812 | ReactiveInput::FullBlock(_)
5813 | ReactiveInput::PendingTxHash(_)
5814 | ReactiveInput::PendingTx(_) => self.registry.handlers().collect(),
5815 };
5816 for registered in candidates {
5817 match audience {
5818 DeliveryAudience::Owners(owners) if !owners.contains(®istered.id) => continue,
5819 DeliveryAudience::AllExcept(excluded) if excluded.contains(®istered.id) => {
5820 continue;
5821 }
5822 DeliveryAudience::All
5823 | DeliveryAudience::Owners(_)
5824 | DeliveryAudience::AllExcept(_) => {}
5825 }
5826 if !registered.matches(&record.input) {
5827 continue;
5828 }
5829
5830 let outcome = registered
5831 .handler
5832 .handle(&record.context, &record.input, cache)
5833 .map_err(|source| ReactiveError::HandlerFailed {
5834 handler_id: registered.id.clone(),
5835 source,
5836 })?;
5837
5838 if let Err(error) =
5839 validate_effects(input_ref, &record.context, ®istered.id, &outcome.effects)
5840 {
5841 if matches!(error, ReactiveError::InvalidPendingEffect { .. }) {
5842 self.metrics
5843 .pending_contamination
5844 .fetch_add(1, Ordering::Relaxed);
5845 }
5846 return Err(error);
5847 }
5848 executions.push(HandlerExecution::from_outcome(
5849 registered.id.clone(),
5850 input_ref,
5851 outcome,
5852 matches!(
5853 record.context.chain_status,
5854 ChainStatus::Preconfirmed { .. }
5855 ),
5856 ));
5857 }
5858 Ok(executions)
5859 }
5860
5861 fn dispatch_reports(&self, reports: &[Arc<ReactiveReport<N>>]) {
5862 for report in reports {
5863 for hook in &self.hooks {
5864 hook.on_report(report.clone());
5865 }
5866 }
5867 }
5868
5869 fn apply_chain_control(
5870 &mut self,
5871 cache: &mut EvmCache,
5872 control: ChainControl,
5873 batch_report: &mut ReactiveBatchReport<N>,
5874 reports: &mut Vec<Arc<ReactiveReport<N>>>,
5875 ) {
5876 match &control {
5877 ChainControl::Safe(block) => set_or_enrich_block_ref(&mut self.safe_head, block),
5878 ChainControl::Finalized(block) => {
5879 set_or_enrich_block_ref(&mut self.finalized_head, block);
5880 }
5881 ChainControl::CanonicalProgress(block)
5882 | ChainControl::Barrier {
5883 block: Some(block), ..
5884 } => {
5885 let preserve_env = self.coverage_head.as_ref().is_some_and(|current| {
5886 optional_block_refs_are_compatible(Some(current), Some(block))
5887 });
5888 cache.advance_compact_block(
5889 block.number,
5890 block.hash,
5891 block.timestamp,
5892 preserve_env,
5893 );
5894 advance_or_enrich_coverage(&mut self.coverage_head, block);
5895 let enriched = self.journal_entry_mut(block).block;
5896 advance_or_enrich_coverage(&mut self.coverage_head, &enriched);
5897 self.trim_journal();
5898 }
5899 ChainControl::LogCoverage(block) => {
5900 set_or_enrich_block_ref(&mut self.log_coverage_head, block);
5903 }
5904 ChainControl::Barrier { block: None, .. } => {}
5905 ChainControl::Reorg {
5906 common_ancestor,
5907 old_tip,
5908 ..
5909 } => {
5910 cache.invalidate_cached_block_hashes_from(common_ancestor.number.saturating_add(1));
5911 self.rebase_validation_state_from(common_ancestor.number.saturating_add(1));
5912 let dropped = if let Some(ancestor_index) = self.journal.iter().rposition(|entry| {
5913 entry.block.number == common_ancestor.number
5914 && entry.block.hash == common_ancestor.hash
5915 }) {
5916 self.drain_journal_after(ancestor_index)
5917 } else {
5918 if self
5923 .journal
5924 .front()
5925 .is_none_or(|entry| entry.block.number > common_ancestor.number)
5926 {
5927 reports.extend(
5928 self.warn_under_recovery(common_ancestor.number.saturating_add(1)),
5929 );
5930 }
5931 self.drain_journal_from_number(common_ancestor.number.saturating_add(1))
5932 };
5933
5934 let reorg_report = self
5935 .recover_dropped_journals(cache, dropped, ReorgReason::Explicit)
5936 .unwrap_or_else(|| ReorgReport {
5937 dropped: Some(*old_tip),
5938 dropped_blocks: Vec::new(),
5939 dropped_inputs: Vec::new(),
5940 rollback_updates: Vec::new(),
5941 rollback_diff: StateDiff::default(),
5942 purge_updates: Vec::new(),
5943 purge_diff: StateDiff::default(),
5944 canceled_resyncs: self
5945 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(old_tip)),
5946 reason: ReorgReason::Explicit,
5947 _network: PhantomData,
5948 });
5949 remove_canceled_resyncs_from_batch(
5950 &mut batch_report.resyncs,
5951 &reorg_report.canceled_resyncs,
5952 );
5953 self.metrics
5954 .reorgs_recovered
5955 .fetch_add(1, Ordering::Relaxed);
5956 reports.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5957
5958 if self.safe_head.as_ref().is_some_and(|head| {
5959 head.number > common_ancestor.number
5960 || (head.number == common_ancestor.number
5961 && head.hash != common_ancestor.hash)
5962 }) {
5963 self.safe_head = None;
5964 }
5965 if self.finalized_head.as_ref().is_some_and(|head| {
5966 head.number > common_ancestor.number
5967 || (head.number == common_ancestor.number
5968 && head.hash != common_ancestor.hash)
5969 }) {
5970 self.finalized_head = None;
5971 }
5972 let mut enriched_ancestor = *common_ancestor;
5973 if let Some(entry) = self.journal.iter().find(|entry| {
5974 entry.block.number == common_ancestor.number
5975 && entry.block.hash == common_ancestor.hash
5976 }) {
5977 enrich_block_ref(&mut enriched_ancestor, &entry.block);
5978 }
5979 if let Some(current) = self.coverage_head.as_ref()
5980 && current.number == common_ancestor.number
5981 && current.hash == common_ancestor.hash
5982 {
5983 enrich_block_ref(&mut enriched_ancestor, current);
5984 }
5985 self.coverage_head = Some(enriched_ancestor);
5986 cache.advance_compact_block(
5987 enriched_ancestor.number,
5988 enriched_ancestor.hash,
5989 enriched_ancestor.timestamp,
5990 false,
5991 );
5992 let enriched_ancestor = self.journal_entry_mut(&enriched_ancestor).block;
5993 self.coverage_head = Some(enriched_ancestor);
5994 self.trim_journal();
5995 }
5996 }
5997 reports.push(Arc::new(ReactiveReport::ChainControl(ChainControlReport {
5998 control,
5999 })));
6000 }
6001
6002 fn validate_ingest_sequence(
6003 &self,
6004 pre_record_controls: &[ChainControl],
6005 post_record_controls: &[ChainControl],
6006 records: &[(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)],
6007 ) -> Result<ChainControlState, ReactiveError> {
6008 let mut controls =
6009 Vec::with_capacity(pre_record_controls.len() + post_record_controls.len());
6010 controls.extend_from_slice(pre_record_controls);
6011 controls.extend_from_slice(post_record_controls);
6012 let state = CanonicalSequenceState::new(
6013 self.journal.iter().map(|entry| entry.block).collect(),
6014 self.coverage_head,
6015 self.safe_head,
6016 self.finalized_head,
6017 )
6018 .with_log_coverage_head(self.log_coverage_head);
6019 let record_metadata = records
6020 .iter()
6021 .map(|(record, _, scope)| (record, *scope))
6022 .collect::<Vec<_>>();
6023 let validation = validate_canonical_sequence_parts(
6024 &state,
6025 &controls,
6026 &record_metadata,
6027 CanonicalSequenceValidationPolicy::ObserveIncompleteRollback,
6028 )
6029 .map_err(CanonicalSequenceError::into_reactive_error)?;
6030 let mut resolved_canonical_blocks = HashMap::new();
6031 for mutation in validation.mutations() {
6032 if let CanonicalSequenceMutation::Canonical(block) = mutation {
6033 resolved_canonical_blocks
6034 .entry((block.number, block.hash))
6035 .and_modify(|known| enrich_block_ref(known, block))
6036 .or_insert(*block);
6037 }
6038 }
6039 Ok(ChainControlState {
6040 journal_invalidated_from: pre_record_controls
6041 .iter()
6042 .filter_map(|control| match control {
6043 ChainControl::Reorg {
6044 common_ancestor, ..
6045 } => Some(common_ancestor.number.saturating_add(1)),
6046 _ => None,
6047 })
6048 .min(),
6049 resolved_canonical_blocks,
6050 })
6051 }
6052
6053 fn recover_for_canonical_input(
6054 &mut self,
6055 cache: &mut EvmCache,
6056 block: &BlockRef,
6057 gap_is_certified: bool,
6058 parentless_replacement_is_proven: bool,
6059 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
6060 ) -> Option<ReorgReport<N>> {
6061 let latest = self
6062 .coverage_head
6063 .or_else(|| self.journal.back().map(|entry| entry.block))?;
6064
6065 if latest.number == block.number && latest.hash == block.hash {
6066 return None;
6067 }
6068
6069 if self
6070 .journal
6071 .iter()
6072 .any(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
6073 {
6074 return None;
6075 }
6076
6077 if latest.number.checked_add(1) == Some(block.number)
6078 && (block.parent_hash == Some(latest.hash)
6079 || (parentless_replacement_is_proven && block.parent_hash.is_none()))
6080 {
6081 return None;
6082 }
6083
6084 if latest
6085 .number
6086 .checked_add(1)
6087 .is_some_and(|next| block.number > next)
6088 {
6089 if !gap_is_certified {
6096 self.metrics.missed_ranges.fetch_add(1, Ordering::Relaxed);
6097 health_reports.extend(self.escalate_trust(block.number));
6098 health_reports.push(Arc::new(ReactiveReport::MissedBlockRange(
6099 MissedRangeReport {
6100 from: latest.number + 1,
6101 to: block.number - 1,
6102 block: block.number,
6103 _network: PhantomData,
6104 },
6105 )));
6106 }
6107 return None;
6108 }
6109
6110 let (dropped, authenticated_anchor) = if let Some(parent_hash) = block.parent_hash {
6111 if let Some(parent_index) = self.journal.iter().rposition(|entry| {
6112 entry.block.number.checked_add(1) == Some(block.number)
6113 && entry.block.hash == parent_hash
6114 }) {
6115 let parent = self.journal[parent_index].block;
6116 cache.invalidate_cached_block_hashes_from(parent.number.saturating_add(1));
6117 (self.drain_journal_after(parent_index), Some(parent))
6118 } else {
6119 let proven_finalized_anchor = self.finalized_head.filter(|finalized| {
6125 finalized.number.checked_add(1) == Some(block.number)
6126 && parent_hash == finalized.hash
6127 });
6128 let invalidated_from = proven_finalized_anchor
6129 .map_or(0, |finalized| finalized.number.saturating_add(1));
6130 cache.invalidate_cached_block_hashes_from(invalidated_from);
6131 if block.number > 0 {
6132 cache.set_cached_block_hash(block.number.saturating_sub(1), parent_hash);
6136 }
6137 health_reports.extend(self.warn_under_recovery(block.number));
6138 let dropped = if let Some(finalized) = proven_finalized_anchor {
6139 self.drain_journal_from_number(finalized.number.saturating_add(1))
6140 } else {
6141 self.drain_journal_from_number(0)
6142 };
6143 (dropped, proven_finalized_anchor)
6144 }
6145 } else {
6146 cache.invalidate_cached_block_hashes_from(0);
6148 health_reports.extend(self.warn_under_recovery(block.number));
6149 (self.drain_journal_from_number(0), None)
6150 };
6151
6152 self.rebase_validation_state_from(
6153 authenticated_anchor.map_or(0, |anchor| anchor.number.saturating_add(1)),
6154 );
6155 let report = self
6156 .recover_dropped_journals(cache, dropped, ReorgReason::ParentMismatch)
6157 .or_else(|| {
6158 Some(ReorgReport {
6159 dropped: Some(latest),
6160 dropped_blocks: Vec::new(),
6161 dropped_inputs: Vec::new(),
6162 rollback_updates: Vec::new(),
6163 rollback_diff: StateDiff::default(),
6164 purge_updates: Vec::new(),
6165 purge_diff: StateDiff::default(),
6166 canceled_resyncs: self
6167 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&latest)),
6168 reason: ReorgReason::ParentMismatch,
6169 _network: PhantomData,
6170 })
6171 });
6172 self.coverage_head = authenticated_anchor;
6173 for head in [&mut self.safe_head, &mut self.finalized_head] {
6174 if head.is_some_and(|head| {
6175 authenticated_anchor.is_none_or(|anchor| {
6176 head.number > anchor.number
6177 || (head.number == anchor.number && head.hash != anchor.hash)
6178 })
6179 }) {
6180 *head = None;
6181 }
6182 }
6183 if let Some(anchor) = authenticated_anchor {
6184 cache.advance_compact_block(anchor.number, anchor.hash, anchor.timestamp, false);
6185 }
6186 report
6187 }
6188
6189 fn recover_for_reorged_input(
6190 &mut self,
6191 cache: &mut EvmCache,
6192 record: &ReactiveInputRecord<N>,
6193 batch_dropped: &mut BatchDroppedCanonical,
6194 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
6195 ) -> Option<ReorgReport<N>> {
6196 let (incoming_dropped_block, reason) = reorg_signal_block(record)?;
6197 if batch_dropped.contains(&incoming_dropped_block) {
6198 let canceled_resyncs = self
6203 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&incoming_dropped_block));
6204 return (!canceled_resyncs.is_empty()).then(|| ReorgReport {
6205 dropped: Some(incoming_dropped_block),
6206 dropped_blocks: vec![incoming_dropped_block],
6207 dropped_inputs: Vec::new(),
6208 rollback_updates: Vec::new(),
6209 rollback_diff: StateDiff::default(),
6210 purge_updates: Vec::new(),
6211 purge_diff: StateDiff::default(),
6212 canceled_resyncs,
6213 reason,
6214 _network: PhantomData,
6215 });
6216 }
6217 let exact_index = self.journal.iter().position(|entry| {
6218 entry.block.number == incoming_dropped_block.number
6219 && entry.block.hash == incoming_dropped_block.hash
6220 });
6221 let mut dropped_block = exact_index
6222 .map(|index| self.journal[index].block)
6223 .or_else(|| {
6224 self.coverage_head.filter(|known| {
6225 known.number == incoming_dropped_block.number
6226 && known.hash == incoming_dropped_block.hash
6227 })
6228 })
6229 .unwrap_or(incoming_dropped_block);
6230 enrich_block_ref(&mut dropped_block, &incoming_dropped_block);
6231 let replacement_is_known = exact_index.is_none()
6232 && (self.journal.iter().any(|entry| {
6233 entry.block.number == dropped_block.number && entry.block.hash != dropped_block.hash
6234 }) || self.coverage_head.is_some_and(|head| {
6235 head.number == dropped_block.number && head.hash != dropped_block.hash
6236 }));
6237
6238 if replacement_is_known {
6239 let canceled_resyncs =
6243 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
6244 return (!canceled_resyncs.is_empty()).then(|| ReorgReport {
6245 dropped: Some(dropped_block),
6246 dropped_blocks: vec![dropped_block],
6247 dropped_inputs: Vec::new(),
6248 rollback_updates: Vec::new(),
6249 rollback_diff: StateDiff::default(),
6250 purge_updates: Vec::new(),
6251 purge_diff: StateDiff::default(),
6252 canceled_resyncs,
6253 reason,
6254 _network: PhantomData,
6255 });
6256 }
6257
6258 let authenticated_anchor = exact_index.and_then(|index| {
6259 let ancestor_number = dropped_block.number.checked_sub(1)?;
6260 let retained = self
6261 .journal
6262 .iter()
6263 .take(index)
6264 .rev()
6265 .find(|entry| entry.block.number == ancestor_number)
6266 .map(|entry| entry.block);
6267 let synthetic_parent = dropped_block.parent_hash.map(|hash| BlockRef {
6268 number: ancestor_number,
6269 hash,
6270 parent_hash: None,
6271 timestamp: None,
6272 });
6273 let finalized_fallback = self
6274 .finalized_head
6275 .filter(|head| head.number == ancestor_number);
6276 let mut anchor = retained.or(synthetic_parent).or(finalized_fallback)?;
6277 for head in [self.safe_head.as_ref(), self.finalized_head.as_ref()]
6278 .into_iter()
6279 .flatten()
6280 {
6281 if head.number == anchor.number && head.hash == anchor.hash {
6282 enrich_block_ref(&mut anchor, head);
6283 }
6284 }
6285 Some(anchor)
6286 });
6287
6288 cache.invalidate_cached_block_hashes_from(dropped_block.number);
6289 let dropped = if let Some(index) = exact_index {
6290 self.drain_journal_from(index)
6291 } else {
6292 health_reports.extend(self.warn_under_recovery(dropped_block.number));
6293 self.drain_journal_from_number(dropped_block.number)
6294 };
6295 let drained_blocks = dropped.iter().map(|entry| entry.block).collect::<Vec<_>>();
6296 batch_dropped.record_drained(&drained_blocks);
6297 batch_dropped.record_identity(&dropped_block);
6298 self.rebase_validation_state_from(dropped_block.number);
6299
6300 let recovered_journal = !dropped.is_empty();
6301 let report = if !recovered_journal {
6302 let canceled_resyncs =
6303 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
6304 Some(ReorgReport {
6305 dropped: Some(dropped_block),
6306 dropped_blocks: Vec::new(),
6307 dropped_inputs: Vec::new(),
6308 rollback_updates: Vec::new(),
6309 rollback_diff: StateDiff::default(),
6310 purge_updates: Vec::new(),
6311 purge_diff: StateDiff::default(),
6312 canceled_resyncs,
6313 reason,
6314 _network: PhantomData,
6315 })
6316 } else {
6317 self.recover_dropped_journals(cache, dropped, reason)
6318 };
6319
6320 if recovered_journal {
6321 if let Some(anchor) = authenticated_anchor {
6322 self.coverage_head = Some(anchor);
6323 }
6324 let coverage = self.coverage_head;
6325 for head in [&mut self.safe_head, &mut self.finalized_head] {
6326 if head.is_some_and(|head| {
6327 coverage.is_none_or(|coverage| {
6328 head.number > coverage.number
6329 || (head.number == coverage.number && head.hash != coverage.hash)
6330 })
6331 }) {
6332 *head = None;
6333 }
6334 }
6335 }
6336
6337 if recovered_journal
6338 && report.is_some()
6339 && let Some(head) = self.coverage_head
6340 {
6341 cache.advance_compact_block(head.number, head.hash, head.timestamp, false);
6342 }
6343 report
6344 }
6345
6346 fn warn_under_recovery(&mut self, reorg_number: u64) -> Option<Arc<ReactiveReport<N>>> {
6358 let oldest_journaled = self.journal.front().map(|entry| entry.block.number);
6359 tracing::warn!(
6360 reorg_block = reorg_number,
6361 oldest_journaled = ?oldest_journaled,
6362 journal_depth = self.config.journal_depth,
6363 "reactive reorg recovery is incomplete: the reorged block is no longer \
6364 in the journal, so effects from blocks aged out of the journal are \
6365 neither rolled back nor purged (the freshness/validation loop is the \
6366 backstop). Increase ReactiveConfig::journal_depth to recover deeper \
6367 reorgs precisely."
6368 );
6369
6370 self.metrics.deep_reorgs.fetch_add(1, Ordering::Relaxed);
6371
6372 self.escalate_trust(reorg_number)
6373 }
6374
6375 fn record_journal_input(&mut self, block: &BlockRef, input_ref: InputRef) {
6376 advance_or_enrich_coverage(&mut self.coverage_head, block);
6377 let entry = self.journal_entry_mut(block);
6378 let enriched = entry.block;
6379 if !entry.inputs.contains(&input_ref) {
6380 entry.inputs.push(input_ref);
6381 }
6382 advance_or_enrich_coverage(&mut self.coverage_head, &enriched);
6383 self.trim_journal();
6384 }
6385
6386 fn record_journal_applied(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
6387 let entry = self.journal_entry_mut(block);
6388 if !entry.handler_ids.contains(&applied.handler_id) {
6389 entry.handler_ids.push(applied.handler_id.clone());
6390 }
6391 entry.rollback_diffs.push(applied.diff.clone());
6392 entry.applied.push(applied);
6393 self.trim_journal();
6394 }
6395
6396 fn record_journal_applied_if_present(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
6397 let Some(entry) = self
6398 .journal
6399 .iter_mut()
6400 .find(|entry| entry.block.number == block.number && entry.block.hash == block.hash)
6401 else {
6402 return;
6403 };
6404 if !entry.handler_ids.contains(&applied.handler_id) {
6405 entry.handler_ids.push(applied.handler_id.clone());
6406 }
6407 entry.rollback_diffs.push(applied.diff.clone());
6408 entry.applied.push(applied);
6409 }
6410
6411 fn record_journal_resync(&mut self, report: &ResyncReport) {
6412 if report.diff.is_empty() {
6413 return;
6414 }
6415 let Some(block) = single_hash_pinned_resync_block(report) else {
6416 return;
6417 };
6418 let entry = self.journal_entry_mut(&block);
6419 entry.rollback_diffs.push(report.diff.clone());
6420 entry.resynced.push(report.clone());
6421 self.trim_journal();
6422 }
6423
6424 fn journal_entry_mut(&mut self, block: &BlockRef) -> &mut BlockJournal<N> {
6425 if let Some(index) = self
6426 .journal
6427 .iter()
6428 .position(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
6429 {
6430 enrich_block_ref(&mut self.journal[index].block, block);
6431 return &mut self.journal[index];
6432 }
6433
6434 self.journal.push_back(BlockJournal {
6435 block: *block,
6436 inputs: Vec::new(),
6437 applied: Vec::new(),
6438 handler_ids: Vec::new(),
6439 resynced: Vec::new(),
6440 rollback_diffs: Vec::new(),
6441 });
6442 let index = self.journal.len() - 1;
6443 &mut self.journal[index]
6444 }
6445
6446 fn trim_journal(&mut self) {
6447 if self.config.journal_depth == 0 {
6448 self.journal.clear();
6449 return;
6450 }
6451 while self.journal.len() > self.config.journal_depth {
6452 self.journal.pop_front();
6453 }
6454 }
6455
6456 fn drain_journal_after(&mut self, index: usize) -> Vec<BlockJournal<N>> {
6457 self.journal.drain((index + 1)..).collect()
6458 }
6459
6460 fn drain_journal_from(&mut self, index: usize) -> Vec<BlockJournal<N>> {
6461 self.journal.drain(index..).collect()
6462 }
6463
6464 fn drain_journal_from_number(&mut self, number: u64) -> Vec<BlockJournal<N>> {
6465 let Some(index) = self
6466 .journal
6467 .iter()
6468 .position(|entry| entry.block.number >= number)
6469 else {
6470 return Vec::new();
6471 };
6472 self.drain_journal_from(index)
6473 }
6474
6475 fn recover_dropped_journals(
6476 &mut self,
6477 cache: &mut EvmCache,
6478 dropped: Vec<BlockJournal<N>>,
6479 reason: ReorgReason,
6480 ) -> Option<ReorgReport<N>> {
6481 if dropped.is_empty() {
6482 return None;
6483 }
6484
6485 let first_dropped_block = dropped
6486 .iter()
6487 .map(|entry| entry.block.number)
6488 .min()
6489 .expect("non-empty dropped journal set");
6490 self.rebase_validation_state_from(first_dropped_block);
6491 if self
6492 .safe_head
6493 .is_some_and(|head| head.number >= first_dropped_block)
6494 {
6495 self.safe_head = None;
6496 }
6497
6498 let dropped_blocks: Vec<_> = dropped.iter().map(|entry| entry.block).collect();
6499 let dropped_inputs: Vec<_> = dropped
6500 .iter()
6501 .flat_map(|entry| entry.inputs.iter().copied())
6502 .collect();
6503 let canceled_resyncs = self.cancel_resyncs_for_dropped_blocks(&dropped_blocks);
6504 let purge_scopes = purge_scopes_for_dropped_journals(&dropped);
6505 let rollback_updates = rollback_updates_for_dropped_journals(&dropped, &purge_scopes);
6506 let purge_updates: Vec<_> = purge_scopes
6507 .iter()
6508 .map(|(address, scope)| StateUpdate::purge(*address, scope.clone()))
6509 .collect();
6510
6511 let rollback_diff = if rollback_updates.is_empty() {
6512 StateDiff::default()
6513 } else {
6514 cache.apply_updates(&rollback_updates)
6515 };
6516 let purge_diff = if purge_updates.is_empty() {
6517 StateDiff::default()
6518 } else {
6519 cache.apply_updates(&purge_updates)
6520 };
6521 self.coverage_head = self.journal.back().map(|entry| entry.block);
6522
6523 Some(ReorgReport {
6524 dropped: dropped_blocks.first().cloned(),
6525 dropped_blocks,
6526 dropped_inputs,
6527 rollback_updates,
6528 rollback_diff,
6529 purge_updates,
6530 purge_diff,
6531 canceled_resyncs,
6532 reason,
6533 _network: PhantomData,
6534 })
6535 }
6536
6537 fn rebase_validation_state_from(&mut self, first_dropped_block: u64) {
6538 if let Some(freshness) = self.freshness.as_mut() {
6539 freshness.invalidate_valid_through_from(first_dropped_block);
6540 }
6541 self.tracked_roots
6542 .retain(|_, baseline| baseline.last_block < first_dropped_block);
6543 if self
6544 .last_gate_block
6545 .is_some_and(|block| block >= first_dropped_block)
6546 {
6547 self.last_gate_block = self
6548 .tracked_roots
6549 .values()
6550 .map(|baseline| baseline.last_block)
6551 .max();
6552 }
6553 self.touched_since_gate.clear();
6557 }
6558
6559 fn cancel_resyncs_for_dropped_blocks(
6560 &mut self,
6561 dropped_blocks: &[BlockRef],
6562 ) -> Vec<ResyncRequest> {
6563 let mut canceled = Vec::new();
6564 self.pending_resyncs.retain(|request| {
6565 let should_cancel = resync_request_targets_dropped_block(request, dropped_blocks);
6566 if should_cancel {
6567 canceled.push(request.clone());
6568 }
6569 !should_cancel
6570 });
6571 canceled
6572 }
6573
6574 fn remove_pending_resyncs<'a>(&mut self, ids: impl IntoIterator<Item = &'a ResyncId>) {
6575 let ids: HashSet<_> = ids.into_iter().cloned().collect();
6576 self.pending_resyncs
6577 .retain(|request| !ids.contains(&request.id));
6578 }
6579}
6580
6581fn install_preconfirmed_cache_context(cache: &mut EvmCache, flashblock: &FlashblockRef) {
6582 cache.set_block(BlockId::pending());
6583 cache.set_block_context(Some(flashblock.block_number), flashblock.base_fee_per_gas);
6584 cache.set_coinbase(flashblock.beneficiary);
6585 cache.set_prevrandao(flashblock.prevrandao);
6586 cache.set_block_gas_limit(flashblock.gas_limit);
6587 cache.set_timestamp(flashblock.timestamp);
6588}
6589
6590pub fn validate_canonical_sequence<N: Network>(
6622 state: &CanonicalSequenceState,
6623 batch: &ReactiveInputBatch<N>,
6624) -> Result<CanonicalSequenceValidation, ReactiveError> {
6625 validate_canonical_sequence_diagnostic(state, batch)
6626 .map_err(CanonicalSequenceError::into_reactive_error)
6627}
6628
6629pub fn validate_canonical_sequence_diagnostic<N: Network>(
6644 state: &CanonicalSequenceState,
6645 batch: &ReactiveInputBatch<N>,
6646) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6647 validate_canonical_sequence_internal(
6648 state,
6649 batch,
6650 CanonicalSequenceValidationPolicy::RequireCompleteRollback,
6651 )
6652}
6653
6654pub fn normalize_and_validate_canonical_sequence<N: Network>(
6677 state: &CanonicalSequenceState,
6678 batch: &ReactiveInputBatch<N>,
6679) -> Result<CanonicalSequenceValidation, ReactiveError> {
6680 normalize_and_validate_canonical_sequence_diagnostic(state, batch)
6681 .map_err(CanonicalSequenceError::into_reactive_error)
6682}
6683
6684pub fn normalize_and_validate_canonical_sequence_diagnostic<N: Network>(
6699 state: &CanonicalSequenceState,
6700 batch: &ReactiveInputBatch<N>,
6701) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6702 validate_canonical_sequence_internal(
6703 state,
6704 batch,
6705 CanonicalSequenceValidationPolicy::RequireCompleteRollbackNormalizeCoverage,
6706 )
6707}
6708
6709fn validate_canonical_sequence_internal<N: Network>(
6710 state: &CanonicalSequenceState,
6711 batch: &ReactiveInputBatch<N>,
6712 policy: CanonicalSequenceValidationPolicy,
6713) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6714 let records = batch
6715 .records()
6716 .iter()
6717 .enumerate()
6718 .map(|(index, record)| {
6719 (
6720 record.clone(),
6721 DeliveryAudience::All,
6722 batch
6723 .record_delivery_scope(index)
6724 .expect("enumerated record always has a delivery scope"),
6725 )
6726 })
6727 .collect::<Vec<_>>();
6728 let records = sort_scoped_records(dedupe_scoped_records(records)?);
6729 let records = records
6730 .iter()
6731 .map(|(record, _, scope)| (record, *scope))
6732 .collect::<Vec<_>>();
6733 validate_canonical_sequence_parts(state, batch.chain_controls(), &records, policy)
6734}
6735
6736#[derive(Clone, Copy)]
6737enum CanonicalSequenceValidationPolicy {
6738 RequireCompleteRollback,
6739 RequireCompleteRollbackNormalizeCoverage,
6740 ObserveIncompleteRollback,
6741}
6742
6743#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6746#[non_exhaustive]
6747pub enum CanonicalRollbackKind {
6748 Explicit,
6750 Removed,
6752 ImplicitParent,
6754 MissingReplacement,
6756}
6757
6758#[derive(Debug, thiserror::Error)]
6763#[non_exhaustive]
6764pub enum CanonicalSequenceError {
6765 #[error(transparent)]
6767 Invalid(#[from] ReactiveError),
6768 #[error(
6771 "{kind:?} rollback after block {common_ancestor} exceeds retained canonical history starting at {oldest_retained:?}"
6772 )]
6773 IncompleteRollback {
6774 common_ancestor: u64,
6776 oldest_retained: Option<u64>,
6778 kind: CanonicalRollbackKind,
6780 },
6781}
6782
6783#[derive(Clone, Copy, Debug)]
6784struct RequiredReorgAnchor {
6785 number: u64,
6786 block: Option<BlockRef>,
6787 permits_missing_child_parent: bool,
6788 must_be_consumed: bool,
6789}
6790
6791#[derive(Debug)]
6792struct SequenceRewind {
6793 common_ancestor: Option<BlockRef>,
6794 dropped: Vec<BlockRef>,
6795}
6796
6797impl RequiredReorgAnchor {
6798 const fn hash(self) -> Option<B256> {
6799 match self.block {
6800 Some(block) => Some(block.hash),
6801 None => None,
6802 }
6803 }
6804}
6805
6806impl CanonicalSequenceError {
6807 pub const fn requires_history(&self) -> bool {
6810 matches!(self, Self::IncompleteRollback { .. })
6811 }
6812
6813 pub fn into_reactive_error(self) -> ReactiveError {
6815 match self {
6816 Self::Invalid(error) => error,
6817 Self::IncompleteRollback {
6818 common_ancestor,
6819 oldest_retained,
6820 kind,
6821 } => ReactiveError::InvalidChainControl {
6822 message: format!(
6823 "{kind:?} rollback after block {common_ancestor} exceeds retained canonical history starting at {oldest_retained:?}"
6824 ),
6825 },
6826 }
6827 }
6828}
6829
6830impl CanonicalSequenceValidationPolicy {
6831 const fn requires_complete_rollback(self) -> bool {
6832 matches!(
6833 self,
6834 Self::RequireCompleteRollback | Self::RequireCompleteRollbackNormalizeCoverage
6835 )
6836 }
6837
6838 const fn normalizes_coverage(self) -> bool {
6839 matches!(self, Self::RequireCompleteRollbackNormalizeCoverage)
6840 }
6841}
6842
6843fn validate_canonical_sequence_parts<N: Network>(
6844 initial: &CanonicalSequenceState,
6845 controls: &[ChainControl],
6846 records: &[(&ReactiveInputRecord<N>, DeliveryScope)],
6847 policy: CanonicalSequenceValidationPolicy,
6848) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6849 validate_canonical_sequence_snapshot(initial)?;
6850 let control_split = validate_control_phase_order(controls)?;
6851 let (pre_record_controls, post_record_controls) = controls.split_at(control_split);
6852 let mut state = initial.clone();
6853 let mut asserted_blocks = HashMap::<u64, BlockRef>::new();
6854 let mut mutations = Vec::new();
6855 let mut normalized_chain_controls = Vec::with_capacity(controls.len());
6856 let mut batch_dropped = BatchDroppedCanonical::default();
6857 let mut removed_assertions = HashMap::<(u64, B256), BlockRef>::new();
6858 let mut removed_heights_by_hash = HashMap::<B256, u64>::new();
6859 let mut record_proof_control_identities = HashSet::<(u64, B256)>::new();
6860 let rollback_oldest = initial
6861 .retained_canonical_history
6862 .first()
6863 .map(|block| block.number);
6864
6865 for control in pre_record_controls {
6866 normalized_chain_controls.push(control.clone());
6867 validate_sequence_control(&state, control)?;
6868 assert_chain_control_identities(&mut asserted_blocks, control)?;
6869 let ChainControl::Reorg {
6870 common_ancestor,
6871 old_tip,
6872 ..
6873 } = control
6874 else {
6875 unreachable!("phase validation leaves only reorg controls before records")
6876 };
6877 let exact_ancestor = state.retained_canonical_history.iter().any(|block| {
6878 block.number == common_ancestor.number && block.hash == common_ancestor.hash
6879 });
6880 let rollback_horizon_covers_ancestor = state
6881 .retained_canonical_history
6882 .first()
6883 .is_some_and(|oldest| oldest.number <= common_ancestor.number);
6884 if policy.requires_complete_rollback()
6885 && !exact_ancestor
6886 && !rollback_horizon_covers_ancestor
6887 {
6888 return Err(CanonicalSequenceError::IncompleteRollback {
6889 common_ancestor: common_ancestor.number,
6890 oldest_retained: rollback_oldest,
6891 kind: CanonicalRollbackKind::Explicit,
6892 });
6893 }
6894 let dropped = state
6895 .retained_canonical_history
6896 .iter()
6897 .copied()
6898 .filter(|block| block.number > common_ancestor.number)
6899 .collect::<Vec<_>>();
6900 state
6901 .retained_canonical_history
6902 .retain(|block| block.number <= common_ancestor.number);
6903 upsert_sequence_history(&mut state.retained_canonical_history, common_ancestor)?;
6904 let mut enriched_ancestor = *common_ancestor;
6905 if let Some(retained) = state.retained_canonical_history.iter().find(|block| {
6906 block.number == common_ancestor.number && block.hash == common_ancestor.hash
6907 }) {
6908 enrich_block_ref(&mut enriched_ancestor, retained);
6909 }
6910 if let Some(coverage) = state.coverage_head.as_ref()
6911 && coverage.number == common_ancestor.number
6912 && coverage.hash == common_ancestor.hash
6913 {
6914 enrich_block_ref(&mut enriched_ancestor, coverage);
6915 }
6916 upsert_sequence_history(&mut state.retained_canonical_history, &enriched_ancestor)?;
6917 state.coverage_head = Some(enriched_ancestor);
6918 clear_sequence_heads_above(&mut state, &enriched_ancestor);
6919 batch_dropped.record_explicit(common_ancestor, old_tip);
6920 batch_dropped.record_drained(&dropped);
6921 mutations.push(CanonicalSequenceMutation::Rewind {
6922 common_ancestor: Some(enriched_ancestor),
6923 dropped,
6924 });
6925 }
6926 let pre_record_state = state.clone();
6927 let mut required_reorg_anchor = None::<RequiredReorgAnchor>;
6928
6929 for (record, scope) in records {
6930 if !scope.advances_canonical_state() {
6931 continue;
6932 }
6933 if let Some((incoming_dropped_block, _)) = reorg_signal_block(record) {
6934 let incoming_dropped_block =
6935 resolve_record_block_payload_metadata(record, incoming_dropped_block)?;
6936 validate_sequence_matching_metadata(&state, &incoming_dropped_block, "removed record")?;
6937 validate_sequence_adjacent_parent_identity(
6938 &state,
6939 &incoming_dropped_block,
6940 "removed record",
6941 )?;
6942 let mut dropped_block = state
6943 .retained_canonical_history
6944 .iter()
6945 .find(|known| {
6946 known.number == incoming_dropped_block.number
6947 && known.hash == incoming_dropped_block.hash
6948 })
6949 .copied()
6950 .or_else(|| {
6951 state.coverage_head.filter(|known| {
6952 known.number == incoming_dropped_block.number
6953 && known.hash == incoming_dropped_block.hash
6954 })
6955 })
6956 .unwrap_or(incoming_dropped_block);
6957 enrich_block_ref(&mut dropped_block, &incoming_dropped_block);
6958 validate_sequence_implicit_finality(&state, record, None)?;
6959 if dropped_block.number == 0 {
6960 return Err(ReactiveError::InvalidChainControl {
6961 message: "a removed/reorged genesis block has no canonical parent anchor"
6962 .into(),
6963 }
6964 .into());
6965 }
6966 let removed_identity = (dropped_block.number, dropped_block.hash);
6967 if let Some(previous_number) =
6968 removed_heights_by_hash.insert(dropped_block.hash, dropped_block.number)
6969 && previous_number != dropped_block.number
6970 {
6971 return Err(ReactiveError::InvalidChainControl {
6972 message: format!(
6973 "removed hash {:?} is reused at heights {} and {}",
6974 dropped_block.hash, previous_number, dropped_block.number
6975 ),
6976 }
6977 .into());
6978 }
6979 if let Some(previous) = removed_assertions.get_mut(&removed_identity) {
6980 if !optional_block_refs_are_compatible(Some(previous), Some(&dropped_block)) {
6981 return Err(ReactiveError::InvalidChainControl {
6982 message: format!(
6983 "duplicate removed block {}:{:?} carries conflicting metadata",
6984 dropped_block.number, dropped_block.hash
6985 ),
6986 }
6987 .into());
6988 }
6989 enrich_block_ref(previous, &dropped_block);
6990 } else {
6991 removed_assertions.insert(removed_identity, dropped_block);
6992 }
6993 if asserted_blocks
6994 .get(&dropped_block.number)
6995 .is_some_and(|asserted| asserted.hash == dropped_block.hash)
6996 {
6997 return Err(ReactiveError::InvalidChainControl {
6998 message: format!(
6999 "removed block {}:{:?} is asserted canonical by the same envelope",
7000 dropped_block.number, dropped_block.hash
7001 ),
7002 }
7003 .into());
7004 }
7005 if batch_dropped.contains(&dropped_block) {
7006 continue;
7007 }
7008 if let Some(index) = state.retained_canonical_history.iter().position(|block| {
7009 block.number == dropped_block.number && block.hash == dropped_block.hash
7010 }) {
7011 let dropped = state.retained_canonical_history.split_off(index);
7012 batch_dropped.record_drained(&dropped);
7013 let ancestor_number = dropped_block
7014 .number
7015 .checked_sub(1)
7016 .expect("genesis removal was rejected above");
7017 let retained_anchor = state
7018 .retained_canonical_history
7019 .iter()
7020 .rev()
7021 .find(|head| head.number == ancestor_number)
7022 .copied();
7023 let authenticated_anchor = retained_anchor
7024 .or_else(|| {
7025 dropped_block.parent_hash.map(|hash| BlockRef {
7026 number: ancestor_number,
7027 hash,
7028 parent_hash: None,
7029 timestamp: None,
7030 })
7031 })
7032 .or_else(|| {
7033 state
7034 .finalized_head
7035 .filter(|head| head.number == ancestor_number)
7036 });
7037 let authenticated_anchor = authenticated_anchor.map(|mut anchor| {
7038 for head in [state.safe_head.as_ref(), state.finalized_head.as_ref()]
7039 .into_iter()
7040 .flatten()
7041 {
7042 if head.number == anchor.number && head.hash == anchor.hash {
7043 enrich_block_ref(&mut anchor, head);
7044 }
7045 }
7046 anchor
7047 });
7048 required_reorg_anchor = Some(RequiredReorgAnchor {
7049 number: ancestor_number,
7050 block: authenticated_anchor,
7051 permits_missing_child_parent: retained_anchor.is_some(),
7052 must_be_consumed: authenticated_anchor.is_none()
7053 && state.retained_canonical_history.is_empty(),
7054 });
7055 state.coverage_head = authenticated_anchor
7056 .or_else(|| state.retained_canonical_history.last().copied());
7057 if let Some(head) = state.coverage_head {
7058 clear_sequence_heads_above(&mut state, &head);
7059 } else {
7060 state.safe_head = None;
7061 state.finalized_head = None;
7062 }
7063 mutations.push(CanonicalSequenceMutation::Rewind {
7064 common_ancestor: state.coverage_head,
7065 dropped,
7066 });
7067 } else {
7068 let replacement_is_known = state.retained_canonical_history.iter().any(|block| {
7069 block.number == dropped_block.number && block.hash != dropped_block.hash
7070 }) || state.coverage_head.is_some_and(|head| {
7071 head.number == dropped_block.number && head.hash != dropped_block.hash
7072 });
7073 if !replacement_is_known {
7074 if policy.requires_complete_rollback() {
7079 return Err(CanonicalSequenceError::IncompleteRollback {
7080 common_ancestor: dropped_block
7081 .number
7082 .checked_sub(1)
7083 .expect("genesis removal was rejected above"),
7084 oldest_retained: rollback_oldest,
7085 kind: CanonicalRollbackKind::Removed,
7086 });
7087 }
7088 continue;
7089 }
7090 }
7091 continue;
7092 }
7093
7094 let Some(context_block) = canonical_record_block(record) else {
7095 continue;
7096 };
7097 let incoming_block = resolve_record_block_payload_metadata(record, *context_block)?;
7098 if post_record_controls
7099 .iter()
7100 .filter_map(canonical_coverage_control_block)
7101 .any(|asserted| {
7102 asserted.number == incoming_block.number
7103 && asserted.hash == incoming_block.hash
7104 && optional_block_refs_are_compatible(Some(asserted), Some(&incoming_block))
7105 && ((incoming_block.parent_hash.is_none() && asserted.parent_hash.is_some())
7106 || (incoming_block.timestamp.is_none() && asserted.timestamp.is_some()))
7107 })
7108 {
7109 record_proof_control_identities.insert((incoming_block.number, incoming_block.hash));
7110 }
7111 let mut resolved_block = incoming_block;
7112 if let Some(asserted) = asserted_blocks
7113 .get(&incoming_block.number)
7114 .filter(|asserted| asserted.hash == incoming_block.hash)
7115 {
7116 if !optional_block_refs_are_compatible(Some(asserted), Some(&incoming_block)) {
7117 return Err(ReactiveError::InvalidChainControl {
7118 message: format!(
7119 "canonical record {}:{:?} conflicts with the same envelope's asserted metadata",
7120 incoming_block.number, incoming_block.hash
7121 ),
7122 }
7123 .into());
7124 }
7125 enrich_block_ref(&mut resolved_block, asserted);
7126 }
7127 for asserted in post_record_controls
7128 .iter()
7129 .filter_map(chain_control_canonical_assertion)
7130 .filter(|asserted| {
7131 asserted.number == incoming_block.number && asserted.hash == incoming_block.hash
7132 })
7133 {
7134 if !optional_block_refs_are_compatible(Some(&resolved_block), Some(asserted)) {
7135 return Err(ReactiveError::InvalidChainControl {
7136 message: format!(
7137 "canonical record {}:{:?} conflicts with the same envelope's asserted metadata",
7138 incoming_block.number, incoming_block.hash
7139 ),
7140 }
7141 .into());
7142 }
7143 enrich_block_ref(&mut resolved_block, asserted);
7144 }
7145 let replacement_anchor =
7146 required_reorg_anchor.filter(|required| resolved_block.number > required.number);
7147 if resolved_block.parent_hash.is_none()
7148 && replacement_anchor.is_some_and(|anchor| {
7149 anchor.permits_missing_child_parent
7150 && anchor.number.checked_add(1) == Some(resolved_block.number)
7151 })
7152 {
7153 resolved_block.parent_hash = replacement_anchor.and_then(RequiredReorgAnchor::hash);
7154 }
7155 let block = &resolved_block;
7156 if removed_assertions.contains_key(&(block.number, block.hash)) {
7157 return Err(ReactiveError::InvalidChainControl {
7158 message: format!(
7159 "canonical block {}:{:?} is also removed by the same envelope",
7160 block.number, block.hash
7161 ),
7162 }
7163 .into());
7164 }
7165 if let Some(removed_number) = removed_heights_by_hash.get(&block.hash)
7166 && *removed_number != block.number
7167 {
7168 return Err(ReactiveError::InvalidChainControl {
7169 message: format!(
7170 "canonical hash {:?} at height {} is removed at height {} by the same envelope",
7171 block.hash, block.number, removed_number
7172 ),
7173 }
7174 .into());
7175 }
7176 let replacement_proven_by_removal =
7177 validate_replacement_reorg_anchor(replacement_anchor, block, policy, rollback_oldest)?;
7178 if replacement_anchor.is_some() {
7179 required_reorg_anchor = None;
7180 }
7181 validate_sequence_matching_metadata(&state, block, "canonical record")?;
7182 validate_sequence_implicit_finality(&state, record, Some(block))?;
7183 let implicit_replacement_requires_history = if replacement_proven_by_removal {
7184 false
7185 } else {
7186 sequence_implicit_replacement_requires_history(&state, block, policy)?
7187 };
7188 if implicit_replacement_requires_history && policy.requires_complete_rollback() {
7189 return Err(CanonicalSequenceError::IncompleteRollback {
7190 common_ancestor: block.number.saturating_sub(1),
7191 oldest_retained: rollback_oldest,
7192 kind: CanonicalRollbackKind::ImplicitParent,
7193 });
7194 }
7195 assert_canonical_block_identity(&mut asserted_blocks, block, "canonical record")?;
7196 let allow_parentless_extension = replacement_anchor.is_some_and(|anchor| {
7197 anchor.permits_missing_child_parent
7198 && anchor.number.checked_add(1) == Some(block.number)
7199 });
7200 if let Some(rewind) =
7201 apply_sequence_canonical_block(&mut state, block, allow_parentless_extension)?
7202 {
7203 mutations.push(CanonicalSequenceMutation::Rewind {
7204 common_ancestor: rewind.common_ancestor,
7205 dropped: rewind.dropped,
7206 });
7207 }
7208 mutations.push(CanonicalSequenceMutation::Canonical(*block));
7209 }
7210
7211 for control in post_record_controls {
7212 if let Some(block) = chain_control_canonical_assertion(control)
7213 && removed_assertions.contains_key(&(block.number, block.hash))
7214 {
7215 return Err(ReactiveError::InvalidChainControl {
7216 message: format!(
7217 "canonical block {}:{:?} is also removed by the same envelope",
7218 block.number, block.hash
7219 ),
7220 }
7221 .into());
7222 }
7223 if let Some(block) = chain_control_canonical_assertion(control)
7224 && let Some(removed_number) = removed_heights_by_hash.get(&block.hash)
7225 && *removed_number != block.number
7226 {
7227 return Err(ReactiveError::InvalidChainControl {
7228 message: format!(
7229 "canonical hash {:?} at height {} is removed at height {} by the same envelope",
7230 block.hash, block.number, removed_number
7231 ),
7232 }
7233 .into());
7234 }
7235 let replacement_anchor = canonical_coverage_control_block(control).and_then(|block| {
7236 required_reorg_anchor.filter(|required| block.number > required.number)
7237 });
7238 if let Some(block) = canonical_coverage_control_block(control) {
7239 validate_replacement_reorg_anchor(replacement_anchor, block, policy, rollback_oldest)?;
7240 if replacement_anchor.is_some() {
7241 required_reorg_anchor = None;
7242 }
7243 }
7244 assert_chain_control_identities(&mut asserted_blocks, control)?;
7245 let preserves_record_proof =
7246 canonical_coverage_control_block(control).is_some_and(|block| {
7247 record_proof_control_identities.contains(&(block.number, block.hash))
7248 });
7249 if policy.normalizes_coverage()
7250 && !preserves_record_proof
7251 && let Some(block) = canonical_coverage_control_block(control)
7252 && state
7253 .coverage_head
7254 .is_some_and(|head| block.number <= head.number)
7255 {
7256 let is_equal_coverage = state
7257 .coverage_head
7258 .is_some_and(|head| block.number == head.number);
7259 let known = state
7260 .coverage_head
7261 .as_ref()
7262 .filter(|head| head.number == block.number && head.hash == block.hash)
7263 .or_else(|| {
7264 state
7265 .retained_canonical_history
7266 .iter()
7267 .find(|entry| entry.number == block.number && entry.hash == block.hash)
7268 });
7269 if let Some(known) = known
7270 && optional_block_refs_are_compatible(Some(known), Some(block))
7271 && (!is_equal_coverage || !sequence_block_adds_metadata(&state, block))
7272 {
7273 if let ChainControl::Barrier { id, .. } = control {
7274 normalized_chain_controls.push(ChainControl::Barrier {
7275 id: id.clone(),
7276 block: None,
7277 });
7278 }
7279 continue;
7280 }
7281 }
7282 validate_sequence_control(&state, control)?;
7283 normalized_chain_controls.push(control.clone());
7284 match control {
7285 ChainControl::Safe(block) => {
7286 set_or_enrich_block_ref(&mut state.safe_head, block);
7287 mutations.push(CanonicalSequenceMutation::Safe(
7288 state.safe_head.expect("safe head was just installed"),
7289 ));
7290 }
7291 ChainControl::Finalized(block) => {
7292 set_or_enrich_block_ref(&mut state.finalized_head, block);
7293 mutations.push(CanonicalSequenceMutation::Finalized(
7294 state
7295 .finalized_head
7296 .expect("finalized head was just installed"),
7297 ));
7298 }
7299 ChainControl::CanonicalProgress(block)
7300 | ChainControl::Barrier {
7301 block: Some(block), ..
7302 } => {
7303 let allow_parentless_extension = replacement_anchor.is_some_and(|anchor| {
7304 anchor.permits_missing_child_parent
7305 && anchor.number.checked_add(1) == Some(block.number)
7306 }) || (replacement_anchor.is_none()
7307 && block.parent_hash.is_none()
7308 && state
7309 .coverage_head
7310 .is_some_and(|head| head.number.checked_add(1) == Some(block.number)));
7311 if let Some(rewind) =
7312 apply_sequence_canonical_block(&mut state, block, allow_parentless_extension)?
7313 {
7314 mutations.push(CanonicalSequenceMutation::Rewind {
7315 common_ancestor: rewind.common_ancestor,
7316 dropped: rewind.dropped,
7317 });
7318 }
7319 mutations.push(CanonicalSequenceMutation::Canonical(*block));
7320 }
7321 ChainControl::LogCoverage(block) => {
7322 set_or_enrich_block_ref(&mut state.log_coverage_head, block);
7323 mutations.push(CanonicalSequenceMutation::LogCoverage(
7324 state
7325 .log_coverage_head
7326 .expect("log coverage head was just installed"),
7327 ));
7328 }
7329 ChainControl::Barrier { block: None, .. } => {}
7330 ChainControl::Reorg { .. } => {
7331 unreachable!("phase validation excludes post-record reorg controls")
7332 }
7333 }
7334 }
7335
7336 if let Some(required) = required_reorg_anchor
7337 && required.must_be_consumed
7338 && policy.requires_complete_rollback()
7339 {
7340 return Err(CanonicalSequenceError::IncompleteRollback {
7341 common_ancestor: required.number,
7342 oldest_retained: rollback_oldest,
7343 kind: CanonicalRollbackKind::MissingReplacement,
7344 });
7345 }
7346
7347 validate_canonical_sequence_snapshot(&state)?;
7348 Ok(CanonicalSequenceValidation {
7349 pre_record_state,
7350 next_state: state,
7351 mutations,
7352 normalized_chain_controls,
7353 })
7354}
7355
7356fn validate_canonical_sequence_snapshot(
7357 state: &CanonicalSequenceState,
7358) -> Result<(), ReactiveError> {
7359 let invalid = |message: String| ReactiveError::InvalidChainControl { message };
7360 let supplied_blocks = state
7361 .retained_canonical_history
7362 .iter()
7363 .chain(state.coverage_head.iter())
7364 .chain(state.safe_head.iter())
7365 .chain(state.finalized_head.iter())
7366 .collect::<Vec<_>>();
7367 validate_known_parent_hash_heights(&supplied_blocks)?;
7368 let mut prior = None::<BlockRef>;
7369 for block in &state.retained_canonical_history {
7370 if let Some(previous) = prior {
7371 if block.number < previous.number {
7372 return Err(invalid(
7373 "retained canonical history is not ordered by block number".into(),
7374 ));
7375 }
7376 if block.number == previous.number {
7377 let qualifier = if optional_block_refs_are_compatible(Some(&previous), Some(block))
7378 {
7379 "duplicate"
7380 } else {
7381 "conflicting"
7382 };
7383 return Err(invalid(format!(
7384 "retained canonical history contains {qualifier} identities at block {}",
7385 block.number
7386 )));
7387 }
7388 if previous.number.checked_add(1) == Some(block.number)
7389 && block.parent_hash.is_some()
7390 && block.parent_hash != Some(previous.hash)
7391 {
7392 return Err(invalid(format!(
7393 "adjacent retained block {}:{:?} does not descend from {}:{:?}",
7394 block.number, block.hash, previous.number, previous.hash
7395 )));
7396 }
7397 }
7398 prior = Some(*block);
7399 }
7400 if state.coverage_head.is_none() && !state.retained_canonical_history.is_empty() {
7401 return Err(invalid(
7402 "retained canonical history requires an authoritative coverage head".into(),
7403 ));
7404 }
7405 if let Some(head) = state.coverage_head.as_ref() {
7406 if let Some(retained) = state
7407 .retained_canonical_history
7408 .iter()
7409 .find(|entry| entry.number == head.number)
7410 && !optional_block_refs_are_compatible(Some(retained), Some(head))
7411 {
7412 return Err(invalid(format!(
7413 "coverage head {}:{:?} conflicts with retained identity {:?}",
7414 head.number, head.hash, retained
7415 )));
7416 }
7417 if state
7418 .retained_canonical_history
7419 .last()
7420 .is_some_and(|retained| retained.number > head.number)
7421 {
7422 return Err(invalid(
7423 "retained canonical history advances beyond the coverage head".into(),
7424 ));
7425 }
7426 if let Some(retained) = state.retained_canonical_history.last()
7427 && retained.number.checked_add(1) == Some(head.number)
7428 && head.parent_hash.is_some()
7429 && head.parent_hash != Some(retained.hash)
7430 {
7431 return Err(invalid(format!(
7432 "coverage head {}:{:?} does not descend from adjacent retained block {}:{:?}",
7433 head.number, head.hash, retained.number, retained.hash
7434 )));
7435 }
7436 }
7437 if let Some(safe) = state.safe_head.as_ref() {
7438 validate_sequence_known_identity(state, safe, "safe")?;
7439 validate_sequence_head_within_coverage(state, safe, "safe")?;
7440 validate_coverage_descends_from_adjacent_head(state.coverage_head.as_ref(), safe, "safe")?;
7441 }
7442 if let Some(finalized) = state.finalized_head.as_ref() {
7443 validate_sequence_known_identity(state, finalized, "finalized")?;
7444 validate_sequence_head_within_coverage(state, finalized, "finalized")?;
7445 validate_coverage_descends_from_adjacent_head(
7446 state.coverage_head.as_ref(),
7447 finalized,
7448 "finalized",
7449 )?;
7450 }
7451 validate_adjacent_finality(state.finalized_head.as_ref(), state.safe_head.as_ref())?;
7452 if let (Some(finalized), Some(safe)) = (state.finalized_head, state.safe_head)
7453 && (finalized.number > safe.number
7454 || (finalized.number == safe.number && finalized.hash != safe.hash))
7455 {
7456 return Err(invalid(
7457 "finalized head cannot advance beyond or conflict with safe head".into(),
7458 ));
7459 }
7460 Ok(())
7461}
7462
7463fn validate_known_parent_hash_heights(blocks: &[&BlockRef]) -> Result<(), ReactiveError> {
7464 let mut heights_by_hash = HashMap::<B256, u64>::with_capacity(blocks.len());
7465 let mut resolved_by_height = HashMap::<u64, BlockRef>::with_capacity(blocks.len());
7466 for block in blocks.iter().copied() {
7467 if let Some(previous_height) = heights_by_hash.insert(block.hash, block.number)
7468 && previous_height != block.number
7469 {
7470 return Err(ReactiveError::InvalidChainControl {
7471 message: format!(
7472 "canonical hash {:?} is reused at heights {} and {}",
7473 block.hash, previous_height, block.number
7474 ),
7475 });
7476 }
7477 if let Some(resolved) = resolved_by_height.get_mut(&block.number) {
7478 if !optional_block_refs_are_compatible(Some(resolved), Some(block)) {
7479 return Err(ReactiveError::InvalidChainControl {
7480 message: format!(
7481 "canonical aliases at height {} carry conflicting identities or metadata",
7482 block.number
7483 ),
7484 });
7485 }
7486 enrich_block_ref(resolved, block);
7487 } else {
7488 resolved_by_height.insert(block.number, *block);
7489 }
7490 }
7491 for child in resolved_by_height.values() {
7492 let Some(parent_hash) = child.parent_hash else {
7493 continue;
7494 };
7495 if let Some(parent_number) = heights_by_hash.get(&parent_hash)
7496 && parent_number.checked_add(1) != Some(child.number)
7497 {
7498 return Err(ReactiveError::InvalidChainControl {
7499 message: format!(
7500 "block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
7501 child.number, child.hash, parent_hash, parent_number
7502 ),
7503 });
7504 }
7505 if let Some(parent_number) = child.number.checked_sub(1)
7506 && let Some(parent) = resolved_by_height.get(&parent_number)
7507 && parent.hash != parent_hash
7508 {
7509 return Err(ReactiveError::InvalidChainControl {
7510 message: format!(
7511 "block {}:{:?} does not descend from supplied adjacent identity {}:{:?}",
7512 child.number, child.hash, parent.number, parent.hash
7513 ),
7514 });
7515 }
7516 }
7517 Ok(())
7518}
7519
7520fn validate_coverage_descends_from_adjacent_head(
7521 coverage: Option<&BlockRef>,
7522 head: &BlockRef,
7523 label: &str,
7524) -> Result<(), ReactiveError> {
7525 let Some(coverage) = coverage else {
7526 return Ok(());
7527 };
7528 if head.number.checked_add(1) == Some(coverage.number)
7529 && coverage
7530 .parent_hash
7531 .is_some_and(|parent| parent != head.hash)
7532 {
7533 return Err(ReactiveError::InvalidChainControl {
7534 message: format!(
7535 "canonical coverage {}:{:?} does not descend from adjacent {label} head {}:{:?}",
7536 coverage.number, coverage.hash, head.number, head.hash
7537 ),
7538 });
7539 }
7540 Ok(())
7541}
7542
7543fn validate_sequence_control(
7544 state: &CanonicalSequenceState,
7545 control: &ChainControl,
7546) -> Result<(), ReactiveError> {
7547 let invalid = |message: String| ReactiveError::InvalidChainControl { message };
7548 match control {
7549 ChainControl::Safe(block) => {
7550 validate_sequence_known_identity(state, block, "safe")?;
7551 validate_sequence_head_within_coverage(state, block, "safe")?;
7552 if let Some(current) = state.safe_head.as_ref()
7553 && (block.number < current.number
7554 || (block.number == current.number
7555 && (block.hash != current.hash
7556 || !optional_block_refs_are_compatible(Some(block), Some(current)))))
7557 {
7558 return Err(invalid(format!(
7559 "safe head {}:{:?} conflicts with current {}:{:?}",
7560 block.number, block.hash, current.number, current.hash
7561 )));
7562 }
7563 if let Some(finalized) = state.finalized_head.as_ref()
7564 && (block.number < finalized.number
7565 || (block.number == finalized.number && block.hash != finalized.hash))
7566 {
7567 return Err(invalid(
7568 "safe head cannot precede or conflict with finalized head".into(),
7569 ));
7570 }
7571 validate_adjacent_finality(state.finalized_head.as_ref(), Some(block))?;
7572 }
7573 ChainControl::Finalized(block) => {
7574 validate_sequence_known_identity(state, block, "finalized")?;
7575 validate_sequence_head_within_coverage(state, block, "finalized")?;
7576 if let Some(current) = state.finalized_head.as_ref()
7577 && (block.number < current.number
7578 || (block.number == current.number
7579 && (block.hash != current.hash
7580 || !optional_block_refs_are_compatible(Some(block), Some(current)))))
7581 {
7582 return Err(invalid(format!(
7583 "finalized head {}:{:?} conflicts with current {}:{:?}",
7584 block.number, block.hash, current.number, current.hash
7585 )));
7586 }
7587 if let Some(safe) = state.safe_head.as_ref()
7588 && (block.number > safe.number
7589 || (block.number == safe.number && block.hash != safe.hash))
7590 {
7591 return Err(invalid(
7592 "finalized head cannot advance beyond or conflict with safe head".into(),
7593 ));
7594 }
7595 validate_adjacent_finality(Some(block), state.safe_head.as_ref())?;
7596 }
7597 ChainControl::CanonicalProgress(block)
7598 | ChainControl::Barrier {
7599 block: Some(block), ..
7600 } => {
7601 validate_sequence_known_identity(state, block, "canonical coverage")?;
7602 if let Some(current) = state.coverage_head.as_ref()
7603 && (block.number < current.number
7604 || (block.number == current.number && block.hash != current.hash))
7605 {
7606 return Err(invalid(format!(
7607 "canonical coverage {}:{:?} conflicts with current {}:{:?}",
7608 block.number, block.hash, current.number, current.hash
7609 )));
7610 }
7611 if let Some(current) = state.coverage_head.as_ref()
7612 && current.number.checked_add(1) == Some(block.number)
7613 && block.parent_hash.is_some()
7614 && block.parent_hash != Some(current.hash)
7615 {
7616 return Err(invalid(format!(
7617 "canonical coverage {}:{:?} does not descend from current {}:{:?}",
7618 block.number, block.hash, current.number, current.hash
7619 )));
7620 }
7621 }
7622 ChainControl::LogCoverage(block) => {
7623 validate_sequence_known_identity(state, block, "log coverage")?;
7624 if let Some(current) = state.log_coverage_head.as_ref()
7628 && (block.number < current.number
7629 || (block.number == current.number && block.hash != current.hash))
7630 {
7631 return Err(invalid(format!(
7632 "log coverage {}:{:?} conflicts with current {}:{:?}",
7633 block.number, block.hash, current.number, current.hash
7634 )));
7635 }
7636 if let Some(coverage) = state.coverage_head.as_ref()
7639 && block.number > coverage.number
7640 {
7641 return Err(invalid(format!(
7642 "log coverage {}:{:?} is ahead of canonical coverage {}:{:?}",
7643 block.number, block.hash, coverage.number, coverage.hash
7644 )));
7645 }
7646 }
7647 ChainControl::Barrier { block: None, .. } => {}
7648 ChainControl::Reorg {
7649 common_ancestor,
7650 old_tip,
7651 new_tip,
7652 } => {
7653 validate_sequence_known_identity(state, common_ancestor, "reorg common ancestor")?;
7654 validate_reorg_ancestor_against_retained_branch(state, common_ancestor)?;
7655 validate_sequence_known_hash_height(state, old_tip, "reorg old tip")?;
7656 validate_sequence_known_hash_height(state, new_tip, "reorg new tip")?;
7657 validate_sequence_known_parent_height(state, old_tip, "reorg old tip")?;
7658 validate_sequence_known_parent_height(state, new_tip, "reorg new tip")?;
7659 validate_sequence_adjacent_parent_identity(state, old_tip, "reorg old tip")?;
7660 if let Some(current) = state.coverage_head.as_ref()
7661 && (old_tip.number != current.number
7662 || old_tip.hash != current.hash
7663 || !optional_block_refs_are_compatible(Some(old_tip), Some(current)))
7664 {
7665 return Err(invalid(format!(
7666 "reorg old tip {}:{:?} does not exactly match current metadata {}:{:?}",
7667 old_tip.number, old_tip.hash, current.number, current.hash
7668 )));
7669 }
7670 if common_ancestor.number > old_tip.number || common_ancestor.number > new_tip.number {
7671 return Err(invalid(
7672 "reorg common ancestor cannot be above either branch tip".into(),
7673 ));
7674 }
7675 if common_ancestor.number == old_tip.number || common_ancestor.number == new_tip.number
7676 {
7677 return Err(invalid(
7678 "reorg must replace non-empty old and new branches above the common ancestor"
7679 .into(),
7680 ));
7681 }
7682 if old_tip.number == new_tip.number && old_tip.hash == new_tip.hash {
7683 return Err(invalid(
7684 "reorg old and new tips cannot have the same canonical identity".into(),
7685 ));
7686 }
7687 for (label, tip) in [("old", old_tip), ("new", new_tip)] {
7688 if common_ancestor.number.checked_add(1) == Some(tip.number)
7689 && tip.parent_hash != Some(common_ancestor.hash)
7690 {
7691 return Err(invalid(format!(
7692 "reorg {label} tip does not descend from the common ancestor"
7693 )));
7694 }
7695 }
7696 if let Some(finalized) = state.finalized_head.as_ref()
7697 && (common_ancestor.number < finalized.number
7698 || (common_ancestor.number == finalized.number
7699 && common_ancestor.hash != finalized.hash))
7700 {
7701 return Err(invalid(
7702 "reorg would cross or conflict with the finalized head".into(),
7703 ));
7704 }
7705 }
7706 }
7707 Ok(())
7708}
7709
7710fn validate_sequence_known_identity(
7711 state: &CanonicalSequenceState,
7712 block: &BlockRef,
7713 label: &str,
7714) -> Result<(), ReactiveError> {
7715 validate_sequence_known_hash_height(state, block, label)?;
7716 validate_sequence_known_parent_height(state, block, label)?;
7717 let known = state
7718 .coverage_head
7719 .as_ref()
7720 .filter(|head| head.number == block.number)
7721 .or_else(|| {
7722 state
7723 .retained_canonical_history
7724 .iter()
7725 .find(|entry| entry.number == block.number)
7726 });
7727 if let Some(known) = known
7728 && !optional_block_refs_are_compatible(Some(known), Some(block))
7729 {
7730 return Err(ReactiveError::InvalidChainControl {
7731 message: format!(
7732 "{label} block {}:{:?} conflicts with known canonical block {:?}",
7733 block.number, block.hash, known
7734 ),
7735 });
7736 }
7737 Ok(())
7738}
7739
7740fn validate_sequence_known_parent_height(
7741 state: &CanonicalSequenceState,
7742 block: &BlockRef,
7743 label: &str,
7744) -> Result<(), ReactiveError> {
7745 let Some(parent_hash) = block.parent_hash else {
7746 return Ok(());
7747 };
7748 let known_parent = state
7749 .retained_canonical_history
7750 .iter()
7751 .chain(state.coverage_head.iter())
7752 .chain(state.safe_head.iter())
7753 .chain(state.finalized_head.iter())
7754 .find(|known| known.hash == parent_hash);
7755 if let Some(parent) = known_parent
7756 && parent.number.checked_add(1) != Some(block.number)
7757 {
7758 return Err(ReactiveError::InvalidChainControl {
7759 message: format!(
7760 "{label} block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
7761 block.number, block.hash, parent.hash, parent.number
7762 ),
7763 });
7764 }
7765 Ok(())
7766}
7767
7768fn validate_sequence_head_within_coverage(
7769 state: &CanonicalSequenceState,
7770 block: &BlockRef,
7771 label: &str,
7772) -> Result<(), ReactiveError> {
7773 let Some(coverage) = state.coverage_head.as_ref() else {
7774 return Err(ReactiveError::InvalidChainControl {
7775 message: format!("{label} head requires an authoritative coverage head"),
7776 });
7777 };
7778 if block.number > coverage.number
7779 || (block.number == coverage.number
7780 && !optional_block_refs_are_compatible(Some(block), Some(coverage)))
7781 {
7782 return Err(ReactiveError::InvalidChainControl {
7783 message: format!(
7784 "{label} head {}:{:?} advances beyond or conflicts with coverage {}:{:?}",
7785 block.number, block.hash, coverage.number, coverage.hash
7786 ),
7787 });
7788 }
7789 Ok(())
7790}
7791
7792fn validate_sequence_matching_metadata(
7793 state: &CanonicalSequenceState,
7794 block: &BlockRef,
7795 label: &str,
7796) -> Result<(), ReactiveError> {
7797 validate_sequence_known_hash_height(state, block, label)?;
7798 validate_sequence_known_parent_height(state, block, label)?;
7799 let known = state
7800 .coverage_head
7801 .as_ref()
7802 .filter(|head| head.number == block.number && head.hash == block.hash)
7803 .or_else(|| {
7804 state
7805 .retained_canonical_history
7806 .iter()
7807 .find(|entry| entry.number == block.number && entry.hash == block.hash)
7808 });
7809 if let Some(known) = known
7810 && !optional_block_refs_are_compatible(Some(known), Some(block))
7811 {
7812 return Err(ReactiveError::InvalidChainControl {
7813 message: format!(
7814 "{label} block {}:{:?} carries metadata conflicting with known canonical block {:?}",
7815 block.number, block.hash, known
7816 ),
7817 });
7818 }
7819 Ok(())
7820}
7821
7822fn validate_sequence_known_hash_height(
7823 state: &CanonicalSequenceState,
7824 block: &BlockRef,
7825 label: &str,
7826) -> Result<(), ReactiveError> {
7827 let known = state
7828 .retained_canonical_history
7829 .iter()
7830 .chain(state.coverage_head.iter())
7831 .chain(state.safe_head.iter())
7832 .chain(state.finalized_head.iter())
7833 .find(|known| known.hash == block.hash);
7834 if let Some(known) = known
7835 && known.number != block.number
7836 {
7837 return Err(ReactiveError::InvalidChainControl {
7838 message: format!(
7839 "{label} block {}:{:?} reuses a canonical hash already known at height {}",
7840 block.number, block.hash, known.number
7841 ),
7842 });
7843 }
7844 Ok(())
7845}
7846
7847fn validate_reorg_ancestor_against_retained_branch(
7848 state: &CanonicalSequenceState,
7849 ancestor: &BlockRef,
7850) -> Result<(), ReactiveError> {
7851 let adjacent_number = ancestor.number.checked_add(1);
7852 for retained in state
7853 .retained_canonical_history
7854 .iter()
7855 .chain(state.coverage_head.iter())
7856 .chain(state.safe_head.iter())
7857 .chain(state.finalized_head.iter())
7858 {
7859 if Some(retained.number) == adjacent_number
7860 && retained
7861 .parent_hash
7862 .is_some_and(|parent| parent != ancestor.hash)
7863 {
7864 return Err(ReactiveError::InvalidChainControl {
7865 message: format!(
7866 "reorg common ancestor {}:{:?} conflicts with retained child {}:{:?} parent {:?}",
7867 ancestor.number,
7868 ancestor.hash,
7869 retained.number,
7870 retained.hash,
7871 retained.parent_hash
7872 ),
7873 });
7874 }
7875 if retained.parent_hash == Some(ancestor.hash) && Some(retained.number) != adjacent_number {
7876 return Err(ReactiveError::InvalidChainControl {
7877 message: format!(
7878 "reorg common ancestor {}:{:?} is named as the non-adjacent parent of retained block {}:{:?}",
7879 ancestor.number, ancestor.hash, retained.number, retained.hash
7880 ),
7881 });
7882 }
7883 }
7884 Ok(())
7885}
7886
7887fn validate_sequence_adjacent_parent_identity(
7888 state: &CanonicalSequenceState,
7889 block: &BlockRef,
7890 label: &str,
7891) -> Result<(), ReactiveError> {
7892 let Some(parent_hash) = block.parent_hash else {
7893 return Ok(());
7894 };
7895 let Some(parent_number) = block.number.checked_sub(1) else {
7896 return Ok(());
7897 };
7898 let known_parent = state
7899 .retained_canonical_history
7900 .iter()
7901 .chain(state.coverage_head.iter())
7902 .chain(state.safe_head.iter())
7903 .chain(state.finalized_head.iter())
7904 .find(|known| known.number == parent_number);
7905 if let Some(known_parent) = known_parent
7906 && known_parent.hash != parent_hash
7907 {
7908 return Err(ReactiveError::InvalidChainControl {
7909 message: format!(
7910 "{label} block {}:{:?} names parent {:?}, which conflicts with known adjacent block {}:{:?}",
7911 block.number, block.hash, parent_hash, known_parent.number, known_parent.hash
7912 ),
7913 });
7914 }
7915 Ok(())
7916}
7917
7918fn sequence_block_adds_metadata(state: &CanonicalSequenceState, incoming: &BlockRef) -> bool {
7919 state
7920 .coverage_head
7921 .iter()
7922 .chain(state.retained_canonical_history.iter())
7923 .filter(|known| known.number == incoming.number && known.hash == incoming.hash)
7924 .any(|known| {
7925 (known.parent_hash.is_none() && incoming.parent_hash.is_some())
7926 || (known.timestamp.is_none() && incoming.timestamp.is_some())
7927 })
7928}
7929
7930fn validate_sequence_implicit_finality<N: Network>(
7931 state: &CanonicalSequenceState,
7932 record: &ReactiveInputRecord<N>,
7933 resolved_canonical_block: Option<&BlockRef>,
7934) -> Result<(), ReactiveError> {
7935 let Some(finalized) = state.finalized_head.as_ref() else {
7936 return Ok(());
7937 };
7938 if let Some((dropped, _)) = reorg_signal_block(record) {
7939 if dropped.number <= finalized.number {
7940 return Err(ReactiveError::InvalidChainControl {
7941 message: format!(
7942 "implicit reorg at {}:{:?} would cross finalized head {}:{:?}",
7943 dropped.number, dropped.hash, finalized.number, finalized.hash
7944 ),
7945 });
7946 }
7947 return Ok(());
7948 }
7949 let Some(block) = resolved_canonical_block.or_else(|| canonical_record_block(record)) else {
7950 return Ok(());
7951 };
7952 let Some(latest) = state.coverage_head.as_ref() else {
7953 return Ok(());
7954 };
7955 if (block.number == latest.number && block.hash == latest.hash)
7956 || state
7957 .retained_canonical_history
7958 .iter()
7959 .any(|entry| entry.number == block.number && entry.hash == block.hash)
7960 || (latest.number.checked_add(1) == Some(block.number)
7961 && block.parent_hash == Some(latest.hash))
7962 || latest
7963 .number
7964 .checked_add(1)
7965 .is_some_and(|next| block.number > next)
7966 {
7967 return Ok(());
7968 }
7969 let crosses_finalized = if block.number <= finalized.number {
7970 true
7971 } else if let Some(parent_hash) = block.parent_hash {
7972 if finalized.number.checked_add(1) == Some(block.number) && parent_hash == finalized.hash {
7973 false
7974 } else if let Some(parent_index) =
7975 state.retained_canonical_history.iter().rposition(|entry| {
7976 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
7977 })
7978 {
7979 state
7980 .retained_canonical_history
7981 .iter()
7982 .skip(parent_index + 1)
7983 .any(|entry| entry.number <= finalized.number)
7984 } else {
7985 true
7986 }
7987 } else {
7988 true
7989 };
7990 if crosses_finalized {
7991 return Err(ReactiveError::InvalidChainControl {
7992 message: format!(
7993 "canonical input {}:{:?} would replace finalized head {}:{:?}",
7994 block.number, block.hash, finalized.number, finalized.hash
7995 ),
7996 });
7997 }
7998 Ok(())
7999}
8000
8001fn validate_required_reorg_anchor(
8002 required: Option<RequiredReorgAnchor>,
8003 block: &BlockRef,
8004) -> Result<(), ReactiveError> {
8005 let Some(required) = required else {
8006 return Ok(());
8007 };
8008 let ancestor_hash = required.hash();
8009 let restores_ancestor =
8010 block.number == required.number && ancestor_hash.is_some_and(|hash| block.hash == hash);
8011 let replaces_removed_child = required.number.checked_add(1) == Some(block.number)
8012 && ancestor_hash.is_some()
8013 && (block.parent_hash == ancestor_hash
8014 || (block.parent_hash.is_none() && required.permits_missing_child_parent));
8015 if restores_ancestor || replaces_removed_child {
8016 return Ok(());
8017 }
8018 Err(ReactiveError::InvalidChainControl {
8019 message: format!(
8020 "canonical replacement {}:{:?} does not prove the removed tip's parent at block {}",
8021 block.number, block.hash, required.number
8022 ),
8023 })
8024}
8025
8026fn validate_replacement_reorg_anchor(
8027 required: Option<RequiredReorgAnchor>,
8028 block: &BlockRef,
8029 policy: CanonicalSequenceValidationPolicy,
8030 oldest_retained: Option<u64>,
8031) -> Result<bool, CanonicalSequenceError> {
8032 let Some(required) = required else {
8033 return Ok(false);
8034 };
8035 match validate_required_reorg_anchor(Some(required), block) {
8036 Ok(()) => Ok(true),
8037 Err(error) if required.block.is_some() => Err(error.into()),
8038 Err(_) if policy.requires_complete_rollback() => {
8039 Err(CanonicalSequenceError::IncompleteRollback {
8040 common_ancestor: required.number,
8041 oldest_retained,
8042 kind: CanonicalRollbackKind::MissingReplacement,
8043 })
8044 }
8045 Err(_) => Ok(false),
8046 }
8047}
8048
8049fn apply_sequence_canonical_block(
8050 state: &mut CanonicalSequenceState,
8051 block: &BlockRef,
8052 allow_parentless_adjacent_extension: bool,
8053) -> Result<Option<SequenceRewind>, ReactiveError> {
8054 let latest = state.coverage_head;
8055 let already_known = state
8056 .retained_canonical_history
8057 .iter()
8058 .any(|entry| entry.number == block.number && entry.hash == block.hash);
8059 let repeats_tip =
8060 latest.is_some_and(|head| head.number == block.number && head.hash == block.hash);
8061 let extends_tip = latest.is_some_and(|head| {
8062 head.number.checked_add(1) == Some(block.number)
8063 && (block.parent_hash == Some(head.hash)
8064 || (allow_parentless_adjacent_extension && block.parent_hash.is_none()))
8065 });
8066 let forward_gap = latest.is_some_and(|head| {
8067 head.number
8068 .checked_add(1)
8069 .is_some_and(|next| block.number > next)
8070 });
8071 let mut rewind = None;
8072
8073 if latest.is_some() && !already_known && !repeats_tip && !extends_tip && !forward_gap {
8074 let retained_parent = block.parent_hash.and_then(|parent_hash| {
8075 state
8076 .retained_canonical_history
8077 .iter()
8078 .rposition(|entry| {
8079 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
8080 })
8081 .map(|index| (index, state.retained_canonical_history[index]))
8082 });
8083 let finalized_parent = block.parent_hash.and_then(|parent_hash| {
8084 state.finalized_head.filter(|finalized| {
8085 finalized.number.checked_add(1) == Some(block.number)
8086 && finalized.hash == parent_hash
8087 })
8088 });
8089 let (common_ancestor, dropped) = if let Some((parent_index, parent)) = retained_parent {
8090 let dropped = state.retained_canonical_history.split_off(parent_index + 1);
8091 (Some(parent), dropped)
8092 } else if let Some(finalized) = finalized_parent {
8093 let dropped = state
8094 .retained_canonical_history
8095 .iter()
8096 .position(|entry| entry.number > finalized.number)
8097 .map_or_else(Vec::new, |index| {
8098 state.retained_canonical_history.split_off(index)
8099 });
8100 (Some(finalized), dropped)
8101 } else {
8102 (None, std::mem::take(&mut state.retained_canonical_history))
8108 };
8109 state.coverage_head = common_ancestor;
8110 if let Some(common_ancestor) = common_ancestor {
8111 clear_sequence_heads_above(state, &common_ancestor);
8112 } else {
8113 state.safe_head = None;
8114 state.finalized_head = None;
8115 }
8116 rewind = Some(SequenceRewind {
8117 common_ancestor,
8118 dropped,
8119 });
8120 }
8121 upsert_sequence_history(&mut state.retained_canonical_history, block)?;
8122 advance_or_enrich_coverage(&mut state.coverage_head, block);
8123 Ok(rewind)
8124}
8125
8126fn sequence_implicit_replacement_requires_history(
8127 state: &CanonicalSequenceState,
8128 block: &BlockRef,
8129 policy: CanonicalSequenceValidationPolicy,
8130) -> Result<bool, ReactiveError> {
8131 let Some(latest) = state.coverage_head else {
8132 return Ok(false);
8133 };
8134 let already_known = state
8135 .retained_canonical_history
8136 .iter()
8137 .any(|entry| entry.number == block.number && entry.hash == block.hash);
8138 let repeats_tip = block.number == latest.number && block.hash == latest.hash;
8139 let extends_tip = latest.number.checked_add(1) == Some(block.number)
8140 && block.parent_hash == Some(latest.hash);
8141 let forward_gap = latest
8142 .number
8143 .checked_add(1)
8144 .is_some_and(|next| block.number > next);
8145 if already_known || repeats_tip || extends_tip || forward_gap {
8146 return Ok(false);
8147 }
8148 let Some(parent_hash) = block.parent_hash else {
8149 if policy.requires_complete_rollback() {
8150 return Err(ReactiveError::InvalidChainControl {
8151 message: format!(
8152 "implicit canonical replacement {}:{:?} must identify its parent",
8153 block.number, block.hash
8154 ),
8155 });
8156 }
8157 return Ok(true);
8158 };
8159 let known_adjacent_parent = block.number.checked_sub(1).and_then(|parent_number| {
8160 state
8161 .retained_canonical_history
8162 .iter()
8163 .chain(state.coverage_head.iter())
8164 .chain(state.safe_head.iter())
8165 .chain(state.finalized_head.iter())
8166 .find(|known| known.number == parent_number)
8167 });
8168 if let Some(known_parent) = known_adjacent_parent
8169 && known_parent.hash != parent_hash
8170 && policy.requires_complete_rollback()
8171 {
8172 return Err(ReactiveError::InvalidChainControl {
8173 message: format!(
8174 "implicit canonical replacement {}:{:?} names parent {:?}, which conflicts with known adjacent block {}:{:?}",
8175 block.number, block.hash, parent_hash, known_parent.number, known_parent.hash
8176 ),
8177 });
8178 }
8179 let retained_parent = state.retained_canonical_history.iter().any(|entry| {
8180 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
8181 });
8182 let finalized_parent = state.finalized_head.is_some_and(|finalized| {
8183 finalized.number.checked_add(1) == Some(block.number) && parent_hash == finalized.hash
8184 });
8185 Ok(!retained_parent && !finalized_parent)
8186}
8187
8188fn upsert_sequence_history(
8189 history: &mut Vec<BlockRef>,
8190 block: &BlockRef,
8191) -> Result<(), ReactiveError> {
8192 if let Some(existing) = history
8193 .iter_mut()
8194 .find(|entry| entry.number == block.number)
8195 {
8196 if existing.hash != block.hash {
8197 return Err(ReactiveError::InvalidChainControl {
8198 message: format!(
8199 "canonical block {}:{:?} conflicts with retained identity {:?}",
8200 block.number, block.hash, existing
8201 ),
8202 });
8203 }
8204 if !optional_block_refs_are_compatible(Some(existing), Some(block)) {
8205 return Err(ReactiveError::InvalidChainControl {
8206 message: format!(
8207 "canonical block {}:{:?} carries conflicting retained metadata",
8208 block.number, block.hash
8209 ),
8210 });
8211 }
8212 enrich_block_ref(existing, block);
8213 } else {
8214 history.push(*block);
8215 history.sort_by_key(|entry| entry.number);
8216 }
8217 Ok(())
8218}
8219
8220fn clear_sequence_heads_above(state: &mut CanonicalSequenceState, ancestor: &BlockRef) {
8221 if state.safe_head.as_ref().is_some_and(|head| {
8222 head.number > ancestor.number
8223 || (head.number == ancestor.number && head.hash != ancestor.hash)
8224 }) {
8225 state.safe_head = None;
8226 }
8227 if state.finalized_head.as_ref().is_some_and(|head| {
8228 head.number > ancestor.number
8229 || (head.number == ancestor.number && head.hash != ancestor.hash)
8230 }) {
8231 state.finalized_head = None;
8232 }
8233}
8234
8235fn validate_control_phase_order(controls: &[ChainControl]) -> Result<usize, ReactiveError> {
8236 let split = controls
8237 .iter()
8238 .position(|control| !matches!(control, ChainControl::Reorg { .. }))
8239 .unwrap_or(controls.len());
8240 if controls[split..]
8241 .iter()
8242 .any(|control| matches!(control, ChainControl::Reorg { .. }))
8243 {
8244 return Err(ReactiveError::InvalidChainControl {
8245 message: "reorg controls must precede records and all post-record controls in a batch"
8246 .into(),
8247 });
8248 }
8249 Ok(split)
8250}
8251
8252fn canonical_coverage_control_block(control: &ChainControl) -> Option<&BlockRef> {
8253 match control {
8254 ChainControl::CanonicalProgress(block)
8255 | ChainControl::Barrier {
8256 block: Some(block), ..
8257 } => Some(block),
8258 ChainControl::Reorg { .. }
8260 | ChainControl::Safe(_)
8261 | ChainControl::Finalized(_)
8262 | ChainControl::LogCoverage(_)
8263 | ChainControl::Barrier { block: None, .. } => None,
8264 }
8265}
8266
8267fn chain_control_canonical_assertion(control: &ChainControl) -> Option<&BlockRef> {
8268 match control {
8269 ChainControl::Safe(block)
8270 | ChainControl::Finalized(block)
8271 | ChainControl::CanonicalProgress(block)
8272 | ChainControl::LogCoverage(block)
8273 | ChainControl::Barrier {
8274 block: Some(block), ..
8275 } => Some(block),
8276 ChainControl::Reorg { .. } | ChainControl::Barrier { block: None, .. } => None,
8277 }
8278}
8279
8280fn assert_chain_control_identities(
8281 asserted_blocks: &mut HashMap<u64, BlockRef>,
8282 control: &ChainControl,
8283) -> Result<(), ReactiveError> {
8284 match control {
8285 ChainControl::Safe(block)
8286 | ChainControl::Finalized(block)
8287 | ChainControl::CanonicalProgress(block)
8288 | ChainControl::LogCoverage(block)
8289 | ChainControl::Barrier {
8290 block: Some(block), ..
8291 } => assert_canonical_block_identity(asserted_blocks, block, "chain control"),
8292 ChainControl::Barrier { block: None, .. } => Ok(()),
8293 ChainControl::Reorg {
8294 common_ancestor,
8295 new_tip,
8296 ..
8297 } => {
8298 asserted_blocks.retain(|number, _| *number <= common_ancestor.number);
8299 assert_canonical_block_identity(
8300 asserted_blocks,
8301 common_ancestor,
8302 "reorg common ancestor",
8303 )?;
8304 assert_canonical_block_identity(asserted_blocks, new_tip, "reorg new tip")
8305 }
8306 }
8307}
8308
8309fn assert_canonical_block_identity(
8310 asserted_blocks: &mut HashMap<u64, BlockRef>,
8311 block: &BlockRef,
8312 label: &str,
8313) -> Result<(), ReactiveError> {
8314 for asserted in asserted_blocks.values() {
8315 if asserted.hash == block.hash && asserted.number != block.number {
8316 return Err(ReactiveError::InvalidChainControl {
8317 message: format!(
8318 "{label} hash {:?} is already asserted at height {}, not {}",
8319 block.hash, asserted.number, block.number
8320 ),
8321 });
8322 }
8323 if block
8324 .parent_hash
8325 .is_some_and(|parent| parent == asserted.hash)
8326 && asserted.number.checked_add(1) != Some(block.number)
8327 {
8328 return Err(ReactiveError::InvalidChainControl {
8329 message: format!(
8330 "{label} block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
8331 block.number, block.hash, asserted.hash, asserted.number
8332 ),
8333 });
8334 }
8335 if asserted
8336 .parent_hash
8337 .is_some_and(|parent| parent == block.hash)
8338 && block.number.checked_add(1) != Some(asserted.number)
8339 {
8340 return Err(ReactiveError::InvalidChainControl {
8341 message: format!(
8342 "block {}:{:?} asserted earlier names {label} hash {:?} from non-adjacent height {} as its parent",
8343 asserted.number, asserted.hash, block.hash, block.number
8344 ),
8345 });
8346 }
8347 }
8348 if let Some(known) = asserted_blocks.get_mut(&block.number) {
8349 if !optional_block_refs_are_compatible(Some(known), Some(block)) {
8350 return Err(ReactiveError::InvalidChainControl {
8351 message: format!(
8352 "{label} block {}:{:?} conflicts with block identity {:?} asserted earlier in the batch",
8353 block.number, block.hash, known
8354 ),
8355 });
8356 }
8357 enrich_block_ref(known, block);
8358 } else {
8359 asserted_blocks.insert(block.number, *block);
8360 }
8361 Ok(())
8362}
8363
8364fn set_or_enrich_block_ref(current: &mut Option<BlockRef>, incoming: &BlockRef) {
8365 match current {
8366 Some(current) if current.number == incoming.number && current.hash == incoming.hash => {
8367 enrich_block_ref(current, incoming);
8368 }
8369 _ => *current = Some(*incoming),
8370 }
8371}
8372
8373fn advance_or_enrich_coverage(current: &mut Option<BlockRef>, incoming: &BlockRef) {
8374 match current {
8375 Some(current) if current.number == incoming.number && current.hash == incoming.hash => {
8376 enrich_block_ref(current, incoming);
8377 }
8378 Some(current) if current.number >= incoming.number => {}
8379 _ => *current = Some(*incoming),
8380 }
8381}
8382
8383fn validate_adjacent_finality(
8384 finalized: Option<&BlockRef>,
8385 safe: Option<&BlockRef>,
8386) -> Result<(), ReactiveError> {
8387 let Some((finalized, safe)) = finalized.zip(safe) else {
8388 return Ok(());
8389 };
8390 if finalized.number.checked_add(1) == Some(safe.number)
8391 && safe.parent_hash != Some(finalized.hash)
8392 {
8393 return Err(ReactiveError::InvalidChainControl {
8394 message: "adjacent safe head does not descend from finalized head".into(),
8395 });
8396 }
8397 Ok(())
8398}
8399
8400fn collect_diff_addresses(diff: &StateDiff, into: &mut HashSet<Address>) {
8406 into.extend(diff.slots.iter().map(|change| change.address));
8407 into.extend(diff.accounts.iter().map(|change| change.address));
8408 into.extend(diff.purged.iter().map(|purge| purge.address));
8409 into.extend(diff.skipped.iter().map(|skipped| skipped.address));
8410 into.extend(diff.skipped_balances.iter().map(|skipped| skipped.address));
8411 into.extend(diff.skipped_masks.iter().map(|skipped| skipped.address));
8412 into.extend(diff.skipped_accounts.iter().map(|skipped| skipped.address));
8413}
8414
8415fn root_moved_account_resync(
8420 address: Address,
8421 block: u64,
8422 fields: AccountFieldMask,
8423) -> ResyncRequest {
8424 ResyncRequest {
8425 id: ResyncId::new(format!("root-moved:{address:#x}:{block}")),
8426 reason: ResyncReason::RootMoved,
8427 block: ResyncBlock::Number(block),
8428 targets: vec![ResyncTarget::Account { address, fields }],
8429 priority: ResyncPriority::Normal,
8430 }
8431}
8432
8433fn batch_preconfirmation<N: Network>(
8434 batch: &ReactiveInputBatch<N>,
8435) -> Result<Option<FlashblockRef>, ReactiveError> {
8436 let mut flashblock: Option<FlashblockRef> = None;
8437 let mut has_non_preconfirmed = false;
8438 for (index, record) in batch.records().iter().enumerate() {
8439 match &record.context.chain_status {
8440 ChainStatus::Preconfirmed {
8441 flashblock: current,
8442 } => {
8443 if batch.record_delivery_scope(index) != Some(DeliveryScope::Preconfirmed) {
8444 return Err(ReactiveError::InvalidInputRecord {
8445 message: "pre-confirmed input requires pre-confirmed delivery scope".into(),
8446 });
8447 }
8448 if flashblock
8449 .as_ref()
8450 .is_some_and(|known| known != current.as_ref())
8451 {
8452 return Err(ReactiveError::InvalidInputRecord {
8453 message: "one batch cannot mix distinct Flashblock snapshots".into(),
8454 });
8455 }
8456 flashblock.get_or_insert_with(|| current.as_ref().clone());
8457 }
8458 _ => has_non_preconfirmed = true,
8459 }
8460 }
8461 if flashblock.is_some() && (has_non_preconfirmed || !batch.chain_controls().is_empty()) {
8462 return Err(ReactiveError::InvalidInputRecord {
8463 message: "pre-confirmed delivery cannot mix canonical inputs or chain controls".into(),
8464 });
8465 }
8466 Ok(flashblock)
8467}
8468
8469fn canonical_record_block<N: Network>(record: &ReactiveInputRecord<N>) -> Option<&BlockRef> {
8470 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
8471 return None;
8472 }
8473 if is_canonical_status(&record.context.chain_status) {
8474 return context_block_ref(&record.context);
8475 }
8476 None
8477}
8478
8479fn resolve_record_block_payload_metadata<N: Network>(
8480 record: &ReactiveInputRecord<N>,
8481 mut block: BlockRef,
8482) -> Result<BlockRef, ReactiveError> {
8483 let ReactiveInput::Log(log) = &record.input else {
8484 return Ok(block);
8485 };
8486 if log.block_number != Some(block.number) || log.block_hash != Some(block.hash) {
8487 return Err(ReactiveError::InvalidInputRecord {
8488 message: "log payload and canonical context carry different block identities".into(),
8489 });
8490 }
8491 if let Some(timestamp) = log.block_timestamp {
8492 if block.timestamp.is_some_and(|known| known != timestamp) {
8493 return Err(ReactiveError::InvalidInputRecord {
8494 message: "log payload and canonical context carry different block timestamps"
8495 .into(),
8496 });
8497 }
8498 block.timestamp = Some(timestamp);
8499 }
8500 Ok(block)
8501}
8502
8503fn validate_input_record<N: Network>(record: &ReactiveInputRecord<N>) -> Result<(), ReactiveError> {
8504 let invalid = |message: String| ReactiveError::InvalidInputRecord { message };
8505 if let ChainStatus::Preconfirmed { flashblock } = &record.context.chain_status
8506 && record.context.block != Some(flashblock.block_ref())
8507 {
8508 return Err(invalid(
8509 "pre-confirmed status and context carry different partial block identities".into(),
8510 ));
8511 }
8512 let status_block = match &record.context.chain_status {
8513 ChainStatus::Included { block, .. }
8514 | ChainStatus::Safe { block }
8515 | ChainStatus::Finalized { block }
8516 | ChainStatus::Reorged {
8517 dropped_from: block,
8518 } => Some(block),
8519 ChainStatus::Preconfirmed { .. } => record.context.block.as_ref(),
8520 ChainStatus::Pending => None,
8521 };
8522 match (status_block, record.context.block.as_ref()) {
8523 (Some(status), Some(context)) if status == context => {}
8524 (Some(_), Some(_)) => {
8525 return Err(invalid(
8526 "chain status and context carry different block identities".into(),
8527 ));
8528 }
8529 (Some(_), None) => {
8530 return Err(invalid(
8531 "included or reorged input is missing its context block".into(),
8532 ));
8533 }
8534 (None, Some(_)) => {
8535 return Err(invalid(
8536 "pending input cannot carry a canonical context block".into(),
8537 ));
8538 }
8539 (None, None) => {}
8540 }
8541
8542 match &record.input {
8543 ReactiveInput::Log(log) => {
8544 let Some(block) = status_block else {
8545 return Err(invalid(
8546 "log input must carry an included or reorged block identity".into(),
8547 ));
8548 };
8549 if log.removed && !matches!(record.context.chain_status, ChainStatus::Reorged { .. }) {
8550 return Err(invalid(
8551 "removed log must carry reorged chain status".into(),
8552 ));
8553 }
8554 let block_number = log
8555 .block_number
8556 .ok_or_else(|| invalid("log is missing its block number".into()))?;
8557 let block_hash = log
8558 .block_hash
8559 .ok_or_else(|| invalid("log is missing its block hash".into()))?;
8560 log.transaction_hash
8561 .ok_or_else(|| invalid("log is missing its transaction hash".into()))?;
8562 let transaction_index = log
8563 .transaction_index
8564 .ok_or_else(|| invalid("log is missing its transaction index".into()))?;
8565 let log_index = log
8566 .log_index
8567 .ok_or_else(|| invalid("log is missing its log index".into()))?;
8568 if block_number != block.number
8569 || block_hash != block.hash
8570 || !optional_metadata_compatible(
8571 log.block_timestamp.as_ref(),
8572 block.timestamp.as_ref(),
8573 )
8574 {
8575 return Err(invalid(
8576 "log payload and context carry different block identities".into(),
8577 ));
8578 }
8579 if record.context.transaction_index != Some(transaction_index)
8580 || record.context.log_index != Some(log_index)
8581 {
8582 return Err(invalid(
8583 "log payload and context carry different transaction/log positions".into(),
8584 ));
8585 }
8586 }
8587 ReactiveInput::BlockHeader(header) => {
8588 if let Some(block) = status_block {
8589 if header.number() != block.number
8590 || header.hash() != block.hash
8591 || Some(header.parent_hash()) != block.parent_hash
8592 || Some(header.timestamp()) != block.timestamp
8593 {
8594 return Err(invalid(
8595 "block header payload and context carry different block identities".into(),
8596 ));
8597 }
8598 } else if !matches!(record.context.chain_status, ChainStatus::Pending) {
8599 return Err(invalid("block header has an unsupported lifecycle".into()));
8600 }
8601 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8602 return Err(invalid(
8603 "block header context cannot carry transaction/log positions".into(),
8604 ));
8605 }
8606 }
8607 ReactiveInput::FullBlock(block_response) => {
8608 let header = block_response.header();
8609 if let Some(block) = status_block {
8610 if header.number() != block.number
8611 || header.hash() != block.hash
8612 || Some(header.parent_hash()) != block.parent_hash
8613 || Some(header.timestamp()) != block.timestamp
8614 {
8615 return Err(invalid(
8616 "full-block payload and context carry different block identities".into(),
8617 ));
8618 }
8619 } else if !matches!(record.context.chain_status, ChainStatus::Pending) {
8620 return Err(invalid("full block has an unsupported lifecycle".into()));
8621 }
8622 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8623 return Err(invalid(
8624 "full-block context cannot carry transaction/log positions".into(),
8625 ));
8626 }
8627 if let Some(transactions) = block_response.transactions().as_transactions() {
8628 for (index, transaction) in transactions.iter().enumerate() {
8629 if transaction
8630 .block_hash()
8631 .is_some_and(|hash| hash != header.hash())
8632 || transaction
8633 .block_number()
8634 .is_some_and(|number| number != header.number())
8635 || transaction
8636 .transaction_index()
8637 .is_some_and(|position| position != index as u64)
8638 {
8639 return Err(invalid(format!(
8640 "full-block transaction {index} carries contradictory inclusion metadata"
8641 )));
8642 }
8643 if transaction
8644 .chain_id()
8645 .zip(record.context.chain_id)
8646 .is_some_and(|(transaction, context)| transaction != context)
8647 {
8648 return Err(invalid(format!(
8649 "full-block transaction {index} carries a chain id conflicting with its context"
8650 )));
8651 }
8652 }
8653 }
8654 }
8655 ReactiveInput::PendingTxHash(_) => {
8656 if !matches!(record.context.chain_status, ChainStatus::Pending) {
8657 return Err(invalid(
8658 "pending transaction input must carry pending chain status".into(),
8659 ));
8660 }
8661 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8662 return Err(invalid(
8663 "pending transaction context cannot carry canonical positions".into(),
8664 ));
8665 }
8666 }
8667 ReactiveInput::PendingTx(transaction) => {
8668 if !matches!(record.context.chain_status, ChainStatus::Pending) {
8669 return Err(invalid(
8670 "pending transaction input must carry pending chain status".into(),
8671 ));
8672 }
8673 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8674 return Err(invalid(
8675 "pending transaction context cannot carry canonical positions".into(),
8676 ));
8677 }
8678 if transaction.block_hash().is_some()
8679 || transaction.block_number().is_some()
8680 || transaction.transaction_index().is_some()
8681 {
8682 return Err(invalid(
8683 "hydrated pending transaction cannot carry inclusion metadata".into(),
8684 ));
8685 }
8686 if transaction
8687 .chain_id()
8688 .zip(record.context.chain_id)
8689 .is_some_and(|(transaction, context)| transaction != context)
8690 {
8691 return Err(invalid(
8692 "pending transaction carries a chain id conflicting with its context".into(),
8693 ));
8694 }
8695 }
8696 }
8697 Ok(())
8698}
8699
8700fn advance_block_for_canonical_record<N: Network>(
8709 cache: &mut EvmCache,
8710 record: &ReactiveInputRecord<N>,
8711) -> Option<Result<(), BlockContextError>> {
8712 if !is_canonical_status(&record.context.chain_status) {
8713 return None;
8714 }
8715 match &record.input {
8716 ReactiveInput::BlockHeader(header) => Some(cache.advance_block(header)),
8717 ReactiveInput::FullBlock(block) => Some(cache.advance_block(block.header())),
8718 _ => None,
8719 }
8720}
8721
8722fn context_block_ref(ctx: &ReactiveContext) -> Option<&BlockRef> {
8723 match &ctx.chain_status {
8724 ChainStatus::Included { block, .. }
8725 | ChainStatus::Safe { block }
8726 | ChainStatus::Finalized { block } => Some(block),
8727 ChainStatus::Reorged { dropped_from } => Some(dropped_from),
8728 ChainStatus::Preconfirmed { .. } => ctx.block.as_ref(),
8729 ChainStatus::Pending => ctx.block.as_ref(),
8730 }
8731}
8732
8733fn reorg_signal_block<N: Network>(
8734 record: &ReactiveInputRecord<N>,
8735) -> Option<(BlockRef, ReorgReason)> {
8736 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
8737 return block_ref_from_record(record).map(|block| (block, ReorgReason::RemovedLog));
8738 }
8739
8740 if let ChainStatus::Reorged { dropped_from } = &record.context.chain_status {
8741 return Some((*dropped_from, ReorgReason::ReorgedInput));
8742 }
8743
8744 None
8745}
8746
8747fn block_ref_from_record<N: Network>(record: &ReactiveInputRecord<N>) -> Option<BlockRef> {
8748 context_block_ref(&record.context)
8749 .cloned()
8750 .or_else(|| match &record.input {
8751 ReactiveInput::Log(log) => Some(BlockRef {
8752 number: log.block_number?,
8753 hash: log.block_hash?,
8754 parent_hash: None,
8755 timestamp: log.block_timestamp,
8756 }),
8757 ReactiveInput::BlockHeader(header) => Some(BlockRef {
8758 number: header.number(),
8759 hash: header.hash(),
8760 parent_hash: Some(header.parent_hash()),
8761 timestamp: Some(header.timestamp()),
8762 }),
8763 ReactiveInput::FullBlock(block) => {
8764 let header = block.header();
8765 Some(BlockRef {
8766 number: header.number(),
8767 hash: header.hash(),
8768 parent_hash: Some(header.parent_hash()),
8769 timestamp: Some(header.timestamp()),
8770 })
8771 }
8772 ReactiveInput::PendingTxHash(_) | ReactiveInput::PendingTx(_) => None,
8773 })
8774}
8775
8776fn remove_canceled_resyncs_from_batch(
8777 resyncs: &mut Vec<ResyncRequest>,
8778 canceled: &[ResyncRequest],
8779) {
8780 if canceled.is_empty() {
8781 return;
8782 }
8783 let canceled_ids: HashSet<_> = canceled.iter().map(|request| request.id.clone()).collect();
8784 resyncs.retain(|request| !canceled_ids.contains(&request.id));
8785}
8786
8787fn resync_target_address(target: &ResyncTarget) -> Address {
8788 match target {
8789 ResyncTarget::StorageSlot { address, .. }
8790 | ResyncTarget::StorageSlots { address, .. }
8791 | ResyncTarget::Account { address, .. } => *address,
8792 }
8793}
8794
8795fn resync_request_targets_dropped_block(
8796 request: &ResyncRequest,
8797 dropped_blocks: &[BlockRef],
8798) -> bool {
8799 let ResyncBlock::Hash { number, hash, .. } = &request.block else {
8800 return false;
8801 };
8802 dropped_blocks
8803 .iter()
8804 .any(|block| block.hash == *hash && block.number == *number)
8805}
8806
8807fn single_hash_pinned_resync_block(report: &ResyncReport) -> Option<BlockRef> {
8808 let first = report.requested.first()?.block.clone();
8809 if !report
8810 .requested
8811 .iter()
8812 .all(|request| request.block == first)
8813 {
8814 return None;
8815 }
8816
8817 let ResyncBlock::Hash { number, hash, .. } = first else {
8818 return None;
8819 };
8820
8821 Some(BlockRef {
8822 number,
8823 hash,
8824 parent_hash: None,
8825 timestamp: None,
8826 })
8827}
8828
8829fn purge_scopes_for_dropped_journals<N: Network>(
8830 dropped: &[BlockJournal<N>],
8831) -> Vec<(Address, PurgeScope)> {
8832 let mut scopes: Vec<(Address, PurgeScope)> = Vec::new();
8833 for entry in dropped.iter().rev() {
8834 for diff in entry.rollback_diffs.iter().rev() {
8835 merge_purge_scopes_for_diff(&mut scopes, diff);
8836 }
8837 }
8838 scopes
8839}
8840
8841fn rollback_updates_for_dropped_journals<N: Network>(
8842 dropped: &[BlockJournal<N>],
8843 purge_scopes: &[(Address, PurgeScope)],
8844) -> Vec<StateUpdate> {
8845 let purge_addresses: HashSet<_> = purge_scopes
8846 .iter()
8847 .map(|(address, _scope)| *address)
8848 .collect();
8849 let mut updates = Vec::new();
8850 for entry in dropped.iter().rev() {
8851 for diff in entry.rollback_diffs.iter().rev() {
8852 push_rollback_updates_for_diff(&mut updates, diff, &purge_addresses);
8853 }
8854 }
8855 updates
8856}
8857
8858fn merge_purge_scopes_for_diff(scopes: &mut Vec<(Address, PurgeScope)>, diff: &StateDiff) {
8859 for change in &diff.accounts {
8860 merge_purge_scope(scopes, change.address, PurgeScope::Account);
8861 }
8862 for record in &diff.purged {
8863 merge_purge_scope(scopes, record.address, record.scope.clone());
8864 }
8865}
8866
8867fn push_rollback_updates_for_diff(
8868 updates: &mut Vec<StateUpdate>,
8869 diff: &StateDiff,
8870 purge_addresses: &HashSet<Address>,
8871) {
8872 for change in diff.slots.iter().rev() {
8873 if purge_addresses.contains(&change.address) {
8874 continue;
8875 }
8876 updates.push(StateUpdate::slot(change.address, change.slot, change.old));
8877 }
8878}
8879
8880fn merge_purge_scope(scopes: &mut Vec<(Address, PurgeScope)>, address: Address, scope: PurgeScope) {
8881 if let Some((_existing_address, existing_scope)) = scopes
8882 .iter_mut()
8883 .find(|(existing_address, _scope)| *existing_address == address)
8884 {
8885 *existing_scope = merged_purge_scope(existing_scope.clone(), scope);
8886 } else {
8887 scopes.push((address, scope));
8888 }
8889}
8890
8891fn merged_purge_scope(left: PurgeScope, right: PurgeScope) -> PurgeScope {
8892 match (left, right) {
8893 (PurgeScope::Account, _) | (_, PurgeScope::Account) => PurgeScope::Account,
8894 (PurgeScope::AllStorage, _) | (_, PurgeScope::AllStorage) => PurgeScope::AllStorage,
8895 (PurgeScope::Slots(mut left), PurgeScope::Slots(right)) => {
8896 for slot in right {
8897 if !left.contains(&slot) {
8898 left.push(slot);
8899 }
8900 }
8901 PurgeScope::Slots(left)
8902 }
8903 }
8904}
8905
8906#[derive(Clone, Debug)]
8907struct StorageFetchSlot {
8908 address: Address,
8909 slot: U256,
8910 origins: Vec<StorageFetchOrigin>,
8911}
8912
8913#[derive(Clone, Debug)]
8914struct StorageFetchOrigin {
8915 request_id: ResyncId,
8916 target: ResyncTarget,
8917}
8918
8919#[derive(Clone, Debug)]
8920struct StorageFetchGroup {
8921 block: ResyncBlock,
8922 slots: Vec<StorageFetchSlot>,
8923 seen: HashSet<(Address, U256)>,
8924}
8925
8926#[derive(Clone, Debug)]
8929struct AccountResyncTarget {
8930 request_id: ResyncId,
8931 block: ResyncBlock,
8932 address: Address,
8933 fields: AccountFieldMask,
8934}
8935
8936fn resolve_trace_resyncs(
8937 cache: &EvmCache,
8938 storage_groups: &mut Vec<StorageFetchGroup>,
8939 account_targets: &mut Vec<AccountResyncTarget>,
8940 state_updates: &mut Vec<StateUpdate>,
8941) {
8942 let Some(fetcher) = cache.block_state_diff_fetcher().cloned() else {
8943 return;
8944 };
8945
8946 let mut blocks = Vec::new();
8947 let mut seen = HashSet::new();
8948 for block in storage_groups
8949 .iter()
8950 .map(|group| group.block.clone())
8951 .chain(account_targets.iter().map(|target| target.block.clone()))
8952 {
8953 if seen.insert(block.clone()) {
8954 blocks.push(block);
8955 }
8956 }
8957
8958 let mut traces = HashMap::new();
8959 for block in blocks {
8960 match (fetcher)(resync_block_to_block_id(&block)) {
8961 Ok(diff) => {
8962 traces.insert(block, diff);
8963 }
8964 Err(error) => {
8965 tracing::debug!(
8966 block = ?block,
8967 error = %error,
8968 "block trace resync source failed; falling back to point resync"
8969 );
8970 }
8971 }
8972 }
8973
8974 for group in storage_groups.iter_mut() {
8975 let Some(trace) = traces.get(&group.block) else {
8976 continue;
8977 };
8978 group.slots.retain(|slot| {
8979 if let Some(value) = trace_storage_value(trace, slot.address, slot.slot) {
8980 state_updates.push(StateUpdate::slot(slot.address, slot.slot, value));
8981 return false;
8982 }
8983 cache
8984 .cached_storage_value(slot.address, slot.slot)
8985 .is_none()
8986 });
8987 group.seen = group
8988 .slots
8989 .iter()
8990 .map(|slot| (slot.address, slot.slot))
8991 .collect();
8992 }
8993 storage_groups.retain(|group| !group.slots.is_empty());
8994
8995 let mut unresolved_accounts = Vec::new();
8996 for mut account in account_targets.drain(..) {
8997 let Some(trace) = traces.get(&account.block) else {
8998 unresolved_accounts.push(account);
8999 continue;
9000 };
9001 let Some(trace_account) = trace
9002 .accounts
9003 .iter()
9004 .find(|diff| diff.address == account.address)
9005 else {
9006 unresolved_accounts.push(account);
9007 continue;
9008 };
9009
9010 let mut patch = AccountPatch::default();
9011 let mut unresolved = AccountFieldMask::default();
9012 if account.fields.balance {
9013 if let Some(balance) = trace_account.balance {
9014 patch = patch.balance(balance);
9015 } else {
9016 unresolved.balance = true;
9017 }
9018 }
9019 if account.fields.nonce {
9020 if let Some(nonce) = trace_account.nonce {
9021 patch = patch.nonce(nonce);
9022 } else {
9023 unresolved.nonce = true;
9024 }
9025 }
9026 if account.fields.code {
9027 if let Some(code) = &trace_account.code {
9028 patch = patch.code(code.clone());
9029 } else {
9030 unresolved.code = true;
9031 }
9032 }
9033
9034 if patch.balance.is_some() || patch.nonce.is_some() || patch.code.is_some() {
9035 state_updates.push(StateUpdate::account_upsert(account.address, patch));
9036 }
9037 if !account_field_mask_empty(unresolved) {
9038 account.fields = unresolved;
9039 unresolved_accounts.push(account);
9040 }
9041 }
9042 *account_targets = unresolved_accounts;
9043}
9044
9045fn trace_storage_value(trace: &BlockStateDiff, address: Address, slot: U256) -> Option<U256> {
9046 trace
9047 .accounts
9048 .iter()
9049 .find(|account| account.address == address)
9050 .and_then(|account| {
9051 account
9052 .storage
9053 .iter()
9054 .find(|entry| entry.slot == slot)
9055 .map(|entry| entry.value)
9056 })
9057}
9058
9059fn account_field_mask_empty(mask: AccountFieldMask) -> bool {
9060 !mask.balance && !mask.nonce && !mask.code
9061}
9062
9063fn execute_resync_requests(cache: &mut EvmCache, requests: &[ResyncRequest]) -> ResyncReport {
9064 let mut failed = Vec::new();
9065 let mut storage_groups: Vec<StorageFetchGroup> = Vec::new();
9066 let mut account_targets: Vec<AccountResyncTarget> = Vec::new();
9067
9068 for request in requests {
9069 for target in &request.targets {
9070 match target {
9071 ResyncTarget::StorageSlot { address, slot } => {
9072 push_storage_resync_slot(
9073 &mut storage_groups,
9074 &request.id,
9075 &request.block,
9076 *address,
9077 *slot,
9078 );
9079 }
9080 ResyncTarget::StorageSlots { address, slots } => {
9081 for slot in slots {
9082 push_storage_resync_slot(
9083 &mut storage_groups,
9084 &request.id,
9085 &request.block,
9086 *address,
9087 *slot,
9088 );
9089 }
9090 }
9091 ResyncTarget::Account { address, fields } => {
9092 account_targets.push(AccountResyncTarget {
9093 request_id: request.id.clone(),
9094 block: request.block.clone(),
9095 address: *address,
9096 fields: *fields,
9097 });
9098 }
9099 }
9100 }
9101 }
9102
9103 let mut state_updates = Vec::new();
9104 resolve_trace_resyncs(
9105 cache,
9106 &mut storage_groups,
9107 &mut account_targets,
9108 &mut state_updates,
9109 );
9110
9111 if !storage_groups.is_empty() {
9112 if let Some(fetcher) = cache.storage_batch_fetcher().cloned() {
9113 for group in storage_groups {
9114 let block = group.block.clone();
9115 let fetches: Vec<(Address, U256)> = group
9116 .slots
9117 .iter()
9118 .map(|slot| (slot.address, slot.slot))
9119 .collect();
9120 let results = (fetcher)(fetches, resync_block_to_block_id(&block));
9121 let mut pending: HashMap<(Address, U256), StorageFetchSlot> = group
9122 .slots
9123 .iter()
9124 .cloned()
9125 .map(|slot| ((slot.address, slot.slot), slot))
9126 .collect();
9127
9128 for (address, slot, fetched) in results {
9129 let Some(requested_slot) = pending.remove(&(address, slot)) else {
9130 continue;
9131 };
9132 match fetched {
9133 Ok(value) => state_updates.push(StateUpdate::slot(address, slot, value)),
9134 Err(error) => {
9135 let message = error.to_string();
9136 push_resync_failures(
9137 &mut failed,
9138 &block,
9139 requested_slot.origins,
9140 ResyncFailureKind::StorageFetchFailed,
9141 message,
9142 );
9143 }
9144 }
9145 }
9146
9147 for requested_slot in group.slots {
9148 if pending
9149 .remove(&(requested_slot.address, requested_slot.slot))
9150 .is_some()
9151 {
9152 push_resync_failures(
9153 &mut failed,
9154 &block,
9155 requested_slot.origins,
9156 ResyncFailureKind::StorageFetchOmitted,
9157 "storage batch fetcher did not return a value for slot".to_string(),
9158 );
9159 }
9160 }
9161 }
9162 } else {
9163 for group in storage_groups {
9164 let block = group.block.clone();
9165 for slot in group.slots {
9166 push_resync_failures(
9167 &mut failed,
9168 &block,
9169 slot.origins,
9170 ResyncFailureKind::MissingStorageFetcher,
9171 "storage resync requires a storage batch fetcher".to_string(),
9172 );
9173 }
9174 }
9175 }
9176 }
9177
9178 if !account_targets.is_empty() {
9179 if let Some(fetcher) = cache.account_proof_fetcher().cloned() {
9180 let mut groups: Vec<(BlockId, Vec<_>)> = Vec::new();
9186 for account in account_targets {
9187 let block_id = resync_block_to_block_id(&account.block);
9188 match groups
9189 .iter_mut()
9190 .find(|(group_block, _)| *group_block == block_id)
9191 {
9192 Some((_, group)) => group.push(account),
9193 None => groups.push((block_id, vec![account])),
9194 }
9195 }
9196 for (block_id, group) in groups {
9197 let probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
9198 group
9199 .iter()
9200 .map(|account| (account.address, vec![]))
9201 .collect(),
9202 block_id,
9203 )
9204 .into_iter()
9205 .collect();
9206 for account in group {
9207 match probes.get(&account.address).cloned() {
9211 Some(Ok(proof)) => {
9212 let mut patch = AccountPatch::default();
9217 if account.fields.balance {
9218 patch = patch.balance(proof.balance);
9219 }
9220 if account.fields.nonce {
9221 patch = patch.nonce(proof.nonce);
9222 }
9223 state_updates.push(StateUpdate::account_upsert(account.address, patch));
9229 }
9230 Some(Err(error)) => {
9231 failed.push(ResyncFailure {
9232 request_id: account.request_id,
9233 block: account.block,
9234 target: ResyncTarget::Account {
9235 address: account.address,
9236 fields: account.fields,
9237 },
9238 kind: ResyncFailureKind::AccountFetchFailed,
9239 message: error.to_string(),
9240 });
9241 }
9242 None => {
9243 failed.push(ResyncFailure {
9244 request_id: account.request_id,
9245 block: account.block,
9246 target: ResyncTarget::Account {
9247 address: account.address,
9248 fields: account.fields,
9249 },
9250 kind: ResyncFailureKind::AccountFetchOmitted,
9251 message:
9252 "account proof fetcher did not return a result for address"
9253 .to_string(),
9254 });
9255 }
9256 }
9257 }
9258 }
9259 } else {
9260 for account in account_targets {
9261 failed.push(ResyncFailure {
9262 request_id: account.request_id,
9263 block: account.block,
9264 target: ResyncTarget::Account {
9265 address: account.address,
9266 fields: account.fields,
9267 },
9268 kind: ResyncFailureKind::MissingAccountFetcher,
9269 message: "account resync requires an account proof fetcher".to_string(),
9270 });
9271 }
9272 }
9273 }
9274
9275 let diff = if state_updates.is_empty() {
9276 StateDiff::default()
9277 } else {
9278 cache.apply_updates(&state_updates)
9279 };
9280
9281 ResyncReport {
9282 requested: requests.to_vec(),
9283 state_updates,
9284 diff,
9285 failed,
9286 }
9287}
9288
9289fn push_resync_failures(
9290 failed: &mut Vec<ResyncFailure>,
9291 block: &ResyncBlock,
9292 origins: Vec<StorageFetchOrigin>,
9293 kind: ResyncFailureKind,
9294 message: String,
9295) {
9296 for origin in origins {
9297 failed.push(ResyncFailure {
9298 request_id: origin.request_id,
9299 block: block.clone(),
9300 target: origin.target,
9301 kind,
9302 message: message.clone(),
9303 });
9304 }
9305}
9306
9307fn push_storage_resync_slot(
9308 groups: &mut Vec<StorageFetchGroup>,
9309 request_id: &ResyncId,
9310 block: &ResyncBlock,
9311 address: Address,
9312 slot: U256,
9313) {
9314 let group_index = if let Some(index) = groups.iter().position(|group| group.block == *block) {
9315 index
9316 } else {
9317 groups.push(StorageFetchGroup {
9318 block: block.clone(),
9319 slots: Vec::new(),
9320 seen: HashSet::new(),
9321 });
9322 groups.len() - 1
9323 };
9324
9325 let group = &mut groups[group_index];
9326 let origin = StorageFetchOrigin {
9327 request_id: request_id.clone(),
9328 target: ResyncTarget::StorageSlot { address, slot },
9329 };
9330 if group.seen.insert((address, slot)) {
9331 group.slots.push(StorageFetchSlot {
9332 address,
9333 slot,
9334 origins: vec![origin],
9335 });
9336 } else if let Some(existing) = group
9337 .slots
9338 .iter_mut()
9339 .find(|existing| existing.address == address && existing.slot == slot)
9340 {
9341 existing.origins.push(origin);
9342 }
9343}
9344
9345fn resync_block_to_block_id(block: &ResyncBlock) -> BlockId {
9346 match block {
9347 ResyncBlock::Latest => BlockId::latest(),
9348 ResyncBlock::Pending => BlockId::pending(),
9349 ResyncBlock::Safe => BlockId::safe(),
9350 ResyncBlock::Finalized => BlockId::finalized(),
9351 ResyncBlock::Number(number) => BlockId::number(*number),
9352 ResyncBlock::Hash {
9353 number: _,
9354 hash,
9355 require_canonical,
9356 } => BlockId::from((*hash, Some(*require_canonical))),
9357 }
9358}
9359
9360impl<N: Network> RegisteredHandler<N> {
9361 fn matches(&self, input: &ReactiveInput<N>) -> bool {
9362 self.interests
9363 .iter()
9364 .any(|interest| interest_matches(interest, input))
9365 }
9366
9367 fn route_log(&self, log: &Log) -> Option<ReactiveLogRoute> {
9368 self.interests.iter().find_map(|interest| match interest {
9369 ReactiveInterest::Logs(interest) if interest.matches(log) => Some(ReactiveLogRoute {
9370 handler_id: self.id.clone(),
9371 route_key: interest.route_key(log),
9372 }),
9373 ReactiveInterest::Logs(_)
9374 | ReactiveInterest::Blocks(_)
9375 | ReactiveInterest::PendingTransactions(_) => None,
9376 })
9377 }
9378}
9379
9380fn merge_log_subscription_filter(filters: &mut Vec<Filter>, next: &Filter) {
9381 let mut candidate = next.clone();
9382 let mut insertion_index = filters.len();
9383 let mut index = 0;
9384 while index < filters.len() {
9385 if filters[index].block_option != candidate.block_option {
9386 index += 1;
9387 continue;
9388 }
9389 if let Some(merged) = exact_filter_union(&candidate, &filters[index]) {
9390 candidate = merged;
9391 insertion_index = insertion_index.min(index);
9392 filters.remove(index);
9393 index = 0;
9394 } else {
9395 index += 1;
9396 }
9397 }
9398 filters.insert(insertion_index.min(filters.len()), candidate);
9399}
9400
9401fn exact_filter_union(left: &Filter, right: &Filter) -> Option<Filter> {
9402 if filter_subsumes(left, right) {
9403 return Some(left.clone());
9404 }
9405 if filter_subsumes(right, left) {
9406 return Some(right.clone());
9407 }
9408 let differing_dimensions = usize::from(left.address != right.address)
9409 + left
9410 .topics
9411 .iter()
9412 .zip(right.topics.iter())
9413 .filter(|(left, right)| left != right)
9414 .count();
9415 if differing_dimensions != 1 {
9416 return None;
9417 }
9418
9419 let mut merged = left.clone();
9420 if merged.address != right.address {
9421 merge_filter_set(&mut merged.address, &right.address);
9422 } else {
9423 for (merged_topic, right_topic) in merged.topics.iter_mut().zip(right.topics.iter()) {
9424 if merged_topic != right_topic {
9425 merge_filter_set(merged_topic, right_topic);
9426 break;
9427 }
9428 }
9429 }
9430 Some(merged)
9431}
9432
9433fn filter_subsumes(left: &Filter, right: &Filter) -> bool {
9434 filter_set_subsumes(&left.address, &right.address)
9435 && left
9436 .topics
9437 .iter()
9438 .zip(right.topics.iter())
9439 .all(|(left, right)| filter_set_subsumes(left, right))
9440}
9441
9442fn filter_set_subsumes<T: Eq + Hash>(left: &FilterSet<T>, right: &FilterSet<T>) -> bool {
9443 left.is_empty()
9444 || (!right.is_empty()
9445 && right
9446 .iter()
9447 .all(|value| left.iter().any(|known| known == value)))
9448}
9449
9450fn merge_filter_set<T: Clone + Eq + Hash>(target: &mut FilterSet<T>, source: &FilterSet<T>) {
9451 if target.is_empty() {
9452 return;
9453 }
9454 if source.is_empty() {
9455 *target = FilterSet::default();
9456 return;
9457 }
9458 for value in source.iter() {
9459 target.insert(value.clone());
9460 }
9461}
9462
9463#[derive(Clone, Debug)]
9464struct HandlerExecution {
9465 handler_id: HandlerId,
9466 quality: StateEffectQuality,
9467 tags: Vec<ReportTag>,
9468 state_updates: Vec<StateUpdate>,
9469 invalidations: Vec<InvalidationRequest>,
9470 resyncs: Vec<ResyncRequest>,
9471 speculative: Vec<SpeculativeRequest>,
9472 hook_signals: Vec<HookSignal>,
9473}
9474
9475impl HandlerExecution {
9476 fn from_outcome(
9477 handler_id: HandlerId,
9478 input_ref: InputRef,
9479 outcome: HandlerOutcome,
9480 preconfirmed: bool,
9481 ) -> Self {
9482 let mut state_updates = Vec::new();
9483 let mut invalidations = Vec::new();
9484 let mut resyncs = Vec::new();
9485 let mut speculative = Vec::new();
9486 let mut hook_signals = Vec::new();
9487
9488 for effect in outcome.effects {
9489 match effect {
9490 ReactiveEffect::StateUpdate(update) => state_updates.push(update),
9491 ReactiveEffect::Invalidate(invalidation) => {
9492 state_updates.push(StateUpdate::purge(
9493 invalidation.address,
9494 invalidation.scope.clone(),
9495 ));
9496 invalidations.push(invalidation);
9497 }
9498 ReactiveEffect::Resync(mut request) => {
9499 if preconfirmed {
9500 request.block = ResyncBlock::Pending;
9501 }
9502 resyncs.push(request);
9503 }
9504 ReactiveEffect::Hook(signal) => hook_signals.push(signal),
9505 ReactiveEffect::Speculative(mut request) => {
9506 request.input_ref = input_ref;
9507 speculative.push(request);
9508 }
9509 }
9510 }
9511
9512 Self {
9513 handler_id,
9514 quality: outcome.quality,
9515 tags: outcome.tags,
9516 state_updates,
9517 invalidations,
9518 resyncs,
9519 speculative,
9520 hook_signals,
9521 }
9522 }
9523}
9524
9525fn dedupe_records<N: Network>(
9526 records: Vec<ReactiveInputRecord<N>>,
9527) -> Result<Vec<ReactiveInputRecord<N>>, ReactiveError> {
9528 let mut positions = HashMap::<ReactiveInputIdentity, usize>::new();
9529 let mut deduped = Vec::with_capacity(records.len());
9530 for record in records {
9531 let identity = record.validated_identity()?;
9532 if !record.is_payload_deduplicable() {
9533 deduped.push(record);
9534 continue;
9535 }
9536 if let Some(index) = positions.get(&identity).copied() {
9537 let merged = deduped[index].merge_compatible_duplicate(&record)?;
9538 debug_assert!(merged, "same indexed identity is deduplicable");
9539 } else {
9540 positions.insert(identity, deduped.len());
9541 deduped.push(record);
9542 }
9543 }
9544 Ok(deduped)
9545}
9546
9547fn dedupe_scoped_records<N: Network>(
9548 records: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
9549) -> Result<Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>, ReactiveError> {
9550 let mut positions: HashMap<ReactiveInputIdentity, usize> = HashMap::new();
9551 let mut deduped: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)> =
9552 Vec::with_capacity(records.len());
9553 for (record, audience, delivery_scope) in records {
9554 let identity = record.validated_identity()?;
9555 if !record.is_payload_deduplicable() {
9556 deduped.push((record, audience, delivery_scope));
9557 continue;
9558 }
9559 if let Some(index) = positions.get(&identity).copied() {
9560 let merged = deduped[index].0.merge_compatible_duplicate(&record)?;
9561 debug_assert!(merged, "same indexed identity is deduplicable");
9562 merge_delivery_audience(&mut deduped[index].1, audience);
9563 merge_delivery_scope(&mut deduped[index].2, delivery_scope);
9564 } else {
9565 positions.insert(identity, deduped.len());
9566 deduped.push((record, audience, delivery_scope));
9567 }
9568 }
9569 Ok(deduped)
9570}
9571
9572fn merge_delivery_scope(into: &mut DeliveryScope, incoming: DeliveryScope) {
9573 *into = match (*into, incoming) {
9574 (DeliveryScope::Canonical, _) | (_, DeliveryScope::Canonical) => DeliveryScope::Canonical,
9575 (DeliveryScope::CanonicalProgress, _) | (_, DeliveryScope::CanonicalProgress) => {
9576 DeliveryScope::CanonicalProgress
9577 }
9578 (DeliveryScope::Preconfirmed, DeliveryScope::Preconfirmed)
9579 | (DeliveryScope::Preconfirmed, DeliveryScope::OwnerCatchup)
9580 | (DeliveryScope::OwnerCatchup, DeliveryScope::Preconfirmed) => DeliveryScope::Preconfirmed,
9581 (DeliveryScope::OwnerCatchup, DeliveryScope::OwnerCatchup) => DeliveryScope::OwnerCatchup,
9582 };
9583}
9584
9585fn merge_delivery_audience(into: &mut DeliveryAudience, incoming: DeliveryAudience) {
9586 match (&mut *into, incoming) {
9587 (DeliveryAudience::All, _) => {}
9588 (current, DeliveryAudience::All) => *current = DeliveryAudience::All,
9589 (DeliveryAudience::Owners(current), DeliveryAudience::Owners(incoming)) => {
9590 for owner in incoming {
9591 if !current.contains(&owner) {
9592 current.push(owner);
9593 }
9594 }
9595 }
9596 (DeliveryAudience::AllExcept(current), DeliveryAudience::AllExcept(incoming)) => {
9597 current.retain(|owner| incoming.contains(owner));
9598 }
9599 (DeliveryAudience::AllExcept(excluded), DeliveryAudience::Owners(included)) => {
9600 excluded.retain(|owner| !included.contains(owner));
9601 }
9602 (current @ DeliveryAudience::Owners(_), DeliveryAudience::AllExcept(mut excluded)) => {
9603 let DeliveryAudience::Owners(included) = current else {
9604 unreachable!("match arm restricts the audience variant")
9605 };
9606 excluded.retain(|owner| !included.contains(owner));
9607 *current = DeliveryAudience::AllExcept(excluded);
9608 }
9609 }
9610}
9611
9612fn sort_records<N: Network>(records: Vec<ReactiveInputRecord<N>>) -> Vec<ReactiveInputRecord<N>> {
9613 let mut indexed: Vec<(usize, ReactiveInputRecord<N>)> =
9614 records.into_iter().enumerate().collect();
9615 indexed.sort_by_key(|(index, record)| record_sort_key(*index, record));
9616 indexed.into_iter().map(|(_, record)| record).collect()
9617}
9618
9619fn sort_scoped_records<N: Network>(
9620 records: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
9621) -> Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)> {
9622 let mut indexed: Vec<_> = records.into_iter().enumerate().collect();
9623 indexed.sort_by_key(|(index, (record, _, _))| record_sort_key(*index, record));
9624 indexed
9625 .into_iter()
9626 .map(|(_, scoped_record)| scoped_record)
9627 .collect()
9628}
9629
9630fn record_sort_key<N: Network>(index: usize, record: &ReactiveInputRecord<N>) -> RecordSortKey {
9631 if let Some((block, _)) = reorg_signal_block(record) {
9632 return RecordSortKey {
9633 class: 0,
9634 block_number: block.number,
9635 record_class: 0,
9636 transaction_index: record.context.transaction_index.unwrap_or(u64::MAX),
9637 log_index: record.context.log_index.unwrap_or(u64::MAX),
9638 original_index: index,
9639 };
9640 }
9641 if is_canonical_status(&record.context.chain_status)
9642 && let Some(block) = record.context.block.as_ref()
9643 {
9644 let (record_class, transaction_index, log_index) = match &record.input {
9645 ReactiveInput::BlockHeader(_) | ReactiveInput::FullBlock(_) => (0, 0, 0),
9646 ReactiveInput::Log(log) if !log.removed => (
9647 1,
9648 log.transaction_index
9649 .or(record.context.transaction_index)
9650 .unwrap_or(u64::MAX),
9651 log.log_index
9652 .or(record.context.log_index)
9653 .unwrap_or(u64::MAX),
9654 ),
9655 ReactiveInput::Log(_)
9656 | ReactiveInput::PendingTxHash(_)
9657 | ReactiveInput::PendingTx(_) => (2, u64::MAX, u64::MAX),
9658 };
9659 return RecordSortKey {
9660 class: 1,
9661 block_number: block.number,
9662 record_class,
9663 transaction_index,
9664 log_index,
9665 original_index: index,
9666 };
9667 }
9668
9669 RecordSortKey {
9670 class: 2,
9671 block_number: 0,
9672 record_class: 0,
9673 transaction_index: 0,
9674 log_index: 0,
9675 original_index: index,
9676 }
9677}
9678
9679#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
9680struct RecordSortKey {
9681 class: u8,
9682 block_number: u64,
9683 record_class: u8,
9684 transaction_index: u64,
9685 log_index: u64,
9686 original_index: usize,
9687}
9688
9689fn interest_matches<N: Network>(interest: &ReactiveInterest<N>, input: &ReactiveInput<N>) -> bool {
9690 match (interest, input) {
9691 (ReactiveInterest::Logs(interest), ReactiveInput::Log(log)) => interest.matches(log),
9692 (
9693 ReactiveInterest::Blocks(BlockInterest {
9694 mode: BlockInterestMode::Header,
9695 }),
9696 ReactiveInput::BlockHeader(_),
9697 ) => true,
9698 (
9699 ReactiveInterest::Blocks(BlockInterest {
9700 mode: BlockInterestMode::FullBlock,
9701 }),
9702 ReactiveInput::FullBlock(_),
9703 ) => true,
9704 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTxHash(_)) => {
9705 interest.matches_hash_only()
9706 }
9707 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTx(tx)) => {
9708 interest.matches_tx(tx)
9709 }
9710 _ => false,
9711 }
9712}
9713
9714fn validate_effects(
9715 input_ref: InputRef,
9716 ctx: &ReactiveContext,
9717 handler_id: &HandlerId,
9718 effects: &[ReactiveEffect],
9719) -> Result<(), ReactiveError> {
9720 let pending = matches!(ctx.chain_status, ChainStatus::Pending)
9721 || matches!(input_ref, InputRef::PendingTx { .. });
9722 if !pending {
9723 return Ok(());
9724 }
9725
9726 for effect in effects {
9727 let effect_kind = match effect {
9728 ReactiveEffect::StateUpdate(_) => Some("state_update"),
9729 ReactiveEffect::Invalidate(_) => Some("invalidate"),
9730 ReactiveEffect::Resync(_) => Some("resync"),
9731 ReactiveEffect::Hook(_) | ReactiveEffect::Speculative(_) => None,
9732 };
9733 if let Some(effect_kind) = effect_kind {
9734 return Err(ReactiveError::InvalidPendingEffect {
9735 input_ref: Box::new(input_ref),
9736 handler_id: handler_id.clone(),
9737 effect_kind,
9738 });
9739 }
9740 }
9741 Ok(())
9742}
9743
9744fn detect_conflicts(
9745 input_ref: InputRef,
9746 executions: &[HandlerExecution],
9747) -> Result<(), ReactiveError> {
9748 let mut writes: HashMap<EffectTarget, (AbsoluteValue, HandlerId)> = HashMap::new();
9749 for execution in executions {
9750 for update in &execution.state_updates {
9751 for (target, value) in absolute_writes(update) {
9752 if let Some((previous_value, previous_handler)) = writes.get(&target) {
9753 if previous_value != &value {
9754 return Err(ReactiveError::ConflictingEffects {
9755 input_ref: Box::new(input_ref),
9756 target: Box::new(target),
9757 first: previous_handler.clone(),
9758 second: execution.handler_id.clone(),
9759 });
9760 }
9761 } else {
9762 writes.insert(target, (value, execution.handler_id.clone()));
9763 }
9764 }
9765 }
9766 }
9767 Ok(())
9768}
9769
9770fn absolute_writes(update: &StateUpdate) -> Vec<(EffectTarget, AbsoluteValue)> {
9771 match update {
9772 StateUpdate::Slot {
9773 address,
9774 slot,
9775 value,
9776 } => vec![(
9777 EffectTarget::StorageSlot {
9778 address: *address,
9779 slot: *slot,
9780 },
9781 AbsoluteValue::U256(*value),
9782 )],
9783 StateUpdate::SlotMasked {
9784 address,
9785 slot,
9786 mask,
9787 value,
9788 } => vec![(
9789 EffectTarget::MaskedStorageSlot {
9790 address: *address,
9791 slot: *slot,
9792 mask: *mask,
9793 },
9794 AbsoluteValue::U256(*value),
9795 )],
9796 StateUpdate::Account { address, patch } | StateUpdate::AccountUpsert { address, patch } => {
9797 account_patch_writes(*address, patch)
9798 }
9799 StateUpdate::SlotDelta { .. }
9800 | StateUpdate::BalanceDelta { .. }
9801 | StateUpdate::Purge { .. } => Vec::new(),
9802 }
9803}
9804
9805fn account_patch_writes(
9806 address: Address,
9807 patch: &AccountPatch,
9808) -> Vec<(EffectTarget, AbsoluteValue)> {
9809 let mut writes = Vec::new();
9810 if let Some(balance) = patch.balance {
9811 writes.push((
9812 EffectTarget::AccountBalance { address },
9813 AbsoluteValue::U256(balance),
9814 ));
9815 }
9816 if let Some(nonce) = patch.nonce {
9817 writes.push((
9818 EffectTarget::AccountNonce { address },
9819 AbsoluteValue::U64(nonce),
9820 ));
9821 }
9822 if let Some(code) = &patch.code {
9823 writes.push((
9824 EffectTarget::AccountCode { address },
9825 AbsoluteValue::Bytes(code.clone()),
9826 ));
9827 }
9828 writes
9829}
9830
9831fn input_ref<N: Network>(input: &ReactiveInput<N>, ctx: &ReactiveContext) -> InputRef {
9832 match input {
9833 ReactiveInput::Log(log) => InputRef::Log {
9834 chain_id: ctx.chain_id,
9835 block_hash: log
9836 .block_hash
9837 .or(ctx.block.as_ref().map(|block| block.hash))
9838 .unwrap_or_default(),
9839 transaction_hash: log.transaction_hash.unwrap_or_default(),
9840 log_index: log.log_index.or(ctx.log_index).unwrap_or_default(),
9841 },
9842 ReactiveInput::PendingTxHash(hash) => InputRef::PendingTx {
9843 chain_id: ctx.chain_id,
9844 hash: *hash,
9845 },
9846 ReactiveInput::PendingTx(tx) => InputRef::PendingTx {
9847 chain_id: ctx.chain_id,
9848 hash: tx.tx_hash(),
9849 },
9850 ReactiveInput::BlockHeader(header) => InputRef::Block {
9851 chain_id: ctx.chain_id,
9852 hash: header.hash(),
9853 number: header.number(),
9854 },
9855 ReactiveInput::FullBlock(block) => {
9856 let header = block.header();
9857 InputRef::Block {
9858 chain_id: ctx.chain_id,
9859 hash: header.hash(),
9860 number: header.number(),
9861 }
9862 }
9863 }
9864}
9865
9866fn is_canonical_status(status: &ChainStatus) -> bool {
9867 matches!(
9868 status,
9869 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. }
9870 )
9871}
9872
9873pub struct EventDecoderHandler {
9875 id: HandlerId,
9876 decoder: Arc<dyn EventDecoder>,
9877 interest: LogInterest,
9878}
9879
9880impl EventDecoderHandler {
9881 pub fn new(id: HandlerId, decoder: Arc<dyn EventDecoder>, interest: LogInterest) -> Self {
9883 Self {
9884 id,
9885 decoder,
9886 interest,
9887 }
9888 }
9889}
9890
9891impl<N: Network> ReactiveHandler<N> for EventDecoderHandler {
9892 fn id(&self) -> HandlerId {
9893 self.id.clone()
9894 }
9895
9896 fn interests(&self) -> Vec<ReactiveInterest<N>> {
9897 vec![ReactiveInterest::Logs(self.interest.clone())]
9898 }
9899
9900 fn handle(
9901 &self,
9902 _ctx: &ReactiveContext,
9903 input: &ReactiveInput<N>,
9904 state: &dyn StateView,
9905 ) -> Result<HandlerOutcome, HandlerError> {
9906 let ReactiveInput::Log(log) = input else {
9907 return Ok(HandlerOutcome::empty(StateEffectQuality::NoStateEffect));
9908 };
9909
9910 Ok(HandlerOutcome {
9911 effects: self
9912 .decoder
9913 .decode(&log.inner, state)
9914 .into_iter()
9915 .map(ReactiveEffect::StateUpdate)
9916 .collect(),
9917 quality: StateEffectQuality::ExactFromInput,
9918 tags: Vec::new(),
9919 })
9920 }
9921}
9922
9923#[derive(
9925 Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
9926)]
9927#[non_exhaustive]
9928pub enum SubscriberCapability {
9929 Logs,
9931 BlockHeaders,
9933 FullBlocks,
9935 PendingTransactionHashes,
9937 PendingTransactions,
9939 HistoricalBackfill,
9941 Live,
9943 LogCoverageAttestation,
9952 DurableReplay,
9962 OwnerScopedDelivery,
9964 DynamicInterests,
9966 ExplicitReorgs,
9968 FinalityUpdates,
9970 Barriers,
9972 Preconfirmations,
9974}
9975
9976#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9982pub struct SubscriberCapabilities {
9983 supported: BTreeSet<SubscriberCapability>,
9984}
9985
9986impl SubscriberCapabilities {
9987 pub fn new(capabilities: impl IntoIterator<Item = SubscriberCapability>) -> Self {
9989 Self {
9990 supported: capabilities.into_iter().collect(),
9991 }
9992 }
9993
9994 pub fn supports(&self, capability: SubscriberCapability) -> bool {
9996 self.supported.contains(&capability)
9997 }
9998
9999 pub fn iter(&self) -> impl Iterator<Item = SubscriberCapability> + '_ {
10001 self.supported.iter().copied()
10002 }
10003
10004 pub fn supports_live(&self) -> bool {
10006 self.supports(SubscriberCapability::Live)
10007 }
10008
10009 pub fn supports_durable_replay(&self) -> bool {
10012 self.supports(SubscriberCapability::DurableReplay)
10013 }
10014
10015 pub fn supports_explicit_reorgs(&self) -> bool {
10017 self.supports(SubscriberCapability::ExplicitReorgs)
10018 }
10019}
10020
10021impl FromIterator<SubscriberCapability> for SubscriberCapabilities {
10022 fn from_iter<T: IntoIterator<Item = SubscriberCapability>>(iter: T) -> Self {
10023 Self::new(iter)
10024 }
10025}
10026
10027pub trait EventSubscriber<N: Network = Ethereum>: Send {
10029 fn chain_id(&self) -> Option<u64> {
10036 None
10037 }
10038
10039 fn capabilities(&self) -> SubscriberCapabilities {
10041 SubscriberCapabilities::default()
10042 }
10043
10044 fn register_interests(
10063 &mut self,
10064 interests: &[ReactiveInterest<N>],
10065 ) -> SubscriberOperation<'_, ()>;
10066
10067 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N>;
10079
10080 fn restore_position(
10103 &mut self,
10104 _position: &SubscriberResumePosition,
10105 ) -> Result<(), SubscriberError> {
10106 Ok(())
10107 }
10108
10109 fn acknowledge_delivery(
10123 &mut self,
10124 _token: SubscriberDeliveryToken,
10125 ) -> SubscriberOperation<'_, ()> {
10126 Box::pin(async { Ok(()) })
10127 }
10128}
10129
10130pub type SubscriberOperation<'a, T> =
10135 Pin<Box<dyn Future<Output = Result<T, SubscriberError>> + Send + 'a>>;
10136
10137pub type SubscriberNextBatch<'a, N> = Pin<
10139 Box<dyn Future<Output = Result<Option<ReactiveInputBatch<N>>, SubscriberError>> + Send + 'a>,
10140>;
10141
10142pub type SubscriberNextScopedBatch<'a, N> = Pin<
10144 Box<dyn Future<Output = Result<Option<SubscriberInputBatch<N>>, SubscriberError>> + Send + 'a>,
10145>;
10146
10147#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
10149pub enum SubscriberMode {
10150 #[default]
10156 Auto,
10157 PubSub,
10159 Polling,
10161}
10162
10163#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10164enum FlashblocksAdapter {
10165 NativeSubscriptions,
10166 PendingStatePolling,
10167}
10168
10169fn flashblocks_adapter(chain_id: u64) -> Option<FlashblocksAdapter> {
10170 match chain_id {
10171 8_453 | 84_532 => Some(FlashblocksAdapter::NativeSubscriptions),
10172 10 | 11_155_420 => Some(FlashblocksAdapter::PendingStatePolling),
10173 _ => None,
10174 }
10175}
10176
10177#[derive(Clone, Debug, PartialEq, Eq)]
10179pub struct SubscriberConfig {
10180 pub preconfirmations: PreconfirmationMode,
10183 pub canonical_head_poll_interval: Duration,
10186 pub canonical_head_request_timeout: Duration,
10189 pub flashblock_poll_interval: Duration,
10218 pub max_consecutive_flashblock_poll_failures: usize,
10224 pub max_pending_transaction_receipts_per_tick: usize,
10232 pub max_flashblock_rpc_requests_per_second: usize,
10242 pub log_channel_size: Option<usize>,
10252 pub hydrate_pending_transactions: bool,
10254 pub verify_log_block_context: bool,
10276 pub max_batch_size: usize,
10278 pub max_log_addresses_per_subscription: usize,
10282 pub max_pending_records: usize,
10287 pub max_pending_backfills: usize,
10289 pub max_backfill_log_bytes: usize,
10292 pub max_reconcile_requests_in_flight: usize,
10295 pub reconnect: SubscriberReconnectConfig,
10297}
10298
10299impl Default for SubscriberConfig {
10300 fn default() -> Self {
10301 Self {
10302 log_channel_size: None,
10303 preconfirmations: PreconfirmationMode::Disabled,
10304 canonical_head_poll_interval: Duration::from_millis(500),
10305 canonical_head_request_timeout: Duration::from_secs(3),
10306 flashblock_poll_interval: Duration::from_millis(250),
10307 max_consecutive_flashblock_poll_failures: 10,
10308 max_pending_transaction_receipts_per_tick: 32,
10309 max_flashblock_rpc_requests_per_second: 40,
10310 hydrate_pending_transactions: false,
10311 verify_log_block_context: false,
10312 max_batch_size: 1024,
10313 max_log_addresses_per_subscription: 1024,
10314 max_pending_records: 16_384,
10315 max_pending_backfills: 4_096,
10316 max_backfill_log_bytes: 64 * 1024 * 1024,
10317 max_reconcile_requests_in_flight: 8,
10318 reconnect: SubscriberReconnectConfig::default(),
10319 }
10320 }
10321}
10322
10323#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10325pub enum FlashblocksDelivery {
10326 NativeSubscriptions,
10328 PendingStatePolling,
10330 #[cfg(feature = "raw-flashblocks-json")]
10332 ExternalUpdates,
10333}
10334
10335#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10337pub struct FlashblocksRpcMetrics {
10338 capability_requests: u64,
10339 provider_pair_chain_requests: u64,
10340 canonical_head_requests: u64,
10341 pending_block_requests: u64,
10342 pending_log_requests: u64,
10343 pending_receipt_requests: u64,
10344 pending_receipts_completed: u64,
10345 pending_receipts_unavailable: u64,
10346 failed_requests: u64,
10347 raced_samples: u64,
10348 suppressed_canonical_head_polls: u64,
10349}
10350
10351impl FlashblocksRpcMetrics {
10352 pub const fn capability_requests(self) -> u64 {
10354 self.capability_requests
10355 }
10356
10357 pub const fn provider_pair_chain_requests(self) -> u64 {
10360 self.provider_pair_chain_requests
10361 }
10362
10363 pub const fn canonical_head_requests(self) -> u64 {
10365 self.canonical_head_requests
10366 }
10367
10368 pub const fn pending_block_requests(self) -> u64 {
10370 self.pending_block_requests
10371 }
10372
10373 pub const fn pending_log_requests(self) -> u64 {
10375 self.pending_log_requests
10376 }
10377
10378 pub const fn pending_receipt_requests(self) -> u64 {
10381 self.pending_receipt_requests
10382 }
10383
10384 pub const fn pending_receipts_completed(self) -> u64 {
10386 self.pending_receipts_completed
10387 }
10388
10389 pub const fn pending_receipts_unavailable(self) -> u64 {
10392 self.pending_receipts_unavailable
10393 }
10394
10395 pub const fn failed_requests(self) -> u64 {
10397 self.failed_requests
10398 }
10399
10400 pub const fn suppressed_canonical_head_polls(self) -> u64 {
10407 self.suppressed_canonical_head_polls
10408 }
10409
10410 pub const fn raced_samples(self) -> u64 {
10414 self.raced_samples
10415 }
10416
10417 pub const fn total_requests(self) -> u64 {
10420 self.capability_requests
10421 .saturating_add(self.provider_pair_chain_requests)
10422 .saturating_add(self.canonical_head_requests)
10423 .saturating_add(self.pending_block_requests)
10424 .saturating_add(self.pending_log_requests)
10425 .saturating_add(self.pending_receipt_requests)
10426 }
10427}
10428
10429#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
10436#[non_exhaustive]
10437pub enum SubscriberRpcMethod {
10438 EthChainId,
10440 EthBlockNumber,
10442 EthGetBlockByNumber,
10444 EthGetBlockByHash,
10446 EthGetLogs,
10448 EthGetTransactionReceipt,
10450 EthSubscribe,
10453 OpSupportedCapabilities,
10455}
10456
10457impl SubscriberRpcMethod {
10458 pub const ALL: [Self; 8] = [
10460 Self::EthChainId,
10461 Self::EthBlockNumber,
10462 Self::EthGetBlockByNumber,
10463 Self::EthGetBlockByHash,
10464 Self::EthGetLogs,
10465 Self::EthGetTransactionReceipt,
10466 Self::EthSubscribe,
10467 Self::OpSupportedCapabilities,
10468 ];
10469
10470 pub const COUNT: usize = Self::ALL.len();
10472
10473 pub const fn as_str(self) -> &'static str {
10475 match self {
10476 Self::EthChainId => "eth_chainId",
10477 Self::EthBlockNumber => "eth_blockNumber",
10478 Self::EthGetBlockByNumber => "eth_getBlockByNumber",
10479 Self::EthGetBlockByHash => "eth_getBlockByHash",
10480 Self::EthGetLogs => "eth_getLogs",
10481 Self::EthGetTransactionReceipt => "eth_getTransactionReceipt",
10482 Self::EthSubscribe => "eth_subscribe",
10483 Self::OpSupportedCapabilities => "op_supportedCapabilities",
10484 }
10485 }
10486
10487 const fn index(self) -> usize {
10488 match self {
10489 Self::EthChainId => 0,
10490 Self::EthBlockNumber => 1,
10491 Self::EthGetBlockByNumber => 2,
10492 Self::EthGetBlockByHash => 3,
10493 Self::EthGetLogs => 4,
10494 Self::EthGetTransactionReceipt => 5,
10495 Self::EthSubscribe => 6,
10496 Self::OpSupportedCapabilities => 7,
10497 }
10498 }
10499}
10500
10501impl fmt::Display for SubscriberRpcMethod {
10502 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10503 f.write_str(self.as_str())
10504 }
10505}
10506
10507#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
10514#[non_exhaustive]
10515pub enum SubscriberRpcCause {
10516 ChainIdentity,
10518 StreamSubscription,
10520 FlashblocksSetup,
10523 CanonicalHeadCertification,
10526 PendingStateSample,
10528 LogBlockVerification,
10531 OwnerReconcile,
10534 LazyBackfill,
10536 ReconnectBackfill,
10538 GapBackfill,
10542}
10543
10544impl SubscriberRpcCause {
10545 pub const ALL: [Self; 10] = [
10548 Self::ChainIdentity,
10549 Self::StreamSubscription,
10550 Self::FlashblocksSetup,
10551 Self::CanonicalHeadCertification,
10552 Self::PendingStateSample,
10553 Self::LogBlockVerification,
10554 Self::OwnerReconcile,
10555 Self::LazyBackfill,
10556 Self::ReconnectBackfill,
10557 Self::GapBackfill,
10558 ];
10559
10560 pub const COUNT: usize = Self::ALL.len();
10562
10563 pub const fn as_str(self) -> &'static str {
10565 match self {
10566 Self::ChainIdentity => "chain_identity",
10567 Self::StreamSubscription => "stream_subscription",
10568 Self::FlashblocksSetup => "flashblocks_setup",
10569 Self::CanonicalHeadCertification => "canonical_head_certification",
10570 Self::PendingStateSample => "pending_state_sample",
10571 Self::LogBlockVerification => "log_block_verification",
10572 Self::OwnerReconcile => "owner_reconcile",
10573 Self::LazyBackfill => "lazy_backfill",
10574 Self::ReconnectBackfill => "reconnect_backfill",
10575 Self::GapBackfill => "gap_backfill",
10576 }
10577 }
10578
10579 const fn index(self) -> usize {
10580 match self {
10581 Self::ChainIdentity => 0,
10582 Self::StreamSubscription => 1,
10583 Self::FlashblocksSetup => 2,
10584 Self::CanonicalHeadCertification => 3,
10585 Self::PendingStateSample => 4,
10586 Self::LogBlockVerification => 5,
10587 Self::OwnerReconcile => 6,
10588 Self::LazyBackfill => 7,
10589 Self::ReconnectBackfill => 8,
10590 Self::GapBackfill => 9,
10591 }
10592 }
10593}
10594
10595impl fmt::Display for SubscriberRpcCause {
10596 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10597 f.write_str(self.as_str())
10598 }
10599}
10600
10601#[derive(Clone, Debug, PartialEq, Eq)]
10629pub struct SubscriberRpcStats {
10630 counts: [[u64; SubscriberRpcMethod::COUNT]; SubscriberRpcCause::COUNT],
10631}
10632
10633impl Default for SubscriberRpcStats {
10634 fn default() -> Self {
10635 Self {
10636 counts: [[0; SubscriberRpcMethod::COUNT]; SubscriberRpcCause::COUNT],
10637 }
10638 }
10639}
10640
10641impl SubscriberRpcStats {
10642 pub const fn get(&self, cause: SubscriberRpcCause, method: SubscriberRpcMethod) -> u64 {
10644 self.counts[cause.index()][method.index()]
10645 }
10646
10647 pub fn by_cause(&self, cause: SubscriberRpcCause) -> u64 {
10649 self.counts[cause.index()]
10650 .iter()
10651 .fold(0u64, |total, count| total.saturating_add(*count))
10652 }
10653
10654 pub fn by_method(&self, method: SubscriberRpcMethod) -> u64 {
10656 self.counts
10657 .iter()
10658 .fold(0u64, |total, row| total.saturating_add(row[method.index()]))
10659 }
10660
10661 pub fn total(&self) -> u64 {
10663 SubscriberRpcCause::ALL
10664 .into_iter()
10665 .fold(0u64, |total, cause| {
10666 total.saturating_add(self.by_cause(cause))
10667 })
10668 }
10669
10670 pub fn entries(
10672 &self,
10673 ) -> impl Iterator<Item = (SubscriberRpcCause, SubscriberRpcMethod, u64)> + '_ {
10674 SubscriberRpcCause::ALL.into_iter().flat_map(move |cause| {
10675 SubscriberRpcMethod::ALL
10676 .into_iter()
10677 .map(move |method| (cause, method, self.get(cause, method)))
10678 })
10679 }
10680
10681 pub fn nonzero(
10684 &self,
10685 ) -> impl Iterator<Item = (SubscriberRpcCause, SubscriberRpcMethod, u64)> + '_ {
10686 self.entries().filter(|(_, _, requests)| *requests > 0)
10687 }
10688}
10689
10690#[derive(Debug)]
10698pub(crate) struct SubscriberRpcCounters {
10699 counts: [[AtomicU64; SubscriberRpcMethod::COUNT]; SubscriberRpcCause::COUNT],
10700}
10701
10702impl Default for SubscriberRpcCounters {
10703 fn default() -> Self {
10704 Self {
10705 counts: std::array::from_fn(|_| std::array::from_fn(|_| AtomicU64::new(0))),
10706 }
10707 }
10708}
10709
10710#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10718pub enum SubscriberStreamGap {
10719 Lagged {
10726 skipped: u64,
10728 },
10729 Undecodable,
10734}
10735
10736impl SubscriberStreamGap {
10737 pub const fn skipped(self) -> Option<u64> {
10739 match self {
10740 Self::Lagged { skipped } => Some(skipped),
10741 Self::Undecodable => None,
10742 }
10743 }
10744
10745 pub const fn as_str(self) -> &'static str {
10747 match self {
10748 Self::Lagged { .. } => "lagged",
10749 Self::Undecodable => "undecodable",
10750 }
10751 }
10752}
10753
10754impl fmt::Display for SubscriberStreamGap {
10755 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10756 match self {
10757 Self::Lagged { skipped } => write!(f, "lagged({skipped})"),
10758 Self::Undecodable => f.write_str("undecodable"),
10759 }
10760 }
10761}
10762
10763#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10773pub struct SubscriberStreamGapStats {
10774 lagged_notifications: u64,
10775 undecodable_notifications: u64,
10776 log_gaps_healed: u64,
10777 header_gaps: u64,
10778 preconfirmation_gaps: u64,
10779}
10780
10781impl SubscriberStreamGapStats {
10782 pub const fn lagged_notifications(self) -> u64 {
10784 self.lagged_notifications
10785 }
10786
10787 pub const fn undecodable_notifications(self) -> u64 {
10789 self.undecodable_notifications
10790 }
10791
10792 pub const fn log_gaps_healed(self) -> u64 {
10797 self.log_gaps_healed
10798 }
10799
10800 pub const fn header_gaps(self) -> u64 {
10807 self.header_gaps
10808 }
10809
10810 pub const fn preconfirmation_gaps(self) -> u64 {
10813 self.preconfirmation_gaps
10814 }
10815
10816 pub const fn total_gaps(self) -> u64 {
10818 self.lagged_notifications
10819 .saturating_add(self.undecodable_notifications)
10820 }
10821}
10822
10823#[derive(Debug, Default)]
10825pub(crate) struct SubscriberStreamGapCounters {
10826 lagged_notifications: AtomicU64,
10827 undecodable_notifications: AtomicU64,
10828 log_gaps_healed: AtomicU64,
10829 header_gaps: AtomicU64,
10830 preconfirmation_gaps: AtomicU64,
10831}
10832
10833impl SubscriberStreamGapCounters {
10834 fn bump(counter: &AtomicU64, amount: u64) {
10835 counter.fetch_add(amount, Ordering::Relaxed);
10836 }
10837
10838 #[cfg(feature = "reactive-ws")]
10839 pub(crate) fn record_gap(&self, gap: SubscriberStreamGap) {
10840 match gap {
10841 SubscriberStreamGap::Lagged { skipped } => {
10842 Self::bump(&self.lagged_notifications, skipped.max(1));
10843 }
10844 SubscriberStreamGap::Undecodable => {
10845 Self::bump(&self.undecodable_notifications, 1);
10846 }
10847 }
10848 }
10849
10850 pub(crate) fn record_log_gap_healed(&self) {
10851 Self::bump(&self.log_gaps_healed, 1);
10852 }
10853
10854 pub(crate) fn record_header_gap(&self) {
10855 Self::bump(&self.header_gaps, 1);
10856 }
10857
10858 pub(crate) fn record_preconfirmation_gap(&self) {
10859 Self::bump(&self.preconfirmation_gaps, 1);
10860 }
10861
10862 pub(crate) fn snapshot(&self) -> SubscriberStreamGapStats {
10863 let load = |counter: &AtomicU64| counter.load(Ordering::Relaxed);
10864 SubscriberStreamGapStats {
10865 lagged_notifications: load(&self.lagged_notifications),
10866 undecodable_notifications: load(&self.undecodable_notifications),
10867 log_gaps_healed: load(&self.log_gaps_healed),
10868 header_gaps: load(&self.header_gaps),
10869 preconfirmation_gaps: load(&self.preconfirmation_gaps),
10870 }
10871 }
10872
10873 pub(crate) fn reset(&self) {
10874 for counter in [
10875 &self.lagged_notifications,
10876 &self.undecodable_notifications,
10877 &self.log_gaps_healed,
10878 &self.header_gaps,
10879 &self.preconfirmation_gaps,
10880 ] {
10881 counter.store(0, Ordering::Relaxed);
10882 }
10883 }
10884}
10885
10886impl SubscriberRpcCounters {
10887 pub(crate) fn record(&self, cause: SubscriberRpcCause, method: SubscriberRpcMethod) {
10889 self.record_many(cause, method, 1);
10890 }
10891
10892 pub(crate) fn record_many(
10895 &self,
10896 cause: SubscriberRpcCause,
10897 method: SubscriberRpcMethod,
10898 requests: u64,
10899 ) {
10900 self.counts[cause.index()][method.index()].fetch_add(requests, Ordering::Relaxed);
10901 }
10902
10903 pub(crate) fn snapshot(&self) -> SubscriberRpcStats {
10905 SubscriberRpcStats {
10906 counts: std::array::from_fn(|cause| {
10907 std::array::from_fn(|method| self.counts[cause][method].load(Ordering::Relaxed))
10908 }),
10909 }
10910 }
10911
10912 pub(crate) fn reset(&self) {
10914 for row in &self.counts {
10915 for counter in row {
10916 counter.store(0, Ordering::Relaxed);
10917 }
10918 }
10919 }
10920}
10921
10922#[derive(Clone, Debug, PartialEq, Eq)]
10931pub struct FlashblocksPreflight {
10932 chain_id: u64,
10933 provider: ProviderRef,
10934 delivery: FlashblocksDelivery,
10935 pending_log_subscriptions: usize,
10936 pending_log_filters: usize,
10937 advertised_capabilities: Option<serde_json::Value>,
10938}
10939
10940impl FlashblocksPreflight {
10941 pub const fn chain_id(&self) -> u64 {
10943 self.chain_id
10944 }
10945
10946 pub const fn provider(&self) -> &ProviderRef {
10952 &self.provider
10953 }
10954
10955 pub const fn delivery(&self) -> FlashblocksDelivery {
10957 self.delivery
10958 }
10959
10960 pub const fn pending_log_subscriptions(&self) -> usize {
10964 self.pending_log_subscriptions
10965 }
10966
10967 pub const fn pending_log_filters(&self) -> usize {
10970 self.pending_log_filters
10971 }
10972
10973 pub const fn advertised_capabilities(&self) -> Option<&serde_json::Value> {
10976 self.advertised_capabilities.as_ref()
10977 }
10978}
10979
10980#[derive(Clone, Debug, PartialEq, Eq)]
10986pub struct SubscriberReconnectConfig {
10987 pub enabled: bool,
10989 pub initial_delay: Duration,
10991 pub retry_delay: Duration,
10994 pub max_delay: Duration,
10996 pub max_attempts: Option<usize>,
10998 pub dedupe_window: usize,
11001}
11002
11003impl Default for SubscriberReconnectConfig {
11004 fn default() -> Self {
11005 Self {
11006 enabled: true,
11007 initial_delay: Duration::ZERO,
11008 retry_delay: Duration::from_millis(250),
11009 max_delay: Duration::from_secs(30),
11010 max_attempts: Some(3),
11011 dedupe_window: 4096,
11012 }
11013 }
11014}
11015
11016#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11028pub struct SubscriberBackfill {
11029 from_block: u64,
11030 to_block: Option<u64>,
11031 retained_anchor: Option<BlockRef>,
11032}
11033
11034impl SubscriberBackfill {
11035 pub fn range(from_block: u64, to_block: u64) -> Self {
11037 Self {
11038 from_block,
11039 to_block: Some(to_block),
11040 retained_anchor: None,
11041 }
11042 }
11043
11044 pub fn from_block(from_block: u64) -> Self {
11046 Self {
11047 from_block,
11048 to_block: None,
11049 retained_anchor: None,
11050 }
11051 }
11052
11053 pub fn from_canonical_block(block: BlockRef) -> Self {
11060 Self {
11061 from_block: block.number,
11062 to_block: None,
11063 retained_anchor: Some(block),
11064 }
11065 }
11066
11067 pub fn from_canonical_block_through(
11075 block: BlockRef,
11076 to_block: u64,
11077 ) -> Result<Self, SubscriberError> {
11078 if to_block < block.number {
11079 return Err(SubscriberError::InvalidConfig(
11080 "inclusive backfill upper bound precedes its retained anchor",
11081 ));
11082 }
11083 Ok(Self {
11084 from_block: block.number,
11085 to_block: Some(to_block),
11086 retained_anchor: Some(block),
11087 })
11088 }
11089
11090 pub fn after_canonical_block(block: BlockRef) -> Result<Self, SubscriberError> {
11106 Self::after_canonical_block_inner(block, None)
11107 }
11108
11109 pub fn after_canonical_block_through(
11121 block: BlockRef,
11122 to_block: u64,
11123 ) -> Result<Self, SubscriberError> {
11124 if to_block < block.number {
11125 return Err(SubscriberError::InvalidConfig(
11126 "exclusive backfill upper bound precedes its retained baseline",
11127 ));
11128 }
11129 Self::after_canonical_block_inner(block, Some(to_block))
11130 }
11131
11132 fn after_canonical_block_inner(
11133 block: BlockRef,
11134 to_block: Option<u64>,
11135 ) -> Result<Self, SubscriberError> {
11136 let from_block = block
11137 .number
11138 .checked_add(1)
11139 .ok_or(SubscriberError::InvalidConfig(
11140 "cannot construct an exclusive backfill after block u64::MAX",
11141 ))?;
11142 Ok(Self {
11143 from_block,
11144 to_block,
11145 retained_anchor: Some(block),
11146 })
11147 }
11148
11149 pub fn start_block(&self) -> u64 {
11151 self.from_block
11152 }
11153
11154 pub fn end_block(&self) -> Option<u64> {
11156 self.to_block
11157 }
11158
11159 pub fn retained_anchor(&self) -> Option<&BlockRef> {
11161 self.retained_anchor.as_ref()
11162 }
11163}
11164
11165#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
11172pub struct SubscriberOwnerEpoch {
11173 owner: HandlerId,
11174 sequence: u64,
11175}
11176
11177#[derive(Clone, Debug, PartialEq, Eq)]
11184#[non_exhaustive]
11185pub enum SubscriberInputScope {
11186 Canonical {
11188 owners: Vec<SubscriberOwnerEpoch>,
11190 },
11191 CanonicalResidual {
11195 owners: Vec<SubscriberOwnerEpoch>,
11197 excluded: Vec<HandlerId>,
11199 },
11200 OwnerOnly {
11202 owners: Vec<SubscriberOwnerEpoch>,
11204 },
11205 OwnerOnlyHandlers {
11207 owners: Vec<HandlerId>,
11209 },
11210 Preconfirmed,
11213}
11214
11215impl SubscriberInputScope {
11216 pub fn owners(&self) -> &[SubscriberOwnerEpoch] {
11218 match self {
11219 Self::Canonical { owners }
11220 | Self::CanonicalResidual { owners, .. }
11221 | Self::OwnerOnly { owners } => owners,
11222 Self::OwnerOnlyHandlers { .. } | Self::Preconfirmed => &[],
11223 }
11224 }
11225
11226 pub const fn is_canonical(&self) -> bool {
11228 matches!(
11229 self,
11230 Self::Canonical { .. } | Self::CanonicalResidual { .. }
11231 )
11232 }
11233
11234 pub const fn is_preconfirmed(&self) -> bool {
11236 matches!(self, Self::Preconfirmed)
11237 }
11238}
11239
11240#[derive(Clone, Debug)]
11242#[cfg_attr(
11243 not(feature = "ingress-timing"),
11244 doc = r#"
11245The receipt API is absent unless `ingress-timing` is enabled:
11246```compile_fail
11247use evm_fork_cache::reactive::TransportReceipt;
11248```
11249```compile_fail
11250fn receipt(record: &evm_fork_cache::reactive::SubscriberInputRecord) {
11251 let _ = record.canonical_timing();
11252}
11253```
11254"#
11255)]
11256pub struct SubscriberInputRecord<N: Network = Ethereum> {
11257 record: ReactiveInputRecord<N>,
11258 scope: SubscriberInputScope,
11259 preconfirmation_timing: Option<FlashblockIngressTiming>,
11260 #[cfg(feature = "ingress-timing")]
11261 canonical_timing: Option<CanonicalIngressTiming>,
11262}
11263
11264impl<N: Network> SubscriberInputRecord<N> {
11265 pub const fn record(&self) -> &ReactiveInputRecord<N> {
11267 &self.record
11268 }
11269
11270 pub const fn scope(&self) -> &SubscriberInputScope {
11272 &self.scope
11273 }
11274
11275 pub const fn preconfirmation_timing(&self) -> Option<FlashblockIngressTiming> {
11277 self.preconfirmation_timing
11278 }
11279
11280 #[cfg(feature = "ingress-timing")]
11282 pub const fn canonical_timing(&self) -> Option<CanonicalIngressTiming> {
11283 self.canonical_timing
11284 }
11285
11286 pub fn into_record(self) -> ReactiveInputRecord<N> {
11288 self.record
11289 }
11290}
11291
11292impl<N: Network> std::ops::Deref for SubscriberInputRecord<N> {
11293 type Target = ReactiveInputRecord<N>;
11294
11295 fn deref(&self) -> &Self::Target {
11296 &self.record
11297 }
11298}
11299
11300#[derive(Clone, Debug)]
11302pub struct SubscriberInputBatch<N: Network = Ethereum> {
11303 records: Vec<SubscriberInputRecord<N>>,
11304 chain_id: Option<u64>,
11305 chain_controls: Vec<ChainControl>,
11306 preconfirmation_invalidated: bool,
11307 preconfirmation_timing: Option<FlashblockIngressTiming>,
11308}
11309
11310#[derive(Debug)]
11313#[non_exhaustive]
11314pub enum SubscriberDriverPoll<C, N: Network = Ethereum> {
11315 Control(C),
11317 Batch(Option<SubscriberInputBatch<N>>),
11319}
11320
11321impl<N: Network> SubscriberInputBatch<N> {
11322 pub fn records(&self) -> &[SubscriberInputRecord<N>] {
11324 &self.records
11325 }
11326
11327 pub fn into_records(self) -> Vec<SubscriberInputRecord<N>> {
11329 self.records
11330 }
11331
11332 pub fn chain_controls(&self) -> &[ChainControl] {
11334 &self.chain_controls
11335 }
11336
11337 pub const fn preconfirmation_invalidated(&self) -> bool {
11340 self.preconfirmation_invalidated
11341 }
11342
11343 pub const fn preconfirmation_timing(&self) -> Option<FlashblockIngressTiming> {
11345 self.preconfirmation_timing
11346 }
11347
11348 pub fn into_reactive_batch(self) -> ReactiveInputBatch<N> {
11354 let chain_id = self.chain_id;
11355 let chain_controls = self.chain_controls;
11356 let preconfirmation_timing = self.preconfirmation_timing;
11357 let mut batch = ReactiveInputBatch::from_scoped_records_with_delivery_scope(
11358 self.records.into_iter().map(|scoped| {
11359 let source = scoped.record.context.source;
11360 let (audience, delivery_scope) = match scoped.scope {
11361 SubscriberInputScope::Canonical { .. } => (
11362 DeliveryAudience::All,
11363 if source == InputSource::Backfill {
11364 DeliveryScope::CanonicalProgress
11365 } else {
11366 DeliveryScope::Canonical
11367 },
11368 ),
11369 SubscriberInputScope::CanonicalResidual { excluded, .. } => (
11370 DeliveryAudience::AllExcept(excluded),
11371 if source == InputSource::Backfill {
11372 DeliveryScope::CanonicalProgress
11373 } else {
11374 DeliveryScope::Canonical
11375 },
11376 ),
11377 SubscriberInputScope::OwnerOnly { owners } => {
11378 let mut handler_ids = Vec::with_capacity(owners.len());
11379 for epoch in owners {
11380 if !handler_ids.contains(epoch.owner()) {
11381 handler_ids.push(epoch.owner().clone());
11382 }
11383 }
11384 (
11385 DeliveryAudience::Owners(handler_ids),
11386 DeliveryScope::OwnerCatchup,
11387 )
11388 }
11389 SubscriberInputScope::OwnerOnlyHandlers { owners } => (
11390 DeliveryAudience::Owners(owners),
11391 DeliveryScope::OwnerCatchup,
11392 ),
11393 SubscriberInputScope::Preconfirmed => {
11394 (DeliveryAudience::All, DeliveryScope::Preconfirmed)
11395 }
11396 };
11397 (scoped.record, audience, delivery_scope)
11398 }),
11399 )
11400 .with_chain_controls(chain_controls);
11401 if let Some(chain_id) = chain_id {
11402 batch = batch.with_chain_id(chain_id);
11403 }
11404 if let Some(timing) = preconfirmation_timing {
11405 batch = batch.with_preconfirmation_timing(timing);
11406 }
11407 batch
11408 }
11409}
11410
11411impl SubscriberOwnerEpoch {
11412 pub const fn owner(&self) -> &HandlerId {
11414 &self.owner
11415 }
11416
11417 pub const fn sequence(&self) -> u64 {
11419 self.sequence
11420 }
11421}
11422
11423#[derive(Clone, Debug, PartialEq, Eq)]
11425#[non_exhaustive]
11426pub enum SubscriberOwnerStart {
11427 Live,
11429 PostBlock(BlockRef),
11436}
11437
11438#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11440#[non_exhaustive]
11441pub enum SubscriberOwnerState {
11442 Staged,
11445 Active,
11447 Removing,
11449}
11450
11451#[derive(Clone, Debug, PartialEq, Eq)]
11457pub struct SubscriberOwnerProgress {
11458 owner: SubscriberOwnerEpoch,
11459 through: BlockRef,
11460}
11461
11462impl SubscriberOwnerProgress {
11463 pub const fn owner(&self) -> &SubscriberOwnerEpoch {
11465 &self.owner
11466 }
11467
11468 pub const fn through(&self) -> &BlockRef {
11470 &self.through
11471 }
11472}
11473
11474#[derive(Debug, thiserror::Error)]
11476#[non_exhaustive]
11477pub enum SubscriberOwnerError {
11478 #[error(transparent)]
11480 Subscriber(#[from] SubscriberError),
11481 #[error("subscriber interest owner `{0}` is already registered")]
11483 AlreadyRegistered(HandlerId),
11484 #[error("post-block subscriber baseline {0} has no following block")]
11486 PostBlockOverflow(u64),
11487 #[error("subscriber owner epoch sequence exhausted")]
11489 EpochExhausted,
11490 #[error("subscriber owner epoch is not staged")]
11492 NotStaged,
11493 #[error("subscriber owner was staged live-only and has no catch-up baseline")]
11495 MissingBaseline,
11496 #[error("post-block subscriber owners support log interests only")]
11498 UnsupportedPostBlockInterest,
11499 #[error("subscriber reconcile target block {0} was not found")]
11501 BlockUnavailable(u64),
11502 #[error(
11504 "subscriber reconcile target mismatch: expected block {expected_number} {expected_hash}, got block {actual_number} {actual_hash}"
11505 )]
11506 BlockMismatch {
11507 expected_number: u64,
11509 expected_hash: B256,
11511 actual_number: u64,
11513 actual_hash: B256,
11515 },
11516 #[error("subscriber reconcile target block {target} precedes current owner position {current}")]
11518 ProgressRegression {
11519 current: u64,
11521 target: u64,
11523 },
11524 #[error(
11527 "subscriber reconcile conflicts with retained block {number} {current_hash}: target chain references {target_hash}"
11528 )]
11529 ProgressConflict {
11530 number: u64,
11532 current_hash: B256,
11534 target_hash: B256,
11536 },
11537 #[error("subscriber reconcile returned an invalid catch-up log: {0}")]
11539 InvalidBackfillLog(&'static str),
11540}
11541
11542pub trait InterestOwnerSubscriber<N: Network = Ethereum>: EventSubscriber<N> {
11557 fn upsert_interest_owners(
11570 &mut self,
11571 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
11572 ) -> SubscriberOperation<'_, ()> {
11573 Box::pin(async {
11574 Err(SubscriberError::Unsupported(
11575 "subscriber does not implement atomic bulk owner upsert",
11576 ))
11577 })
11578 }
11579
11580 fn replace_interest_owners(
11593 &mut self,
11594 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
11595 ) -> SubscriberOperation<'_, ()> {
11596 Box::pin(async {
11597 Err(SubscriberError::Unsupported(
11598 "subscriber does not implement atomic exact owner replacement",
11599 ))
11600 })
11601 }
11602
11603 fn replace_interest_owners_with_global_backfill(
11627 &mut self,
11628 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
11629 _backfill: SubscriberBackfill,
11630 ) -> SubscriberOperation<'_, ()> {
11631 Box::pin(async {
11632 Err(SubscriberError::Unsupported(
11633 "subscriber does not implement atomic owner replacement with global backfill",
11634 ))
11635 })
11636 }
11637
11638 fn add_interest_owner(
11650 &mut self,
11651 owner: HandlerId,
11652 interests: &[ReactiveInterest<N>],
11653 ) -> SubscriberOperation<'_, ()>;
11654
11655 fn add_interest_owner_with_backfill(
11663 &mut self,
11664 owner: HandlerId,
11665 interests: &[ReactiveInterest<N>],
11666 backfill: SubscriberBackfill,
11667 ) -> SubscriberOperation<'_, ()>;
11668
11669 fn add_interest_owner_with_canonical_catchup(
11687 &mut self,
11688 _owner: HandlerId,
11689 _interests: &[ReactiveInterest<N>],
11690 _retained: BlockRef,
11691 ) -> SubscriberOperation<'_, ()> {
11692 Box::pin(async {
11693 Err(SubscriberError::Unsupported(
11694 "subscriber does not implement coordinated canonical owner catch-up",
11695 ))
11696 })
11697 }
11698
11699 fn remove_interest_owner(
11710 &mut self,
11711 owner: &HandlerId,
11712 ) -> SubscriberOperation<'_, Option<Vec<ReactiveInterest<N>>>>;
11713
11714 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]>;
11716}
11717
11718pub struct ReactiveEngine<S, N: Network = Ethereum> {
11759 runtime: ReactiveRuntime<N>,
11760 subscriber: S,
11761 pending_acknowledgement: Option<PendingAcknowledgement<N>>,
11762 pending_checkpoint: Option<PendingCheckpoint<N>>,
11763 last_checkpoint_block: Option<DurableCheckpointBlock>,
11764 last_checkpoint_delivery_token: Option<SubscriberDeliveryToken>,
11765 last_checkpoint_delivery_witness: Option<B256>,
11766 last_subscriber_checkpoint: Option<SubscriberCheckpoint>,
11767 checkpoint_identity: Option<DurableCheckpointIdentity>,
11768}
11769
11770struct PendingAcknowledgement<N: Network> {
11771 token: SubscriberDeliveryToken,
11772 report: ReactiveBatchReport<N>,
11773}
11774
11775struct PendingCheckpoint<N: Network> {
11776 metadata: DurableCheckpointMetadata,
11777 delivery_token: Option<SubscriberDeliveryToken>,
11778 report: ReactiveBatchReport<N>,
11779 saved_to: Option<PathBuf>,
11780 staged_generation: u64,
11781}
11782
11783struct CheckpointStage<N: Network> {
11784 incoming_block: Option<DurableCheckpointBlock>,
11785 delivery_token: Option<SubscriberDeliveryToken>,
11786 delivery_witness: Option<B256>,
11787 subscriber_checkpoint: Option<SubscriberCheckpoint>,
11788 staged_generation: u64,
11789 report: ReactiveBatchReport<N>,
11790}
11791
11792struct DurableResumePlan {
11793 runtime: DurableRuntimeRestorePlan,
11794 position: SubscriberResumePosition,
11795 delivery_witness: Option<B256>,
11796}
11797
11798enum HandlerRegistrationCatchup {
11799 LiveOnly,
11800 OwnerBackfill(SubscriberBackfill),
11801 CoordinatedCanonical(BlockRef),
11802}
11803
11804const DELIVERY_WITNESS_VERSION: u32 = 2;
11808const DELIVERY_WITNESS_DOMAIN: &[u8] = b"evm-fork-cache/reactive-delivery-witness";
11809
11810#[derive(serde::Serialize)]
11811struct DeliveryWitnessEnvelope<'a> {
11812 version: u32,
11813 chain_id: Option<u64>,
11814 records: Vec<DeliveryRecordWitness<'a>>,
11815 chain_controls: &'a [ChainControl],
11816 subscriber_checkpoint: Option<&'a [u8]>,
11817 payload_commitment: Option<B256>,
11818}
11819
11820#[derive(serde::Serialize)]
11821struct DeliveryRecordWitness<'a> {
11822 identity: ReactiveInputIdentity,
11823 context: &'a ReactiveContext,
11824 audience: &'a DeliveryAudience,
11825 scope: DeliveryScope,
11826 payload: DeliveryPayloadWitness<'a>,
11827}
11828
11829#[derive(serde::Serialize)]
11830enum DeliveryPayloadWitness<'a> {
11831 Log {
11834 address: Address,
11835 topics: &'a [B256],
11836 data: &'a Bytes,
11837 block_hash: Option<B256>,
11838 block_number: Option<u64>,
11839 block_timestamp: Option<u64>,
11840 transaction_hash: Option<B256>,
11841 transaction_index: Option<u64>,
11842 log_index: Option<u64>,
11843 removed: bool,
11844 },
11845 IdentityCommitted,
11852}
11853
11854fn durable_delivery_witness<N: Network>(
11855 batch: &ReactiveInputBatch<N>,
11856) -> Result<B256, ReactiveEngineError> {
11857 let requires_payload_commitment = batch.records.iter().any(|record| {
11858 matches!(
11859 &record.input,
11860 ReactiveInput::BlockHeader(_)
11861 | ReactiveInput::FullBlock(_)
11862 | ReactiveInput::PendingTx(_)
11863 )
11864 });
11865 if requires_payload_commitment && batch.payload_commitment.is_none() {
11866 return Err(ReactiveEngineError::MissingPayloadCommitment);
11867 }
11868 let records = batch
11869 .records
11870 .iter()
11871 .enumerate()
11872 .map(|(index, record)| {
11873 let payload = match &record.input {
11874 ReactiveInput::Log(log) => DeliveryPayloadWitness::Log {
11875 address: log.address(),
11876 topics: log.topics(),
11877 data: &log.inner.data.data,
11878 block_hash: log.block_hash,
11879 block_number: log.block_number,
11880 block_timestamp: log.block_timestamp,
11881 transaction_hash: log.transaction_hash,
11882 transaction_index: log.transaction_index,
11883 log_index: log.log_index,
11884 removed: log.removed,
11885 },
11886 ReactiveInput::BlockHeader(_)
11887 | ReactiveInput::FullBlock(_)
11888 | ReactiveInput::PendingTxHash(_)
11889 | ReactiveInput::PendingTx(_) => DeliveryPayloadWitness::IdentityCommitted,
11890 };
11891 Ok(DeliveryRecordWitness {
11892 identity: record.validated_identity()?,
11893 context: &record.context,
11894 audience: batch
11895 .record_audience(index)
11896 .expect("enumerated record always has an audience"),
11897 scope: batch
11898 .record_delivery_scope(index)
11899 .expect("enumerated record always has a delivery scope"),
11900 payload,
11901 })
11902 })
11903 .collect::<Result<Vec<_>, ReactiveError>>()?;
11904 let envelope = DeliveryWitnessEnvelope {
11905 version: DELIVERY_WITNESS_VERSION,
11906 chain_id: batch.chain_id,
11907 records,
11908 chain_controls: &batch.chain_controls,
11909 subscriber_checkpoint: batch
11910 .subscriber_checkpoint
11911 .as_ref()
11912 .map(SubscriberCheckpoint::as_bytes),
11913 payload_commitment: batch
11914 .payload_commitment
11915 .as_ref()
11916 .map(SubscriberPayloadCommitment::digest),
11917 };
11918 let encoded = bincode::DefaultOptions::new()
11919 .with_fixint_encoding()
11920 .serialize(&envelope)
11921 .map_err(|error| ReactiveEngineError::DeliveryWitness(error.to_string()))?;
11922 let mut witness = Keccak256::new();
11923 witness.update(DELIVERY_WITNESS_DOMAIN);
11924 witness.update(encoded);
11925 Ok(witness.finalize())
11926}
11927
11928impl<S, N> ReactiveEngine<S, N>
11929where
11930 N: Network,
11931 S: EventSubscriber<N>,
11932{
11933 pub fn new(runtime: ReactiveRuntime<N>, subscriber: S) -> Self {
11935 Self {
11936 runtime,
11937 subscriber,
11938 pending_acknowledgement: None,
11939 pending_checkpoint: None,
11940 last_checkpoint_block: None,
11941 last_checkpoint_delivery_token: None,
11942 last_checkpoint_delivery_witness: None,
11943 last_subscriber_checkpoint: None,
11944 checkpoint_identity: None,
11945 }
11946 }
11947
11948 pub fn into_parts(self) -> Result<(ReactiveRuntime<N>, S), Box<Self>> {
11962 if self.pending_acknowledgement.is_some() || self.pending_checkpoint.is_some() {
11963 return Err(Box::new(self));
11964 }
11965 Ok((self.runtime, self.subscriber))
11966 }
11967
11968 fn durable_resume_plan(
11969 &self,
11970 metadata: &DurableCheckpointMetadata,
11971 ) -> Result<DurableResumePlan, ReactiveCheckpointRestoreError> {
11972 if !self.subscriber.capabilities().supports_durable_replay() {
11973 return Err(ReactiveCheckpointRestoreError::SubscriberNotDurable);
11974 }
11975 self.ensure_subscriber_restore_chain(metadata.identity.chain_id)?;
11976 if !self.runtime.is_pristine_for_checkpoint_restore()
11977 || self.pending_acknowledgement.is_some()
11978 || self.pending_checkpoint.is_some()
11979 || self.last_checkpoint_block.is_some()
11980 || self.last_checkpoint_delivery_token.is_some()
11981 || self.last_checkpoint_delivery_witness.is_some()
11982 || self.last_subscriber_checkpoint.is_some()
11983 || self.checkpoint_identity.is_some()
11984 {
11985 return Err(ReactiveCheckpointRestoreError::ActiveRuntime);
11986 }
11987
11988 let block = BlockRef {
11989 number: metadata.block.number,
11990 hash: metadata.block.hash,
11991 parent_hash: metadata.block.parent_hash,
11992 timestamp: metadata.block.timestamp,
11993 };
11994 let runtime = match metadata.runtime_checkpoint.as_deref() {
11995 Some(bytes) => self
11996 .runtime
11997 .plan_durable_checkpoint_restore(bytes, &block)?,
11998 None => DurableRuntimeRestorePlan {
11999 checkpoint: None,
12000 fallback_history: (self.runtime.config.journal_depth > 0)
12001 .then_some(block)
12002 .into_iter()
12003 .collect(),
12004 },
12005 };
12006 let delivery_token = metadata
12007 .delivery_token
12008 .clone()
12009 .map(SubscriberDeliveryToken::new);
12010 let subscriber_checkpoint = metadata
12011 .subscriber_checkpoint
12012 .clone()
12013 .map(SubscriberCheckpoint::new);
12014 let position = SubscriberResumePosition::new(
12015 metadata.identity.chain_id,
12016 block,
12017 runtime.canonical_history(),
12018 delivery_token,
12019 subscriber_checkpoint,
12020 );
12021 Ok(DurableResumePlan {
12022 runtime,
12023 position,
12024 delivery_witness: metadata.delivery_witness,
12025 })
12026 }
12027
12028 pub fn preview_durable_resume_position(
12057 &self,
12058 metadata: &DurableCheckpointMetadata,
12059 ) -> Result<SubscriberResumePosition, ReactiveCheckpointRestoreError> {
12060 Ok(self.durable_resume_plan(metadata)?.position)
12061 }
12062
12063 pub fn resume_from_durable_checkpoint(
12085 &mut self,
12086 metadata: &DurableCheckpointMetadata,
12087 ) -> Result<(), ReactiveCheckpointRestoreError> {
12088 let plan = self.durable_resume_plan(metadata)?;
12089 let prior_runtime = self.runtime.checkpoint_state();
12090
12091 let DurableResumePlan {
12092 runtime,
12093 position,
12094 delivery_witness,
12095 } = plan;
12096 self.runtime.apply_durable_checkpoint_restore(runtime);
12097 self.runtime.coverage_head = Some(position.coverage_head);
12098 if let Err(error) = self.subscriber.restore_position(&position) {
12099 self.runtime.restore_state(prior_runtime);
12100 return Err(ReactiveCheckpointRestoreError::Subscriber(error));
12101 }
12102 if let Err(error) = self.ensure_subscriber_restore_chain(metadata.identity.chain_id) {
12103 self.runtime.restore_state(prior_runtime);
12104 return Err(error);
12105 }
12106 self.last_checkpoint_block = Some(metadata.block.clone());
12107 self.last_checkpoint_delivery_token = position.delivery_token;
12108 self.last_checkpoint_delivery_witness = delivery_witness;
12109 self.last_subscriber_checkpoint = position.subscriber_checkpoint;
12110 self.checkpoint_identity = Some(metadata.identity.clone());
12111 Ok(())
12112 }
12113
12114 pub fn restore_durable_checkpoint(
12130 &mut self,
12131 cache: &mut EvmCache,
12132 loaded: LoadedDurableCheckpoint,
12133 expected: &DurableCheckpointIdentity,
12134 ) -> Result<DurableCheckpointMetadata, ReactiveCheckpointRestoreError> {
12135 if !self.subscriber.capabilities().supports_durable_replay() {
12136 return Err(ReactiveCheckpointRestoreError::SubscriberNotDurable);
12137 }
12138 self.ensure_subscriber_restore_chain(expected.chain_id)?;
12139 if !self.runtime.is_pristine_for_checkpoint_restore()
12140 || self.pending_acknowledgement.is_some()
12141 || self.pending_checkpoint.is_some()
12142 || self.last_checkpoint_block.is_some()
12143 || self.last_checkpoint_delivery_token.is_some()
12144 || self.last_checkpoint_delivery_witness.is_some()
12145 || self.last_subscriber_checkpoint.is_some()
12146 || self.checkpoint_identity.is_some()
12147 {
12148 return Err(ReactiveCheckpointRestoreError::ActiveRuntime);
12149 }
12150
12151 let prior_cache = EvmCacheStateSnapshot::capture(cache);
12152 let metadata = loaded.restore_into(cache, expected)?;
12153 if let Err(error) = self.resume_from_durable_checkpoint(&metadata) {
12154 prior_cache.restore(cache);
12155 return Err(error);
12156 }
12157 Ok(metadata)
12158 }
12159
12160 pub fn runtime(&self) -> &ReactiveRuntime<N> {
12162 &self.runtime
12163 }
12164
12165 pub fn runtime_mut(&mut self) -> &mut ReactiveRuntime<N> {
12167 &mut self.runtime
12168 }
12169
12170 pub fn subscriber(&self) -> &S {
12172 &self.subscriber
12173 }
12174
12175 pub fn subscriber_mut(&mut self) -> &mut S {
12177 &mut self.subscriber
12178 }
12179
12180 pub fn adopt_canonical_baseline(
12198 &mut self,
12199 cache: &EvmCache,
12200 baseline: ReactiveCanonicalBaseline,
12201 ) -> Result<(), ReactiveEngineError> {
12202 if self.pending_acknowledgement.is_some()
12203 || self.pending_checkpoint.is_some()
12204 || self.last_checkpoint_block.is_some()
12205 || self.last_checkpoint_delivery_token.is_some()
12206 || self.last_checkpoint_delivery_witness.is_some()
12207 || self.last_subscriber_checkpoint.is_some()
12208 || self.checkpoint_identity.is_some()
12209 {
12210 return Err(ReactiveBaselineError::ActiveRuntime.into());
12211 }
12212 self.runtime
12216 .validate_canonical_baseline_adoption(baseline.block)?;
12217 if baseline.chain_id != cache.chain_id() {
12218 return Err(ReactiveBaselineError::CacheChainMismatch {
12219 baseline_chain_id: baseline.chain_id,
12220 cache_chain_id: cache.chain_id(),
12221 }
12222 .into());
12223 }
12224 self.ensure_subscriber_chain(cache)?;
12225 let exact_selector = BlockId::from((baseline.block.hash, Some(true)));
12226 let context_matches = cache.block_number() == Some(baseline.block.number)
12227 && baseline
12228 .block
12229 .timestamp
12230 .is_none_or(|timestamp| cache.timestamp() == Some(timestamp));
12231 if cache.block() != exact_selector || !context_matches {
12232 return Err(ReactiveBaselineError::CacheBlockMismatch {
12233 number: baseline.block.number,
12234 hash: baseline.block.hash,
12235 }
12236 .into());
12237 }
12238 self.runtime.adopt_canonical_baseline(baseline.block)?;
12239 Ok(())
12240 }
12241
12242 pub fn next_batch(
12255 &mut self,
12256 cache: &EvmCache,
12257 ) -> Result<SubscriberNextBatch<'_, N>, ReactiveEngineError> {
12258 if self.pending_checkpoint.is_some() {
12259 return Err(ReactiveEngineError::PendingCheckpointCommit);
12260 }
12261 if self.pending_acknowledgement.is_some() {
12262 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
12263 }
12264 self.ensure_subscriber_chain(cache)?;
12265 Ok(self.subscriber.next_batch())
12266 }
12267
12268 pub fn ingest_batch(
12277 &mut self,
12278 cache: &mut EvmCache,
12279 batch: ReactiveInputBatch<N>,
12280 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
12281 self.ensure_raw_ingest_is_safe(cache, &batch)?;
12282 Ok(self.runtime.ingest_batch(cache, batch)?)
12283 }
12284
12285 pub fn ingest_batch_with_resync(
12295 &mut self,
12296 cache: &mut EvmCache,
12297 batch: ReactiveInputBatch<N>,
12298 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
12299 self.ensure_raw_ingest_is_safe(cache, &batch)?;
12300 Ok(self.runtime.ingest_batch_with_resync(cache, batch)?)
12301 }
12302
12303 fn ensure_raw_ingest_is_safe(
12304 &self,
12305 cache: &EvmCache,
12306 batch: &ReactiveInputBatch<N>,
12307 ) -> Result<(), ReactiveEngineError> {
12308 if self.pending_checkpoint.is_some() {
12309 return Err(ReactiveEngineError::PendingCheckpointCommit);
12310 }
12311 if self.pending_acknowledgement.is_some() {
12312 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
12313 }
12314 if batch.delivery_token().is_some() || batch.subscriber_checkpoint().is_some() {
12315 return Err(ReactiveEngineError::UncommittedDeliveryMetadata);
12316 }
12317 self.ensure_subscriber_chain(cache)?;
12318 Ok(())
12319 }
12320
12321 fn ensure_subscriber_chain(&self, cache: &EvmCache) -> Result<(), ReactiveEngineError> {
12322 if let Some(subscriber_chain_id) = self.subscriber.chain_id()
12323 && subscriber_chain_id != cache.chain_id()
12324 {
12325 return Err(ReactiveEngineError::SubscriberChainMismatch {
12326 subscriber_chain_id,
12327 cache_chain_id: cache.chain_id(),
12328 });
12329 }
12330 Ok(())
12331 }
12332
12333 fn ensure_subscriber_restore_chain(
12334 &self,
12335 checkpoint_chain_id: u64,
12336 ) -> Result<(), ReactiveCheckpointRestoreError> {
12337 if let Some(subscriber_chain_id) = self.subscriber.chain_id()
12338 && subscriber_chain_id != checkpoint_chain_id
12339 {
12340 return Err(ReactiveCheckpointRestoreError::SubscriberChainMismatch {
12341 subscriber_chain_id,
12342 checkpoint_chain_id,
12343 });
12344 }
12345 Ok(())
12346 }
12347
12348 pub async fn next_ingest(
12358 &mut self,
12359 cache: &mut EvmCache,
12360 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
12361 self.ensure_subscriber_chain(cache)?;
12362 if self.pending_checkpoint.is_some() {
12363 return Err(ReactiveEngineError::PendingCheckpointCommit);
12364 }
12365 if self.pending_acknowledgement.is_some() {
12366 return self.commit_pending_acknowledgement().await.map(Some);
12367 }
12368 let batch = self.subscriber.next_batch().await?;
12369 self.ensure_subscriber_chain(cache)?;
12370 let Some(mut batch) = batch else {
12371 return Ok(None);
12372 };
12373 let delivery_token = batch.take_delivery_token();
12374 let report = self.runtime.ingest_batch(cache, batch)?;
12375 self.stage_or_return_acknowledgement(delivery_token, report)
12376 .await
12377 }
12378
12379 pub async fn next_ingest_with_resync(
12390 &mut self,
12391 cache: &mut EvmCache,
12392 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
12393 self.ensure_subscriber_chain(cache)?;
12394 if self.pending_checkpoint.is_some() {
12395 return Err(ReactiveEngineError::PendingCheckpointCommit);
12396 }
12397 if self.pending_acknowledgement.is_some() {
12398 return self.commit_pending_acknowledgement().await.map(Some);
12399 }
12400 let batch = self.subscriber.next_batch().await?;
12401 self.ensure_subscriber_chain(cache)?;
12402 let Some(mut batch) = batch else {
12403 return Ok(None);
12404 };
12405 let delivery_token = batch.take_delivery_token();
12406 let report = self.runtime.ingest_batch_with_resync(cache, batch)?;
12407 self.stage_or_return_acknowledgement(delivery_token, report)
12408 .await
12409 }
12410
12411 pub async fn next_ingest_checkpointed(
12442 &mut self,
12443 cache: &mut EvmCache,
12444 store: &DurableCheckpointStore,
12445 identity: &DurableCheckpointIdentity,
12446 ) -> Result<Option<CheckpointedIngest<N>>, ReactiveEngineError> {
12447 if !self.subscriber.capabilities().supports_durable_replay() {
12448 return Err(ReactiveEngineError::SubscriberNotDurable);
12449 }
12450 self.ensure_subscriber_chain(cache)?;
12451 if self.pending_acknowledgement.is_some() {
12452 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
12453 }
12454 self.ensure_checkpoint_identity(cache, identity)?;
12455 if self.pending_checkpoint.is_some() {
12456 return self.commit_pending_checkpoint(cache, store).await.map(Some);
12457 }
12458
12459 let batch = self.subscriber.next_batch().await?;
12460 self.ensure_subscriber_chain(cache)?;
12461 let Some(mut batch) = batch else {
12462 return Ok(None);
12463 };
12464 if batch_preconfirmation(&batch)?.is_some() {
12465 return Err(ReactiveEngineError::PreconfirmationNotCheckpointable);
12466 }
12467 self.runtime.discard_preconfirmed_branch(cache);
12468 let delivery_witness = batch
12469 .delivery_token()
12470 .map(|_| durable_delivery_witness(&batch))
12471 .transpose()?;
12472 let delivery_token = batch.take_delivery_token();
12473 let subscriber_checkpoint = batch.take_subscriber_checkpoint();
12474 if let (Some(replay_token), Some(committed_token)) = (
12475 delivery_token.as_ref(),
12476 self.last_checkpoint_delivery_token.as_ref(),
12477 ) && replay_token == committed_token
12478 {
12479 let committed_witness = self
12480 .last_checkpoint_delivery_witness
12481 .ok_or(ReactiveEngineError::MissingReplayWitness)?;
12482 if delivery_witness != Some(committed_witness) {
12483 return Err(ReactiveEngineError::ReplayDeliveryMismatch);
12484 }
12485 self.subscriber
12486 .acknowledge_delivery(replay_token.clone())
12487 .await
12488 .map_err(ReactiveEngineError::Acknowledgement)?;
12489 return Ok(Some(CheckpointedIngest::ReplayAcknowledged));
12490 }
12491
12492 self.ensure_checkpointable_reorgs(&batch)?;
12493
12494 let incoming_block = latest_canonical_batch_block(&batch);
12495 let cache_state = EvmCacheStateSnapshot::capture(cache);
12496 let runtime_state = self.runtime.checkpoint_state();
12497 let report = match self.runtime.ingest_batch_direct(cache, batch) {
12498 Ok(report) => report,
12499 Err(error) => {
12500 cache_state.restore(cache);
12501 self.runtime.restore_transaction_state(runtime_state);
12502 return Err(error.into());
12503 }
12504 };
12505 let reports = report.reports.clone();
12506 let stage = CheckpointStage {
12507 incoming_block,
12508 delivery_token,
12509 delivery_witness,
12510 subscriber_checkpoint,
12511 staged_generation: cache.snapshot_generation(),
12512 report,
12513 };
12514 if let Err(error) = self.stage_checkpoint(identity, stage) {
12515 cache_state.restore(cache);
12516 self.runtime.restore_transaction_state(runtime_state);
12517 return Err(error);
12518 }
12519 self.runtime.dispatch_reports(&reports);
12520 self.commit_pending_checkpoint(cache, store).await.map(Some)
12521 }
12522
12523 pub async fn next_ingest_with_resync_checkpointed(
12534 &mut self,
12535 cache: &mut EvmCache,
12536 store: &DurableCheckpointStore,
12537 identity: &DurableCheckpointIdentity,
12538 ) -> Result<Option<CheckpointedIngest<N>>, ReactiveEngineError> {
12539 if !self.subscriber.capabilities().supports_durable_replay() {
12540 return Err(ReactiveEngineError::SubscriberNotDurable);
12541 }
12542 self.ensure_subscriber_chain(cache)?;
12543 if self.pending_acknowledgement.is_some() {
12544 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
12545 }
12546 self.ensure_checkpoint_identity(cache, identity)?;
12547 if self.pending_checkpoint.is_some() {
12548 return self.commit_pending_checkpoint(cache, store).await.map(Some);
12549 }
12550
12551 let batch = self.subscriber.next_batch().await?;
12552 self.ensure_subscriber_chain(cache)?;
12553 let Some(mut batch) = batch else {
12554 return Ok(None);
12555 };
12556 if batch_preconfirmation(&batch)?.is_some() {
12557 return Err(ReactiveEngineError::PreconfirmationNotCheckpointable);
12558 }
12559 self.runtime.discard_preconfirmed_branch(cache);
12560 let delivery_witness = batch
12561 .delivery_token()
12562 .map(|_| durable_delivery_witness(&batch))
12563 .transpose()?;
12564 let delivery_token = batch.take_delivery_token();
12565 let subscriber_checkpoint = batch.take_subscriber_checkpoint();
12566 if let (Some(replay_token), Some(committed_token)) = (
12567 delivery_token.as_ref(),
12568 self.last_checkpoint_delivery_token.as_ref(),
12569 ) && replay_token == committed_token
12570 {
12571 let committed_witness = self
12572 .last_checkpoint_delivery_witness
12573 .ok_or(ReactiveEngineError::MissingReplayWitness)?;
12574 if delivery_witness != Some(committed_witness) {
12575 return Err(ReactiveEngineError::ReplayDeliveryMismatch);
12576 }
12577 self.subscriber
12578 .acknowledge_delivery(replay_token.clone())
12579 .await
12580 .map_err(ReactiveEngineError::Acknowledgement)?;
12581 return Ok(Some(CheckpointedIngest::ReplayAcknowledged));
12582 }
12583
12584 self.ensure_checkpointable_reorgs(&batch)?;
12585
12586 let incoming_block = latest_canonical_batch_block(&batch);
12587 let cache_state = EvmCacheStateSnapshot::capture(cache);
12588 let runtime_state = self.runtime.checkpoint_state();
12589 let report = match self.runtime.ingest_batch_with_resync_direct(cache, batch) {
12590 Ok(report) => report,
12591 Err(error) => {
12592 cache_state.restore(cache);
12593 self.runtime.restore_transaction_state(runtime_state);
12594 return Err(error.into());
12595 }
12596 };
12597 let reports = report.reports.clone();
12598 let stage = CheckpointStage {
12599 incoming_block,
12600 delivery_token,
12601 delivery_witness,
12602 subscriber_checkpoint,
12603 staged_generation: cache.snapshot_generation(),
12604 report,
12605 };
12606 if let Err(error) = self.stage_checkpoint(identity, stage) {
12607 cache_state.restore(cache);
12608 self.runtime.restore_transaction_state(runtime_state);
12609 return Err(error);
12610 }
12611 self.runtime.dispatch_reports(&reports);
12612 self.commit_pending_checkpoint(cache, store).await.map(Some)
12613 }
12614
12615 fn stage_checkpoint(
12616 &mut self,
12617 identity: &DurableCheckpointIdentity,
12618 stage: CheckpointStage<N>,
12619 ) -> Result<(), ReactiveEngineError> {
12620 let CheckpointStage {
12621 incoming_block,
12622 delivery_token,
12623 delivery_witness,
12624 subscriber_checkpoint,
12625 staged_generation,
12626 report,
12627 } = stage;
12628 if delivery_token.is_some() != delivery_witness.is_some() {
12629 return Err(ReactiveEngineError::DeliveryWitness(
12630 "delivery token and witness must be staged together".into(),
12631 ));
12632 }
12633 let runtime_checkpoint = self.runtime.durable_checkpoint_bytes()?;
12634 let block = self
12635 .runtime
12636 .last_canonical_block()
12637 .map(|block| DurableCheckpointBlock {
12638 number: block.number,
12639 hash: block.hash,
12640 parent_hash: block.parent_hash,
12641 timestamp: block.timestamp,
12642 })
12643 .or(incoming_block)
12644 .or_else(|| self.last_checkpoint_block.clone())
12645 .ok_or(ReactiveEngineError::MissingCheckpointBlock)?;
12646 let metadata = DurableCheckpointMetadata {
12647 identity: identity.clone(),
12648 block,
12649 delivery_token: delivery_token
12650 .as_ref()
12651 .or(self.last_checkpoint_delivery_token.as_ref())
12652 .map(|token| token.as_bytes().to_vec()),
12653 delivery_witness: if delivery_token.is_some() {
12654 delivery_witness
12655 } else {
12656 self.last_checkpoint_delivery_witness
12657 },
12658 subscriber_checkpoint: subscriber_checkpoint
12659 .as_ref()
12660 .or(self.last_subscriber_checkpoint.as_ref())
12661 .map(|checkpoint| checkpoint.as_bytes().to_vec()),
12662 runtime_checkpoint: Some(runtime_checkpoint),
12663 };
12664 self.pending_checkpoint = Some(PendingCheckpoint {
12665 metadata,
12666 delivery_token,
12667 report,
12668 saved_to: None,
12669 staged_generation,
12670 });
12671 Ok(())
12672 }
12673
12674 fn ensure_checkpointable_reorgs(
12675 &self,
12676 batch: &ReactiveInputBatch<N>,
12677 ) -> Result<(), ReactiveEngineError> {
12678 let state = CanonicalSequenceState::new(
12679 self.runtime
12680 .journal
12681 .iter()
12682 .map(|entry| entry.block)
12683 .collect(),
12684 self.runtime.coverage_head,
12685 self.runtime.safe_head,
12686 self.runtime.finalized_head,
12687 )
12688 .with_log_coverage_head(self.runtime.log_coverage_head);
12689 match validate_canonical_sequence_internal(
12690 &state,
12691 batch,
12692 CanonicalSequenceValidationPolicy::RequireCompleteRollback,
12693 ) {
12694 Ok(_) => Ok(()),
12695 Err(CanonicalSequenceError::Invalid(error)) => Err(error.into()),
12696 Err(CanonicalSequenceError::IncompleteRollback {
12697 common_ancestor,
12698 oldest_retained,
12699 ..
12700 }) => Err(ReactiveEngineError::CheckpointReorgOutsideJournal {
12701 common_ancestor,
12702 oldest_journaled: oldest_retained,
12703 journal_depth: self.runtime.config.journal_depth,
12704 }),
12705 }
12706 }
12707
12708 async fn stage_or_return_acknowledgement(
12709 &mut self,
12710 delivery_token: Option<SubscriberDeliveryToken>,
12711 report: ReactiveBatchReport<N>,
12712 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
12713 let Some(token) = delivery_token else {
12714 return Ok(Some(report));
12715 };
12716 self.pending_acknowledgement = Some(PendingAcknowledgement { token, report });
12717 self.commit_pending_acknowledgement().await.map(Some)
12718 }
12719
12720 async fn commit_pending_acknowledgement(
12721 &mut self,
12722 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
12723 let token = self
12724 .pending_acknowledgement
12725 .as_ref()
12726 .expect("caller checked pending acknowledgement")
12727 .token
12728 .clone();
12729 self.subscriber
12730 .acknowledge_delivery(token)
12731 .await
12732 .map_err(ReactiveEngineError::Acknowledgement)?;
12733 Ok(self
12734 .pending_acknowledgement
12735 .take()
12736 .expect("pending acknowledgement remains until commit")
12737 .report)
12738 }
12739
12740 async fn commit_pending_checkpoint(
12741 &mut self,
12742 cache: &EvmCache,
12743 store: &DurableCheckpointStore,
12744 ) -> Result<CheckpointedIngest<N>, ReactiveEngineError> {
12745 let pending = self
12746 .pending_checkpoint
12747 .as_mut()
12748 .expect("caller checked pending checkpoint");
12749 let cache_generation = cache.snapshot_generation();
12750 if cache_generation != pending.staged_generation {
12751 return Err(ReactiveEngineError::PendingCheckpointCacheChanged {
12752 staged_generation: pending.staged_generation,
12753 current_generation: cache_generation,
12754 });
12755 }
12756 if pending.saved_to.as_deref() != Some(store.path()) {
12757 store
12758 .save_async(cache, pending.metadata.clone())
12759 .await
12760 .map_err(ReactiveEngineError::Checkpoint)?;
12761 pending.saved_to = Some(store.path().to_path_buf());
12762 }
12763 if let Some(token) = pending.delivery_token.clone() {
12764 self.subscriber
12765 .acknowledge_delivery(token)
12766 .await
12767 .map_err(ReactiveEngineError::Acknowledgement)?;
12768 }
12769
12770 let pending = self
12771 .pending_checkpoint
12772 .take()
12773 .expect("pending checkpoint remains until commit");
12774 self.last_checkpoint_block = Some(pending.metadata.block);
12775 self.checkpoint_identity = Some(pending.metadata.identity);
12776 self.last_checkpoint_delivery_token = pending
12777 .metadata
12778 .delivery_token
12779 .map(SubscriberDeliveryToken::new);
12780 self.last_checkpoint_delivery_witness = pending.metadata.delivery_witness;
12781 self.last_subscriber_checkpoint = pending
12782 .metadata
12783 .subscriber_checkpoint
12784 .map(SubscriberCheckpoint::new);
12785 Ok(CheckpointedIngest::Applied(pending.report))
12786 }
12787
12788 fn ensure_checkpoint_identity(
12789 &self,
12790 cache: &EvmCache,
12791 identity: &DurableCheckpointIdentity,
12792 ) -> Result<(), ReactiveEngineError> {
12793 if identity.chain_id != cache.chain_id() {
12794 return Err(ReactiveEngineError::Checkpoint(
12795 DurableCheckpointError::CacheChainMismatch {
12796 cache_chain_id: cache.chain_id(),
12797 checkpoint_chain_id: identity.chain_id,
12798 },
12799 ));
12800 }
12801 if let Some(actual) = self.checkpoint_identity.as_ref()
12802 && actual != identity
12803 {
12804 return Err(ReactiveEngineError::Checkpoint(
12805 DurableCheckpointError::IdentityMismatch {
12806 expected: identity.clone(),
12807 actual: actual.clone(),
12808 },
12809 ));
12810 }
12811 if let Some(pending) = self.pending_checkpoint.as_ref()
12812 && &pending.metadata.identity != identity
12813 {
12814 return Err(ReactiveEngineError::Checkpoint(
12815 DurableCheckpointError::IdentityMismatch {
12816 expected: identity.clone(),
12817 actual: pending.metadata.identity.clone(),
12818 },
12819 ));
12820 }
12821 Ok(())
12822 }
12823}
12824
12825fn latest_canonical_batch_block<N: Network>(
12826 batch: &ReactiveInputBatch<N>,
12827) -> Option<DurableCheckpointBlock> {
12828 let record_block = batch
12829 .records()
12830 .iter()
12831 .enumerate()
12832 .filter(|(index, _)| {
12833 batch
12834 .record_delivery_scope(*index)
12835 .is_some_and(DeliveryScope::advances_canonical_state)
12836 })
12837 .filter_map(|(_, record)| canonical_record_block(record))
12838 .max_by_key(|block| block.number)
12839 .cloned();
12840 let control_block = batch
12841 .chain_controls()
12842 .iter()
12843 .filter_map(|control| match control {
12844 ChainControl::Reorg {
12845 common_ancestor, ..
12846 } => Some(common_ancestor),
12847 ChainControl::Barrier {
12848 block: Some(block), ..
12849 }
12850 | ChainControl::CanonicalProgress(block) => Some(block),
12851 ChainControl::Safe(_)
12852 | ChainControl::Finalized(_)
12853 | ChainControl::LogCoverage(_)
12854 | ChainControl::Barrier { block: None, .. } => None,
12855 })
12856 .max_by_key(|block| block.number)
12857 .cloned();
12858
12859 record_block
12860 .into_iter()
12861 .chain(control_block)
12862 .max_by_key(|block| block.number)
12863 .map(|block| DurableCheckpointBlock {
12864 number: block.number,
12865 hash: block.hash,
12866 parent_hash: block.parent_hash,
12867 timestamp: block.timestamp,
12868 })
12869}
12870
12871impl<S, N> ReactiveEngine<S, N>
12872where
12873 N: Network,
12874 S: InterestOwnerSubscriber<N>,
12875{
12876 pub async fn register_handler(
12902 &mut self,
12903 handler: Arc<dyn ReactiveHandler<N>>,
12904 ) -> Result<(), ReactiveEngineRegisterError> {
12905 let backfill = self
12906 .runtime
12907 .last_canonical_block()
12908 .filter(|retained| {
12909 self.runtime.journal.iter().any(|entry| {
12910 optional_block_refs_are_compatible(Some(&entry.block), Some(retained))
12911 })
12912 })
12913 .map(HandlerRegistrationCatchup::CoordinatedCanonical)
12914 .unwrap_or(HandlerRegistrationCatchup::LiveOnly);
12915 self.register_handler_inner(handler, backfill).await
12916 }
12917
12918 pub async fn register_handler_with_backfill(
12937 &mut self,
12938 handler: Arc<dyn ReactiveHandler<N>>,
12939 backfill: SubscriberBackfill,
12940 ) -> Result<(), ReactiveEngineRegisterError> {
12941 self.register_handler_inner(handler, HandlerRegistrationCatchup::OwnerBackfill(backfill))
12942 .await
12943 }
12944
12945 pub async fn register_handler_live_only(
12956 &mut self,
12957 handler: Arc<dyn ReactiveHandler<N>>,
12958 ) -> Result<(), ReactiveEngineRegisterError> {
12959 self.register_handler_inner(handler, HandlerRegistrationCatchup::LiveOnly)
12960 .await
12961 }
12962
12963 async fn register_handler_inner(
12964 &mut self,
12965 handler: Arc<dyn ReactiveHandler<N>>,
12966 catchup: HandlerRegistrationCatchup,
12967 ) -> Result<(), ReactiveEngineRegisterError> {
12968 let id = handler.id();
12969 if self.runtime.contains_handler(&id) {
12970 return Err(RegisterError::DuplicateHandler(id).into());
12971 }
12972 let interests = handler.interests();
12973
12974 if let HandlerRegistrationCatchup::OwnerBackfill(backfill) = &catchup {
12975 let retained_anchor = backfill.retained_anchor().copied();
12976 let is_exact_retained_block = retained_anchor.is_some_and(|anchor| {
12977 backfill.start_block() == anchor.number
12978 && backfill.end_block() == Some(anchor.number)
12979 && self.runtime.journal.iter().any(|entry| {
12980 optional_block_refs_are_compatible(Some(&entry.block), Some(&anchor))
12981 })
12982 });
12983 if !is_exact_retained_block {
12984 return Err(ReactiveEngineRegisterError::BackfillOutsideJournal {
12985 start_block: backfill.start_block(),
12986 end_block: backfill.end_block(),
12987 retained_anchor,
12988 });
12989 }
12990 }
12991
12992 let subscribed = match catchup {
12993 HandlerRegistrationCatchup::OwnerBackfill(backfill) => {
12994 self.subscriber
12995 .add_interest_owner_with_backfill(id.clone(), &interests, backfill)
12996 .await
12997 }
12998 HandlerRegistrationCatchup::CoordinatedCanonical(retained) => {
12999 self.subscriber
13000 .add_interest_owner_with_canonical_catchup(id.clone(), &interests, retained)
13001 .await
13002 }
13003 HandlerRegistrationCatchup::LiveOnly => {
13004 self.subscriber
13005 .add_interest_owner(id.clone(), &interests)
13006 .await
13007 }
13008 };
13009 if let Err(error) = subscribed {
13010 return Err(error.into());
13011 }
13012
13013 self.runtime
13018 .registry
13019 .insert_handler_prepared(id, handler, interests);
13020 Ok(())
13021 }
13022
13023 pub async fn sync_handler_interests(&mut self) -> Result<(), SubscriberError> {
13048 let owners = self
13049 .runtime
13050 .handler_ids()
13051 .into_iter()
13052 .map(|id| {
13053 let interests = self
13054 .runtime
13055 .handler_interests(&id)
13056 .map(<[ReactiveInterest<N>]>::to_vec)
13057 .unwrap_or_default();
13058 (id, interests)
13059 })
13060 .collect();
13061 self.subscriber.replace_interest_owners(owners).await
13062 }
13063
13064 pub async fn sync_handler_interests_with_backfill(&mut self) -> Result<(), SubscriberError> {
13092 let baseline =
13093 self.runtime
13094 .last_canonical_block()
13095 .ok_or(SubscriberError::InvalidConfig(
13096 "cannot continuity-sync handlers before a canonical runtime position exists",
13097 ))?;
13098 let backfill = SubscriberBackfill::after_canonical_block(baseline)?;
13099 let owners = self
13100 .runtime
13101 .handler_ids()
13102 .into_iter()
13103 .map(|id| {
13104 let interests = self
13105 .runtime
13106 .handler_interests(&id)
13107 .map(<[ReactiveInterest<N>]>::to_vec)
13108 .unwrap_or_default();
13109 (id, interests)
13110 })
13111 .collect();
13112 self.subscriber
13113 .replace_interest_owners_with_global_backfill(owners, backfill)
13114 .await
13115 }
13116
13117 pub async fn unregister_handler(
13149 &mut self,
13150 id: &HandlerId,
13151 ) -> Result<Option<Arc<dyn ReactiveHandler<N>>>, SubscriberError> {
13152 self.subscriber.remove_interest_owner(id).await?;
13153 Ok(self.runtime.unregister_handler(id))
13154 }
13155}
13156
13157type FlashblockReconnectFuture<N> = Pin<
13158 Box<
13159 dyn Future<
13160 Output = (
13161 SubscriberStreamSource,
13162 Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>,
13163 ),
13164 > + Send,
13165 >,
13166>;
13167
13168pub struct AlloySubscriber<P, N: Network = Ethereum> {
13189 provider: P,
13190 #[cfg(feature = "ingress-timing")]
13191 transport_receipts: Option<TransportReceiptLookup>,
13192 #[cfg(feature = "raw-flashblocks-json")]
13195 external_flashblocks_provider: Option<ProviderRef>,
13196 #[cfg(feature = "raw-flashblocks-json")]
13198 external_flashblock_updates:
13199 Option<tokio::sync::mpsc::Receiver<raw_json_flashblocks::QueuedFlashblockUpdate>>,
13200 #[cfg(feature = "raw-flashblocks-json")]
13202 external_flashblock_update_channel_opened: bool,
13203 #[cfg(feature = "raw-flashblocks-json")]
13205 rejected_external_flashblock_generation: Option<u64>,
13206 #[cfg(feature = "raw-flashblocks-json")]
13209 last_external_flashblock_snapshot: Option<FlashblockSnapshot>,
13210 flashblocks_state_provider: Option<P>,
13214 provider_ref: Option<ProviderRef>,
13217 log_verification_provider: Option<P>,
13221 chain_id: Option<u64>,
13223 mode: SubscriberMode,
13224 config: SubscriberConfig,
13225 base_interests: Vec<ReactiveInterest<N>>,
13226 owned_interests: Vec<OwnedSubscriberInterests<N>>,
13227 next_owner_epoch: u64,
13228 interests: Vec<ReactiveInterest<N>>,
13229 log_source_ids: HashMap<Filter, usize>,
13234 next_log_source_id: usize,
13235 pending_backfills: VecDeque<QueuedSubscriberBackfill>,
13236 pending_source_backfills: VecDeque<SubscriberStreamSource>,
13241 sources_dirty: bool,
13244 stream_revision: u64,
13247 state: AlloySubscriberState<N>,
13248 pending_records: VecDeque<SubscriberInputRecord<N>>,
13249 pending_chain_controls: VecDeque<ChainControl>,
13250 pending_reconcile_owner_records: VecDeque<BufferedSubscriberOwnerRecord<N>>,
13255 resource_error: Option<String>,
13258 last_seen_log_blocks: HashMap<usize, u64>,
13259 verified_log_blocks: HashMap<(u64, B256), BlockRef>,
13260 verified_log_block_order: VecDeque<(u64, B256)>,
13261 recent_input_refs: VecDeque<InputRef>,
13262 recent_input_ref_set: HashSet<InputRef>,
13263 recent_owner_input_refs: HashMap<SubscriberOwnerEpoch, VecDeque<InputRef>>,
13264 recent_owner_input_ref_sets: HashMap<SubscriberOwnerEpoch, HashSet<InputRef>>,
13265 recent_compat_owner_input_refs: HashMap<HandlerId, VecDeque<InputRef>>,
13266 recent_compat_owner_input_ref_sets: HashMap<HandlerId, HashSet<InputRef>>,
13267 base_flashblock_header: Option<(FixedBytes<8>, BaseFlashblockBase)>,
13268 base_flashblock_transactions: Option<(FixedBytes<8>, u64, Vec<B256>, Vec<B256>)>,
13269 unmatched_pending_logs: VecDeque<(usize, Log, FlashblockIngressTiming)>,
13270 latest_preconfirmation: Option<FlashblockRef>,
13271 preconfirmed_seen_logs: HashSet<(B256, u64)>,
13272 preconfirmed_receipted_transactions: HashSet<B256>,
13276 preconfirmed_unavailable_receipts: HashSet<B256>,
13281 last_certified_canonical_head: Option<BlockRef>,
13282 last_canonical_head_certification: Option<Instant>,
13290 sealed_block_pending_certification: bool,
13293 attestable_canonical_head: Option<BlockRef>,
13297 attested_log_coverage: Option<BlockRef>,
13298 pending_preconfirmation_invalidation: bool,
13299 pending_flashblock_reconnects: FuturesUnordered<FlashblockReconnectFuture<N>>,
13300 pending_flashblock_reconnect_sources: Vec<SubscriberStreamSource>,
13301 flashblocks_rpc_metrics: FlashblocksRpcMetrics,
13302 rpc_counters: Arc<SubscriberRpcCounters>,
13306 gap_counters: Arc<SubscriberStreamGapCounters>,
13310 consecutive_flashblock_poll_failures: usize,
13311 flashblock_rpc_request_times: VecDeque<Instant>,
13312 _network: PhantomData<N>,
13313}
13314
13315struct OwnedSubscriberInterests<N: Network = Ethereum> {
13316 owner: HandlerId,
13317 interests: Vec<ReactiveInterest<N>>,
13318 epoch: Option<SubscriberOwnerEpoch>,
13319 state: SubscriberOwnerState,
13320 baseline: Option<BlockRef>,
13321 progress: Option<SubscriberOwnerProgress>,
13322 progress_stream_revision: Option<u64>,
13323}
13324
13325#[derive(Clone)]
13326struct SubscriberOwnerReconcilePlan<N: Network = Ethereum> {
13327 epoch: SubscriberOwnerEpoch,
13328 interests: Vec<ReactiveInterest<N>>,
13329 retained: BlockRef,
13330 from_block: u64,
13331}
13332
13333struct SubscriberOwnerCatchup {
13334 logs: Vec<Log>,
13335 certified: BlockRef,
13336}
13337
13338#[derive(Clone, Copy)]
13339struct SubscriberOwnerCatchupOptions {
13340 target_preverified: bool,
13341 max_logs: usize,
13342 max_log_bytes: usize,
13343 max_requests_in_flight: usize,
13344 cause: SubscriberRpcCause,
13347}
13348
13349struct SubscriberOwnerReconcileFilter {
13350 filter: Filter,
13351 from_block: u64,
13352}
13353
13354struct BufferedSubscriberOwnerRecord<N: Network = Ethereum> {
13355 record: ReactiveInputRecord<N>,
13356 owners: Vec<SubscriberOwnerEpoch>,
13357}
13358
13359const OWNER_RECONCILE_FILTERS_PER_CHUNK: usize = 256;
13360
13361struct QueuedSubscriberBackfill {
13362 owner: Option<HandlerId>,
13365 epoch: Option<SubscriberOwnerEpoch>,
13366 filters: Vec<Filter>,
13368 backfill: SubscriberBackfill,
13369}
13370
13371#[cfg(feature = "reactive-ws")]
13377fn ensure_ring_crypto_provider() {
13378 use std::sync::Once;
13379 static INSTALL: Once = Once::new();
13380 INSTALL.call_once(|| {
13381 let _ = rustls::crypto::ring::default_provider().install_default();
13382 });
13383}
13384
13385impl<P, N: Network> AlloySubscriber<P, N> {
13386 pub fn new(provider: P, mode: SubscriberMode, config: SubscriberConfig) -> Self {
13388 #[cfg(feature = "reactive-ws")]
13389 ensure_ring_crypto_provider();
13390 Self {
13391 provider,
13392 #[cfg(feature = "raw-flashblocks-json")]
13393 external_flashblocks_provider: None,
13394 #[cfg(feature = "raw-flashblocks-json")]
13395 external_flashblock_updates: None,
13396 #[cfg(feature = "raw-flashblocks-json")]
13397 external_flashblock_update_channel_opened: false,
13398 #[cfg(feature = "raw-flashblocks-json")]
13399 rejected_external_flashblock_generation: None,
13400 #[cfg(feature = "raw-flashblocks-json")]
13401 last_external_flashblock_snapshot: None,
13402 flashblocks_state_provider: None,
13403 provider_ref: None,
13404 #[cfg(feature = "ingress-timing")]
13405 transport_receipts: None,
13406 log_verification_provider: None,
13407 chain_id: None,
13408 mode,
13409 config,
13410 base_interests: Vec::new(),
13411 owned_interests: Vec::new(),
13412 next_owner_epoch: 0,
13413 interests: Vec::new(),
13414 log_source_ids: HashMap::new(),
13415 next_log_source_id: 0,
13416 pending_backfills: VecDeque::new(),
13417 pending_source_backfills: VecDeque::new(),
13418 sources_dirty: true,
13419 stream_revision: 0,
13420 state: AlloySubscriberState::Uninitialized,
13421 pending_records: VecDeque::new(),
13422 pending_chain_controls: VecDeque::new(),
13423 pending_reconcile_owner_records: VecDeque::new(),
13424 resource_error: None,
13425 last_seen_log_blocks: HashMap::new(),
13426 verified_log_blocks: HashMap::new(),
13427 verified_log_block_order: VecDeque::new(),
13428 recent_input_refs: VecDeque::new(),
13429 recent_input_ref_set: HashSet::new(),
13430 recent_owner_input_refs: HashMap::new(),
13431 recent_owner_input_ref_sets: HashMap::new(),
13432 recent_compat_owner_input_refs: HashMap::new(),
13433 recent_compat_owner_input_ref_sets: HashMap::new(),
13434 base_flashblock_header: None,
13435 base_flashblock_transactions: None,
13436 unmatched_pending_logs: VecDeque::new(),
13437 latest_preconfirmation: None,
13438 preconfirmed_seen_logs: HashSet::new(),
13439 preconfirmed_receipted_transactions: HashSet::new(),
13440 preconfirmed_unavailable_receipts: HashSet::new(),
13441 last_certified_canonical_head: None,
13442 last_canonical_head_certification: None,
13443 sealed_block_pending_certification: false,
13444 attestable_canonical_head: None,
13445 attested_log_coverage: None,
13446 pending_preconfirmation_invalidation: false,
13447 pending_flashblock_reconnects: FuturesUnordered::new(),
13448 pending_flashblock_reconnect_sources: Vec::new(),
13449 flashblocks_rpc_metrics: FlashblocksRpcMetrics::default(),
13450 rpc_counters: Arc::new(SubscriberRpcCounters::default()),
13451 gap_counters: Arc::new(SubscriberStreamGapCounters::default()),
13452 consecutive_flashblock_poll_failures: 0,
13453 flashblock_rpc_request_times: VecDeque::new(),
13454 _network: PhantomData,
13455 }
13456 }
13457
13458 pub fn provider(&self) -> &P {
13460 &self.provider
13461 }
13462
13463 #[cfg(feature = "ingress-timing")]
13466 pub fn with_transport_receipt_lookup(mut self, lookup: TransportReceiptLookup) -> Self {
13467 self.transport_receipts = Some(lookup);
13468 self
13469 }
13470
13471 #[must_use]
13475 pub fn with_provider_ref(mut self, provider: ProviderRef) -> Self {
13476 self.provider_ref = Some(provider);
13477 self
13478 }
13479
13480 #[cfg(feature = "raw-flashblocks-json")]
13506 pub fn configure_external_flashblock_updates(
13507 &mut self,
13508 provider: ProviderRef,
13509 ) -> Result<(), SubscriberError> {
13510 if self.external_flashblocks_provider.is_some() {
13511 return Err(SubscriberError::InvalidConfig(
13512 "external Flashblock updates were already configured",
13513 ));
13514 }
13515 if self.external_flashblock_update_channel_opened
13516 || self.external_flashblock_updates.is_some()
13517 || self.chain_id.is_some()
13518 || !self.base_interests.is_empty()
13519 || !self.owned_interests.is_empty()
13520 || !self.interests.is_empty()
13521 || !self.pending_records.is_empty()
13522 || !self.pending_chain_controls.is_empty()
13523 || !self.pending_backfills.is_empty()
13524 || !matches!(self.state, AlloySubscriberState::Uninitialized)
13525 {
13526 return Err(SubscriberError::InvalidConfig(
13527 "external Flashblock updates must be configured before subscriber registration",
13528 ));
13529 }
13530 self.external_flashblocks_provider = Some(provider);
13531 Ok(())
13532 }
13533
13534 #[cfg(feature = "raw-flashblocks-json")]
13549 pub fn open_external_flashblock_update_channel(
13550 &mut self,
13551 capacity: usize,
13552 ) -> Result<FlashblockUpdateSender, SubscriberError> {
13553 if capacity == 0 {
13554 return Err(SubscriberError::InvalidConfig(
13555 "external Flashblock update channel capacity must be greater than zero",
13556 ));
13557 }
13558 let provider = self.external_flashblocks_provider.clone().ok_or(
13559 SubscriberError::InvalidConfig(
13560 "external Flashblock update channel requires configure_external_flashblock_updates",
13561 ),
13562 )?;
13563 if self.external_flashblock_update_channel_opened {
13564 return Err(SubscriberError::InvalidConfig(
13565 "external Flashblock update channel was already opened",
13566 ));
13567 }
13568 let (sender, receiver) =
13569 raw_json_flashblocks::flashblock_update_channel(provider, capacity);
13570 self.external_flashblock_updates = Some(receiver);
13571 self.external_flashblock_update_channel_opened = true;
13572 self.sources_dirty = true;
13573 Ok(sender)
13574 }
13575
13576 fn uses_external_flashblock_updates(&self) -> bool {
13577 #[cfg(feature = "raw-flashblocks-json")]
13578 {
13579 self.external_flashblocks_provider.is_some()
13580 }
13581 #[cfg(not(feature = "raw-flashblocks-json"))]
13582 {
13583 false
13584 }
13585 }
13586
13587 #[must_use]
13595 pub fn with_flashblocks_state_provider(mut self, provider: P) -> Self {
13596 self.flashblocks_state_provider = Some(provider);
13597 self
13598 }
13599
13600 #[must_use]
13607 pub fn with_log_verification_provider(mut self, provider: P) -> Self {
13608 self.log_verification_provider = Some(provider);
13609 self
13610 }
13611
13612 pub fn mode(&self) -> SubscriberMode {
13614 self.mode
13615 }
13616
13617 pub fn config(&self) -> &SubscriberConfig {
13619 &self.config
13620 }
13621
13622 pub const fn flashblocks_rpc_metrics(&self) -> FlashblocksRpcMetrics {
13625 self.flashblocks_rpc_metrics
13626 }
13627
13628 pub fn rpc_stats(&self) -> SubscriberRpcStats {
13637 self.rpc_counters.snapshot()
13638 }
13639
13640 pub fn reset_rpc_stats(&self) {
13644 self.rpc_counters.reset();
13645 }
13646
13647 pub fn stream_gap_stats(&self) -> SubscriberStreamGapStats {
13656 self.gap_counters.snapshot()
13657 }
13658
13659 pub fn reset_stream_gap_stats(&self) {
13661 self.gap_counters.reset();
13662 }
13663
13664 fn record_rpc(&self, cause: SubscriberRpcCause, method: SubscriberRpcMethod) {
13666 self.rpc_counters.record(cause, method);
13667 }
13668
13669 #[cfg(feature = "reactive-ws")]
13671 fn log_channel_size(&self) -> usize {
13672 self.config
13673 .log_channel_size
13674 .unwrap_or(self.config.max_batch_size)
13675 .max(1)
13676 }
13677
13678 pub fn registered_interests(&self) -> &[ReactiveInterest<N>] {
13680 &self.interests
13681 }
13682
13683 pub fn stage_interest_owner(
13700 &mut self,
13701 owner: HandlerId,
13702 interests: &[ReactiveInterest<N>],
13703 start: SubscriberOwnerStart,
13704 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
13705 validate_subscriber_config(&self.config)?;
13706 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
13707 && interests
13708 .iter()
13709 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
13710 {
13711 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
13712 }
13713 if self
13714 .owned_interests
13715 .iter()
13716 .any(|entry| entry.owner == owner)
13717 {
13718 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
13719 }
13720
13721 let mut next_owned = self.clone_owned_interests();
13722 next_owned.push(OwnedSubscriberInterests {
13723 owner: owner.clone(),
13724 interests: interests.to_vec(),
13725 epoch: None,
13726 state: SubscriberOwnerState::Staged,
13727 baseline: None,
13728 progress: None,
13729 progress_stream_revision: None,
13730 });
13731 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
13732 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13733
13734 let baseline = match start {
13735 SubscriberOwnerStart::Live => None,
13736 SubscriberOwnerStart::PostBlock(block) => {
13737 block
13738 .number
13739 .checked_add(1)
13740 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
13741 Some(block)
13742 }
13743 };
13744 let sequence = self
13745 .next_owner_epoch
13746 .checked_add(1)
13747 .ok_or(SubscriberOwnerError::EpochExhausted)?;
13748 let epoch = SubscriberOwnerEpoch {
13749 owner: owner.clone(),
13750 sequence,
13751 };
13752
13753 self.next_owner_epoch = sequence;
13754 let entry = next_owned
13755 .last_mut()
13756 .expect("staged owner was appended during preflight");
13757 entry.epoch = Some(epoch.clone());
13758 entry.baseline = baseline;
13759 self.owned_interests = next_owned;
13760 self.interests = next_registered;
13761 self.sources_dirty = true;
13762
13763 Ok(epoch)
13764 }
13765
13766 pub fn stage_interest_owner_replacement(
13780 &mut self,
13781 owner: HandlerId,
13782 interests: &[ReactiveInterest<N>],
13783 start: SubscriberOwnerStart,
13784 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
13785 validate_subscriber_config(&self.config)?;
13786 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
13787 && interests
13788 .iter()
13789 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
13790 {
13791 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
13792 }
13793 let active_count = self
13794 .owned_interests
13795 .iter()
13796 .filter(|entry| {
13797 entry.owner == owner
13798 && entry.state == SubscriberOwnerState::Active
13799 && entry.epoch.is_some()
13800 })
13801 .count();
13802 if active_count != 1
13803 || self
13804 .owned_interests
13805 .iter()
13806 .any(|entry| entry.owner == owner && entry.state != SubscriberOwnerState::Active)
13807 {
13808 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
13809 }
13810
13811 let mut next_owned = self.clone_owned_interests();
13812 next_owned.push(OwnedSubscriberInterests {
13813 owner: owner.clone(),
13814 interests: interests.to_vec(),
13815 epoch: None,
13816 state: SubscriberOwnerState::Staged,
13817 baseline: None,
13818 progress: None,
13819 progress_stream_revision: None,
13820 });
13821 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
13822 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13823
13824 let baseline = match start {
13825 SubscriberOwnerStart::Live => None,
13826 SubscriberOwnerStart::PostBlock(block) => {
13827 block
13828 .number
13829 .checked_add(1)
13830 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
13831 Some(block)
13832 }
13833 };
13834 let sequence = self
13835 .next_owner_epoch
13836 .checked_add(1)
13837 .ok_or(SubscriberOwnerError::EpochExhausted)?;
13838 let epoch = SubscriberOwnerEpoch {
13839 owner: owner.clone(),
13840 sequence,
13841 };
13842
13843 self.next_owner_epoch = sequence;
13844 let entry = next_owned
13845 .last_mut()
13846 .expect("staged replacement owner was appended during preflight");
13847 entry.epoch = Some(epoch.clone());
13848 entry.baseline = baseline;
13849 self.owned_interests = next_owned;
13850 self.interests = next_registered;
13851 self.sources_dirty = true;
13852 Ok(epoch)
13853 }
13854
13855 pub fn interest_owner_state(
13857 &self,
13858 epoch: &SubscriberOwnerEpoch,
13859 ) -> Option<SubscriberOwnerState> {
13860 self.owned_interests
13861 .iter()
13862 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13863 .map(|entry| entry.state)
13864 }
13865
13866 pub fn interest_owner_progress(
13868 &self,
13869 epoch: &SubscriberOwnerEpoch,
13870 ) -> Option<&SubscriberOwnerProgress> {
13871 self.owned_interests
13872 .iter()
13873 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13874 .and_then(|entry| entry.progress.as_ref())
13875 }
13876
13877 pub fn activate_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
13881 let stream_revision = self.stream_revision;
13882 let sources_dirty = self.sources_dirty;
13883 let Some(entry) = self
13884 .owned_interests
13885 .iter_mut()
13886 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13887 else {
13888 return false;
13889 };
13890 if entry.state != SubscriberOwnerState::Staged
13891 || (entry.baseline.is_some()
13892 && (entry.progress.is_none()
13893 || entry.progress_stream_revision != Some(stream_revision)
13894 || sources_dirty))
13895 {
13896 return false;
13897 }
13898 entry.state = SubscriberOwnerState::Active;
13899 true
13900 }
13901
13902 pub fn commit_interest_owner_replacement(
13904 &mut self,
13905 active: &SubscriberOwnerEpoch,
13906 replacement: &SubscriberOwnerEpoch,
13907 ) -> bool {
13908 let Some(active_index) = self
13909 .owned_interests
13910 .iter()
13911 .position(|entry| entry.epoch.as_ref() == Some(active))
13912 else {
13913 return false;
13914 };
13915 let Some(replacement_index) = self
13916 .owned_interests
13917 .iter()
13918 .position(|entry| entry.epoch.as_ref() == Some(replacement))
13919 else {
13920 return false;
13921 };
13922 if active_index == replacement_index
13923 || active.owner() != replacement.owner()
13924 || self.owned_interests[active_index].state != SubscriberOwnerState::Active
13925 || self.owned_interests[replacement_index].state != SubscriberOwnerState::Staged
13926 || (self.owned_interests[replacement_index].baseline.is_some()
13927 && (self.owned_interests[replacement_index].progress.is_none()
13928 || self.owned_interests[replacement_index].progress_stream_revision
13929 != Some(self.stream_revision)
13930 || self.sources_dirty))
13931 {
13932 return false;
13933 }
13934
13935 self.owned_interests[replacement_index].state = SubscriberOwnerState::Active;
13936 self.owned_interests.remove(active_index);
13937 self.purge_owner_epoch(active);
13938 self.rebuild_registered_interests();
13939 self.retire_unreferenced_filters();
13940 self.sources_dirty = true;
13941 true
13942 }
13943
13944 pub fn prepare_interest_owner_removal(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
13953 let Some(entry) = self
13954 .owned_interests
13955 .iter_mut()
13956 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13957 else {
13958 return false;
13959 };
13960 if entry.state != SubscriberOwnerState::Active {
13961 return false;
13962 }
13963 entry.state = SubscriberOwnerState::Removing;
13964 true
13965 }
13966
13967 pub fn finalize_interest_owner_removal(
13973 &mut self,
13974 epoch: &SubscriberOwnerEpoch,
13975 ) -> Option<Vec<ReactiveInterest<N>>> {
13976 let index = self.owned_interests.iter().position(|entry| {
13977 entry.epoch.as_ref() == Some(epoch) && entry.state == SubscriberOwnerState::Removing
13978 })?;
13979 let removed = self.owned_interests.remove(index).interests;
13980 self.purge_owner_epoch(epoch);
13981 self.rebuild_registered_interests();
13982 self.retire_unreferenced_filters();
13983 self.sources_dirty = true;
13984 Some(removed)
13985 }
13986
13987 pub fn abort_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
13993 let Some(index) = self
13994 .owned_interests
13995 .iter()
13996 .position(|entry| entry.epoch.as_ref() == Some(epoch))
13997 else {
13998 return false;
13999 };
14000 match self.owned_interests[index].state {
14001 SubscriberOwnerState::Staged => {
14002 self.owned_interests.remove(index);
14003 self.purge_owner_epoch(epoch);
14004 self.rebuild_registered_interests();
14005 self.retire_unreferenced_filters();
14006 self.sources_dirty = true;
14007 true
14008 }
14009 SubscriberOwnerState::Removing => {
14010 self.owned_interests[index].state = SubscriberOwnerState::Active;
14011 true
14012 }
14013 SubscriberOwnerState::Active => false,
14014 }
14015 }
14016
14017 fn purge_owner_epoch(&mut self, epoch: &SubscriberOwnerEpoch) {
14018 self.pending_backfills
14019 .retain(|backfill| backfill.epoch.as_ref() != Some(epoch));
14020 self.pending_records
14021 .retain_mut(|pending| match &mut pending.scope {
14022 SubscriberInputScope::Canonical { owners }
14023 | SubscriberInputScope::CanonicalResidual { owners, .. } => {
14024 owners.retain(|owner| owner != epoch);
14025 true
14026 }
14027 SubscriberInputScope::OwnerOnly { owners } => {
14028 owners.retain(|owner| owner != epoch);
14029 !owners.is_empty()
14030 }
14031 SubscriberInputScope::OwnerOnlyHandlers { .. }
14032 | SubscriberInputScope::Preconfirmed => true,
14033 });
14034 self.pending_reconcile_owner_records.retain_mut(|pending| {
14035 pending.owners.retain(|owner| owner != epoch);
14036 !pending.owners.is_empty()
14037 });
14038 self.recent_owner_input_refs.remove(epoch);
14039 self.recent_owner_input_ref_sets.remove(epoch);
14040 }
14041
14042 pub fn upsert_interest_owners(
14050 &mut self,
14051 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14052 ) -> Result<(), SubscriberError> {
14053 self.upsert_interest_owners_inner(owners, None)
14054 }
14055
14056 pub fn upsert_interest_owners_with_backfill(
14065 &mut self,
14066 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14067 backfill: SubscriberBackfill,
14068 ) -> Result<(), SubscriberError> {
14069 self.upsert_interest_owners_inner(owners, Some(backfill))
14070 }
14071
14072 fn upsert_interest_owners_inner(
14073 &mut self,
14074 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14075 explicit_backfill: Option<SubscriberBackfill>,
14076 ) -> Result<(), SubscriberError> {
14077 validate_subscriber_config(&self.config)?;
14078 let mut seen = HashSet::with_capacity(owners.len());
14079 let mut next_owned = self.clone_owned_interests();
14080 for (owner, interests) in &owners {
14081 if !seen.insert(owner.clone()) {
14082 return Err(SubscriberError::InvalidConfig(
14083 "bulk owner upsert contains a duplicate owner",
14084 ));
14085 }
14086 if self
14087 .owned_interests
14088 .iter()
14089 .any(|entry| &entry.owner == owner && entry.epoch.is_some())
14090 {
14091 return Err(SubscriberError::InvalidConfig(
14092 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
14093 ));
14094 }
14095 if let Some(entry) = next_owned.iter_mut().find(|entry| &entry.owner == owner) {
14096 entry.interests = interests.clone();
14097 entry.state = SubscriberOwnerState::Active;
14098 entry.baseline = None;
14099 entry.progress = None;
14100 entry.progress_stream_revision = None;
14101 } else {
14102 next_owned.push(OwnedSubscriberInterests {
14103 owner: owner.clone(),
14104 interests: interests.clone(),
14105 epoch: None,
14106 state: SubscriberOwnerState::Active,
14107 baseline: None,
14108 progress: None,
14109 progress_stream_revision: None,
14110 });
14111 }
14112 }
14113 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
14114 validate_supported_interests(self.mode, &self.config, &next_registered)?;
14115
14116 let mut replacement_backfills = Vec::new();
14121 for (owner, interests) in &owners {
14122 let previous_filters: Vec<Filter> = self
14123 .owner_interests(owner)
14124 .map(log_filters)
14125 .unwrap_or_default();
14126 let continuity_anchor = previous_filters
14127 .iter()
14128 .filter_map(|filter| self.log_anchor(filter))
14129 .min();
14130 let filters = log_filters(interests);
14131 if let Some(backfill) = explicit_backfill
14132 && !filters.is_empty()
14133 {
14134 replacement_backfills.push(QueuedSubscriberBackfill {
14135 owner: Some(owner.clone()),
14136 epoch: None,
14137 filters: filters.clone(),
14138 backfill,
14139 });
14140 }
14141 let explicit_covers = explicit_backfill.is_some_and(|explicit| {
14142 explicit.end_block().is_none()
14143 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
14144 });
14145 let continuity_filters: Vec<_> = filters
14146 .into_iter()
14147 .filter(|filter| !previous_filters.contains(filter))
14148 .collect();
14149 if let Some(anchor) = continuity_anchor
14150 && !continuity_filters.is_empty()
14151 && !explicit_covers
14152 {
14153 replacement_backfills.push(QueuedSubscriberBackfill {
14154 owner: Some(owner.clone()),
14155 epoch: None,
14156 filters: continuity_filters,
14157 backfill: SubscriberBackfill::from_block(anchor),
14158 });
14159 }
14160 }
14161
14162 let retained_backfills = self
14163 .pending_backfills
14164 .iter()
14165 .filter(|queued| {
14166 queued
14167 .owner
14168 .as_ref()
14169 .is_none_or(|owner| !seen.contains(owner))
14170 })
14171 .map(|queued| queued.filters.len())
14172 .sum::<usize>();
14173 let replacement_units = replacement_backfills
14174 .iter()
14175 .map(|queued| queued.filters.len())
14176 .sum::<usize>();
14177 if retained_backfills.saturating_add(replacement_units) > self.config.max_pending_backfills
14178 {
14179 return Err(SubscriberError::ResourceExhausted(format!(
14180 "bulk owner update would queue more than {} lazy backfills",
14181 self.config.max_pending_backfills
14182 )));
14183 }
14184
14185 self.owned_interests = next_owned;
14189 self.interests = next_registered;
14190 for owner in &seen {
14191 self.recent_compat_owner_input_refs.remove(owner);
14192 self.recent_compat_owner_input_ref_sets.remove(owner);
14193 }
14194 self.retire_unreferenced_filters();
14195 self.sources_dirty = true;
14196 self.pending_backfills.retain(|queued| {
14197 queued
14198 .owner
14199 .as_ref()
14200 .is_none_or(|owner| !seen.contains(owner))
14201 });
14202 self.pending_backfills.extend(replacement_backfills);
14203 Ok(())
14204 }
14205
14206 pub fn replace_interest_owners(
14215 &mut self,
14216 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14217 ) -> Result<(), SubscriberError> {
14218 self.replace_interest_owners_inner(owners, None)
14219 }
14220
14221 pub fn replace_interest_owners_with_global_backfill(
14234 &mut self,
14235 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14236 backfill: SubscriberBackfill,
14237 ) -> Result<(), SubscriberError> {
14238 self.replace_interest_owners_inner(owners, Some(backfill))
14239 }
14240
14241 fn replace_interest_owners_inner(
14242 &mut self,
14243 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14244 backfill: Option<SubscriberBackfill>,
14245 ) -> Result<(), SubscriberError> {
14246 validate_subscriber_config(&self.config)?;
14247 if self
14248 .owned_interests
14249 .iter()
14250 .any(|entry| entry.epoch.is_some())
14251 {
14252 return Err(SubscriberError::InvalidConfig(
14253 "cannot replace compatibility owners while an epoch-scoped lifecycle exists",
14254 ));
14255 }
14256
14257 let mut seen = HashSet::with_capacity(owners.len());
14258 let mut next_owned = Vec::with_capacity(owners.len());
14259 for (owner, interests) in owners {
14260 if !seen.insert(owner.clone()) {
14261 return Err(SubscriberError::InvalidConfig(
14262 "owner replacement contains a duplicate owner",
14263 ));
14264 }
14265 next_owned.push(OwnedSubscriberInterests {
14266 owner,
14267 interests,
14268 epoch: None,
14269 state: SubscriberOwnerState::Active,
14270 baseline: None,
14271 progress: None,
14272 progress_stream_revision: None,
14273 });
14274 }
14275 let next_registered = aggregate_interests(&[], &next_owned);
14276 validate_supported_interests(self.mode, &self.config, &next_registered)?;
14277 let mut filters = log_filters(&next_registered);
14278 let mut unique_filters = Vec::with_capacity(filters.len());
14279 for filter in filters.drain(..) {
14280 if !unique_filters.contains(&filter) {
14281 unique_filters.push(filter);
14282 }
14283 }
14284 let replacement_backfills: VecDeque<_> = match backfill {
14285 Some(backfill) if !unique_filters.is_empty() => {
14286 VecDeque::from([QueuedSubscriberBackfill {
14287 owner: None,
14288 epoch: None,
14289 filters: unique_filters,
14290 backfill,
14291 }])
14292 }
14293 Some(_) | None => VecDeque::new(),
14294 };
14295 let replacement_units = replacement_backfills
14296 .iter()
14297 .map(|queued| queued.filters.len())
14298 .sum::<usize>();
14299 if replacement_units > self.config.max_pending_backfills {
14300 return Err(SubscriberError::ResourceExhausted(format!(
14301 "owner replacement would queue more than {} lazy backfills",
14302 self.config.max_pending_backfills
14303 )));
14304 }
14305
14306 let revoke_preconfirmation = self.latest_preconfirmation.is_some()
14311 || self.pending_preconfirmation_invalidation
14312 || self.pending_records.iter().any(|record| {
14313 record.scope == SubscriberInputScope::Preconfirmed
14314 || matches!(
14315 &record.record.context.chain_status,
14316 ChainStatus::Preconfirmed { .. }
14317 )
14318 });
14319 self.base_interests.clear();
14320 self.owned_interests = next_owned;
14321 self.interests = next_registered;
14322 self.reset_delivery_state();
14323 self.pending_preconfirmation_invalidation = revoke_preconfirmation;
14324 self.pending_backfills = replacement_backfills;
14325 self.reset_stream_topology();
14326 Ok(())
14327 }
14328
14329 pub fn add_interest_owner(
14352 &mut self,
14353 owner: HandlerId,
14354 interests: &[ReactiveInterest<N>],
14355 ) -> Result<(), SubscriberError> {
14356 self.set_interest_owner(owner, interests, None)
14357 }
14358
14359 pub fn add_interest_owner_with_backfill(
14378 &mut self,
14379 owner: HandlerId,
14380 interests: &[ReactiveInterest<N>],
14381 backfill: SubscriberBackfill,
14382 ) -> Result<(), SubscriberError> {
14383 self.set_interest_owner(owner, interests, Some(backfill))
14384 }
14385
14386 pub fn add_interest_owner_with_canonical_catchup(
14405 &mut self,
14406 owner: HandlerId,
14407 interests: &[ReactiveInterest<N>],
14408 retained: BlockRef,
14409 ) -> Result<(), SubscriberError> {
14410 validate_subscriber_config(&self.config)?;
14411 if self
14412 .owned_interests
14413 .iter()
14414 .any(|entry| entry.owner == owner && entry.epoch.is_some())
14415 {
14416 return Err(SubscriberError::InvalidConfig(
14417 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
14418 ));
14419 }
14420
14421 let mut next_owned = self.clone_owned_interests();
14422 if let Some(entry) = next_owned.iter_mut().find(|entry| entry.owner == owner) {
14423 entry.interests = interests.to_vec();
14424 entry.state = SubscriberOwnerState::Active;
14425 entry.baseline = None;
14426 entry.progress = None;
14427 entry.progress_stream_revision = None;
14428 entry.epoch = None;
14429 } else {
14430 next_owned.push(OwnedSubscriberInterests {
14431 owner: owner.clone(),
14432 interests: interests.to_vec(),
14433 epoch: None,
14434 state: SubscriberOwnerState::Active,
14435 baseline: None,
14436 progress: None,
14437 progress_stream_revision: None,
14438 });
14439 }
14440 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
14441 validate_supported_interests(self.mode, &self.config, &next_registered)?;
14442 if next_registered
14443 .iter()
14444 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
14445 {
14446 return Err(SubscriberError::Unsupported(
14447 "Alloy coordinated registration supports log-only interest topologies",
14448 ));
14449 }
14450
14451 let mut owner_filters = Vec::new();
14452 for filter in log_filters(interests) {
14453 if !owner_filters.contains(&filter) {
14454 owner_filters.push(filter);
14455 }
14456 }
14457 let mut global_filters = Vec::new();
14458 for filter in log_filters(&next_registered) {
14459 if !global_filters.contains(&filter) {
14460 global_filters.push(filter);
14461 }
14462 }
14463 let owner_backfill =
14464 SubscriberBackfill::from_canonical_block_through(retained, retained.number)?;
14465 let global_backfill = SubscriberBackfill::after_canonical_block(retained)?;
14466 let replacement_units = owner_filters.len().saturating_add(global_filters.len());
14467 let retained_units = self
14468 .pending_backfills
14469 .iter()
14470 .filter(|queued| queued.owner.as_ref() != Some(&owner))
14471 .map(|queued| queued.filters.len())
14472 .sum::<usize>();
14473 if retained_units.saturating_add(replacement_units) > self.config.max_pending_backfills {
14474 return Err(SubscriberError::ResourceExhausted(format!(
14475 "coordinated owner registration would queue more than {} lazy backfills",
14476 self.config.max_pending_backfills
14477 )));
14478 }
14479
14480 let mut replacement_backfills = VecDeque::new();
14481 if !owner_filters.is_empty() {
14482 replacement_backfills.push_back(QueuedSubscriberBackfill {
14483 owner: Some(owner.clone()),
14484 epoch: None,
14485 filters: owner_filters,
14486 backfill: owner_backfill,
14487 });
14488 }
14489 replacement_backfills.push_back(QueuedSubscriberBackfill {
14492 owner: None,
14493 epoch: None,
14494 filters: global_filters,
14495 backfill: global_backfill,
14496 });
14497
14498 self.owned_interests = next_owned;
14501 self.interests = next_registered;
14502 self.recent_compat_owner_input_refs.remove(&owner);
14503 self.recent_compat_owner_input_ref_sets.remove(&owner);
14504 self.pending_backfills
14505 .retain(|queued| queued.owner.as_ref() != Some(&owner));
14506 self.pending_backfills.extend(replacement_backfills);
14507 self.retire_unreferenced_filters();
14508 self.sources_dirty = true;
14509 Ok(())
14510 }
14511
14512 pub fn remove_interest_owner(&mut self, owner: &HandlerId) -> Option<Vec<ReactiveInterest<N>>> {
14521 let index = self
14522 .owned_interests
14523 .iter()
14524 .position(|entry| &entry.owner == owner && entry.epoch.is_none())?;
14525 let removed = self.owned_interests.remove(index);
14526 if let Some(epoch) = &removed.epoch {
14527 self.purge_owner_epoch(epoch);
14528 } else {
14529 self.pending_backfills
14530 .retain(|backfill| backfill.owner.as_ref() != Some(owner));
14531 self.recent_compat_owner_input_refs.remove(owner);
14532 self.recent_compat_owner_input_ref_sets.remove(owner);
14533 }
14534 self.rebuild_registered_interests();
14535 self.retire_unreferenced_filters();
14536 self.sources_dirty = true;
14537 Some(removed.interests)
14538 }
14539
14540 pub fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
14542 self.owned_interests
14543 .iter()
14544 .find(|entry| &entry.owner == owner)
14545 .map(|entry| entry.interests.as_slice())
14546 }
14547
14548 fn set_interest_owner(
14549 &mut self,
14550 owner: HandlerId,
14551 interests: &[ReactiveInterest<N>],
14552 backfill: Option<SubscriberBackfill>,
14553 ) -> Result<(), SubscriberError> {
14554 validate_subscriber_config(&self.config)?;
14555 if self
14556 .owned_interests
14557 .iter()
14558 .any(|entry| entry.owner == owner && entry.epoch.is_some())
14559 {
14560 return Err(SubscriberError::InvalidConfig(
14561 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
14562 ));
14563 }
14564
14565 let mut next_owned = self.clone_owned_interests();
14566 let replaced_epoch = match next_owned.iter_mut().find(|entry| entry.owner == owner) {
14567 Some(entry) => {
14568 entry.interests = interests.to_vec();
14569 entry.state = SubscriberOwnerState::Active;
14570 entry.baseline = None;
14571 entry.progress = None;
14572 entry.progress_stream_revision = None;
14573 entry.epoch.take()
14574 }
14575 None => {
14576 next_owned.push(OwnedSubscriberInterests {
14577 owner: owner.clone(),
14578 interests: interests.to_vec(),
14579 epoch: None,
14580 state: SubscriberOwnerState::Active,
14581 baseline: None,
14582 progress: None,
14583 progress_stream_revision: None,
14584 });
14585 None
14586 }
14587 };
14588 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
14589 validate_supported_interests(self.mode, &self.config, &next_registered)?;
14590
14591 let previous_filters: Vec<Filter> = self
14598 .owner_interests(&owner)
14599 .map(log_filters)
14600 .unwrap_or_default();
14601 let continuity_anchor: Option<u64> = previous_filters
14602 .iter()
14603 .filter_map(|filter| self.log_anchor(filter))
14604 .min();
14605
14606 let mut replacement_backfills = Vec::new();
14610 let filters = log_filters(interests);
14611 if let Some(backfill) = backfill
14612 && !filters.is_empty()
14613 {
14614 replacement_backfills.push(QueuedSubscriberBackfill {
14615 owner: Some(owner.clone()),
14616 epoch: None,
14617 filters: filters.clone(),
14618 backfill,
14619 });
14620 }
14621 let explicit_covers = backfill.is_some_and(|explicit| {
14622 explicit.end_block().is_none()
14623 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
14624 });
14625 let continuity_filters: Vec<_> = filters
14626 .into_iter()
14627 .filter(|filter| !previous_filters.contains(filter))
14628 .collect();
14629 if let Some(anchor) = continuity_anchor
14630 && !continuity_filters.is_empty()
14631 && !explicit_covers
14632 {
14633 replacement_backfills.push(QueuedSubscriberBackfill {
14634 owner: Some(owner.clone()),
14635 epoch: None,
14636 filters: continuity_filters,
14637 backfill: SubscriberBackfill::from_block(anchor),
14638 });
14639 }
14640 let retained_backfills = self
14641 .pending_backfills
14642 .iter()
14643 .filter(|queued| queued.owner.as_ref() != Some(&owner))
14644 .map(|queued| queued.filters.len())
14645 .sum::<usize>();
14646 let replacement_units = replacement_backfills
14647 .iter()
14648 .map(|queued| queued.filters.len())
14649 .sum::<usize>();
14650 if retained_backfills.saturating_add(replacement_units) > self.config.max_pending_backfills
14651 {
14652 return Err(SubscriberError::ResourceExhausted(format!(
14653 "owner update would queue more than {} lazy backfills",
14654 self.config.max_pending_backfills
14655 )));
14656 }
14657
14658 self.owned_interests = next_owned;
14659 self.interests = next_registered;
14660 if let Some(epoch) = replaced_epoch {
14661 self.purge_owner_epoch(&epoch);
14662 } else {
14663 self.recent_compat_owner_input_refs.remove(&owner);
14664 self.recent_compat_owner_input_ref_sets.remove(&owner);
14665 }
14666 self.retire_unreferenced_filters();
14667 self.sources_dirty = true;
14668
14669 self.pending_backfills
14672 .retain(|queued| queued.owner.as_ref() != Some(&owner));
14673 self.pending_backfills.extend(replacement_backfills);
14674 Ok(())
14675 }
14676
14677 fn clone_owned_interests(&self) -> Vec<OwnedSubscriberInterests<N>> {
14678 self.owned_interests
14679 .iter()
14680 .map(|entry| OwnedSubscriberInterests {
14681 owner: entry.owner.clone(),
14682 interests: entry.interests.clone(),
14683 epoch: entry.epoch.clone(),
14684 state: entry.state,
14685 baseline: entry.baseline,
14686 progress: entry.progress.clone(),
14687 progress_stream_revision: entry.progress_stream_revision,
14688 })
14689 .collect()
14690 }
14691
14692 fn rebuild_registered_interests(&mut self) {
14693 self.interests = aggregate_interests(&self.base_interests, &self.owned_interests);
14694 }
14695
14696 fn log_anchor(&self, filter: &Filter) -> Option<u64> {
14699 if let Some(anchor) = self
14700 .log_source_ids
14701 .get(filter)
14702 .and_then(|id| self.last_seen_log_blocks.get(id))
14703 {
14704 return Some(*anchor);
14705 }
14706
14707 self.log_source_ids
14711 .values()
14712 .filter_map(|id| self.last_seen_log_blocks.get(id).copied())
14713 .min()
14714 }
14715
14716 #[allow(clippy::mutable_key_type)]
14723 fn logical_log_filters(&self) -> Vec<Filter> {
14724 let mut filters = log_filters(&self.base_interests);
14725 for entry in &self.owned_interests {
14726 filters.extend(log_filters(&entry.interests));
14727 }
14728 let mut seen = HashSet::new();
14729 filters.retain(|filter| seen.insert(filter.clone()));
14730 filters
14731 }
14732
14733 fn log_stream_filters(&self) -> Vec<Filter> {
14739 let mut merged = Vec::new();
14740 for filter in self.logical_log_filters() {
14741 merge_log_subscription_filter(&mut merged, &filter);
14742 }
14743
14744 let max_addresses = self.config.max_log_addresses_per_subscription.max(1);
14745 let mut planned = Vec::new();
14746 for filter in merged {
14747 let mut addresses: Vec<_> = filter.address.iter().copied().collect();
14748 if addresses.len() <= max_addresses {
14749 planned.push(filter);
14750 continue;
14751 }
14752 addresses.sort_unstable();
14753 for chunk in addresses.chunks(max_addresses) {
14754 let mut split = filter.clone();
14755 split.address = FilterSet::default();
14756 for address in chunk {
14757 split.address.insert(*address);
14758 }
14759 planned.push(split);
14760 }
14761 }
14762 planned
14763 }
14764
14765 #[allow(clippy::mutable_key_type)]
14772 fn retire_unreferenced_filters(&mut self) {
14773 let mut live: HashSet<Filter> = self.log_stream_filters().into_iter().collect();
14774 if let AlloySubscriberState::Active(streams) = &self.state {
14775 for entry in &streams.entries {
14776 match &entry.source {
14777 SubscriberStreamSource::PubSubLog { filter, .. }
14778 | SubscriberStreamSource::BasePendingLog { filter, .. }
14779 | SubscriberStreamSource::PollingLog { filter } => {
14780 live.insert(filter.clone());
14781 }
14782 SubscriberStreamSource::BaseFlashblocks
14783 | SubscriberStreamSource::OpPendingFlashblocks
14784 | SubscriberStreamSource::CanonicalHeadPolling
14785 | SubscriberStreamSource::PubSubPendingHashes
14786 | SubscriberStreamSource::PubSubBlockHeaders
14787 | SubscriberStreamSource::PollingPendingHashes => {}
14788 #[cfg(feature = "raw-flashblocks-json")]
14789 SubscriberStreamSource::ExternalFlashblockUpdates => {}
14790 }
14791 }
14792 }
14793 self.log_source_ids
14794 .retain(|filter, _| live.contains(filter));
14795 let live_ids: HashSet<usize> = self.log_source_ids.values().copied().collect();
14796 self.last_seen_log_blocks
14797 .retain(|id, _| live_ids.contains(id));
14798 }
14799
14800 fn note_attestable_canonical_block(&mut self, record: &ReactiveInputRecord<N>) {
14807 if !self.attests_log_coverage() {
14808 return;
14809 }
14810 let Some(block) = record.context.block.as_ref() else {
14811 return;
14812 };
14813 let advances = self
14814 .attestable_canonical_head
14815 .as_ref()
14816 .is_none_or(|current| block.number > current.number);
14817 if advances {
14818 self.attestable_canonical_head = Some(*block);
14819 }
14820 }
14821
14822 fn reset_log_attestation(&mut self) {
14828 self.attestable_canonical_head = None;
14829 }
14830
14831 fn attests_log_coverage(&self) -> bool {
14837 matches!(
14838 resolve_subscriber_transport(self.mode),
14839 Ok(SubscriberTransport::PubSub)
14840 ) && self
14841 .interests
14842 .iter()
14843 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)))
14844 }
14845
14846 fn queue_log_coverage_attestation(&mut self) {
14848 if !self.attests_log_coverage() {
14849 return;
14850 }
14851 let Some(candidate) = self.attestable_canonical_head else {
14852 return;
14853 };
14854 let advances = self
14855 .attested_log_coverage
14856 .as_ref()
14857 .is_none_or(|attested| candidate.number > attested.number);
14858 if !advances {
14859 return;
14860 }
14861 self.attested_log_coverage = Some(candidate);
14862 self.pending_chain_controls
14863 .push_back(ChainControl::LogCoverage(candidate));
14864 }
14865
14866 fn drain_next_scoped_batch(&mut self) -> Option<SubscriberInputBatch<N>> {
14867 self.queue_log_coverage_attestation();
14871 if self.pending_records.is_empty()
14872 && self.pending_chain_controls.is_empty()
14873 && !self.pending_preconfirmation_invalidation
14874 {
14875 return None;
14876 }
14877
14878 let first_preconfirmation = self.pending_records.front().and_then(|record| {
14879 if record.scope != SubscriberInputScope::Preconfirmed {
14880 return None;
14881 }
14882 match &record.record.context.chain_status {
14883 ChainStatus::Preconfirmed { flashblock } => Some(flashblock.clone()),
14884 _ => None,
14885 }
14886 });
14887 let len = self
14888 .pending_records
14889 .iter()
14890 .take(self.config.max_batch_size)
14891 .take_while(|record| match &first_preconfirmation {
14892 Some(expected) => {
14893 record.scope == SubscriberInputScope::Preconfirmed
14894 && matches!(
14895 &record.record.context.chain_status,
14896 ChainStatus::Preconfirmed { flashblock } if flashblock == expected
14897 )
14898 }
14899 None => record.scope != SubscriberInputScope::Preconfirmed,
14900 })
14901 .count();
14902 let preconfirmation_timing = self
14903 .pending_records
14904 .iter()
14905 .take(len)
14906 .filter_map(SubscriberInputRecord::preconfirmation_timing)
14907 .reduce(FlashblockIngressTiming::earliest);
14908 let records = self.pending_records.drain(..len).collect();
14909 let chain_controls = if first_preconfirmation.is_none() && self.pending_records.is_empty() {
14910 self.pending_chain_controls.drain(..).collect()
14911 } else {
14912 Vec::new()
14913 };
14914 Some(SubscriberInputBatch {
14915 records,
14916 chain_id: self.chain_id,
14917 chain_controls,
14918 preconfirmation_invalidated: std::mem::take(
14919 &mut self.pending_preconfirmation_invalidation,
14920 ),
14921 preconfirmation_timing,
14922 })
14923 }
14924
14925 fn reset_delivery_state(&mut self) {
14926 self.pending_records.clear();
14927 self.pending_chain_controls.clear();
14928 self.pending_reconcile_owner_records.clear();
14929 self.resource_error = None;
14930 self.last_seen_log_blocks.clear();
14931 self.verified_log_blocks.clear();
14932 self.verified_log_block_order.clear();
14933 self.recent_input_refs.clear();
14934 self.recent_input_ref_set.clear();
14935 self.recent_owner_input_refs.clear();
14936 self.recent_owner_input_ref_sets.clear();
14937 self.recent_compat_owner_input_refs.clear();
14938 self.recent_compat_owner_input_ref_sets.clear();
14939 self.pending_backfills.clear();
14940 self.pending_source_backfills.clear();
14941 self.pending_preconfirmation_invalidation = false;
14942 self.pending_flashblock_reconnects.clear();
14943 self.pending_flashblock_reconnect_sources.clear();
14944 self.flashblocks_rpc_metrics = FlashblocksRpcMetrics::default();
14945 self.log_source_ids.clear();
14946 self.next_log_source_id = 0;
14947 self.sources_dirty = true;
14948 self.last_certified_canonical_head = None;
14949 self.last_canonical_head_certification = None;
14950 self.sealed_block_pending_certification = false;
14951 self.attestable_canonical_head = None;
14952 self.attested_log_coverage = None;
14953 self.reset_flashblock_tracking();
14954 }
14955
14956 fn reset_stream_topology(&mut self) {
14957 #[cfg(feature = "raw-flashblocks-json")]
14958 let external = match &mut self.state {
14959 AlloySubscriberState::Active(streams) => streams
14960 .entries
14961 .iter()
14962 .position(|entry| entry.source.is_external_flashblocks())
14963 .map(|index| streams.entries.remove(index)),
14964 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => None,
14965 };
14966
14967 #[cfg(feature = "raw-flashblocks-json")]
14968 if let Some(external) = external {
14969 let mut streams = SubscriberStreams::new();
14970 streams.entries.push(external);
14971 self.state = AlloySubscriberState::Active(streams);
14972 return;
14973 }
14974
14975 self.state = AlloySubscriberState::Uninitialized;
14976 }
14977
14978 fn reset_flashblock_tracking(&mut self) {
14979 self.base_flashblock_header = None;
14980 self.base_flashblock_transactions = None;
14981 self.unmatched_pending_logs.clear();
14982 self.latest_preconfirmation = None;
14983 self.preconfirmed_seen_logs.clear();
14984 self.preconfirmed_receipted_transactions.clear();
14985 self.preconfirmed_unavailable_receipts.clear();
14986 #[cfg(feature = "raw-flashblocks-json")]
14987 {
14988 self.last_external_flashblock_snapshot = None;
14989 }
14990 self.consecutive_flashblock_poll_failures = 0;
14991 }
14992
14993 fn invalidate_preconfirmation_snapshot(&mut self) {
15000 self.pending_records
15001 .retain(|record| record.scope != SubscriberInputScope::Preconfirmed);
15002 self.pending_preconfirmation_invalidation = true;
15003 self.latest_preconfirmation = None;
15004 self.preconfirmed_seen_logs.clear();
15005 self.preconfirmed_receipted_transactions.clear();
15006 self.preconfirmed_unavailable_receipts.clear();
15007 }
15008
15009 fn bump_stream_revision(&mut self) {
15010 self.stream_revision = self.stream_revision.saturating_add(1);
15011 }
15012}
15013
15014impl<P, N> InterestOwnerSubscriber<N> for AlloySubscriber<P, N>
15015where
15016 P: Provider<N> + Send + Sync,
15017 N: Network + 'static,
15018 N::HeaderResponse: Send + 'static,
15019{
15020 fn upsert_interest_owners(
15021 &mut self,
15022 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
15023 ) -> SubscriberOperation<'_, ()> {
15024 Box::pin(async move {
15025 if !owners.is_empty() {
15026 self.ensure_chain_id().await?;
15027 }
15028 AlloySubscriber::upsert_interest_owners(self, owners)
15029 })
15030 }
15031
15032 fn replace_interest_owners(
15033 &mut self,
15034 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
15035 ) -> SubscriberOperation<'_, ()> {
15036 Box::pin(async move {
15037 if owners.iter().any(|(_, interests)| !interests.is_empty()) {
15038 self.ensure_chain_id().await?;
15039 }
15040 AlloySubscriber::replace_interest_owners(self, owners)
15041 })
15042 }
15043
15044 fn replace_interest_owners_with_global_backfill(
15045 &mut self,
15046 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
15047 backfill: SubscriberBackfill,
15048 ) -> SubscriberOperation<'_, ()> {
15049 Box::pin(async move {
15050 if owners.iter().any(|(_, interests)| !interests.is_empty()) {
15051 self.ensure_chain_id().await?;
15052 }
15053 AlloySubscriber::replace_interest_owners_with_global_backfill(self, owners, backfill)
15054 })
15055 }
15056
15057 fn add_interest_owner(
15058 &mut self,
15059 owner: HandlerId,
15060 interests: &[ReactiveInterest<N>],
15061 ) -> SubscriberOperation<'_, ()> {
15062 let interests = interests.to_vec();
15063 Box::pin(async move {
15064 if !interests.is_empty() {
15065 self.ensure_chain_id().await?;
15066 }
15067 AlloySubscriber::add_interest_owner(self, owner, &interests)
15068 })
15069 }
15070
15071 fn add_interest_owner_with_backfill(
15072 &mut self,
15073 owner: HandlerId,
15074 interests: &[ReactiveInterest<N>],
15075 backfill: SubscriberBackfill,
15076 ) -> SubscriberOperation<'_, ()> {
15077 let interests = interests.to_vec();
15078 Box::pin(async move {
15079 if !interests.is_empty() {
15080 self.ensure_chain_id().await?;
15081 }
15082 AlloySubscriber::add_interest_owner_with_backfill(self, owner, &interests, backfill)
15083 })
15084 }
15085
15086 fn add_interest_owner_with_canonical_catchup(
15087 &mut self,
15088 owner: HandlerId,
15089 interests: &[ReactiveInterest<N>],
15090 retained: BlockRef,
15091 ) -> SubscriberOperation<'_, ()> {
15092 let interests = interests.to_vec();
15093 Box::pin(async move {
15094 self.ensure_chain_id().await?;
15097 AlloySubscriber::add_interest_owner_with_canonical_catchup(
15098 self, owner, &interests, retained,
15099 )
15100 })
15101 }
15102
15103 fn remove_interest_owner(
15104 &mut self,
15105 owner: &HandlerId,
15106 ) -> SubscriberOperation<'_, Option<Vec<ReactiveInterest<N>>>> {
15107 let owner = owner.clone();
15108 Box::pin(async move { Ok(AlloySubscriber::remove_interest_owner(self, &owner)) })
15109 }
15110
15111 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
15112 AlloySubscriber::owner_interests(self, owner)
15113 }
15114}
15115
15116enum AlloySubscriberState<N: Network> {
15117 Uninitialized,
15118 Active(SubscriberStreams<N>),
15119 Empty,
15120}
15121
15122struct SubscriberStreams<N: Network> {
15123 entries: Vec<SubscriberStreamEntry<N>>,
15124 next_index: usize,
15125}
15126
15127struct SubscriberStreamEntry<N: Network> {
15128 source: SubscriberStreamSource,
15129 stream: BoxStream<'static, SubscriberEvent<N>>,
15130}
15131
15132impl<N: Network> SubscriberStreams<N> {
15133 fn new() -> Self {
15134 Self {
15135 entries: Vec::new(),
15136 next_index: 0,
15137 }
15138 }
15139
15140 fn is_empty(&self) -> bool {
15141 self.entries.is_empty()
15142 }
15143
15144 fn push(
15145 &mut self,
15146 source: SubscriberStreamSource,
15147 stream: BoxStream<'static, SubscriberEvent<N>>,
15148 ) {
15149 self.entries.push(SubscriberStreamEntry { source, stream });
15150 }
15151
15152 #[cfg(all(test, any(feature = "reactive-polling", feature = "reactive-ws")))]
15153 fn len(&self) -> usize {
15154 self.entries.len()
15155 }
15156
15157 fn contains_source(&self, source: &SubscriberStreamSource) -> bool {
15158 self.entries
15159 .iter()
15160 .any(|entry| entry.source.same_key(source))
15161 }
15162
15163 fn retain_sources(&mut self, sources: &[SubscriberStreamSource]) {
15164 self.entries
15165 .retain(|entry| sources.iter().any(|source| entry.source.same_key(source)));
15166 self.normalize_next_index();
15167 }
15168
15169 fn normalize_next_index(&mut self) {
15170 if self.entries.is_empty() {
15171 self.next_index = 0;
15172 } else if self.next_index >= self.entries.len() {
15173 self.next_index %= self.entries.len();
15174 }
15175 }
15176
15177 async fn next(&mut self) -> Option<SubscriberEvent<N>> {
15178 poll_fn(|cx| {
15179 self.normalize_next_index();
15180 if self.entries.is_empty() {
15181 return std::task::Poll::Ready(None);
15182 }
15183
15184 let mut index = self.next_index;
15185 let mut checked = 0usize;
15186 while checked < self.entries.len() {
15187 if index >= self.entries.len() {
15188 index = 0;
15189 }
15190 match self.entries[index].stream.as_mut().poll_next(cx) {
15191 std::task::Poll::Ready(Some(event)) => {
15192 if matches!(event, SubscriberEvent::StreamTerminated(_)) {
15193 self.entries.remove(index);
15194 self.next_index = if self.entries.is_empty() {
15195 0
15196 } else {
15197 index % self.entries.len()
15198 };
15199 } else {
15200 self.next_index = (index + 1) % self.entries.len();
15201 }
15202 return std::task::Poll::Ready(Some(event));
15203 }
15204 std::task::Poll::Ready(None) => {
15205 self.entries.remove(index);
15206 if self.entries.is_empty() {
15207 self.next_index = 0;
15208 return std::task::Poll::Ready(None);
15209 }
15210 }
15211 std::task::Poll::Pending => {
15212 checked += 1;
15213 index += 1;
15214 }
15215 }
15216 }
15217
15218 if self.entries.is_empty() {
15219 std::task::Poll::Ready(None)
15220 } else {
15221 self.next_index = index % self.entries.len();
15222 std::task::Poll::Pending
15223 }
15224 })
15225 .await
15226 }
15227}
15228
15229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15230#[allow(dead_code)]
15231enum SubscriberTransport {
15232 PubSub,
15233 Polling,
15234}
15235
15236#[derive(Clone, Debug)]
15237enum SubscriberStreamSource {
15238 PubSubLog {
15239 id: usize,
15240 filter: Filter,
15241 },
15242 BasePendingLog {
15243 id: usize,
15244 filter: Filter,
15245 },
15246 BaseFlashblocks,
15247 OpPendingFlashblocks,
15248 CanonicalHeadPolling,
15249 PubSubPendingHashes,
15250 PubSubBlockHeaders,
15251 PollingLog {
15252 filter: Filter,
15253 },
15254 PollingPendingHashes,
15255 #[cfg(feature = "raw-flashblocks-json")]
15256 ExternalFlashblockUpdates,
15257}
15258
15259impl SubscriberStreamSource {
15260 fn label(&self) -> &'static str {
15261 match self {
15262 Self::PubSubLog { .. } => "pubsub log",
15263 Self::BasePendingLog { .. } => "OP Stack pendingLogs",
15264 Self::BaseFlashblocks => "OP Stack newFlashblocks",
15265 Self::OpPendingFlashblocks => "Optimism pending Flashblocks",
15266 Self::CanonicalHeadPolling => "certified canonical head",
15267 Self::PubSubPendingHashes => "pubsub pending transaction hash",
15268 Self::PubSubBlockHeaders => "pubsub block header",
15269 Self::PollingLog { .. } => "polling log",
15270 Self::PollingPendingHashes => "polling pending transaction hash",
15271 #[cfg(feature = "raw-flashblocks-json")]
15272 Self::ExternalFlashblockUpdates => "external standardized Flashblock update",
15273 }
15274 }
15275
15276 fn is_pubsub(&self) -> bool {
15277 matches!(
15278 self,
15279 Self::PubSubLog { .. }
15280 | Self::BasePendingLog { .. }
15281 | Self::BaseFlashblocks
15282 | Self::OpPendingFlashblocks
15283 | Self::PubSubPendingHashes
15284 | Self::PubSubBlockHeaders
15285 )
15286 }
15287
15288 fn is_flashblocks(&self) -> bool {
15289 matches!(
15290 self,
15291 Self::BasePendingLog { .. } | Self::BaseFlashblocks | Self::OpPendingFlashblocks
15292 )
15293 }
15294
15295 fn same_key(&self, other: &Self) -> bool {
15296 match (self, other) {
15297 (Self::PubSubLog { filter: left, .. }, Self::PubSubLog { filter: right, .. })
15298 | (
15299 Self::BasePendingLog { filter: left, .. },
15300 Self::BasePendingLog { filter: right, .. },
15301 )
15302 | (Self::PollingLog { filter: left }, Self::PollingLog { filter: right }) => {
15303 left == right
15304 }
15305 (Self::BaseFlashblocks, Self::BaseFlashblocks)
15306 | (Self::OpPendingFlashblocks, Self::OpPendingFlashblocks)
15307 | (Self::CanonicalHeadPolling, Self::CanonicalHeadPolling)
15308 | (Self::PubSubPendingHashes, Self::PubSubPendingHashes)
15309 | (Self::PubSubBlockHeaders, Self::PubSubBlockHeaders)
15310 | (Self::PollingPendingHashes, Self::PollingPendingHashes) => true,
15311 #[cfg(feature = "raw-flashblocks-json")]
15312 (Self::ExternalFlashblockUpdates, Self::ExternalFlashblockUpdates) => true,
15313 _ => false,
15314 }
15315 }
15316
15317 fn is_external_flashblocks(&self) -> bool {
15318 #[cfg(feature = "raw-flashblocks-json")]
15319 {
15320 matches!(self, Self::ExternalFlashblockUpdates)
15321 }
15322 #[cfg(not(feature = "raw-flashblocks-json"))]
15323 {
15324 false
15325 }
15326 }
15327}
15328
15329#[allow(dead_code)]
15330enum SubscriberEvent<N: Network> {
15331 Log {
15332 source_id: usize,
15333 log: Log,
15334 #[cfg(feature = "ingress-timing")]
15335 timing: Option<CanonicalIngressTiming>,
15336 },
15337 BackfilledLogs {
15338 source_id: usize,
15339 logs: Vec<Log>,
15340 },
15341 Logs(Vec<Log>),
15342 BlockHeader {
15343 header: N::HeaderResponse,
15344 #[cfg(feature = "ingress-timing")]
15345 timing: Option<CanonicalIngressTiming>,
15346 },
15347 PendingHash(B256),
15348 PendingHashes(Vec<B256>),
15349 BasePendingLog {
15350 source_id: usize,
15351 log: Log,
15352 },
15353 BasePendingLogTimed {
15354 source_id: usize,
15355 log: Log,
15356 timing: FlashblockIngressTiming,
15357 },
15358 BaseFlashblock(BaseFlashblockWirePayload),
15359 BaseFlashblockTimed {
15360 payload: BaseFlashblockWirePayload,
15361 timing: FlashblockIngressTiming,
15362 },
15363 OpFlashblockTick,
15364 OpFlashblockTickTimed(FlashblockIngressTiming),
15365 CanonicalHeadTick,
15366 PreconfirmedLogs {
15367 flashblock: FlashblockRef,
15368 logs: Vec<Log>,
15369 timing: FlashblockIngressTiming,
15370 },
15371 FlashblockInvalidated,
15372 FlashblockObserved,
15373 #[cfg(feature = "raw-flashblocks-json")]
15374 ExternalFlashblockUpdate(raw_json_flashblocks::QueuedFlashblockUpdate),
15375 StreamTerminated(SubscriberStreamSource),
15376 StreamGap {
15380 source: SubscriberStreamSource,
15381 gap: SubscriberStreamGap,
15382 },
15383}
15384
15385enum SubscriberReady<N: Network> {
15386 Event(Option<SubscriberEvent<N>>),
15387 FlashblockReconnect(
15388 SubscriberStreamSource,
15389 Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>,
15390 ),
15391}
15392
15393#[derive(Debug)]
15394enum PendingFlashblockPollError {
15395 Request(SubscriberError),
15396 Integrity(SubscriberError),
15397}
15398
15399impl PendingFlashblockPollError {
15400 fn into_subscriber(self) -> SubscriberError {
15401 match self {
15402 Self::Request(error) | Self::Integrity(error) => error,
15403 }
15404 }
15405}
15406
15407fn pending_flashblock_request_error(error: impl fmt::Display) -> PendingFlashblockPollError {
15408 PendingFlashblockPollError::Request(provider_error(error))
15409}
15410
15411fn normalize_op_pending_block<N: Network>(
15412 mut value: serde_json::Value,
15413) -> Result<N::BlockResponse, SubscriberError> {
15414 let object = value.as_object_mut().ok_or_else(|| {
15415 SubscriberError::Provider("OP pending block response is not an object".into())
15416 })?;
15417 let transactions = object
15418 .get_mut("transactions")
15419 .and_then(serde_json::Value::as_array_mut)
15420 .ok_or_else(|| {
15421 SubscriberError::Provider(
15422 "OP pending block response is missing its transaction array".into(),
15423 )
15424 })?;
15425 for transaction in transactions {
15426 if transaction.is_string() {
15427 continue;
15428 }
15429 let hash = transaction
15430 .as_object()
15431 .and_then(|object| object.get("hash"))
15432 .filter(|hash| hash.is_string())
15433 .cloned()
15434 .ok_or_else(|| {
15435 SubscriberError::Provider("OP pending block transaction is missing its hash".into())
15436 })?;
15437 *transaction = hash;
15438 }
15439 if object.get("hash").is_none_or(serde_json::Value::is_null) {
15440 object.insert(
15441 "hash".into(),
15442 serde_json::Value::String(B256::ZERO.to_string()),
15443 );
15444 }
15445 if object.get("nonce").is_none_or(serde_json::Value::is_null) {
15446 object.insert(
15447 "nonce".into(),
15448 serde_json::Value::String("0x0000000000000000".into()),
15449 );
15450 }
15451 if object.get("miner").is_none_or(serde_json::Value::is_null)
15452 || object
15453 .get("beneficiary")
15454 .is_none_or(serde_json::Value::is_null)
15455 {
15456 object.insert(
15457 "miner".into(),
15458 serde_json::Value::String(Address::ZERO.to_string()),
15459 );
15460 }
15461 serde_json::from_value(value).map_err(|error| {
15462 SubscriberError::Provider(format!(
15463 "failed to decode normalized OP pending block: {error}"
15464 ))
15465 })
15466}
15467
15468fn normalize_pending_transaction_receipt(
15469 expected_transaction_hash: B256,
15470 value: serde_json::Value,
15471) -> Result<Option<Vec<Log>>, SubscriberError> {
15472 if value.is_null() {
15473 return Ok(None);
15474 }
15475 let receipt = value.as_object().ok_or_else(|| {
15476 SubscriberError::Provider("pending transaction receipt response is not an object".into())
15477 })?;
15478 let transaction_hash: B256 =
15479 serde_json::from_value(receipt.get("transactionHash").cloned().ok_or_else(|| {
15480 SubscriberError::Provider(
15481 "pending transaction receipt is missing its transaction hash".into(),
15482 )
15483 })?)
15484 .map_err(|error| {
15485 SubscriberError::Provider(format!(
15486 "failed to decode pending transaction receipt hash: {error}"
15487 ))
15488 })?;
15489 if transaction_hash != expected_transaction_hash {
15490 return Err(SubscriberError::Provider(
15491 "pending transaction receipt hash disagrees with its request".into(),
15492 ));
15493 }
15494 let receipt_logs = receipt
15495 .get("logs")
15496 .and_then(serde_json::Value::as_array)
15497 .ok_or_else(|| {
15498 SubscriberError::Provider("pending transaction receipt is missing its log array".into())
15499 })?;
15500 let mut logs = Vec::new();
15501 for log in receipt_logs {
15502 let log: Log = serde_json::from_value(log.clone()).map_err(|error| {
15503 SubscriberError::Provider(format!(
15504 "failed to decode pending transaction receipt log: {error}"
15505 ))
15506 })?;
15507 if log.transaction_hash != Some(expected_transaction_hash) {
15508 return Err(SubscriberError::Provider(
15509 "pending transaction receipt log hash disagrees with its receipt".into(),
15510 ));
15511 }
15512 logs.push(log);
15513 }
15514 Ok(Some(logs))
15515}
15516
15517impl<P, N> EventSubscriber<N> for AlloySubscriber<P, N>
15518where
15519 P: Provider<N> + Send + Sync,
15520 N: Network + 'static,
15521 N::HeaderResponse: Send + 'static,
15522{
15523 fn chain_id(&self) -> Option<u64> {
15524 self.chain_id
15525 }
15526
15527 fn capabilities(&self) -> SubscriberCapabilities {
15528 let Ok(transport) = resolve_subscriber_transport(self.mode) else {
15529 return SubscriberCapabilities::default();
15530 };
15531 let mut capabilities = vec![
15532 SubscriberCapability::Logs,
15533 SubscriberCapability::PendingTransactionHashes,
15534 SubscriberCapability::HistoricalBackfill,
15535 SubscriberCapability::Live,
15536 SubscriberCapability::OwnerScopedDelivery,
15537 SubscriberCapability::DynamicInterests,
15538 ];
15539 if transport == SubscriberTransport::PubSub {
15540 capabilities.push(SubscriberCapability::BlockHeaders);
15541 capabilities.push(SubscriberCapability::LogCoverageAttestation);
15546 }
15547 if self.config.preconfirmations != PreconfirmationMode::Disabled
15548 && (self.uses_external_flashblock_updates()
15549 || (self.provider_ref.is_some()
15550 && self.chain_id.and_then(flashblocks_adapter).is_some()))
15551 {
15552 capabilities.push(SubscriberCapability::Preconfirmations);
15553 }
15554 SubscriberCapabilities::new(capabilities)
15555 }
15556
15557 fn register_interests(
15558 &mut self,
15559 interests: &[ReactiveInterest<N>],
15560 ) -> SubscriberOperation<'_, ()> {
15561 let interests = interests.to_vec();
15562 Box::pin(async move {
15563 validate_subscriber_config(&self.config)?;
15564 validate_supported_interests(self.mode, &self.config, &interests)?;
15565 if !interests.is_empty() {
15566 self.ensure_chain_id().await?;
15567 }
15568 self.validate_flashblocks_setup()?;
15569
15570 self.base_interests = interests;
15571 self.owned_interests.clear();
15572 self.rebuild_registered_interests();
15573 self.reset_delivery_state();
15574 self.reset_stream_topology();
15575 Ok(())
15576 })
15577 }
15578
15579 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N> {
15580 Box::pin(async {
15581 Ok(self
15582 .next_scoped_batch()
15583 .await?
15584 .map(SubscriberInputBatch::into_reactive_batch))
15585 })
15586 }
15587}
15588
15589impl<P, N> AlloySubscriber<P, N>
15590where
15591 P: Provider<N> + Send + Sync,
15592 N: Network + 'static,
15593 N::HeaderResponse: Send + 'static,
15594{
15595 pub async fn establish_flashblocks_preflight(
15617 &mut self,
15618 expected_chain_id: u64,
15619 ) -> Result<FlashblocksPreflight, SubscriberError> {
15620 validate_subscriber_config(&self.config)?;
15621 if self.config.preconfirmations == PreconfirmationMode::Disabled {
15622 return Err(SubscriberError::InvalidConfig(
15623 "Flashblocks preflight requires preconfirmations",
15624 ));
15625 }
15626 if !self
15627 .interests
15628 .iter()
15629 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)))
15630 {
15631 return Err(SubscriberError::InvalidConfig(
15632 "Flashblocks preflight requires at least one active log interest",
15633 ));
15634 }
15635 let chain_id = self.ensure_chain_id().await?;
15636 if chain_id != expected_chain_id {
15637 return Err(SubscriberError::ChainMismatch {
15638 expected: expected_chain_id,
15639 actual: chain_id,
15640 });
15641 }
15642 self.validate_flashblocks_setup()?;
15643 #[cfg(feature = "raw-flashblocks-json")]
15644 if let Some(provider) = self.external_flashblocks_provider.clone() {
15645 self.ensure_streams().await?;
15646 return Ok(FlashblocksPreflight {
15647 chain_id,
15648 provider,
15649 delivery: FlashblocksDelivery::ExternalUpdates,
15650 pending_log_subscriptions: 0,
15651 pending_log_filters: self.log_stream_filters().len(),
15652 advertised_capabilities: None,
15653 });
15654 }
15655 let adapter = flashblocks_adapter(chain_id).ok_or(SubscriberError::Unsupported(
15656 "Flashblocks are currently implemented for Base and OP chains",
15657 ))?;
15658 let provider = self
15659 .provider_ref
15660 .clone()
15661 .ok_or(SubscriberError::InvalidConfig(
15662 "Flashblocks preflight requires a stable provider ref",
15663 ))?;
15664 self.record_rpc(
15665 SubscriberRpcCause::FlashblocksSetup,
15666 SubscriberRpcMethod::OpSupportedCapabilities,
15667 );
15668 self.flashblocks_rpc_metrics.capability_requests = self
15669 .flashblocks_rpc_metrics
15670 .capability_requests
15671 .saturating_add(1);
15672 let capability_provider = if adapter == FlashblocksAdapter::PendingStatePolling {
15673 self.flashblocks_state_provider
15674 .as_ref()
15675 .unwrap_or(&self.provider)
15676 } else {
15677 &self.provider
15678 };
15679 let advertised_capabilities = capability_provider
15680 .client()
15681 .request::<_, serde_json::Value>("op_supportedCapabilities", ())
15682 .await
15683 .ok();
15684
15685 self.ensure_streams().await?;
15686 let pending_log_filters = self.log_stream_filters();
15687 if adapter == FlashblocksAdapter::PendingStatePolling
15688 && self.pending_receipt_requests_per_tick_capacity() == 0
15689 {
15690 return Err(SubscriberError::InvalidConfig(
15691 "Flashblocks RPC budget leaves no capacity for OP transaction receipts",
15692 ));
15693 }
15694 let (delivery, pending_log_subscriptions) = match adapter {
15695 FlashblocksAdapter::NativeSubscriptions => {
15696 if resolve_subscriber_transport(self.mode)? != SubscriberTransport::PubSub {
15697 return Err(SubscriberError::Unsupported(
15698 "Base Flashblocks preflight requires pubsub",
15699 ));
15700 }
15701 let pending_sources = self
15702 .pubsub_stream_sources()
15703 .into_iter()
15704 .filter(|source| {
15705 matches!(source, SubscriberStreamSource::BasePendingLog { .. })
15706 })
15707 .collect::<Vec<_>>();
15708 let AlloySubscriberState::Active(streams) = &self.state else {
15709 return Err(SubscriberError::Provider(
15710 "Flashblocks preflight subscriptions did not become active".to_owned(),
15711 ));
15712 };
15713 if !streams.contains_source(&SubscriberStreamSource::BaseFlashblocks)
15714 || pending_sources
15715 .iter()
15716 .any(|source| !streams.contains_source(source))
15717 {
15718 return Err(SubscriberError::Provider(
15719 "Base Flashblocks preflight did not retain both subscription lanes"
15720 .to_owned(),
15721 ));
15722 }
15723 (
15724 FlashblocksDelivery::NativeSubscriptions,
15725 pending_sources.len(),
15726 )
15727 }
15728 FlashblocksAdapter::PendingStatePolling => {
15729 if let Some(state_provider) = self.flashblocks_state_provider.as_ref() {
15730 self.record_rpc(
15731 SubscriberRpcCause::FlashblocksSetup,
15732 SubscriberRpcMethod::EthChainId,
15733 );
15734 self.flashblocks_rpc_metrics.provider_pair_chain_requests = self
15735 .flashblocks_rpc_metrics
15736 .provider_pair_chain_requests
15737 .saturating_add(1);
15738 let actual = state_provider
15739 .get_chain_id()
15740 .await
15741 .map_err(provider_error)?;
15742 if actual != expected_chain_id {
15743 return Err(SubscriberError::ChainMismatch {
15744 expected: expected_chain_id,
15745 actual,
15746 });
15747 }
15748 }
15749 let AlloySubscriberState::Active(streams) = &self.state else {
15750 return Err(SubscriberError::Provider(
15751 "Flashblocks preflight streams did not become active".to_owned(),
15752 ));
15753 };
15754 if !streams.contains_source(&SubscriberStreamSource::OpPendingFlashblocks) {
15755 return Err(SubscriberError::Provider(
15756 "Optimism Flashblocks preflight did not retain its pending-state sampler"
15757 .to_owned(),
15758 ));
15759 }
15760 self.probe_pending_state(&pending_log_filters).await?;
15761 (FlashblocksDelivery::PendingStatePolling, 0)
15762 }
15763 };
15764 Ok(FlashblocksPreflight {
15765 chain_id,
15766 provider,
15767 delivery,
15768 pending_log_subscriptions,
15769 pending_log_filters: pending_log_filters.len(),
15770 advertised_capabilities,
15771 })
15772 }
15773
15774 #[cfg(feature = "raw-flashblocks-json")]
15785 pub fn ingest_flashblock_update(
15786 &mut self,
15787 update: FlashblockUpdate,
15788 ) -> Result<(), SubscriberError> {
15789 self.ingest_flashblock_update_with_ingress(
15790 update,
15791 FlashblockIngressTiming::new(Instant::now()),
15792 )
15793 }
15794
15795 #[cfg(feature = "raw-flashblocks-json")]
15805 pub fn ingest_flashblock_update_with_ingress(
15806 &mut self,
15807 update: FlashblockUpdate,
15808 timing: FlashblockIngressTiming,
15809 ) -> Result<(), SubscriberError> {
15810 validate_subscriber_config(&self.config)?;
15811 self.validate_flashblocks_setup()?;
15812 let configured =
15813 self.external_flashblocks_provider
15814 .as_ref()
15815 .ok_or(SubscriberError::InvalidConfig(
15816 "standardized Flashblock updates require configure_external_flashblock_updates",
15817 ))?;
15818
15819 match update {
15820 FlashblockUpdate::Snapshot(batch) => {
15821 if batch.flashblock.provider.endpoint != configured.endpoint {
15822 return Err(SubscriberError::Provider(
15823 "external Flashblock update came from an unexpected provider endpoint"
15824 .into(),
15825 ));
15826 }
15827 if batch.flashblock.provider.generation < configured.generation
15828 || self.latest_preconfirmation.as_ref().is_some_and(|latest| {
15829 latest.provider.endpoint == batch.flashblock.provider.endpoint
15830 && latest.provider.generation > batch.flashblock.provider.generation
15831 })
15832 {
15833 return Ok(());
15834 }
15835 if self
15836 .rejected_external_flashblock_generation
15837 .is_some_and(|rejected| batch.flashblock.provider.generation <= rejected)
15838 {
15839 return Err(SubscriberError::Provider(
15840 "external Flashblock provider generation was previously rejected".into(),
15841 ));
15842 }
15843 validate_standard_flashblock_snapshot(&batch)?;
15844 if self.validate_external_flashblock_sequence(&batch)? {
15845 return Ok(());
15846 }
15847 let required = self.pending_record_count().saturating_add(batch.logs.len());
15848 if required > self.config.max_pending_records {
15849 self.invalidate_preconfirmation_snapshot();
15850 self.last_external_flashblock_snapshot = None;
15851 return Err(SubscriberError::ResourceExhausted(format!(
15852 "external preconfirmation records require {required} pending records, above the configured limit of {}",
15853 self.config.max_pending_records
15854 )));
15855 }
15856 let accepted_snapshot = (*batch).clone();
15857 let FlashblockSnapshot { flashblock, logs } = *batch;
15858 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
15859 self.last_external_flashblock_snapshot = Some(accepted_snapshot);
15860 if let Some(provider) = self.external_flashblocks_provider.as_mut() {
15861 provider.generation = provider.generation.max(flashblock.provider.generation);
15862 }
15863 if !logs.is_empty() {
15864 self.enqueue_event(SubscriberEvent::PreconfirmedLogs {
15865 flashblock,
15866 logs,
15867 timing,
15868 });
15869 }
15870 }
15871 FlashblockUpdate::Invalidated(invalidation) => {
15872 if invalidation.provider.endpoint != configured.endpoint {
15873 return Err(SubscriberError::Provider(
15874 "external Flashblock invalidation came from an unexpected provider endpoint"
15875 .into(),
15876 ));
15877 }
15878 if self.latest_preconfirmation.as_ref().is_some_and(|latest| {
15879 latest.provider == invalidation.provider
15880 && latest.payload_id == Some(invalidation.payload_id)
15881 }) {
15882 self.invalidate_preconfirmation_snapshot();
15883 self.last_external_flashblock_snapshot = None;
15884 }
15885 }
15886 }
15887 Ok(())
15888 }
15889
15890 #[cfg(feature = "raw-flashblocks-json")]
15891 fn validate_external_flashblock_sequence(
15892 &self,
15893 snapshot: &FlashblockSnapshot,
15894 ) -> Result<bool, SubscriberError> {
15895 let Some(previous) = self.last_external_flashblock_snapshot.as_ref() else {
15896 if snapshot.flashblock.index != Some(0) {
15897 return Err(SubscriberError::Provider(
15898 "external Flashblock payload generation must begin at index zero".into(),
15899 ));
15900 }
15901 return Ok(false);
15902 };
15903
15904 if previous.flashblock.provider == snapshot.flashblock.provider
15905 && previous.flashblock.payload_id == snapshot.flashblock.payload_id
15906 {
15907 let previous_index = previous
15908 .flashblock
15909 .index
15910 .expect("validated indexed snapshot");
15911 let current_index = snapshot
15912 .flashblock
15913 .index
15914 .expect("validated indexed snapshot");
15915 if current_index == previous_index {
15916 if previous == snapshot {
15917 return Ok(true);
15918 }
15919 return Err(SubscriberError::Provider(
15920 "external Flashblock repeated the same index with conflicting content".into(),
15921 ));
15922 }
15923 if current_index < previous_index {
15924 return Err(SubscriberError::Provider(format!(
15925 "external Flashblock index regressed from {previous_index} to {current_index}"
15926 )));
15927 }
15928 if current_index > previous_index.saturating_add(1) {
15929 return Err(SubscriberError::Provider(format!(
15930 "external Flashblock index skipped from {previous_index} to {current_index}"
15931 )));
15932 }
15933 if current_index == previous_index.saturating_add(1)
15934 && !previous.flashblock.same_base_identity(&snapshot.flashblock)
15935 {
15936 return Err(SubscriberError::Provider(
15937 "external Flashblock base identity changed within one payload generation"
15938 .into(),
15939 ));
15940 }
15941 if current_index == previous_index.saturating_add(1)
15942 && !snapshot
15943 .flashblock
15944 .transaction_hashes
15945 .starts_with(&previous.flashblock.transaction_hashes)
15946 {
15947 return Err(SubscriberError::Provider(
15948 "external Flashblock cumulative transaction membership changed its prior prefix"
15949 .into(),
15950 ));
15951 }
15952 let prior_transaction_count =
15953 u64::try_from(previous.flashblock.transaction_hashes.len()).unwrap_or(u64::MAX);
15954 if current_index == previous_index.saturating_add(1)
15955 && snapshot.logs.iter().any(|log| {
15956 log.transaction_index
15957 .is_some_and(|index| index < prior_transaction_count)
15958 })
15959 {
15960 return Err(SubscriberError::Provider(
15961 "external Flashblock delta log does not belong to a newly appended transaction"
15962 .into(),
15963 ));
15964 }
15965 } else if snapshot.flashblock.index != Some(0) {
15966 return Err(SubscriberError::Provider(
15967 "external Flashblock payload generation must begin at index zero".into(),
15968 ));
15969 }
15970 Ok(false)
15971 }
15972
15973 async fn probe_pending_state(&mut self, filters: &[Filter]) -> Result<(), SubscriberError> {
15974 self.flashblocks_rpc_metrics.pending_block_requests = self
15975 .flashblocks_rpc_metrics
15976 .pending_block_requests
15977 .saturating_add(1);
15978 let pending = self
15979 .fetch_op_pending_block()
15980 .await
15981 .map_err(PendingFlashblockPollError::into_subscriber)?
15982 .ok_or_else(|| {
15983 SubscriberError::Provider(
15984 "provider returned no pending block during Flashblocks preflight".into(),
15985 )
15986 })?;
15987 self.certify_op_pending_parent(&pending)
15988 .await
15989 .map_err(PendingFlashblockPollError::into_subscriber)?;
15990 for filter in filters {
15991 self.record_rpc(
15992 SubscriberRpcCause::PendingStateSample,
15993 SubscriberRpcMethod::EthGetLogs,
15994 );
15995 self.flashblocks_rpc_metrics.pending_log_requests = self
15996 .flashblocks_rpc_metrics
15997 .pending_log_requests
15998 .saturating_add(1);
15999 self.flashblocks_state_provider
16000 .as_ref()
16001 .unwrap_or(&self.provider)
16002 .get_logs(
16003 &filter
16004 .clone()
16005 .from_block(BlockNumberOrTag::Latest)
16006 .to_block(BlockNumberOrTag::Pending),
16007 )
16008 .await
16009 .map_err(provider_error)?;
16010 }
16011 self.record_rpc(
16012 SubscriberRpcCause::PendingStateSample,
16013 SubscriberRpcMethod::EthGetTransactionReceipt,
16014 );
16015 self.flashblocks_rpc_metrics.pending_receipt_requests = self
16016 .flashblocks_rpc_metrics
16017 .pending_receipt_requests
16018 .saturating_add(1);
16019 let _: serde_json::Value = self
16020 .flashblocks_state_provider
16021 .as_ref()
16022 .unwrap_or(&self.provider)
16023 .raw_request(Cow::Borrowed("eth_getTransactionReceipt"), (B256::ZERO,))
16024 .await
16025 .map_err(provider_error)?;
16026 Ok(())
16027 }
16028
16029 async fn certify_op_pending_parent(
16030 &mut self,
16031 pending: &N::BlockResponse,
16032 ) -> Result<N::HeaderResponse, PendingFlashblockPollError> {
16033 let pending_header = pending.header();
16034 let pending_number = pending_header.number();
16035 let parent_hash = pending_header.parent_hash();
16036 if pending_number == 0 || parent_hash.is_zero() {
16037 return Err(PendingFlashblockPollError::Integrity(
16038 SubscriberError::Provider(
16039 "OP pending block omitted a certifiable canonical parent".into(),
16040 ),
16041 ));
16042 }
16043 self.record_rpc(
16044 SubscriberRpcCause::CanonicalHeadCertification,
16045 SubscriberRpcMethod::EthGetBlockByHash,
16046 );
16047 self.flashblocks_rpc_metrics.canonical_head_requests = self
16048 .flashblocks_rpc_metrics
16049 .canonical_head_requests
16050 .saturating_add(1);
16051 let parent = self
16052 .flashblocks_state_provider
16053 .as_ref()
16054 .unwrap_or(&self.provider)
16055 .get_block_by_hash(parent_hash)
16056 .await
16057 .map_err(pending_flashblock_request_error)?
16058 .ok_or_else(|| {
16059 PendingFlashblockPollError::Request(SubscriberError::Provider(
16060 "Flashblocks provider returned no exact OP pending parent block".into(),
16061 ))
16062 })?;
16063 let parent_header = parent.header();
16064 if parent_header.hash() != parent_hash
16065 || parent_header.number().checked_add(1) != Some(pending_number)
16066 {
16067 return Err(PendingFlashblockPollError::Integrity(
16068 SubscriberError::Provider(
16069 "OP pending block does not extend its exact certified parent".into(),
16070 ),
16071 ));
16072 }
16073 Ok(parent_header.clone())
16074 }
16075
16076 async fn fetch_op_pending_block(
16077 &mut self,
16078 ) -> Result<Option<N::BlockResponse>, PendingFlashblockPollError> {
16079 self.record_rpc(
16080 SubscriberRpcCause::PendingStateSample,
16081 SubscriberRpcMethod::EthGetBlockByNumber,
16082 );
16083 let state_provider = self
16084 .flashblocks_state_provider
16085 .as_ref()
16086 .unwrap_or(&self.provider);
16087 let value: Option<serde_json::Value> = state_provider
16088 .raw_request(
16089 Cow::Borrowed("eth_getBlockByNumber"),
16090 (BlockNumberOrTag::Pending, true),
16091 )
16092 .await
16093 .map_err(pending_flashblock_request_error)?;
16094 value
16095 .map(normalize_op_pending_block::<N>)
16096 .transpose()
16097 .map_err(PendingFlashblockPollError::Integrity)
16098 }
16099
16100 async fn ensure_chain_id(&mut self) -> Result<u64, SubscriberError> {
16104 if let Some(chain_id) = self.chain_id {
16105 return Ok(chain_id);
16106 }
16107 self.record_rpc(
16108 SubscriberRpcCause::ChainIdentity,
16109 SubscriberRpcMethod::EthChainId,
16110 );
16111 let chain_id = self.provider.get_chain_id().await.map_err(provider_error)?;
16112 self.chain_id = Some(chain_id);
16113 Ok(chain_id)
16114 }
16115
16116 fn validate_flashblocks_setup(&self) -> Result<(), SubscriberError> {
16117 if self.config.preconfirmations == PreconfirmationMode::Disabled {
16118 if self.uses_external_flashblock_updates() {
16119 return Err(SubscriberError::InvalidConfig(
16120 "external Flashblock updates require preconfirmations to be preferred or required",
16121 ));
16122 }
16123 return Ok(());
16124 }
16125 if self.uses_external_flashblock_updates() {
16126 return Ok(());
16127 }
16128 if self.provider_ref.is_none() {
16129 return Err(SubscriberError::InvalidConfig(
16130 "Flashblocks require a stable provider ref from a pinned provider lease",
16131 ));
16132 }
16133 let Some(chain_id) = self.chain_id else {
16134 return Ok(());
16135 };
16136 match flashblocks_adapter(chain_id) {
16137 Some(FlashblocksAdapter::NativeSubscriptions)
16138 if resolve_subscriber_transport(self.mode)? != SubscriberTransport::PubSub
16139 && self.config.preconfirmations == PreconfirmationMode::Required =>
16140 {
16141 return Err(SubscriberError::Unsupported(
16142 "Base Flashblocks require pubsub for newFlashblocks and pendingLogs",
16143 ));
16144 }
16145 Some(FlashblocksAdapter::NativeSubscriptions) => {}
16146 Some(_) => {}
16147 None if self.config.preconfirmations == PreconfirmationMode::Required => {
16148 return Err(SubscriberError::Unsupported(
16149 "Flashblocks are currently implemented for Base and OP chains",
16150 ));
16151 }
16152 None => {}
16153 }
16154 Ok(())
16155 }
16156
16157 pub async fn reconcile_interest_owner(
16171 &mut self,
16172 epoch: &SubscriberOwnerEpoch,
16173 through: BlockRef,
16174 ) -> Result<SubscriberOwnerProgress, SubscriberOwnerError>
16175 where
16176 P: Clone,
16177 {
16178 self.reconcile_interest_owners(std::slice::from_ref(epoch), through)
16179 .await?
16180 .pop()
16181 .ok_or(SubscriberOwnerError::NotStaged)
16182 }
16183
16184 pub async fn reconcile_interest_owners(
16208 &mut self,
16209 epochs: &[SubscriberOwnerEpoch],
16210 through: BlockRef,
16211 ) -> Result<Vec<SubscriberOwnerProgress>, SubscriberOwnerError>
16212 where
16213 P: Clone,
16214 {
16215 if epochs.is_empty() {
16216 return Ok(Vec::new());
16217 }
16218
16219 self.ensure_chain_id().await?;
16220
16221 let mut seen = HashSet::new();
16222 let mut plans = Vec::with_capacity(epochs.len());
16223 for epoch in epochs {
16224 if !seen.insert(epoch.clone()) {
16225 continue;
16226 }
16227 let entry = self
16228 .owned_interests
16229 .iter()
16230 .find(|entry| {
16231 entry.epoch.as_ref() == Some(epoch)
16232 && entry.state == SubscriberOwnerState::Staged
16233 })
16234 .ok_or(SubscriberOwnerError::NotStaged)?;
16235 let position = entry
16236 .progress
16237 .as_ref()
16238 .map(|progress| &progress.through)
16239 .or(entry.baseline.as_ref())
16240 .ok_or(SubscriberOwnerError::MissingBaseline)?;
16241 let baseline = position.number;
16242 if through.number < baseline {
16243 return Err(SubscriberOwnerError::ProgressRegression {
16244 current: baseline,
16245 target: through.number,
16246 });
16247 }
16248 let from_block = baseline
16249 .checked_add(1)
16250 .ok_or(SubscriberOwnerError::PostBlockOverflow(baseline))?;
16251 if through.number == baseline && through.hash != position.hash {
16252 return Err(SubscriberOwnerError::ProgressConflict {
16253 number: baseline,
16254 current_hash: position.hash,
16255 target_hash: through.hash,
16256 });
16257 }
16258 if through.number == from_block
16259 && through
16260 .parent_hash
16261 .is_some_and(|parent| parent != position.hash)
16262 {
16263 return Err(SubscriberOwnerError::ProgressConflict {
16264 number: baseline,
16265 current_hash: position.hash,
16266 target_hash: through.parent_hash.expect("checked as present above"),
16267 });
16268 }
16269 if entry
16270 .interests
16271 .iter()
16272 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
16273 {
16274 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
16275 }
16276 plans.push(SubscriberOwnerReconcilePlan {
16277 epoch: epoch.clone(),
16278 interests: entry.interests.clone(),
16279 retained: *position,
16280 from_block,
16281 });
16282 }
16283
16284 self.ensure_streams().await?;
16287 let provider = self.provider.clone();
16288 let filters = merged_owner_reconcile_filters(&plans, through.number);
16289 let retained = plans.iter().map(|plan| plan.retained).collect();
16290 let target_epochs: HashSet<_> = plans.iter().map(|plan| plan.epoch.clone()).collect();
16291 let fetch = fetch_owner_catchup::<P, N>(
16292 provider,
16293 filters,
16294 retained,
16295 through,
16296 SubscriberOwnerCatchupOptions {
16297 target_preverified: false,
16298 max_logs: self.config.max_pending_records,
16299 max_log_bytes: self.config.max_backfill_log_bytes,
16300 max_requests_in_flight: self.config.max_reconcile_requests_in_flight,
16301 cause: SubscriberRpcCause::OwnerReconcile,
16302 },
16303 Arc::clone(&self.rpc_counters),
16304 );
16305 let SubscriberOwnerCatchup { logs, certified } =
16306 self.drive_reconcile_fetch(fetch, &target_epochs).await?;
16307
16308 let records = logs
16309 .into_iter()
16310 .map(|log| log_input_record(log, InputSource::Backfill))
16311 .collect();
16312 let mut routed_records = Vec::new();
16313 for record in dedupe_records(sort_records(records)).map_err(|error| {
16314 SubscriberError::InvalidBackfill(format!(
16315 "conflicting duplicate owner catch-up record: {error}"
16316 ))
16317 })? {
16318 let block_number = match &record.input {
16319 ReactiveInput::Log(log) => log
16320 .block_number
16321 .expect("bulk catch-up logs were validated before commit"),
16322 _ => unreachable!("bulk owner catch-up contains log records only"),
16323 };
16324 let owners: Vec<SubscriberOwnerEpoch> = plans
16325 .iter()
16326 .filter(|plan| block_number >= plan.from_block)
16327 .filter(|plan| {
16328 plan.interests
16329 .iter()
16330 .any(|interest| interest_matches(interest, &record.input))
16331 })
16332 .map(|plan| plan.epoch.clone())
16333 .collect();
16334 if !owners.is_empty() {
16335 routed_records.push((record, owners));
16336 }
16337 }
16338 self.ensure_pending_record_capacity(
16339 routed_records.len(),
16340 "owner reconciliation historical records",
16341 )?;
16342
16343 self.pending_backfills.retain(|queued| {
16347 queued
16348 .epoch
16349 .as_ref()
16350 .is_none_or(|epoch| !target_epochs.contains(epoch))
16351 });
16352 for (record, owners) in routed_records {
16353 self.enqueue_owner_record_for_owners_unmerged(record, owners);
16354 }
16355 self.promote_reconcile_owner_records(&target_epochs);
16356 self.seed_reconciled_filter_anchors(&plans, certified.number);
16357
16358 let stream_revision = self.stream_revision;
16359 let mut progress = Vec::with_capacity(plans.len());
16360 for plan in plans {
16361 let item = SubscriberOwnerProgress {
16362 owner: plan.epoch.clone(),
16363 through: certified,
16364 };
16365 let entry = self
16366 .owned_interests
16367 .iter_mut()
16368 .find(|entry| entry.epoch.as_ref() == Some(&plan.epoch))
16369 .expect("bulk reconcile holds exclusive access after epoch preflight");
16370 entry.progress = Some(item.clone());
16371 entry.progress_stream_revision = Some(stream_revision);
16372 progress.push(item);
16373 }
16374 Ok(progress)
16375 }
16376
16377 async fn drive_reconcile_fetch<T, F>(
16378 &mut self,
16379 fetch: F,
16380 target_epochs: &HashSet<SubscriberOwnerEpoch>,
16381 ) -> Result<T, SubscriberOwnerError>
16382 where
16383 F: Future<Output = Result<T, SubscriberOwnerError>>,
16384 {
16385 if !matches!(&self.state, AlloySubscriberState::Active(_)) {
16386 return fetch.await;
16387 }
16388 let mut fetch = Box::pin(fetch);
16389 loop {
16390 let event = {
16391 let live = Box::pin(self.next_event());
16392 match select(fetch, live).await {
16393 Either::Left((result, pending_live)) => {
16394 drop(pending_live);
16395 return result;
16396 }
16397 Either::Right((event, pending_fetch)) => {
16398 fetch = pending_fetch;
16399 event
16400 }
16401 }
16402 };
16403 let event = event?.ok_or_else(|| {
16404 SubscriberError::Provider(
16405 "Alloy subscriber streams ended during owner reconcile".to_owned(),
16406 )
16407 })?;
16408 self.buffer_reconcile_event_for_owners(&event, target_epochs);
16409 self.enqueue_event_excluding_owners(event, target_epochs);
16410 self.check_resource_error()?;
16411 }
16412 }
16413
16414 pub async fn next_scoped_batch_or<C, F>(
16433 &mut self,
16434 control: Pin<&mut F>,
16435 ) -> Result<SubscriberDriverPoll<C, N>, SubscriberError>
16436 where
16437 C: Send,
16438 F: Future<Output = C> + Send,
16439 {
16440 let batch = self.next_scoped_batch();
16441 match select(control, batch).await {
16442 Either::Left((control, pending_batch)) => {
16443 drop(pending_batch);
16444 Ok(SubscriberDriverPoll::Control(control))
16445 }
16446 Either::Right((batch, _pending_control)) => batch.map(SubscriberDriverPoll::Batch),
16447 }
16448 }
16449
16450 pub fn next_scoped_batch(&mut self) -> SubscriberNextScopedBatch<'_, N> {
16461 Box::pin(async {
16462 self.check_resource_error()?;
16463 if self.chain_id.is_none()
16464 && (!self.pending_records.is_empty()
16465 || !self.pending_chain_controls.is_empty()
16466 || !self.pending_backfills.is_empty()
16467 || !self.interests.is_empty())
16468 {
16469 self.ensure_chain_id().await?;
16470 }
16471 if let Some(batch) = self.drain_next_scoped_batch() {
16472 return Ok(Some(batch));
16473 }
16474
16475 self.ensure_streams().await?;
16480 self.check_resource_error()?;
16481 if let Some(batch) = self.drain_next_scoped_batch() {
16482 return Ok(Some(batch));
16483 }
16484
16485 self.drain_pending_backfills().await?;
16486 self.check_resource_error()?;
16487 if let Some(batch) = self.drain_next_scoped_batch() {
16488 return Ok(Some(batch));
16489 }
16490
16491 if self.interests.is_empty() {
16492 return Ok(None);
16493 }
16494
16495 loop {
16496 let Some(event) = self.next_event().await? else {
16497 return Ok(None);
16498 };
16499
16500 self.enqueue_event(event);
16501 self.check_resource_error()?;
16502 if let Some(batch) = self.drain_next_scoped_batch() {
16503 return Ok(Some(batch));
16504 }
16505 }
16506 })
16507 }
16508
16509 async fn ensure_streams(&mut self) -> Result<(), SubscriberError> {
16524 if !self.sources_dirty {
16525 return Ok(());
16526 }
16527 if matches!(self.state, AlloySubscriberState::Uninitialized) && self.interests.is_empty() {
16532 self.bump_stream_revision();
16533 self.sources_dirty = false;
16534 return Ok(());
16535 }
16536
16537 let desired = self.stream_sources()?;
16538 let missing: Vec<SubscriberStreamSource> = match &self.state {
16539 AlloySubscriberState::Active(streams) => desired
16540 .iter()
16541 .filter(|source| !streams.contains_source(source))
16542 .cloned()
16543 .collect(),
16544 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => desired.clone(),
16545 };
16546
16547 for source in missing {
16548 let stream = match self.connect_source_stream(source.clone()).await {
16549 Ok(stream) => stream,
16550 Err(error)
16551 if source.is_flashblocks()
16552 && self.config.preconfirmations == PreconfirmationMode::Preferred =>
16553 {
16554 tracing::warn!(
16555 stream = source.label(),
16556 error = %error,
16557 "Flashblocks source unavailable; canonical delivery remains active"
16558 );
16559 if self.config.reconnect.enabled {
16560 self.schedule_flashblock_reconnect(
16561 source,
16562 self.config.reconnect.retry_delay,
16563 );
16564 }
16565 continue;
16566 }
16567 Err(error) => return Err(error),
16568 };
16569 self.install_source_stream(source.clone(), stream);
16573 if self.source_requires_backfill(&source) {
16574 self.queue_source_backfill(source);
16575 }
16576 }
16577
16578 while let Some(source) = self.pending_source_backfills.front().cloned() {
16579 let desired_and_live = desired.iter().any(|item| item.same_key(&source))
16580 && matches!(
16581 &self.state,
16582 AlloySubscriberState::Active(streams) if streams.contains_source(&source)
16583 );
16584 if !desired_and_live {
16585 self.pending_source_backfills.pop_front();
16586 continue;
16587 }
16588
16589 let event = self.backfill_reconnected_source(&source).await?;
16595 self.pending_source_backfills.pop_front();
16596 if let Some(event) = event {
16597 self.enqueue_event(event);
16598 }
16599 }
16600
16601 if let AlloySubscriberState::Active(streams) = &mut self.state {
16602 streams.retain_sources(&desired);
16603 if streams.is_empty() {
16604 self.state = AlloySubscriberState::Empty;
16605 }
16606 }
16607
16608 self.bump_stream_revision();
16609 self.sources_dirty = false;
16610 self.retire_unreferenced_filters();
16611 Ok(())
16612 }
16613
16614 fn install_source_stream(
16615 &mut self,
16616 source: SubscriberStreamSource,
16617 stream: BoxStream<'static, SubscriberEvent<N>>,
16618 ) {
16619 match &mut self.state {
16620 AlloySubscriberState::Active(streams) => {
16621 if streams.contains_source(&source) {
16622 return;
16623 }
16624 streams.push(source, stream);
16625 }
16626 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
16627 let mut streams = SubscriberStreams::new();
16628 streams.push(source, stream);
16629 self.state = AlloySubscriberState::Active(streams);
16630 }
16631 }
16632 self.bump_stream_revision();
16635 }
16636
16637 fn schedule_flashblock_reconnect(
16638 &mut self,
16639 source: SubscriberStreamSource,
16640 first_delay: Duration,
16641 ) {
16642 if self
16643 .pending_flashblock_reconnect_sources
16644 .iter()
16645 .any(|pending| pending.same_key(&source))
16646 {
16647 return;
16648 }
16649 self.pending_flashblock_reconnect_sources
16650 .push(source.clone());
16651 self.pending_flashblock_reconnects
16652 .push(flashblock_reconnect_future(
16653 self.provider.root().clone(),
16654 source,
16655 self.config.max_batch_size,
16656 self.config.reconnect.clone(),
16657 first_delay,
16658 self.config.flashblock_poll_interval,
16659 Arc::clone(&self.rpc_counters),
16660 ));
16661 }
16662
16663 fn reschedule_preferred_flashblock(&mut self, source: SubscriberStreamSource) {
16664 if !self.config.reconnect.enabled {
16665 return;
16666 }
16667 self.schedule_flashblock_reconnect(source, self.config.reconnect.max_delay);
16668 }
16669
16670 fn source_requires_backfill(&self, source: &SubscriberStreamSource) -> bool {
16671 matches!(source, SubscriberStreamSource::PubSubLog { id, .. }
16672 if self.last_seen_log_blocks.contains_key(id))
16673 }
16674
16675 fn queue_source_backfill(&mut self, source: SubscriberStreamSource) {
16676 if !self
16677 .pending_source_backfills
16678 .iter()
16679 .any(|pending| pending.same_key(&source))
16680 {
16681 self.pending_source_backfills.push_back(source);
16682 }
16683 }
16684
16685 async fn drain_pending_backfills(&mut self) -> Result<(), SubscriberError> {
16697 while let Some(queued) = self.pending_backfills.front() {
16698 let epoch = queued.epoch.clone();
16700 let owner = queued.owner.clone();
16701 let owner_exists = match (&epoch, &owner) {
16702 (Some(epoch), _) => self.interest_owner_state(epoch).is_some(),
16703 (None, Some(owner)) => self.owner_interests(owner).is_some(),
16704 (None, None) => true,
16705 };
16706 if !owner_exists {
16707 self.pending_backfills.pop_front();
16708 continue;
16709 }
16710 let filters = queued.filters.clone();
16711 let backfill = queued.backfill;
16712
16713 let to_block = match backfill.end_block() {
16714 Some(to_block) => to_block,
16715 None => {
16716 self.record_rpc(
16717 SubscriberRpcCause::LazyBackfill,
16718 SubscriberRpcMethod::EthBlockNumber,
16719 );
16720 self.provider
16721 .get_block_number()
16722 .await
16723 .map_err(provider_error)?
16724 }
16725 };
16726 if to_block < backfill.start_block() {
16727 let certified = if let Some(retained) = backfill.retained_anchor() {
16732 let actual = fetch_provider_block_ref::<P, N>(
16733 &self.provider,
16734 retained.number,
16735 &self.rpc_counters,
16736 SubscriberRpcCause::LazyBackfill,
16737 )
16738 .await?;
16739 if !block_ref_satisfies_expected(&actual, retained) {
16740 return Err(SubscriberError::InvalidBackfill(format!(
16741 "retained anchor {}:{:?} conflicts with provider block {}:{:?}",
16742 retained.number, retained.hash, actual.number, actual.hash
16743 )));
16744 }
16745 if to_block < retained.number {
16746 return Err(SubscriberError::InvalidBackfill(format!(
16747 "backfill upper bound {to_block} precedes retained anchor {}",
16748 retained.number
16749 )));
16750 }
16751 Some(actual)
16752 } else {
16753 None
16754 };
16755 self.pending_backfills.pop_front();
16756 for filter in &filters {
16757 let source_id = self.log_source_id(filter);
16758 if let Some(certified) = certified {
16759 self.last_seen_log_blocks
16760 .entry(source_id)
16761 .and_modify(|anchor| *anchor = (*anchor).max(certified.number))
16762 .or_insert(certified.number);
16763 }
16764 }
16765 if owner.is_none()
16766 && let Some(certified) = certified
16767 {
16768 self.pending_chain_controls
16769 .push_back(global_backfill_barrier(backfill, certified));
16770 }
16771 if !self.pending_chain_controls.is_empty() {
16772 break;
16773 }
16774 continue;
16775 }
16776
16777 let through = fetch_provider_block_ref::<P, N>(
16778 &self.provider,
16779 to_block,
16780 &self.rpc_counters,
16781 SubscriberRpcCause::LazyBackfill,
16782 )
16783 .await?;
16784 let request_filters =
16785 merged_lazy_backfill_filters(&filters, backfill.start_block(), through.number);
16786 let retained = backfill.retained_anchor().copied().into_iter().collect();
16787 let SubscriberOwnerCatchup {
16788 mut logs,
16789 certified,
16790 } = fetch_owner_catchup::<&P, N>(
16791 &self.provider,
16792 request_filters,
16793 retained,
16794 through,
16795 SubscriberOwnerCatchupOptions {
16796 target_preverified: true,
16797 max_logs: self.config.max_pending_records,
16798 max_log_bytes: self.config.max_backfill_log_bytes,
16799 max_requests_in_flight: self.config.max_reconcile_requests_in_flight,
16800 cause: SubscriberRpcCause::LazyBackfill,
16801 },
16802 Arc::clone(&self.rpc_counters),
16803 )
16804 .await
16805 .map_err(lazy_backfill_error)?;
16806 logs.sort_by_key(|log| {
16807 (
16808 log.block_number.unwrap_or_default(),
16809 log.transaction_index.unwrap_or_default(),
16810 log.log_index.unwrap_or_default(),
16811 )
16812 });
16813 logs.dedup();
16814 self.ensure_pending_record_capacity(logs.len(), "lazy subscriber backfill records")?;
16815
16816 self.pending_backfills.pop_front();
16819 if let Some(epoch) = epoch.as_ref() {
16820 self.enqueue_backfilled_logs(logs, None, Some(epoch), Some(backfill));
16821 } else if let Some(owner) = owner.as_ref() {
16822 self.enqueue_compat_owner_backfilled_logs(logs, owner, backfill);
16823 } else {
16824 self.enqueue_backfilled_logs(logs, None, None, Some(backfill));
16825 self.pending_chain_controls
16826 .push_back(global_backfill_barrier(backfill, certified));
16827 }
16828 for filter in &filters {
16829 let source_id = self.log_source_id(filter);
16830 let anchor = self
16831 .last_seen_log_blocks
16832 .entry(source_id)
16833 .or_insert(certified.number);
16834 *anchor = (*anchor).max(certified.number);
16835 }
16836
16837 if !self.pending_records.is_empty() || !self.pending_chain_controls.is_empty() {
16838 break;
16839 }
16840 }
16841 Ok(())
16842 }
16843
16844 fn stream_sources(&mut self) -> Result<Vec<SubscriberStreamSource>, SubscriberError> {
16845 let sources = match resolve_subscriber_transport(self.mode)? {
16846 SubscriberTransport::PubSub => self.pubsub_stream_sources(),
16847 SubscriberTransport::Polling => self.polling_stream_sources(),
16848 };
16849 #[cfg(feature = "raw-flashblocks-json")]
16850 let sources = {
16851 let mut sources = sources;
16852 if self.external_flashblock_update_channel_opened {
16853 sources.push(SubscriberStreamSource::ExternalFlashblockUpdates);
16854 }
16855 sources
16856 };
16857 Ok(sources)
16858 }
16859
16860 fn pubsub_stream_sources(&mut self) -> Vec<SubscriberStreamSource> {
16861 let mut sources = Vec::new();
16862 let inherited_anchor = self.last_seen_log_blocks.values().copied().min();
16863
16864 for filter in self.log_stream_filters() {
16865 let id = self.log_source_id(&filter);
16866 if let Some(anchor) = inherited_anchor {
16867 self.last_seen_log_blocks.entry(id).or_insert(anchor);
16868 }
16869 sources.push(SubscriberStreamSource::PubSubLog { id, filter });
16870 }
16871
16872 if needs_pending_hash_stream(&self.interests) {
16873 sources.push(SubscriberStreamSource::PubSubPendingHashes);
16874 }
16875
16876 if needs_header_block_stream(&self.interests) {
16877 if !self.uses_external_flashblock_updates()
16878 && self.config.preconfirmations != PreconfirmationMode::Disabled
16879 && self.chain_id.and_then(flashblocks_adapter).is_some()
16880 {
16881 sources.push(SubscriberStreamSource::CanonicalHeadPolling);
16882 } else {
16883 sources.push(SubscriberStreamSource::PubSubBlockHeaders);
16884 }
16885 }
16886
16887 if self.config.preconfirmations != PreconfirmationMode::Disabled
16888 && !self.uses_external_flashblock_updates()
16889 {
16890 match self.chain_id.and_then(flashblocks_adapter) {
16891 Some(FlashblocksAdapter::NativeSubscriptions) => {
16892 sources.push(SubscriberStreamSource::BaseFlashblocks);
16893 for filter in self.log_stream_filters() {
16894 let id = self.log_source_id(&filter);
16895 sources.push(SubscriberStreamSource::BasePendingLog { id, filter });
16896 }
16897 }
16898 Some(FlashblocksAdapter::PendingStatePolling) => {
16899 sources.push(SubscriberStreamSource::OpPendingFlashblocks);
16900 }
16901 None => {}
16902 }
16903 }
16904
16905 sources
16906 }
16907
16908 fn polling_stream_sources(&self) -> Vec<SubscriberStreamSource> {
16909 let mut sources = Vec::new();
16910
16911 for filter in self.log_stream_filters() {
16912 sources.push(SubscriberStreamSource::PollingLog { filter });
16913 }
16914
16915 if needs_pending_hash_stream(&self.interests) {
16916 sources.push(SubscriberStreamSource::PollingPendingHashes);
16917 }
16918
16919 if self.config.preconfirmations != PreconfirmationMode::Disabled
16920 && !self.uses_external_flashblock_updates()
16921 && self.chain_id.and_then(flashblocks_adapter)
16922 == Some(FlashblocksAdapter::PendingStatePolling)
16923 {
16924 sources.push(SubscriberStreamSource::OpPendingFlashblocks);
16925 }
16926
16927 sources
16928 }
16929
16930 fn log_source_id(&mut self, filter: &Filter) -> usize {
16931 if let Some(id) = self.log_source_ids.get(filter) {
16932 return *id;
16933 }
16934
16935 let id = self.next_log_source_id;
16936 self.next_log_source_id = self.next_log_source_id.saturating_add(1);
16937 self.log_source_ids.insert(filter.clone(), id);
16938 id
16939 }
16940
16941 async fn connect_source_stream(
16942 &mut self,
16943 source: SubscriberStreamSource,
16944 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16945 match source {
16946 SubscriberStreamSource::PubSubLog { id, filter } => {
16947 self.connect_pubsub_log_stream(id, filter).await
16948 }
16949 SubscriberStreamSource::BasePendingLog { id, filter } => {
16950 self.connect_base_pending_log_stream(id, filter).await
16951 }
16952 SubscriberStreamSource::BaseFlashblocks => self.connect_base_flashblock_stream().await,
16953 SubscriberStreamSource::OpPendingFlashblocks => {
16954 self.connect_op_flashblock_tick_stream()
16955 }
16956 SubscriberStreamSource::CanonicalHeadPolling => {
16957 self.connect_canonical_head_tick_stream()
16958 }
16959 SubscriberStreamSource::PubSubPendingHashes => {
16960 self.connect_pubsub_pending_hash_stream().await
16961 }
16962 SubscriberStreamSource::PubSubBlockHeaders => {
16963 self.connect_pubsub_block_header_stream().await
16964 }
16965 SubscriberStreamSource::PollingLog { filter } => {
16966 self.connect_polling_log_stream(filter).await
16967 }
16968 SubscriberStreamSource::PollingPendingHashes => {
16969 self.connect_polling_pending_hash_stream().await
16970 }
16971 #[cfg(feature = "raw-flashblocks-json")]
16972 SubscriberStreamSource::ExternalFlashblockUpdates => {
16973 let receiver = self.external_flashblock_updates.take().ok_or_else(|| {
16974 SubscriberError::Provider(
16975 "external Flashblock update channel receiver is unavailable".into(),
16976 )
16977 })?;
16978 let updates = stream::unfold(receiver, |mut receiver| async move {
16979 receiver
16980 .recv()
16981 .await
16982 .map(|update| (SubscriberEvent::ExternalFlashblockUpdate(update), receiver))
16983 });
16984 Ok(stream_with_termination(
16985 updates,
16986 SubscriberStreamSource::ExternalFlashblockUpdates,
16987 ))
16988 }
16989 }
16990 }
16991
16992 async fn connect_pubsub_log_stream(
16993 &mut self,
16994 id: usize,
16995 filter: Filter,
16996 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16997 #[cfg(feature = "reactive-ws")]
16998 {
16999 let source = SubscriberStreamSource::PubSubLog {
17000 id,
17001 filter: filter.clone(),
17002 };
17003 self.record_rpc(
17004 SubscriberRpcCause::StreamSubscription,
17005 SubscriberRpcMethod::EthSubscribe,
17006 );
17007 let subscription = self
17008 .provider
17009 .subscribe_logs(&filter)
17010 .channel_size(self.log_channel_size())
17011 .await
17012 .map_err(provider_error)?;
17013 Ok(gap_observing_stream(
17014 subscription,
17015 source,
17016 Arc::clone(&self.gap_counters),
17017 #[cfg(feature = "ingress-timing")]
17018 self.transport_receipts.clone(),
17019 move |log, #[cfg(feature = "ingress-timing")] timing| SubscriberEvent::Log {
17020 source_id: id,
17021 log,
17022 #[cfg(feature = "ingress-timing")]
17023 timing,
17024 },
17025 ))
17026 }
17027
17028 #[cfg(not(feature = "reactive-ws"))]
17029 {
17030 let _ = (id, filter);
17031 Err(SubscriberError::Unsupported(
17032 "AlloySubscriber pubsub mode requires the reactive-ws feature",
17033 ))
17034 }
17035 }
17036
17037 async fn connect_base_pending_log_stream(
17038 &mut self,
17039 id: usize,
17040 filter: Filter,
17041 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17042 #[cfg(feature = "reactive-ws")]
17043 {
17044 let source = SubscriberStreamSource::BasePendingLog {
17045 id,
17046 filter: filter.clone(),
17047 };
17048 let params = base_pending_log_filter(&filter)?;
17049 self.record_rpc(
17050 SubscriberRpcCause::StreamSubscription,
17051 SubscriberRpcMethod::EthSubscribe,
17052 );
17053 let subscription = self
17054 .provider
17055 .subscribe::<_, Log>(("pendingLogs", params))
17056 .channel_size(self.log_channel_size())
17057 .await
17058 .map_err(provider_error)?;
17059 Ok(gap_observing_stream(
17060 subscription,
17061 source,
17062 Arc::clone(&self.gap_counters),
17063 #[cfg(feature = "ingress-timing")]
17064 None,
17065 move |log, #[cfg(feature = "ingress-timing")] _| {
17066 SubscriberEvent::BasePendingLogTimed {
17067 source_id: id,
17068 log,
17069 timing: FlashblockIngressTiming::new(Instant::now()),
17070 }
17071 },
17072 ))
17073 }
17074
17075 #[cfg(not(feature = "reactive-ws"))]
17076 {
17077 let _ = (id, filter);
17078 Err(SubscriberError::Unsupported(
17079 "Base Flashblocks require the reactive-ws feature",
17080 ))
17081 }
17082 }
17083
17084 async fn connect_base_flashblock_stream(
17085 &mut self,
17086 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17087 #[cfg(feature = "reactive-ws")]
17088 {
17089 self.record_rpc(
17090 SubscriberRpcCause::StreamSubscription,
17091 SubscriberRpcMethod::EthSubscribe,
17092 );
17093 let stream = self
17094 .provider
17095 .subscribe::<_, BaseFlashblockWirePayload>(("newFlashblocks",))
17096 .channel_size(self.config.max_batch_size.max(1))
17097 .await
17098 .map_err(provider_error)?
17099 .into_stream()
17100 .map(|payload| SubscriberEvent::BaseFlashblockTimed {
17101 payload,
17102 timing: FlashblockIngressTiming::new(Instant::now()),
17103 });
17104 Ok(stream_with_termination(
17105 stream,
17106 SubscriberStreamSource::BaseFlashblocks,
17107 ))
17108 }
17109
17110 #[cfg(not(feature = "reactive-ws"))]
17111 {
17112 Err(SubscriberError::Unsupported(
17113 "Base Flashblocks require the reactive-ws feature",
17114 ))
17115 }
17116 }
17117
17118 fn connect_canonical_head_tick_stream(
17119 &self,
17120 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17121 let mut interval = tokio::time::interval(self.config.canonical_head_poll_interval);
17122 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
17123 let stream = stream::unfold(interval, |mut interval| async move {
17124 interval.tick().await;
17125 Some((SubscriberEvent::CanonicalHeadTick, interval))
17126 });
17127 Ok(stream_with_termination(
17128 stream,
17129 SubscriberStreamSource::CanonicalHeadPolling,
17130 ))
17131 }
17132
17133 fn connect_op_flashblock_tick_stream(
17134 &self,
17135 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17136 let first_tick = tokio::time::Instant::now() + self.config.flashblock_poll_interval;
17137 let mut interval =
17138 tokio::time::interval_at(first_tick, self.config.flashblock_poll_interval);
17139 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
17140 let stream = stream::unfold(interval, |mut interval| async move {
17141 interval.tick().await;
17142 Some((
17143 SubscriberEvent::OpFlashblockTickTimed(
17144 FlashblockIngressTiming::new(Instant::now()),
17145 ),
17146 interval,
17147 ))
17148 });
17149 Ok(stream_with_termination(
17150 stream,
17151 SubscriberStreamSource::OpPendingFlashblocks,
17152 ))
17153 }
17154
17155 async fn connect_pubsub_pending_hash_stream(
17156 &mut self,
17157 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17158 #[cfg(feature = "reactive-ws")]
17159 {
17160 self.record_rpc(
17161 SubscriberRpcCause::StreamSubscription,
17162 SubscriberRpcMethod::EthSubscribe,
17163 );
17164 let stream = self
17165 .provider
17166 .subscribe_pending_transactions()
17167 .channel_size(self.config.max_batch_size.max(1))
17168 .await
17169 .map_err(provider_error)?
17170 .into_stream()
17171 .map(SubscriberEvent::PendingHash);
17172 Ok(stream_with_termination(
17173 stream,
17174 SubscriberStreamSource::PubSubPendingHashes,
17175 ))
17176 }
17177
17178 #[cfg(not(feature = "reactive-ws"))]
17179 {
17180 Err(SubscriberError::Unsupported(
17181 "AlloySubscriber pubsub mode requires the reactive-ws feature",
17182 ))
17183 }
17184 }
17185
17186 async fn connect_pubsub_block_header_stream(
17187 &mut self,
17188 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17189 #[cfg(feature = "reactive-ws")]
17190 {
17191 self.record_rpc(
17192 SubscriberRpcCause::StreamSubscription,
17193 SubscriberRpcMethod::EthSubscribe,
17194 );
17195 let subscription = self
17196 .provider
17197 .subscribe_blocks()
17198 .channel_size(self.config.max_batch_size.max(1))
17199 .await
17200 .map_err(provider_error)?;
17201 Ok(gap_observing_stream(
17202 subscription,
17203 SubscriberStreamSource::PubSubBlockHeaders,
17204 Arc::clone(&self.gap_counters),
17205 #[cfg(feature = "ingress-timing")]
17206 self.transport_receipts.clone(),
17207 |header, #[cfg(feature = "ingress-timing")] timing| SubscriberEvent::BlockHeader {
17208 header,
17209 #[cfg(feature = "ingress-timing")]
17210 timing,
17211 },
17212 ))
17213 }
17214
17215 #[cfg(not(feature = "reactive-ws"))]
17216 {
17217 Err(SubscriberError::Unsupported(
17218 "AlloySubscriber pubsub mode requires the reactive-ws feature",
17219 ))
17220 }
17221 }
17222
17223 async fn connect_polling_log_stream(
17224 &mut self,
17225 filter: Filter,
17226 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17227 #[cfg(feature = "reactive-polling")]
17228 {
17229 let source = SubscriberStreamSource::PollingLog {
17230 filter: filter.clone(),
17231 };
17232 self.record_rpc(
17233 SubscriberRpcCause::StreamSubscription,
17234 SubscriberRpcMethod::EthSubscribe,
17235 );
17236 let stream = self
17237 .provider
17238 .watch_logs(&filter)
17239 .await
17240 .map_err(provider_error)?
17241 .with_channel_size(self.config.max_batch_size.max(1))
17242 .into_stream()
17243 .map(SubscriberEvent::Logs);
17244 Ok(stream_with_termination(stream, source))
17245 }
17246
17247 #[cfg(not(feature = "reactive-polling"))]
17248 {
17249 let _ = filter;
17250 Err(SubscriberError::Unsupported(
17251 "AlloySubscriber polling mode requires the reactive-polling feature",
17252 ))
17253 }
17254 }
17255
17256 async fn connect_polling_pending_hash_stream(
17257 &mut self,
17258 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17259 #[cfg(feature = "reactive-polling")]
17260 {
17261 self.record_rpc(
17262 SubscriberRpcCause::StreamSubscription,
17263 SubscriberRpcMethod::EthSubscribe,
17264 );
17265 let stream = self
17266 .provider
17267 .watch_pending_transactions()
17268 .await
17269 .map_err(provider_error)?
17270 .with_channel_size(self.config.max_batch_size.max(1))
17271 .into_stream()
17272 .map(SubscriberEvent::PendingHashes);
17273 Ok(stream_with_termination(
17274 stream,
17275 SubscriberStreamSource::PollingPendingHashes,
17276 ))
17277 }
17278
17279 #[cfg(not(feature = "reactive-polling"))]
17280 {
17281 Err(SubscriberError::Unsupported(
17282 "AlloySubscriber polling mode requires the reactive-polling feature",
17283 ))
17284 }
17285 }
17286
17287 async fn next_event(&mut self) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17288 loop {
17289 let ready = match &mut self.state {
17290 AlloySubscriberState::Active(streams)
17291 if !self.pending_flashblock_reconnects.is_empty() =>
17292 {
17293 let stream_event = Box::pin(streams.next());
17294 let reconnect = Box::pin(self.pending_flashblock_reconnects.next());
17295 match select(reconnect, stream_event).await {
17296 Either::Left((reconnect, pending_event)) => {
17297 drop(pending_event);
17298 let Some((source, result)) = reconnect else {
17299 continue;
17300 };
17301 SubscriberReady::FlashblockReconnect(source, result)
17302 }
17303 Either::Right((event, pending_reconnect)) => {
17304 drop(pending_reconnect);
17305 SubscriberReady::Event(event)
17306 }
17307 }
17308 }
17309 AlloySubscriberState::Active(streams) => {
17310 SubscriberReady::Event(streams.next().await)
17311 }
17312 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty
17313 if !self.pending_flashblock_reconnects.is_empty() =>
17314 {
17315 let Some((source, result)) = self.pending_flashblock_reconnects.next().await
17316 else {
17317 continue;
17318 };
17319 SubscriberReady::FlashblockReconnect(source, result)
17320 }
17321 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
17322 return Ok(None);
17323 }
17324 };
17325
17326 let event = match ready {
17327 SubscriberReady::Event(event) => event,
17328 SubscriberReady::FlashblockReconnect(source, result) => {
17329 self.pending_flashblock_reconnect_sources
17330 .retain(|pending| !pending.same_key(&source));
17331 match result {
17332 Ok(stream) => {
17333 self.install_source_stream(source, stream);
17334 }
17335 Err(error)
17336 if self.config.preconfirmations == PreconfirmationMode::Preferred =>
17337 {
17338 tracing::warn!(
17339 stream = source.label(),
17340 error = %error,
17341 "Flashblocks reconnect window exhausted; canonical delivery remains active"
17342 );
17343 self.reschedule_preferred_flashblock(source);
17344 }
17345 Err(error) => return Err(error),
17346 }
17347 continue;
17348 }
17349 };
17350
17351 let Some(event) = event else {
17352 return Err(SubscriberError::Provider(
17353 "Alloy subscriber streams terminated before the subscriber was stopped"
17354 .to_owned(),
17355 ));
17356 };
17357
17358 match event {
17359 SubscriberEvent::StreamTerminated(source) => {
17360 if source.is_external_flashblocks() {
17361 #[cfg(feature = "raw-flashblocks-json")]
17362 {
17363 self.external_flashblock_update_channel_opened = false;
17364 }
17365 if let AlloySubscriberState::Active(streams) = &mut self.state {
17366 streams
17367 .entries
17368 .retain(|entry| !entry.source.is_external_flashblocks());
17369 streams.normalize_next_index();
17370 }
17371 self.invalidate_preconfirmation_snapshot();
17372 if self.config.preconfirmations == PreconfirmationMode::Required {
17373 return Err(SubscriberError::Provider(
17374 "required external Flashblock update channel closed".into(),
17375 ));
17376 }
17377 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
17378 }
17379 if source.is_flashblocks() {
17383 self.invalidate_flashblock_generation();
17384 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
17385 }
17386 self.sources_dirty = true;
17387 self.bump_stream_revision();
17388 if let Some(backfill_event) = self.reconnect_source_stream(source).await? {
17389 self.sources_dirty = false;
17390 if let Some(backfill_event) =
17391 self.normalize_flashblock_event(backfill_event).await?
17392 {
17393 self.verify_event_log_blocks(&backfill_event).await?;
17394 return Ok(Some(backfill_event));
17395 }
17396 }
17397 self.sources_dirty = false;
17398 }
17399 SubscriberEvent::StreamGap { source, gap } => {
17400 if let Some(backfill_event) = self.recover_stream_gap(&source, gap).await? {
17401 self.verify_event_log_blocks(&backfill_event).await?;
17402 return Ok(Some(backfill_event));
17403 }
17404 }
17405 event => {
17406 let Some(event) = self.normalize_flashblock_event(event).await? else {
17407 continue;
17408 };
17409 self.verify_event_log_blocks(&event).await?;
17410 return Ok(Some(event));
17411 }
17412 }
17413 }
17414 }
17415
17416 fn invalidate_flashblock_generation(&mut self) {
17417 self.pending_records
17418 .retain(|record| record.scope != SubscriberInputScope::Preconfirmed);
17419 self.pending_preconfirmation_invalidation = true;
17420 self.reset_flashblock_tracking();
17421 if let Some(provider) = self.provider_ref.as_mut() {
17422 provider.generation = provider.generation.saturating_add(1);
17423 }
17424 if let AlloySubscriberState::Active(streams) = &mut self.state {
17425 streams
17426 .entries
17427 .retain(|entry| !entry.source.is_flashblocks());
17428 streams.normalize_next_index();
17429 }
17430 let reconnect_sources = self
17431 .stream_sources()
17432 .unwrap_or_default()
17433 .into_iter()
17434 .filter(SubscriberStreamSource::is_flashblocks)
17435 .collect::<Vec<_>>();
17436 self.pending_flashblock_reconnects.clear();
17437 self.pending_flashblock_reconnect_sources.clear();
17438 if self.config.preconfirmations == PreconfirmationMode::Required
17439 || self.config.reconnect.enabled
17440 {
17441 for source in reconnect_sources {
17442 self.schedule_flashblock_reconnect(source, self.config.reconnect.initial_delay);
17443 }
17444 }
17445 self.sources_dirty = false;
17446 self.bump_stream_revision();
17447 }
17448
17449 async fn normalize_flashblock_event(
17450 &mut self,
17451 event: SubscriberEvent<N>,
17452 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17453 let event = match event {
17454 SubscriberEvent::BasePendingLog { source_id, log } => {
17455 SubscriberEvent::BasePendingLogTimed {
17456 source_id,
17457 log,
17458 timing: FlashblockIngressTiming::new(Instant::now()),
17459 }
17460 }
17461 SubscriberEvent::BaseFlashblock(payload) => SubscriberEvent::BaseFlashblockTimed {
17462 payload,
17463 timing: FlashblockIngressTiming::new(Instant::now()),
17464 },
17465 SubscriberEvent::OpFlashblockTick => {
17466 SubscriberEvent::OpFlashblockTickTimed(FlashblockIngressTiming::new(Instant::now()))
17467 }
17468 event => event,
17469 };
17470 match event {
17471 #[cfg(feature = "raw-flashblocks-json")]
17472 SubscriberEvent::ExternalFlashblockUpdate(queued) => {
17473 let provider = queued.update.provider().clone();
17474 match self.ingest_flashblock_update_with_ingress(queued.update, queued.timing) {
17475 Ok(()) => {
17476 let _ = queued.acknowledgement.send(Ok(()));
17477 Ok(Some(SubscriberEvent::FlashblockObserved))
17478 }
17479 Err(error)
17480 if self.config.preconfirmations == PreconfirmationMode::Preferred =>
17481 {
17482 let recoverable_capacity =
17483 matches!(error, SubscriberError::ResourceExhausted(_));
17484 if !recoverable_capacity
17485 && let Some(configured) = self.external_flashblocks_provider.as_mut()
17486 && configured.endpoint == provider.endpoint
17487 {
17488 self.rejected_external_flashblock_generation = Some(
17489 self.rejected_external_flashblock_generation
17490 .map_or(provider.generation, |rejected| {
17491 rejected.max(provider.generation)
17492 }),
17493 );
17494 configured.generation = configured
17495 .generation
17496 .max(provider.generation.saturating_add(1));
17497 }
17498 self.invalidate_preconfirmation_snapshot();
17499 self.last_external_flashblock_snapshot = None;
17500 let _ = queued
17501 .acknowledgement
17502 .send(Err(FlashblockUpdateChannelError::Rejected));
17503 tracing::warn!(
17504 provider = %provider.endpoint,
17505 generation = provider.generation,
17506 error = %error,
17507 "external Flashblock update rejected; canonical delivery remains active"
17508 );
17509 Ok(Some(SubscriberEvent::FlashblockInvalidated))
17510 }
17511 Err(error) => {
17512 let _ = queued
17513 .acknowledgement
17514 .send(Err(FlashblockUpdateChannelError::Rejected));
17515 Err(error)
17516 }
17517 }
17518 }
17519 SubscriberEvent::BasePendingLogTimed {
17520 source_id,
17521 log,
17522 timing,
17523 } => {
17524 let block_number = log.block_number.ok_or_else(|| {
17525 SubscriberError::Provider(
17526 "pendingLogs item is missing its pending block number".into(),
17527 )
17528 })?;
17529 let transaction_hash = log.transaction_hash.ok_or_else(|| {
17530 SubscriberError::Provider(
17531 "pendingLogs item is missing its transaction hash".into(),
17532 )
17533 })?;
17534 let matching = self.latest_preconfirmation.as_ref().filter(|flashblock| {
17535 flashblock.block_number == block_number
17536 && flashblock.contains_transaction(&transaction_hash)
17537 });
17538 let Some(flashblock) = matching.cloned() else {
17539 if self
17540 .latest_preconfirmation
17541 .as_ref()
17542 .is_some_and(|latest| block_number < latest.block_number)
17543 {
17544 return Ok(None);
17545 }
17546 if self.unmatched_pending_logs.len() >= self.config.max_pending_records {
17547 return Err(SubscriberError::ResourceExhausted(
17548 "unmatched pendingLogs exceeded max_pending_records".into(),
17549 ));
17550 }
17551 self.unmatched_pending_logs
17552 .push_back((source_id, log, timing));
17553 return Ok(None);
17554 };
17555 let logs = self.filter_preconfirmed_logs(&flashblock, vec![log])?;
17556 Ok(Some(if logs.is_empty() {
17557 SubscriberEvent::FlashblockObserved
17558 } else {
17559 SubscriberEvent::PreconfirmedLogs {
17560 flashblock,
17561 logs,
17562 timing,
17563 }
17564 }))
17565 }
17566 SubscriberEvent::BaseFlashblockTimed {
17567 payload,
17568 timing: source_timing,
17569 } => {
17570 let (flashblock, recover_pending_snapshot) =
17571 self.accept_base_flashblock(payload)?;
17572 if std::mem::take(&mut self.sealed_block_pending_certification)
17573 && let Some(header_event) =
17574 self.certify_canonical_head_on_sealed_block().await?
17575 {
17576 self.enqueue_event(header_event);
17579 }
17580 let mut logs = Vec::new();
17581 let mut retained = VecDeque::new();
17582 let mut timing = source_timing;
17583 while let Some((source_id, log, log_timing)) =
17584 self.unmatched_pending_logs.pop_front()
17585 {
17586 let transaction_hash = log.transaction_hash;
17587 if log.block_number == Some(flashblock.block_number)
17588 && transaction_hash
17589 .as_ref()
17590 .is_some_and(|hash| flashblock.contains_transaction(hash))
17591 {
17592 let _ = source_id;
17593 timing = timing.earliest(log_timing);
17594 logs.push(log);
17595 } else if log
17596 .block_number
17597 .is_some_and(|number| number >= flashblock.block_number)
17598 {
17599 retained.push_back((source_id, log, log_timing));
17600 } else {
17601 }
17604 }
17605 self.unmatched_pending_logs = retained;
17606
17607 let indexed_recovery = recover_pending_snapshot.then(|| {
17608 let payload_id = flashblock
17609 .payload_id
17610 .expect("indexed recovery carries a payload id");
17611 let index = flashblock.index.expect("indexed recovery carries an index");
17612 let last_diff = self
17613 .base_flashblock_transactions
17614 .as_ref()
17615 .filter(|(known_payload, known_index, _, _)| {
17616 *known_payload == payload_id && *known_index == index
17617 })
17618 .map(|(_, _, _, last_diff)| last_diff.clone())
17619 .unwrap_or_default();
17620 (payload_id, index, last_diff)
17621 });
17622 if recover_pending_snapshot {
17623 if let Some(event) = self
17624 .fetch_pending_flashblock_with_timing(indexed_recovery, timing)
17625 .await
17626 .map_err(PendingFlashblockPollError::into_subscriber)?
17627 {
17628 return Ok(Some(event));
17629 }
17630 self.invalidate_flashblock_generation();
17631 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
17632 }
17633 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
17634 Ok(Some(if logs.is_empty() {
17635 SubscriberEvent::FlashblockObserved
17636 } else {
17637 SubscriberEvent::PreconfirmedLogs {
17638 flashblock,
17639 logs,
17640 timing,
17641 }
17642 }))
17643 }
17644 SubscriberEvent::OpFlashblockTickTimed(timing) => {
17645 self.poll_op_pending_flashblock(timing).await
17646 }
17647 SubscriberEvent::CanonicalHeadTick => {
17648 if self.canonical_head_certification_is_current() {
17649 self.flashblocks_rpc_metrics.suppressed_canonical_head_polls = self
17652 .flashblocks_rpc_metrics
17653 .suppressed_canonical_head_polls
17654 .saturating_add(1);
17655 Ok(None)
17656 } else {
17657 self.fetch_certified_canonical_head().await
17658 }
17659 }
17660 SubscriberEvent::PreconfirmedLogs {
17661 flashblock,
17662 logs,
17663 timing,
17664 } => {
17665 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
17666 Ok(Some(if logs.is_empty() {
17667 SubscriberEvent::FlashblockObserved
17668 } else {
17669 SubscriberEvent::PreconfirmedLogs {
17670 flashblock,
17671 logs,
17672 timing,
17673 }
17674 }))
17675 }
17676 SubscriberEvent::FlashblockObserved => Ok(None),
17677 SubscriberEvent::BasePendingLog { .. }
17678 | SubscriberEvent::BaseFlashblock(_)
17679 | SubscriberEvent::OpFlashblockTick => unreachable!("normalized above"),
17680 event => Ok(Some(event)),
17681 }
17682 }
17683
17684 fn canonical_head_certification_is_current(&self) -> bool {
17686 self.last_canonical_head_certification
17687 .is_some_and(|at| at.elapsed() < self.config.canonical_head_poll_interval)
17688 }
17689
17690 async fn certify_canonical_head_on_sealed_block(
17698 &mut self,
17699 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17700 if !needs_header_block_stream(&self.interests) {
17701 return Ok(None);
17702 }
17703 if self.canonical_head_certification_is_current() {
17704 return Ok(None);
17705 }
17706 self.fetch_certified_canonical_head().await
17707 }
17708
17709 async fn fetch_certified_canonical_head(
17710 &mut self,
17711 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17712 self.last_canonical_head_certification = Some(Instant::now());
17713 tokio::time::timeout(
17714 self.config.canonical_head_request_timeout,
17715 self.fetch_certified_canonical_head_inner(),
17716 )
17717 .await
17718 .map_err(|_| {
17719 SubscriberError::Provider(format!(
17720 "canonical head certification timed out after {:?}",
17721 self.config.canonical_head_request_timeout
17722 ))
17723 })?
17724 }
17725
17726 async fn fetch_certified_canonical_head_inner(
17727 &mut self,
17728 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17729 if self.chain_id.and_then(flashblocks_adapter)
17730 == Some(FlashblocksAdapter::PendingStatePolling)
17731 {
17732 if !self.reserve_flashblock_rpc_methods(2) {
17733 return Ok(None);
17734 }
17735 self.flashblocks_rpc_metrics.pending_block_requests = self
17736 .flashblocks_rpc_metrics
17737 .pending_block_requests
17738 .saturating_add(1);
17739 let pending = self
17740 .fetch_op_pending_block()
17741 .await
17742 .map_err(PendingFlashblockPollError::into_subscriber)?
17743 .ok_or_else(|| {
17744 SubscriberError::Provider(
17745 "provider returned no OP pending block while certifying its parent".into(),
17746 )
17747 })?;
17748 let header = self
17749 .certify_op_pending_parent(&pending)
17750 .await
17751 .map_err(PendingFlashblockPollError::into_subscriber)?;
17752 let certified = BlockRef {
17753 number: header.number(),
17754 hash: header.hash(),
17755 parent_hash: Some(header.parent_hash()),
17756 timestamp: Some(header.timestamp()),
17757 };
17758 if self.last_certified_canonical_head.as_ref() == Some(&certified) {
17759 return Ok(None);
17760 }
17761 self.last_certified_canonical_head = Some(certified);
17762 return Ok(Some(SubscriberEvent::BlockHeader {
17763 header,
17764 #[cfg(feature = "ingress-timing")]
17765 timing: None,
17766 }));
17767 }
17768 self.record_rpc(
17769 SubscriberRpcCause::CanonicalHeadCertification,
17770 SubscriberRpcMethod::EthGetBlockByNumber,
17771 );
17772 self.flashblocks_rpc_metrics.canonical_head_requests = self
17773 .flashblocks_rpc_metrics
17774 .canonical_head_requests
17775 .saturating_add(1);
17776 let block = self
17777 .provider
17778 .get_block_by_number(BlockNumberOrTag::Latest)
17779 .await
17780 .map_err(provider_error)?
17781 .ok_or_else(|| {
17782 SubscriberError::Provider(
17783 "provider returned no latest block while certifying canonical head".into(),
17784 )
17785 })?;
17786 let header = block.header();
17787 if header.hash().is_zero() {
17788 return Err(SubscriberError::Provider(
17789 "provider returned a placeholder hash for the latest canonical head".into(),
17790 ));
17791 }
17792 let certified = BlockRef {
17793 number: header.number(),
17794 hash: header.hash(),
17795 parent_hash: Some(header.parent_hash()),
17796 timestamp: Some(header.timestamp()),
17797 };
17798 if self.last_certified_canonical_head.as_ref() == Some(&certified) {
17799 return Ok(None);
17800 }
17801 self.last_certified_canonical_head = Some(certified);
17802 Ok(Some(SubscriberEvent::BlockHeader {
17803 header: header.clone(),
17804 #[cfg(feature = "ingress-timing")]
17805 timing: None,
17806 }))
17807 }
17808
17809 fn accept_base_flashblock(
17810 &mut self,
17811 payload: BaseFlashblockWirePayload,
17812 ) -> Result<(FlashblockRef, bool), SubscriberError> {
17813 let provider = self.provider_ref.clone().ok_or({
17814 SubscriberError::InvalidConfig(
17815 "Flashblocks require a stable provider ref from a pinned provider lease",
17816 )
17817 })?;
17818
17819 let (flashblock, recover_pending_snapshot) = match payload {
17820 BaseFlashblockWirePayload::Indexed(payload) => {
17821 if payload.index == 0 {
17822 let base = payload.base.clone().ok_or_else(|| {
17823 SubscriberError::Provider(
17824 "indexed newFlashblocks item zero omitted its base header".into(),
17825 )
17826 })?;
17827 if self
17831 .base_flashblock_header
17832 .as_ref()
17833 .is_none_or(|(known, _)| *known != payload.payload_id)
17834 {
17835 self.sealed_block_pending_certification = true;
17836 }
17837 self.base_flashblock_header = Some((payload.payload_id, base));
17838 }
17839
17840 let base = self
17841 .base_flashblock_header
17842 .as_ref()
17843 .filter(|(payload_id, _)| *payload_id == payload.payload_id)
17844 .map(|(_, base)| base);
17845 let block_number = base.map(|base| base.block_number).or_else(|| {
17846 payload
17847 .metadata
17848 .as_ref()
17849 .map(|metadata| metadata.block_number)
17850 });
17851 let block_number = block_number.ok_or_else(|| {
17852 SubscriberError::Provider(
17853 "indexed newFlashblocks payload omitted both base and metadata block number"
17854 .into(),
17855 )
17856 })?;
17857 let diff_transactions = flashblock_transaction_hashes(&payload.diff.transactions)?;
17858 let transaction_hashes = match self.base_flashblock_transactions.as_mut() {
17859 Some((known_payload, known_index, transactions, last_diff))
17860 if *known_payload == payload.payload_id =>
17861 {
17862 if payload.index < *known_index {
17863 return self
17864 .latest_preconfirmation
17865 .clone()
17866 .map(|flashblock| (flashblock, false))
17867 .ok_or_else(|| {
17868 SubscriberError::Provider(
17869 "regressive indexed Flashblock arrived without an active snapshot"
17870 .into(),
17871 )
17872 });
17873 }
17874 if payload.index == *known_index {
17875 if *last_diff != diff_transactions {
17876 return Err(SubscriberError::Provider(
17877 "conflicting duplicate indexed Flashblock payload".into(),
17878 ));
17879 }
17880 } else {
17881 if diff_transactions
17882 .iter()
17883 .any(|hash| transactions.contains(hash))
17884 {
17885 return Err(SubscriberError::Provider(
17886 "indexed Flashblock repeated a transaction from an earlier diff"
17887 .into(),
17888 ));
17889 }
17890 transactions.extend(diff_transactions.iter().copied());
17891 *known_index = payload.index;
17892 *last_diff = diff_transactions;
17893 }
17894 transactions.clone()
17895 }
17896 _ => {
17897 self.base_flashblock_transactions = Some((
17898 payload.payload_id,
17899 payload.index,
17900 diff_transactions.clone(),
17901 diff_transactions.clone(),
17902 ));
17903 diff_transactions
17904 }
17905 };
17906 let partial_block_hash = non_placeholder_hash(payload.diff.block_hash);
17907 let transactions_root = payload
17908 .diff
17909 .transactions_root
17910 .and_then(non_placeholder_hash);
17911 let parent_hash = base.and_then(|base| non_placeholder_hash(base.parent_hash));
17912 let state_root = non_placeholder_hash(payload.diff.state_root);
17913 let timestamp = base.map(|base| base.timestamp);
17914 let base_fee_per_gas = base.and_then(|base| base.base_fee_per_gas);
17915 let beneficiary = base.and_then(|base| base.beneficiary);
17916 let prevrandao = base
17917 .and_then(|base| base.prevrandao)
17918 .and_then(non_placeholder_hash);
17919 let gas_limit = base.and_then(|base| base.gas_limit);
17920 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
17921 provider: &provider,
17922 payload_id: Some(payload.payload_id),
17923 index: Some(payload.index),
17924 block_number,
17925 partial_block_hash,
17926 parent_hash,
17927 state_root,
17928 transactions_root,
17929 transaction_hashes: &transaction_hashes,
17930 timestamp,
17931 base_fee_per_gas,
17932 beneficiary,
17933 prevrandao,
17934 gas_limit,
17935 });
17936 let flashblock = FlashblockRef {
17937 provider,
17938 payload_id: Some(payload.payload_id),
17939 index: Some(payload.index),
17940 block_number,
17941 content_hash,
17942 partial_block_hash,
17943 parent_hash,
17944 state_root,
17945 transactions_root,
17946 transaction_hashes,
17947 timestamp,
17948 base_fee_per_gas,
17949 beneficiary,
17950 prevrandao,
17951 gas_limit,
17952 };
17953 if let Some(previous) = self.latest_preconfirmation.as_ref()
17954 && previous.same_payload(&flashblock)
17955 && previous.index == flashblock.index
17956 && previous.content_hash != flashblock.content_hash
17957 {
17958 return Err(SubscriberError::Provider(
17959 "conflicting duplicate indexed Flashblock content".into(),
17960 ));
17961 }
17962 let recover = match self.latest_preconfirmation.as_ref() {
17963 Some(previous) if previous.same_payload(&flashblock) => {
17964 if let (Some(previous), Some(current)) = (previous.index, flashblock.index)
17965 {
17966 if current < previous {
17967 return Ok((flashblock, false));
17968 }
17969 current > previous.saturating_add(1)
17970 } else {
17971 false
17972 }
17973 }
17974 Some(_) => payload.index != 0,
17975 None => payload.index != 0,
17976 };
17977 (flashblock, recover)
17978 }
17979 BaseFlashblockWirePayload::Block(payload) => {
17980 let transaction_hashes = flashblock_transaction_hashes(&payload.transactions)?;
17981 let parent_hash = non_placeholder_hash(payload.parent_hash);
17982 let state_root = non_placeholder_hash(payload.state_root);
17983 let transactions_root = payload.transactions_root.and_then(non_placeholder_hash);
17984 let partial_block_hash = non_placeholder_hash(payload.hash);
17985 let prevrandao = payload.mix_hash.and_then(non_placeholder_hash);
17986 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
17987 provider: &provider,
17988 payload_id: None,
17989 index: None,
17990 block_number: payload.number,
17991 partial_block_hash,
17992 parent_hash,
17993 state_root,
17994 transactions_root,
17995 transaction_hashes: &transaction_hashes,
17996 timestamp: Some(payload.timestamp),
17997 base_fee_per_gas: payload.base_fee_per_gas,
17998 beneficiary: payload.miner,
17999 prevrandao,
18000 gas_limit: payload.gas_limit,
18001 });
18002 let flashblock = FlashblockRef {
18003 provider,
18004 payload_id: None,
18005 index: None,
18006 block_number: payload.number,
18007 content_hash,
18008 partial_block_hash,
18009 parent_hash,
18010 state_root,
18011 transactions_root,
18012 transaction_hashes,
18013 timestamp: Some(payload.timestamp),
18014 base_fee_per_gas: payload.base_fee_per_gas,
18015 beneficiary: payload.miner,
18016 prevrandao,
18017 gas_limit: payload.gas_limit,
18018 };
18019 if let Some(previous) = self.latest_preconfirmation.as_ref()
18020 && flashblock.same_payload(previous)
18021 && flashblock.content_hash != previous.content_hash
18022 && !flashblock.is_cumulative_successor_of(previous)
18023 {
18024 return Err(SubscriberError::Provider(
18025 "cumulative Flashblock transaction membership is non-monotonic".into(),
18026 ));
18027 }
18028 (flashblock, false)
18029 }
18030 };
18031 Ok((flashblock, recover_pending_snapshot))
18032 }
18033
18034 async fn poll_op_pending_flashblock(
18035 &mut self,
18036 timing: FlashblockIngressTiming,
18037 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
18038 match self
18039 .fetch_pending_flashblock_with_timing(None, timing)
18040 .await
18041 {
18042 Ok(event) => {
18043 self.consecutive_flashblock_poll_failures = 0;
18044 Ok(event)
18045 }
18046 Err(PendingFlashblockPollError::Request(error)) => {
18047 self.flashblocks_rpc_metrics.failed_requests = self
18048 .flashblocks_rpc_metrics
18049 .failed_requests
18050 .saturating_add(1);
18051 self.consecutive_flashblock_poll_failures =
18052 self.consecutive_flashblock_poll_failures.saturating_add(1);
18053 if self.consecutive_flashblock_poll_failures
18054 >= self.config.max_consecutive_flashblock_poll_failures
18055 {
18056 return Err(error);
18057 }
18058 tracing::warn!(
18059 consecutive_failures = self.consecutive_flashblock_poll_failures,
18060 failure_limit = self.config.max_consecutive_flashblock_poll_failures,
18061 error = %error,
18062 "Optimism pending-state Flashblocks request failed; retrying on the next tick"
18063 );
18064 Ok(None)
18065 }
18066 Err(PendingFlashblockPollError::Integrity(error)) => Err(error),
18067 }
18068 }
18069
18070 #[cfg(test)]
18071 async fn fetch_pending_flashblock(
18072 &mut self,
18073 indexed_recovery: Option<(FixedBytes<8>, u64, Vec<B256>)>,
18074 ) -> Result<Option<SubscriberEvent<N>>, PendingFlashblockPollError> {
18075 self.fetch_pending_flashblock_with_timing(
18076 indexed_recovery,
18077 FlashblockIngressTiming::new(Instant::now()),
18078 )
18079 .await
18080 }
18081
18082 async fn fetch_pending_flashblock_with_timing(
18083 &mut self,
18084 indexed_recovery: Option<(FixedBytes<8>, u64, Vec<B256>)>,
18085 timing: FlashblockIngressTiming,
18086 ) -> Result<Option<SubscriberEvent<N>>, PendingFlashblockPollError> {
18087 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
18088 == Some(FlashblocksAdapter::PendingStatePolling);
18089 if samples_pending_range {
18090 let fixed_methods = 2_usize.saturating_add(self.log_stream_filters().len());
18091 if !self.reserve_flashblock_rpc_methods(fixed_methods) {
18092 return Ok(None);
18093 }
18094 }
18095 let state_provider = if samples_pending_range {
18096 self.flashblocks_state_provider
18097 .as_ref()
18098 .unwrap_or(&self.provider)
18099 } else {
18100 &self.provider
18101 };
18102 let latest = if samples_pending_range {
18103 None
18104 } else {
18105 self.record_rpc(
18106 SubscriberRpcCause::CanonicalHeadCertification,
18107 SubscriberRpcMethod::EthBlockNumber,
18108 );
18109 self.flashblocks_rpc_metrics.canonical_head_requests = self
18110 .flashblocks_rpc_metrics
18111 .canonical_head_requests
18112 .saturating_add(1);
18113 Some(
18114 state_provider
18115 .get_block_number()
18116 .await
18117 .map_err(pending_flashblock_request_error)?,
18118 )
18119 };
18120 self.flashblocks_rpc_metrics.pending_block_requests = self
18121 .flashblocks_rpc_metrics
18122 .pending_block_requests
18123 .saturating_add(1);
18124 let pending_block = if samples_pending_range {
18125 self.fetch_op_pending_block().await?
18126 } else {
18127 self.record_rpc(
18128 SubscriberRpcCause::PendingStateSample,
18129 SubscriberRpcMethod::EthGetBlockByNumber,
18130 );
18131 self.provider
18132 .get_block_by_number(BlockNumberOrTag::Pending)
18133 .await
18134 .map_err(pending_flashblock_request_error)?
18135 };
18136 let Some(block) = pending_block else {
18137 if self.config.preconfirmations == PreconfirmationMode::Required {
18138 return Err(PendingFlashblockPollError::Request(
18139 SubscriberError::Provider(
18140 "Flashblocks provider returned no pending block".into(),
18141 ),
18142 ));
18143 }
18144 return Ok(None);
18145 };
18146 let latest = if samples_pending_range {
18147 self.certify_op_pending_parent(&block).await?.number()
18148 } else {
18149 latest.expect("non-OP pending recovery fetched a canonical height")
18150 };
18151 let header = block.header();
18152 if header.number() <= latest {
18153 return Ok(None);
18154 }
18155
18156 let provider = self.provider_ref.clone().ok_or({
18157 PendingFlashblockPollError::Integrity(SubscriberError::InvalidConfig(
18158 "Flashblocks require a stable provider ref from a pinned provider lease",
18159 ))
18160 })?;
18161 let parent_hash = Some(header.parent_hash());
18162 let transaction_hashes = if let Some(hashes) = block.transactions().as_hashes() {
18163 hashes.to_vec()
18164 } else if let Some(transactions) = block.transactions().as_transactions() {
18165 transactions
18166 .iter()
18167 .map(|transaction| transaction.tx_hash())
18168 .collect()
18169 } else {
18170 Vec::new()
18171 };
18172 let state_root = non_placeholder_hash(header.state_root());
18173 let transactions_root = non_placeholder_hash(header.transactions_root());
18174 let partial_block_hash = non_placeholder_hash(header.hash());
18175 let prevrandao = header.mix_hash().and_then(non_placeholder_hash);
18176 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
18177 provider: &provider,
18178 payload_id: None,
18179 index: None,
18180 block_number: header.number(),
18181 partial_block_hash,
18182 parent_hash,
18183 state_root,
18184 transactions_root,
18185 transaction_hashes: &transaction_hashes,
18186 timestamp: Some(header.timestamp()),
18187 base_fee_per_gas: header.base_fee_per_gas(),
18188 beneficiary: Some(header.beneficiary()),
18189 prevrandao,
18190 gas_limit: Some(header.gas_limit()),
18191 });
18192 let flashblock = FlashblockRef {
18193 provider,
18194 payload_id: None,
18195 index: None,
18196 block_number: header.number(),
18197 content_hash,
18198 partial_block_hash,
18199 parent_hash,
18200 state_root,
18201 transactions_root,
18202 transaction_hashes,
18203 timestamp: Some(header.timestamp()),
18204 base_fee_per_gas: header.base_fee_per_gas(),
18205 beneficiary: Some(header.beneficiary()),
18206 prevrandao,
18207 gas_limit: Some(header.gas_limit()),
18208 };
18209 if samples_pending_range
18210 && self
18211 .latest_preconfirmation
18212 .as_ref()
18213 .is_some_and(|previous| !previous.same_payload(&flashblock))
18214 {
18215 self.invalidate_preconfirmation_snapshot();
18218 }
18219 if let Some((payload_id, index, last_diff)) = indexed_recovery {
18220 self.base_flashblock_transactions = Some((
18221 payload_id,
18222 index,
18223 flashblock.transaction_hashes.clone(),
18224 last_diff,
18225 ));
18226 }
18227 let repeats_pending_snapshot = self
18228 .latest_preconfirmation
18229 .as_ref()
18230 .is_some_and(|previous| previous == &flashblock);
18231 if repeats_pending_snapshot && !samples_pending_range {
18232 return Ok(None);
18233 }
18234
18235 if let Some(previous) = self.latest_preconfirmation.as_ref()
18236 && flashblock.same_payload(previous)
18237 && !flashblock.is_cumulative_successor_of(previous)
18238 {
18239 if samples_pending_range {
18240 self.invalidate_preconfirmation_snapshot();
18245 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
18246 }
18247 return Err(PendingFlashblockPollError::Integrity(
18248 SubscriberError::Provider(
18249 "sampled cumulative Flashblock transaction membership is non-monotonic".into(),
18250 ),
18251 ));
18252 }
18253
18254 let mut logs = self.fetch_pending_logs(flashblock.block_number).await?;
18255 if samples_pending_range {
18256 let (mut receipt_logs, completed_receipts, unavailable_receipts) =
18257 self.fetch_pending_transaction_receipts(&flashblock).await?;
18258 logs.append(&mut receipt_logs);
18259 logs.retain(|log| log.block_number == Some(flashblock.block_number));
18260 for log in &logs {
18261 let transaction_hash = log.transaction_hash.ok_or_else(|| {
18262 PendingFlashblockPollError::Integrity(SubscriberError::Provider(
18263 "pre-confirmed log is missing its transaction hash".into(),
18264 ))
18265 })?;
18266 if !flashblock.contains_transaction(&transaction_hash) {
18267 self.flashblocks_rpc_metrics.raced_samples =
18268 self.flashblocks_rpc_metrics.raced_samples.saturating_add(1);
18269 return Ok(None);
18270 }
18271 }
18272 let logs = self
18273 .filter_preconfirmed_logs(&flashblock, logs)
18274 .map_err(PendingFlashblockPollError::Integrity)?;
18275 self.preconfirmed_unavailable_receipts
18276 .extend(unavailable_receipts);
18277 for transaction_hash in &completed_receipts {
18278 self.preconfirmed_unavailable_receipts
18279 .remove(transaction_hash);
18280 }
18281 self.preconfirmed_receipted_transactions
18282 .extend(completed_receipts);
18283 if repeats_pending_snapshot && logs.is_empty() {
18284 return Ok(None);
18285 }
18286 return Ok(Some(if logs.is_empty() {
18287 SubscriberEvent::FlashblockObserved
18288 } else {
18289 SubscriberEvent::PreconfirmedLogs {
18290 flashblock,
18291 logs,
18292 timing,
18293 }
18294 }));
18295 }
18296 let logs = self
18297 .filter_preconfirmed_logs(&flashblock, logs)
18298 .map_err(PendingFlashblockPollError::Integrity)?;
18299 Ok(Some(if logs.is_empty() {
18300 SubscriberEvent::FlashblockObserved
18301 } else {
18302 SubscriberEvent::PreconfirmedLogs {
18303 flashblock,
18304 logs,
18305 timing,
18306 }
18307 }))
18308 }
18309
18310 async fn fetch_pending_logs(
18311 &mut self,
18312 pending_block_number: u64,
18313 ) -> Result<Vec<Log>, PendingFlashblockPollError> {
18314 let mut logs = Vec::new();
18315 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
18316 == Some(FlashblocksAdapter::PendingStatePolling);
18317 let state_provider = if samples_pending_range {
18318 self.flashblocks_state_provider
18319 .as_ref()
18320 .unwrap_or(&self.provider)
18321 } else {
18322 &self.provider
18323 };
18324 for filter in self.log_stream_filters() {
18325 self.record_rpc(
18326 SubscriberRpcCause::PendingStateSample,
18327 SubscriberRpcMethod::EthGetLogs,
18328 );
18329 self.flashblocks_rpc_metrics.pending_log_requests = self
18330 .flashblocks_rpc_metrics
18331 .pending_log_requests
18332 .saturating_add(1);
18333 let filter = if samples_pending_range {
18334 filter
18335 .from_block(pending_block_number)
18336 .to_block(BlockNumberOrTag::Pending)
18337 } else {
18338 filter
18339 .from_block(BlockNumberOrTag::Pending)
18340 .to_block(BlockNumberOrTag::Pending)
18341 };
18342 logs.extend(
18343 state_provider
18344 .get_logs(&filter)
18345 .await
18346 .map_err(pending_flashblock_request_error)?,
18347 );
18348 }
18349 if samples_pending_range {
18350 logs.retain(|log| log.block_number == Some(pending_block_number));
18351 }
18352 Ok(logs)
18353 }
18354
18355 async fn fetch_pending_transaction_receipts(
18356 &mut self,
18357 flashblock: &FlashblockRef,
18358 ) -> Result<(Vec<Log>, Vec<B256>, Vec<B256>), PendingFlashblockPollError> {
18359 let receipt_allowance = self.pending_receipt_request_allowance();
18360 let receipt_limit = self
18361 .config
18362 .max_pending_transaction_receipts_per_tick
18363 .min(receipt_allowance);
18364 if receipt_limit == 0 {
18365 return Ok((Vec::new(), Vec::new(), Vec::new()));
18366 }
18367 let mut transaction_hashes = Vec::with_capacity(receipt_limit);
18368 for transaction_hash in &flashblock.transaction_hashes {
18369 if !self
18370 .preconfirmed_receipted_transactions
18371 .contains(transaction_hash)
18372 && !self
18373 .preconfirmed_unavailable_receipts
18374 .contains(transaction_hash)
18375 {
18376 transaction_hashes.push(*transaction_hash);
18377 if transaction_hashes.len() == receipt_limit {
18378 break;
18379 }
18380 }
18381 }
18382 if transaction_hashes.len() < receipt_limit {
18383 for transaction_hash in &flashblock.transaction_hashes {
18384 if self
18385 .preconfirmed_unavailable_receipts
18386 .contains(transaction_hash)
18387 {
18388 transaction_hashes.push(*transaction_hash);
18389 if transaction_hashes.len() == receipt_limit {
18390 break;
18391 }
18392 }
18393 }
18394 }
18395 if transaction_hashes.is_empty() {
18396 return Ok((Vec::new(), Vec::new(), Vec::new()));
18397 }
18398 let reserved = self.reserve_flashblock_rpc_methods(transaction_hashes.len());
18399 debug_assert!(reserved, "receipt allowance must remain reserved until use");
18400 if !reserved {
18401 return Ok((Vec::new(), Vec::new(), Vec::new()));
18402 }
18403 self.rpc_counters.record_many(
18404 SubscriberRpcCause::PendingStateSample,
18405 SubscriberRpcMethod::EthGetTransactionReceipt,
18406 transaction_hashes.len() as u64,
18407 );
18408 self.flashblocks_rpc_metrics.pending_receipt_requests = self
18409 .flashblocks_rpc_metrics
18410 .pending_receipt_requests
18411 .saturating_add(transaction_hashes.len() as u64);
18412 let state_provider = self
18413 .flashblocks_state_provider
18414 .as_ref()
18415 .unwrap_or(&self.provider);
18416 let client = state_provider.client();
18417 let mut batch = BatchRequest::new(client);
18418 let mut waiters = Vec::with_capacity(transaction_hashes.len());
18419 for transaction_hash in transaction_hashes {
18420 let waiter = batch
18421 .add_call::<_, serde_json::Value>("eth_getTransactionReceipt", &(transaction_hash,))
18422 .map_err(pending_flashblock_request_error)?;
18423 waiters.push((transaction_hash, waiter));
18424 }
18425 batch
18426 .send()
18427 .await
18428 .map_err(pending_flashblock_request_error)?;
18429 let mut logs = Vec::new();
18430 let mut completed = Vec::new();
18431 let mut unavailable = Vec::new();
18432 for (transaction_hash, waiter) in waiters {
18433 let value = waiter.await.map_err(pending_flashblock_request_error)?;
18434 if let Some(mut receipt_logs) =
18435 normalize_pending_transaction_receipt(transaction_hash, value)
18436 .map_err(PendingFlashblockPollError::Integrity)?
18437 {
18438 self.flashblocks_rpc_metrics.pending_receipts_completed = self
18439 .flashblocks_rpc_metrics
18440 .pending_receipts_completed
18441 .saturating_add(1);
18442 logs.append(&mut receipt_logs);
18443 completed.push(transaction_hash);
18444 } else {
18445 self.flashblocks_rpc_metrics.pending_receipts_unavailable = self
18446 .flashblocks_rpc_metrics
18447 .pending_receipts_unavailable
18448 .saturating_add(1);
18449 unavailable.push(transaction_hash);
18450 }
18451 }
18452 Ok((logs, completed, unavailable))
18453 }
18454
18455 fn pending_receipt_request_allowance(&mut self) -> usize {
18456 self.prune_flashblock_rpc_request_times();
18457 let rolling_capacity = self
18458 .config
18459 .max_flashblock_rpc_requests_per_second
18460 .saturating_sub(self.flashblock_rpc_request_times.len());
18461 rolling_capacity.min(self.pending_receipt_requests_per_tick_capacity())
18462 }
18463
18464 fn pending_receipt_requests_per_tick_capacity(&self) -> usize {
18465 let interval_nanos = self.config.flashblock_poll_interval.as_nanos().max(1);
18466 let ticks_per_second = Duration::from_secs(1).as_nanos().div_ceil(interval_nanos);
18467 let ticks_per_second = usize::try_from(ticks_per_second).unwrap_or(usize::MAX);
18468 self.pending_receipt_requests_per_second_capacity()
18469 .checked_div(ticks_per_second)
18470 .unwrap_or(0)
18471 }
18472
18473 fn pending_receipt_requests_per_second_capacity(&self) -> usize {
18474 let interval_nanos = self.config.flashblock_poll_interval.as_nanos().max(1);
18475 let ticks_per_second = Duration::from_secs(1).as_nanos().div_ceil(interval_nanos);
18476 let ticks_per_second = usize::try_from(ticks_per_second).unwrap_or(usize::MAX);
18477 let fixed_methods_per_tick = 2_usize.saturating_add(self.log_stream_filters().len());
18478 let mut reserved_methods = ticks_per_second.saturating_mul(fixed_methods_per_tick);
18479 if needs_header_block_stream(&self.interests) {
18480 let canonical_interval_nanos =
18481 self.config.canonical_head_poll_interval.as_nanos().max(1);
18482 let canonical_ticks = Duration::from_secs(1)
18483 .as_nanos()
18484 .div_ceil(canonical_interval_nanos);
18485 let canonical_ticks = usize::try_from(canonical_ticks).unwrap_or(usize::MAX);
18486 reserved_methods = reserved_methods.saturating_add(canonical_ticks.saturating_mul(2));
18487 }
18488 self.config
18489 .max_flashblock_rpc_requests_per_second
18490 .saturating_sub(reserved_methods)
18491 }
18492
18493 fn reserve_flashblock_rpc_methods(&mut self, methods: usize) -> bool {
18494 self.prune_flashblock_rpc_request_times();
18495 if self
18496 .flashblock_rpc_request_times
18497 .len()
18498 .saturating_add(methods)
18499 > self.config.max_flashblock_rpc_requests_per_second
18500 {
18501 return false;
18502 }
18503 let now = Instant::now();
18504 for _ in 0..methods {
18505 self.flashblock_rpc_request_times.push_back(now);
18506 }
18507 true
18508 }
18509
18510 fn prune_flashblock_rpc_request_times(&mut self) {
18511 let now = Instant::now();
18512 while self
18513 .flashblock_rpc_request_times
18514 .front()
18515 .is_some_and(|requested| now.duration_since(*requested) >= Duration::from_secs(1))
18516 {
18517 self.flashblock_rpc_request_times.pop_front();
18518 }
18519 }
18520
18521 fn filter_preconfirmed_logs(
18522 &mut self,
18523 flashblock: &FlashblockRef,
18524 mut logs: Vec<Log>,
18525 ) -> Result<Vec<Log>, SubscriberError> {
18526 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
18527 == Some(FlashblocksAdapter::PendingStatePolling);
18528 if self
18529 .latest_preconfirmation
18530 .as_ref()
18531 .is_some_and(|previous| !previous.same_payload(flashblock))
18532 {
18533 self.invalidate_preconfirmation_snapshot();
18538 }
18539 if self
18540 .latest_preconfirmation
18541 .as_ref()
18542 .is_none_or(|previous| !previous.same_payload(flashblock))
18543 {
18544 self.preconfirmed_seen_logs.clear();
18545 }
18546 if let Some(previous) = self.latest_preconfirmation.as_ref()
18547 && previous.same_payload(flashblock)
18548 && let (Some(previous_index), Some(current_index)) = (previous.index, flashblock.index)
18549 && current_index < previous_index
18550 {
18551 return Ok(Vec::new());
18552 }
18553 self.latest_preconfirmation = Some(flashblock.clone());
18554
18555 logs.sort_by_key(|log| (log.transaction_index.unwrap_or(u64::MAX), log.log_index));
18556 let mut filtered = Vec::new();
18557 for mut log in logs {
18558 if log.removed || log.block_number != Some(flashblock.block_number) {
18559 return Err(SubscriberError::Provider(
18560 "pre-confirmed log disagrees with its Flashblock snapshot".into(),
18561 ));
18562 }
18563 let transaction_hash = log.transaction_hash.ok_or_else(|| {
18564 SubscriberError::Provider(
18565 "pre-confirmed log is missing its transaction hash".into(),
18566 )
18567 })?;
18568 let log_index = log.log_index.ok_or_else(|| {
18569 SubscriberError::Provider("pre-confirmed log is missing its log index".into())
18570 })?;
18571 let transaction_index =
18572 flashblock
18573 .transaction_index(&transaction_hash)
18574 .ok_or_else(|| {
18575 SubscriberError::Provider(
18576 "pre-confirmed log transaction is absent from the cumulative Flashblock"
18577 .into(),
18578 )
18579 })?;
18580 if log
18581 .transaction_index
18582 .is_some_and(|reported| reported != transaction_index)
18583 {
18584 return Err(SubscriberError::Provider(
18585 "pre-confirmed log transaction index disagrees with cumulative membership"
18586 .into(),
18587 ));
18588 }
18589 let reported_hash = log.block_hash.and_then(non_placeholder_hash);
18590 if !samples_pending_range
18591 && let Some(reported) = reported_hash
18592 && reported != flashblock.content_hash
18593 && flashblock
18594 .partial_block_hash
18595 .is_some_and(|expected| reported != expected)
18596 {
18597 return Err(SubscriberError::Provider(
18598 "pre-confirmed log partial block hash disagrees with its Flashblock snapshot"
18599 .into(),
18600 ));
18601 }
18602 log.block_hash = Some(flashblock.content_hash);
18603 log.block_timestamp = flashblock.timestamp.or(log.block_timestamp);
18604 log.transaction_index = Some(transaction_index);
18605 if self
18606 .preconfirmed_seen_logs
18607 .insert((transaction_hash, log_index))
18608 && log_matches_any_interest(&log, &self.interests)
18609 {
18610 filtered.push(log);
18611 }
18612 }
18613 Ok(filtered)
18614 }
18615
18616 async fn verify_event_log_blocks(
18617 &mut self,
18618 event: &SubscriberEvent<N>,
18619 ) -> Result<(), SubscriberError> {
18620 if !self.config.verify_log_block_context {
18621 return Ok(());
18622 }
18623 match event {
18624 SubscriberEvent::Log { log, .. } => self.verify_log_block_context(log).await,
18625 SubscriberEvent::BackfilledLogs { logs, .. } | SubscriberEvent::Logs(logs) => {
18626 for log in logs {
18627 self.verify_log_block_context(log).await?;
18628 }
18629 Ok(())
18630 }
18631 #[cfg(feature = "raw-flashblocks-json")]
18632 SubscriberEvent::ExternalFlashblockUpdate(_) => Ok(()),
18633 SubscriberEvent::BlockHeader { .. }
18634 | SubscriberEvent::PendingHash(_)
18635 | SubscriberEvent::PendingHashes(_)
18636 | SubscriberEvent::BasePendingLog { .. }
18637 | SubscriberEvent::BasePendingLogTimed { .. }
18638 | SubscriberEvent::BaseFlashblock { .. }
18639 | SubscriberEvent::BaseFlashblockTimed { .. }
18640 | SubscriberEvent::OpFlashblockTick
18641 | SubscriberEvent::OpFlashblockTickTimed(_)
18642 | SubscriberEvent::CanonicalHeadTick
18643 | SubscriberEvent::PreconfirmedLogs { .. }
18644 | SubscriberEvent::FlashblockInvalidated
18645 | SubscriberEvent::FlashblockObserved
18646 | SubscriberEvent::StreamTerminated(_)
18647 | SubscriberEvent::StreamGap { .. } => Ok(()),
18648 }
18649 }
18650
18651 async fn verify_log_block_context(&mut self, log: &Log) -> Result<(), SubscriberError> {
18652 if log.removed {
18653 return Ok(());
18654 }
18655 let number = log.block_number.ok_or_else(|| {
18656 SubscriberError::Provider(
18657 "canonical log is missing its block number during context verification".into(),
18658 )
18659 })?;
18660 let hash = log.block_hash.ok_or_else(|| {
18661 SubscriberError::Provider(
18662 "canonical log is missing its block hash during context verification".into(),
18663 )
18664 })?;
18665 let key = (number, hash);
18666 if self.verified_log_blocks.contains_key(&key) {
18667 return Ok(());
18668 }
18669 let provider = self
18670 .log_verification_provider
18671 .as_ref()
18672 .unwrap_or(&self.provider);
18673 self.rpc_counters.record(
18674 SubscriberRpcCause::LogBlockVerification,
18675 SubscriberRpcMethod::EthGetBlockByNumber,
18676 );
18677 let block = provider
18678 .get_block_by_number(BlockNumberOrTag::Number(number))
18679 .await
18680 .map_err(provider_error)?
18681 .ok_or_else(|| {
18682 SubscriberError::Provider(format!(
18683 "canonical log block {number} is unavailable during context verification"
18684 ))
18685 })?;
18686 let header = block.header();
18687 let verified = BlockRef {
18688 number: header.number(),
18689 hash: header.hash(),
18690 parent_hash: Some(header.parent_hash()),
18691 timestamp: Some(header.timestamp()),
18692 };
18693 if verified.number != number
18694 || verified.hash != hash
18695 || log
18696 .block_timestamp
18697 .is_some_and(|timestamp| verified.timestamp != Some(timestamp))
18698 {
18699 return Err(SubscriberError::Provider(format!(
18700 "canonical log block {number}:{hash:?} disagrees with the provider's current canonical identity"
18701 )));
18702 }
18703 self.verified_log_blocks.insert(key, verified);
18704 self.verified_log_block_order.push_back(key);
18705 let capacity = self.config.reconnect.dedupe_window.max(1);
18706 while self.verified_log_block_order.len() > capacity {
18707 if let Some(evicted) = self.verified_log_block_order.pop_front() {
18708 self.verified_log_blocks.remove(&evicted);
18709 }
18710 }
18711 Ok(())
18712 }
18713
18714 fn enqueue_event(&mut self, event: SubscriberEvent<N>) {
18715 self.enqueue_event_with_excluded_owners(event, None);
18716 }
18717
18718 fn buffer_reconcile_event_for_owners(
18719 &mut self,
18720 event: &SubscriberEvent<N>,
18721 target_epochs: &HashSet<SubscriberOwnerEpoch>,
18722 ) {
18723 match event {
18724 SubscriberEvent::Log { log, .. } => {
18725 self.buffer_reconcile_log_for_owners(log, InputSource::Subscription, target_epochs)
18726 }
18727 SubscriberEvent::BackfilledLogs { logs, .. } => {
18728 for log in logs {
18729 self.buffer_reconcile_log_for_owners(log, InputSource::Backfill, target_epochs);
18730 }
18731 }
18732 SubscriberEvent::Logs(logs) => {
18733 for log in logs {
18734 self.buffer_reconcile_log_for_owners(log, InputSource::Poll, target_epochs);
18735 }
18736 }
18737 #[cfg(feature = "raw-flashblocks-json")]
18738 SubscriberEvent::ExternalFlashblockUpdate(_) => {}
18739 SubscriberEvent::BlockHeader { .. }
18740 | SubscriberEvent::PendingHash(_)
18741 | SubscriberEvent::PendingHashes(_)
18742 | SubscriberEvent::BasePendingLog { .. }
18743 | SubscriberEvent::BasePendingLogTimed { .. }
18744 | SubscriberEvent::BaseFlashblock { .. }
18745 | SubscriberEvent::BaseFlashblockTimed { .. }
18746 | SubscriberEvent::OpFlashblockTick
18747 | SubscriberEvent::OpFlashblockTickTimed(_)
18748 | SubscriberEvent::CanonicalHeadTick
18749 | SubscriberEvent::PreconfirmedLogs { .. }
18750 | SubscriberEvent::FlashblockInvalidated
18751 | SubscriberEvent::FlashblockObserved
18752 | SubscriberEvent::StreamTerminated(_)
18753 | SubscriberEvent::StreamGap { .. } => {}
18754 }
18755 }
18756
18757 fn buffer_reconcile_log_for_owners(
18758 &mut self,
18759 log: &Log,
18760 source: InputSource,
18761 target_epochs: &HashSet<SubscriberOwnerEpoch>,
18762 ) {
18763 let record = self.with_chain_id(log_input_record(log.clone(), source));
18764 let owners = self
18765 .staged_owners_for_record(&record)
18766 .into_iter()
18767 .filter(|owner| target_epochs.contains(owner))
18768 .collect::<Vec<_>>();
18769 if !owners.is_empty() {
18770 self.push_pending_reconcile_record(BufferedSubscriberOwnerRecord { record, owners });
18771 }
18772 }
18773
18774 fn promote_reconcile_owner_records(&mut self, target_epochs: &HashSet<SubscriberOwnerEpoch>) {
18775 let mut retained = VecDeque::new();
18776 while let Some(mut buffered) = self.pending_reconcile_owner_records.pop_front() {
18777 let mut promoted = Vec::new();
18778 buffered.owners.retain(|owner| {
18779 if target_epochs.contains(owner) {
18780 promoted.push(owner.clone());
18781 false
18782 } else {
18783 true
18784 }
18785 });
18786 if promoted.is_empty() {
18787 retained.push_back(buffered);
18788 continue;
18789 }
18790 let promoted_record = if buffered.owners.is_empty() {
18791 buffered.record
18792 } else {
18793 let record = buffered.record.clone();
18794 retained.push_back(buffered);
18795 record
18796 };
18797 self.enqueue_owner_record_for_owners_unmerged(promoted_record, promoted);
18798 }
18799 self.pending_reconcile_owner_records = retained;
18800 }
18801
18802 fn seed_reconciled_filter_anchors(
18803 &mut self,
18804 plans: &[SubscriberOwnerReconcilePlan<N>],
18805 through: u64,
18806 ) {
18807 for filter in plans.iter().flat_map(|plan| log_filters(&plan.interests)) {
18808 let Some(source_id) = self.log_source_ids.get(&filter).copied() else {
18809 continue;
18810 };
18811 let anchor = self
18812 .last_seen_log_blocks
18813 .entry(source_id)
18814 .or_insert(through);
18815 *anchor = (*anchor).max(through);
18816 }
18817 }
18818
18819 fn enqueue_event_excluding_owners(
18820 &mut self,
18821 event: SubscriberEvent<N>,
18822 excluded: &HashSet<SubscriberOwnerEpoch>,
18823 ) {
18824 self.enqueue_event_with_excluded_owners(event, Some(excluded));
18825 }
18826
18827 fn enqueue_event_with_excluded_owners(
18828 &mut self,
18829 event: SubscriberEvent<N>,
18830 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
18831 ) {
18832 match event {
18833 SubscriberEvent::Log {
18834 source_id,
18835 log,
18836 #[cfg(feature = "ingress-timing")]
18837 timing,
18838 } => {
18839 if log_matches_any_interest(&log, &self.interests) {
18840 let record = log_input_record(log, InputSource::Subscription);
18841 self.note_log_block(source_id, &record);
18842 self.enqueue_timed_record_with_excluded_owners(
18843 record,
18844 excluded,
18845 #[cfg(feature = "ingress-timing")]
18846 timing,
18847 );
18848 }
18849 }
18850 SubscriberEvent::BackfilledLogs { source_id, logs } => {
18851 self.enqueue_backfilled_logs_with_excluded_owners(
18852 logs,
18853 Some(source_id),
18854 None,
18855 None,
18856 excluded,
18857 );
18858 }
18859 SubscriberEvent::Logs(logs) => {
18860 for log in logs {
18861 if log_matches_any_interest(&log, &self.interests) {
18862 self.enqueue_record_with_excluded_owners(
18863 log_input_record(log, InputSource::Poll),
18864 excluded,
18865 );
18866 }
18867 }
18868 }
18869 SubscriberEvent::BlockHeader {
18870 header,
18871 #[cfg(feature = "ingress-timing")]
18872 timing,
18873 } => {
18874 if needs_header_block_stream(&self.interests) {
18875 let record = block_header_input_record::<N>(header);
18876 self.note_attestable_canonical_block(&record);
18877 self.enqueue_timed_record_with_excluded_owners(
18878 record,
18879 excluded,
18880 #[cfg(feature = "ingress-timing")]
18881 timing,
18882 );
18883 }
18884 }
18885 SubscriberEvent::PendingHash(hash) => {
18886 let record = pending_hash_input_record::<N>(hash, InputSource::Subscription);
18887 self.enqueue_record_with_excluded_owners(record, excluded);
18888 }
18889 SubscriberEvent::PendingHashes(hashes) => {
18890 for hash in hashes {
18891 self.enqueue_record_with_excluded_owners(
18892 pending_hash_input_record::<N>(hash, InputSource::Poll),
18893 excluded,
18894 );
18895 }
18896 }
18897 SubscriberEvent::PreconfirmedLogs {
18898 flashblock,
18899 logs,
18900 timing,
18901 } => {
18902 for log in logs {
18903 let record = self
18904 .with_chain_id(preconfirmed_log_input_record::<N>(log, flashblock.clone()));
18905 self.push_pending_record(SubscriberInputRecord {
18906 record,
18907 scope: SubscriberInputScope::Preconfirmed,
18908 preconfirmation_timing: Some(timing),
18909 #[cfg(feature = "ingress-timing")]
18910 canonical_timing: None,
18911 });
18912 }
18913 }
18914 SubscriberEvent::FlashblockInvalidated => {
18915 self.pending_preconfirmation_invalidation = true;
18916 }
18917 SubscriberEvent::BasePendingLog { .. }
18918 | SubscriberEvent::BasePendingLogTimed { .. }
18919 | SubscriberEvent::BaseFlashblock { .. }
18920 | SubscriberEvent::BaseFlashblockTimed { .. }
18921 | SubscriberEvent::OpFlashblockTick
18922 | SubscriberEvent::OpFlashblockTickTimed(_)
18923 | SubscriberEvent::CanonicalHeadTick
18924 | SubscriberEvent::FlashblockObserved => {}
18925 #[cfg(feature = "raw-flashblocks-json")]
18926 SubscriberEvent::ExternalFlashblockUpdate(_) => {}
18927 SubscriberEvent::StreamTerminated(_) | SubscriberEvent::StreamGap { .. } => {}
18930 }
18931 }
18932
18933 fn enqueue_backfilled_logs(
18934 &mut self,
18935 logs: Vec<Log>,
18936 source_id: Option<usize>,
18937 owner: Option<&SubscriberOwnerEpoch>,
18938 range: Option<SubscriberBackfill>,
18939 ) {
18940 self.enqueue_backfilled_logs_with_excluded_owners(logs, source_id, owner, range, None);
18941 }
18942
18943 fn enqueue_backfilled_logs_with_excluded_owners(
18944 &mut self,
18945 logs: Vec<Log>,
18946 source_id: Option<usize>,
18947 owner: Option<&SubscriberOwnerEpoch>,
18948 range: Option<SubscriberBackfill>,
18949 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
18950 ) {
18951 for log in logs {
18952 if range.as_ref().is_some_and(|range| {
18953 log.block_number.is_some_and(|block| {
18954 block < range.start_block() || range.end_block().is_some_and(|end| block > end)
18955 })
18956 }) {
18957 continue;
18958 }
18959 let matches = match owner {
18960 Some(epoch) => self
18961 .owned_interests
18962 .iter()
18963 .find(|entry| entry.epoch.as_ref() == Some(epoch))
18964 .is_some_and(|entry| log_matches_any_interest(&log, &entry.interests)),
18965 None => log_matches_any_interest(&log, &self.interests),
18966 };
18967 if matches {
18968 let record = log_input_record(log, InputSource::Backfill);
18969 if let Some(epoch) = owner {
18970 self.enqueue_owner_record(record, epoch.clone());
18971 } else {
18972 if let Some(source_id) = source_id {
18973 self.note_log_block(source_id, &record);
18974 }
18975 self.enqueue_record_with_excluded_owners(record, excluded);
18976 }
18977 }
18978 }
18979 }
18980
18981 fn enqueue_compat_owner_backfilled_logs(
18982 &mut self,
18983 logs: Vec<Log>,
18984 owner: &HandlerId,
18985 range: SubscriberBackfill,
18986 ) {
18987 let interests = self
18988 .owned_interests
18989 .iter()
18990 .find(|entry| {
18991 &entry.owner == owner
18992 && entry.epoch.is_none()
18993 && entry.state == SubscriberOwnerState::Active
18994 })
18995 .map(|entry| entry.interests.clone());
18996 let Some(interests) = interests else {
18997 return;
18998 };
18999 for log in logs {
19000 if log.block_number.is_some_and(|block| {
19001 block < range.start_block() || range.end_block().is_some_and(|end| block > end)
19002 }) || !log_matches_any_interest(&log, &interests)
19003 {
19004 continue;
19005 }
19006 let record = log_input_record(log, InputSource::Backfill);
19007 self.enqueue_compat_owner_record(record, owner.clone());
19008 }
19009 }
19010
19011 async fn reconnect_source_stream(
19012 &mut self,
19013 source: SubscriberStreamSource,
19014 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
19015 if !source.is_pubsub() {
19016 return Err(stream_terminated_error(&source));
19017 }
19018
19019 if !self.config.reconnect.enabled {
19020 return Err(SubscriberError::Provider(format!(
19021 "Alloy subscriber {} stream terminated and reconnect is disabled",
19022 source.label()
19023 )));
19024 }
19025
19026 let mut attempts = 0usize;
19027 let mut delay = self.config.reconnect.initial_delay;
19028 let mut retry_delay = self.config.reconnect.retry_delay;
19029
19030 loop {
19031 attempts = attempts.saturating_add(1);
19032 if !delay.is_zero() {
19033 tokio::time::sleep(delay).await;
19034 }
19035
19036 match self.reconnect_source_once(source.clone()).await {
19037 Ok(backfill_event) => return Ok(backfill_event),
19038 Err(error) if reconnect_attempts_exhausted(attempts, &self.config.reconnect) => {
19039 return Err(SubscriberError::Provider(format!(
19040 "Alloy subscriber {} stream terminated and reconnect failed after {attempts} attempt(s): {error}",
19041 source.label()
19042 )));
19043 }
19044 Err(error) => {
19045 tracing::warn!(
19046 stream = source.label(),
19047 attempts,
19048 error = %error,
19049 "Alloy subscriber reconnect attempt failed"
19050 );
19051 delay = retry_delay;
19052 retry_delay =
19053 next_reconnect_delay(retry_delay, self.config.reconnect.max_delay);
19054 }
19055 }
19056 }
19057 }
19058
19059 async fn reconnect_source_once(
19060 &mut self,
19061 source: SubscriberStreamSource,
19062 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
19063 if matches!(
19064 &self.state,
19065 AlloySubscriberState::Active(streams) if streams.contains_source(&source)
19066 ) {
19067 let backfill_event = self.backfill_reconnected_source(&source).await?;
19071 self.pending_source_backfills
19072 .retain(|pending| !pending.same_key(&source));
19073 return Ok(backfill_event);
19074 }
19075 let stream = self.connect_source_stream(source.clone()).await?;
19076 if !matches!(self.state, AlloySubscriberState::Active(_)) {
19077 return Err(SubscriberError::Provider(
19078 "Alloy subscriber state changed before reconnect completed".to_owned(),
19079 ));
19080 }
19081 self.install_source_stream(source.clone(), stream);
19082 if self.source_requires_backfill(&source) {
19083 self.queue_source_backfill(source.clone());
19084 }
19085 let backfill_event = self.backfill_reconnected_source(&source).await?;
19086 self.pending_source_backfills
19087 .retain(|pending| !pending.same_key(&source));
19088
19089 Ok(backfill_event)
19090 }
19091
19092 async fn recover_stream_gap(
19115 &mut self,
19116 source: &SubscriberStreamSource,
19117 gap: SubscriberStreamGap,
19118 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
19119 match source {
19120 SubscriberStreamSource::PubSubLog { id, .. } => {
19121 self.reset_log_attestation();
19122 if !self.last_seen_log_blocks.contains_key(id) {
19123 return Err(SubscriberError::Provider(format!(
19124 "Alloy subscriber {} lost notifications ({gap}) before establishing a \
19125 delivery anchor, so the missed range cannot be bounded",
19126 source.label()
19127 )));
19128 }
19129 let event = self
19130 .backfill_source_window(source, SubscriberRpcCause::GapBackfill)
19131 .await?;
19132 self.gap_counters.record_log_gap_healed();
19133 Ok(event)
19134 }
19135 SubscriberStreamSource::PubSubBlockHeaders => {
19136 self.gap_counters.record_header_gap();
19137 Ok(None)
19138 }
19139 SubscriberStreamSource::BasePendingLog { .. } => {
19140 self.gap_counters.record_preconfirmation_gap();
19141 self.invalidate_preconfirmation_snapshot();
19142 Ok(Some(SubscriberEvent::FlashblockInvalidated))
19143 }
19144 _ => Ok(None),
19145 }
19146 }
19147
19148 async fn backfill_reconnected_source(
19149 &mut self,
19150 source: &SubscriberStreamSource,
19151 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
19152 self.backfill_source_window(source, SubscriberRpcCause::ReconnectBackfill)
19153 .await
19154 }
19155
19156 async fn backfill_source_window(
19166 &mut self,
19167 source: &SubscriberStreamSource,
19168 cause: SubscriberRpcCause,
19169 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
19170 if source.is_flashblocks() {
19171 return Ok(None);
19172 }
19173 let SubscriberStreamSource::PubSubLog { id, filter } = source else {
19174 return Ok(None);
19175 };
19176 let Some(from_block) = self.last_seen_log_blocks.get(id).copied() else {
19177 return Ok(None);
19178 };
19179
19180 self.record_rpc(cause, SubscriberRpcMethod::EthBlockNumber);
19181 let latest = self
19182 .provider
19183 .get_block_number()
19184 .await
19185 .map_err(provider_error)?;
19186 if latest < from_block {
19187 return Ok(None);
19188 }
19189
19190 self.record_rpc(cause, SubscriberRpcMethod::EthGetLogs);
19191 let logs = self
19192 .provider
19193 .get_logs(&filter.clone().from_block(from_block).to_block(latest))
19194 .await
19195 .map_err(provider_error)?;
19196 Ok(Some(SubscriberEvent::BackfilledLogs {
19197 source_id: *id,
19198 logs,
19199 }))
19200 }
19201
19202 fn note_log_block(&mut self, source_id: usize, record: &ReactiveInputRecord<N>) {
19203 if let Some(block) = record.context.block.as_ref() {
19204 self.last_seen_log_blocks.insert(source_id, block.number);
19205 }
19206 }
19207
19208 fn enqueue_record_with_excluded_owners(
19209 &mut self,
19210 record: ReactiveInputRecord<N>,
19211 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
19212 ) {
19213 self.enqueue_timed_record_with_excluded_owners(
19214 record,
19215 excluded,
19216 #[cfg(feature = "ingress-timing")]
19217 None,
19218 );
19219 }
19220
19221 fn enqueue_timed_record_with_excluded_owners(
19222 &mut self,
19223 record: ReactiveInputRecord<N>,
19224 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
19225 #[cfg(feature = "ingress-timing")] timing: Option<CanonicalIngressTiming>,
19226 ) {
19227 let record = self.with_chain_id(record);
19228 let mut owners = self.staged_owners_for_record(&record);
19229 if let Some(excluded) = excluded {
19230 owners.retain(|owner| !excluded.contains(owner));
19231 }
19232 let canonical_duplicate = self.should_skip_recent_duplicate(&record);
19233 let owners = self.filter_recent_owner_duplicates(&record, owners);
19234 let compatibility_owners = self.compatibility_owners_for_record(&record);
19235 let (already_served, newly_served): (Vec<_>, Vec<_>) = compatibility_owners
19236 .into_iter()
19237 .partition(|owner| self.compatibility_owner_has_seen(&record, owner));
19238 if canonical_duplicate {
19239 if !owners.is_empty() {
19240 self.push_pending_record(SubscriberInputRecord {
19241 record: record.clone(),
19242 scope: SubscriberInputScope::OwnerOnly { owners },
19243 preconfirmation_timing: None,
19244 #[cfg(feature = "ingress-timing")]
19245 canonical_timing: timing,
19246 });
19247 }
19248 if !newly_served.is_empty() {
19249 for owner in &newly_served {
19250 self.remember_compatibility_owner_record(&record, owner);
19251 }
19252 self.push_pending_record(SubscriberInputRecord {
19253 record,
19254 scope: SubscriberInputScope::OwnerOnlyHandlers {
19255 owners: newly_served,
19256 },
19257 preconfirmation_timing: None,
19258 #[cfg(feature = "ingress-timing")]
19259 canonical_timing: timing,
19260 });
19261 }
19262 return;
19263 }
19264 self.remember_record(&record);
19265 for owner in already_served.iter().chain(&newly_served) {
19266 self.remember_compatibility_owner_record(&record, owner);
19267 }
19268 self.push_pending_record(SubscriberInputRecord {
19269 record,
19270 scope: if already_served.is_empty() {
19271 SubscriberInputScope::Canonical { owners }
19272 } else {
19273 SubscriberInputScope::CanonicalResidual {
19274 owners,
19275 excluded: already_served,
19276 }
19277 },
19278 preconfirmation_timing: None,
19279 #[cfg(feature = "ingress-timing")]
19280 canonical_timing: timing,
19281 });
19282 }
19283
19284 fn enqueue_compat_owner_record(&mut self, record: ReactiveInputRecord<N>, owner: HandlerId) {
19285 let record = self.with_chain_id(record);
19286 if self.compatibility_owner_has_seen(&record, &owner) {
19287 return;
19288 }
19289 self.remember_compatibility_owner_record(&record, &owner);
19290 self.push_pending_record(SubscriberInputRecord {
19291 record,
19292 scope: SubscriberInputScope::OwnerOnlyHandlers {
19293 owners: vec![owner],
19294 },
19295 preconfirmation_timing: None,
19296 #[cfg(feature = "ingress-timing")]
19297 canonical_timing: None,
19298 });
19299 }
19300
19301 fn compatibility_owners_for_record(&self, record: &ReactiveInputRecord<N>) -> Vec<HandlerId> {
19302 self.owned_interests
19303 .iter()
19304 .filter(|entry| entry.epoch.is_none() && entry.state == SubscriberOwnerState::Active)
19305 .filter(|entry| {
19306 entry
19307 .interests
19308 .iter()
19309 .any(|interest| interest_matches(interest, &record.input))
19310 })
19311 .map(|entry| entry.owner.clone())
19312 .collect()
19313 }
19314
19315 fn compatibility_owner_has_seen(
19316 &self,
19317 record: &ReactiveInputRecord<N>,
19318 owner: &HandlerId,
19319 ) -> bool {
19320 should_dedupe_record(record)
19321 && self
19322 .recent_compat_owner_input_ref_sets
19323 .get(owner)
19324 .is_some_and(|seen| seen.contains(&record.input_ref()))
19325 }
19326
19327 fn remember_compatibility_owner_record(
19328 &mut self,
19329 record: &ReactiveInputRecord<N>,
19330 owner: &HandlerId,
19331 ) {
19332 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
19333 return;
19334 }
19335 let input_ref = record.input_ref();
19336 let seen = self
19337 .recent_compat_owner_input_ref_sets
19338 .entry(owner.clone())
19339 .or_default();
19340 if !seen.insert(input_ref) {
19341 return;
19342 }
19343 let recent = self
19344 .recent_compat_owner_input_refs
19345 .entry(owner.clone())
19346 .or_default();
19347 recent.push_back(input_ref);
19348 while recent.len() > self.config.reconnect.dedupe_window {
19349 if let Some(evicted) = recent.pop_front() {
19350 seen.remove(&evicted);
19351 }
19352 }
19353 }
19354
19355 fn enqueue_owner_record(
19356 &mut self,
19357 record: ReactiveInputRecord<N>,
19358 owner: SubscriberOwnerEpoch,
19359 ) {
19360 self.enqueue_owner_record_for_owners(record, vec![owner]);
19361 }
19362
19363 fn enqueue_owner_record_for_owners(
19364 &mut self,
19365 record: ReactiveInputRecord<N>,
19366 owners: Vec<SubscriberOwnerEpoch>,
19367 ) {
19368 self.enqueue_owner_record_for_owners_inner(record, owners, true);
19369 }
19370
19371 fn enqueue_owner_record_for_owners_unmerged(
19372 &mut self,
19373 record: ReactiveInputRecord<N>,
19374 owners: Vec<SubscriberOwnerEpoch>,
19375 ) {
19376 self.enqueue_owner_record_for_owners_inner(record, owners, false);
19377 }
19378
19379 fn enqueue_owner_record_for_owners_inner(
19380 &mut self,
19381 record: ReactiveInputRecord<N>,
19382 owners: Vec<SubscriberOwnerEpoch>,
19383 merge_pending: bool,
19384 ) {
19385 let record = self.with_chain_id(record);
19386 let owners = self.filter_recent_owner_duplicates(&record, owners);
19387 if owners.is_empty() {
19388 return;
19389 }
19390 if merge_pending
19391 && should_dedupe_record(&record)
19392 && self.config.reconnect.dedupe_window != 0
19393 {
19394 let input_ref = record.input_ref();
19395 if let Some(pending) = self
19396 .pending_records
19397 .iter_mut()
19398 .rev()
19399 .find(|pending| pending.record.input_ref() == input_ref)
19400 {
19401 let pending_owners = match &mut pending.scope {
19402 SubscriberInputScope::Canonical { owners }
19403 | SubscriberInputScope::CanonicalResidual { owners, .. }
19404 | SubscriberInputScope::OwnerOnly { owners } => Some(owners),
19405 SubscriberInputScope::OwnerOnlyHandlers { .. }
19406 | SubscriberInputScope::Preconfirmed => None,
19407 };
19408 if let Some(pending_owners) = pending_owners {
19409 for owner in owners {
19410 if !pending_owners.contains(&owner) {
19411 pending_owners.push(owner);
19412 }
19413 }
19414 return;
19415 }
19416 }
19417 }
19418 self.push_pending_record(SubscriberInputRecord {
19419 record,
19420 scope: SubscriberInputScope::OwnerOnly { owners },
19421 preconfirmation_timing: None,
19422 #[cfg(feature = "ingress-timing")]
19423 canonical_timing: None,
19424 });
19425 }
19426
19427 fn push_pending_record(&mut self, record: SubscriberInputRecord<N>) {
19428 if self.pending_record_count() >= self.config.max_pending_records {
19429 self.note_resource_error(format!(
19430 "pending record queues reached the configured limit of {}",
19431 self.config.max_pending_records
19432 ));
19433 return;
19434 }
19435 self.pending_records.push_back(record);
19436 }
19437
19438 fn ensure_pending_record_capacity(
19439 &mut self,
19440 additional: usize,
19441 operation: &str,
19442 ) -> Result<(), SubscriberError> {
19443 let required = self.pending_record_count().saturating_add(additional);
19444 if required > self.config.max_pending_records {
19445 self.note_resource_error(format!(
19446 "{operation} require {required} pending records, above the configured limit of {}",
19447 self.config.max_pending_records
19448 ));
19449 return self.check_resource_error();
19450 }
19451 Ok(())
19452 }
19453
19454 fn push_pending_reconcile_record(&mut self, record: BufferedSubscriberOwnerRecord<N>) {
19455 if self.pending_record_count() >= self.config.max_pending_records {
19456 self.note_resource_error(format!(
19457 "pending record queues reached the configured limit of {}",
19458 self.config.max_pending_records
19459 ));
19460 return;
19461 }
19462 self.pending_reconcile_owner_records.push_back(record);
19463 }
19464
19465 fn pending_record_count(&self) -> usize {
19466 self.pending_records
19467 .len()
19468 .saturating_add(self.pending_reconcile_owner_records.len())
19469 }
19470
19471 fn note_resource_error(&mut self, message: String) {
19472 if self.resource_error.is_none() {
19473 self.resource_error = Some(message);
19474 }
19475 }
19476
19477 fn check_resource_error(&self) -> Result<(), SubscriberError> {
19478 match &self.resource_error {
19479 Some(message) => Err(SubscriberError::ResourceExhausted(message.clone())),
19480 None => Ok(()),
19481 }
19482 }
19483
19484 fn with_chain_id(&self, mut record: ReactiveInputRecord<N>) -> ReactiveInputRecord<N> {
19485 record.context.chain_id = self.chain_id;
19486 if self.config.verify_log_block_context
19487 && let ReactiveInput::Log(log) = &record.input
19488 && !log.removed
19489 && let (Some(number), Some(hash)) = (log.block_number, log.block_hash)
19490 && let Some(verified) = self.verified_log_blocks.get(&(number, hash)).copied()
19491 {
19492 record.context.block = Some(verified);
19493 record.context.chain_status = ChainStatus::Included {
19494 block: verified,
19495 confirmations: 0,
19496 };
19497 }
19498 record
19499 }
19500
19501 fn staged_owners_for_record(
19502 &self,
19503 record: &ReactiveInputRecord<N>,
19504 ) -> Vec<SubscriberOwnerEpoch> {
19505 self.owned_interests
19506 .iter()
19507 .filter(|entry| entry.state == SubscriberOwnerState::Staged)
19508 .filter(|entry| {
19509 entry
19510 .interests
19511 .iter()
19512 .any(|interest| interest_matches(interest, &record.input))
19513 })
19514 .filter_map(|entry| entry.epoch.clone())
19515 .collect()
19516 }
19517
19518 fn filter_recent_owner_duplicates(
19519 &mut self,
19520 record: &ReactiveInputRecord<N>,
19521 owners: Vec<SubscriberOwnerEpoch>,
19522 ) -> Vec<SubscriberOwnerEpoch> {
19523 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
19524 return owners;
19525 }
19526 let input_ref = record.input_ref();
19527 let window = self.config.reconnect.dedupe_window;
19528 owners
19529 .into_iter()
19530 .filter(|owner| {
19531 let seen = self
19532 .recent_owner_input_ref_sets
19533 .entry(owner.clone())
19534 .or_default();
19535 if !seen.insert(input_ref) {
19536 return false;
19537 }
19538 let recent = self
19539 .recent_owner_input_refs
19540 .entry(owner.clone())
19541 .or_default();
19542 recent.push_back(input_ref);
19543 while recent.len() > window {
19544 if let Some(evicted) = recent.pop_front() {
19545 seen.remove(&evicted);
19546 }
19547 }
19548 true
19549 })
19550 .collect()
19551 }
19552
19553 fn should_skip_recent_duplicate(&self, record: &ReactiveInputRecord<N>) -> bool {
19554 if !should_dedupe_record(record) {
19555 return false;
19556 }
19557 self.recent_input_ref_set.contains(&record.input_ref())
19558 }
19559
19560 fn remember_record(&mut self, record: &ReactiveInputRecord<N>) {
19561 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
19562 return;
19563 }
19564
19565 let input_ref = record.input_ref();
19566 if !self.recent_input_ref_set.insert(input_ref) {
19567 return;
19568 }
19569 self.recent_input_refs.push_back(input_ref);
19570
19571 while self.recent_input_refs.len() > self.config.reconnect.dedupe_window {
19572 if let Some(evicted) = self.recent_input_refs.pop_front() {
19573 self.recent_input_ref_set.remove(&evicted);
19574 }
19575 }
19576 }
19577}
19578
19579#[cfg(feature = "reactive-ws")]
19591fn gap_observing_stream<
19592 N,
19593 T,
19594 #[cfg(feature = "ingress-timing")] F: FnMut(T, Option<CanonicalIngressTiming>) -> SubscriberEvent<N> + Send + 'static,
19595 #[cfg(not(feature = "ingress-timing"))] F: FnMut(T) -> SubscriberEvent<N> + Send + 'static,
19596>(
19597 subscription: alloy_pubsub::Subscription<T>,
19598 source: SubscriberStreamSource,
19599 gap_counters: Arc<SubscriberStreamGapCounters>,
19600 #[cfg(feature = "ingress-timing")] receipts: Option<TransportReceiptLookup>,
19601 to_event: F,
19602) -> BoxStream<'static, SubscriberEvent<N>>
19603where
19604 N: Network + 'static,
19605 T: serde::de::DeserializeOwned + Send + 'static,
19606{
19607 struct GapState<T, F> {
19610 raw: alloy_pubsub::RawSubscription,
19611 to_event: F,
19612 source: SubscriberStreamSource,
19613 gap_counters: Arc<SubscriberStreamGapCounters>,
19614 #[cfg(feature = "ingress-timing")]
19615 receipts: Option<TransportReceiptLookup>,
19616 _item: PhantomData<fn() -> T>,
19617 }
19618
19619 let state = GapState {
19620 raw: subscription.into_raw(),
19621 to_event,
19622 source: source.clone(),
19623 gap_counters,
19624 #[cfg(feature = "ingress-timing")]
19625 receipts,
19626 _item: PhantomData::<fn() -> T>,
19627 };
19628
19629 let stream = stream::unfold(state, |mut state| async move {
19630 let gap = match state.raw.recv().await {
19631 Ok(value) => {
19632 #[cfg(feature = "ingress-timing")]
19633 let dequeued = state.receipts.as_ref().map(|_| Instant::now());
19634 #[cfg(feature = "ingress-timing")]
19635 let receipt = state
19636 .receipts
19637 .as_ref()
19638 .and_then(|lookup| lookup(value.get()));
19639 match serde_json::from_str::<T>(value.get()) {
19640 Ok(item) => {
19641 #[cfg(feature = "ingress-timing")]
19642 let timing = receipt.zip(dequeued).and_then(|(receipt, dequeued)| {
19643 CanonicalIngressTiming::new(receipt, dequeued, Instant::now())
19644 });
19645 let event = (state.to_event)(
19646 item,
19647 #[cfg(feature = "ingress-timing")]
19648 timing,
19649 );
19650 return Some((event, state));
19651 }
19652 Err(error) => {
19653 tracing::warn!(
19654 stream = state.source.label(),
19655 error = %error,
19656 "pubsub notification did not decode; treating it as lost data"
19657 );
19658 SubscriberStreamGap::Undecodable
19659 }
19660 }
19661 }
19662 Err(broadcast::error::RecvError::Lagged(skipped)) => {
19663 tracing::warn!(
19664 stream = state.source.label(),
19665 skipped,
19666 "pubsub notification channel overflowed; the missed window will be recovered"
19667 );
19668 SubscriberStreamGap::Lagged { skipped }
19669 }
19670 Err(broadcast::error::RecvError::Closed) => return None,
19673 };
19674 state.gap_counters.record_gap(gap);
19675 let event = SubscriberEvent::StreamGap {
19676 source: state.source.clone(),
19677 gap,
19678 };
19679 Some((event, state))
19680 });
19681
19682 stream_with_termination(stream, source)
19683}
19684
19685fn stream_with_termination<N, S>(
19686 stream: S,
19687 source: SubscriberStreamSource,
19688) -> BoxStream<'static, SubscriberEvent<N>>
19689where
19690 N: Network + 'static,
19691 S: futures::Stream<Item = SubscriberEvent<N>> + Send + 'static,
19692{
19693 stream
19694 .chain(stream::once(async move {
19695 SubscriberEvent::StreamTerminated(source)
19696 }))
19697 .boxed()
19698}
19699
19700fn flashblock_reconnect_future<N>(
19701 provider: RootProvider<N>,
19702 source: SubscriberStreamSource,
19703 channel_size: usize,
19704 reconnect: SubscriberReconnectConfig,
19705 first_delay: Duration,
19706 flashblock_poll_interval: Duration,
19707 counters: Arc<SubscriberRpcCounters>,
19708) -> FlashblockReconnectFuture<N>
19709where
19710 N: Network + 'static,
19711{
19712 Box::pin(async move {
19713 if !reconnect.enabled {
19714 let error = SubscriberError::Provider(format!(
19715 "Alloy subscriber {} stream terminated and reconnect is disabled",
19716 source.label()
19717 ));
19718 return (source, Err(error));
19719 }
19720
19721 let mut attempts = 0_usize;
19722 let mut delay = first_delay;
19723 let mut retry_delay = reconnect.retry_delay;
19724 loop {
19725 attempts = attempts.saturating_add(1);
19726 if !delay.is_zero() {
19727 tokio::time::sleep(delay).await;
19728 }
19729 match connect_flashblock_source_once(
19730 &provider,
19731 source.clone(),
19732 channel_size,
19733 flashblock_poll_interval,
19734 counters.as_ref(),
19735 )
19736 .await
19737 {
19738 Ok(stream) => return (source, Ok(stream)),
19739 Err(error) if reconnect_attempts_exhausted(attempts, &reconnect) => {
19740 return (
19741 source.clone(),
19742 Err(SubscriberError::Provider(format!(
19743 "Alloy subscriber {} stream reconnect failed after {attempts} attempt(s): {error}",
19744 source.label()
19745 ))),
19746 );
19747 }
19748 Err(error) => {
19749 tracing::warn!(
19750 stream = source.label(),
19751 attempts,
19752 error = %error,
19753 "Flashblocks reconnect attempt failed"
19754 );
19755 delay = retry_delay;
19756 retry_delay = next_reconnect_delay(retry_delay, reconnect.max_delay);
19757 }
19758 }
19759 }
19760 })
19761}
19762
19763async fn connect_flashblock_source_once<N>(
19764 provider: &RootProvider<N>,
19765 source: SubscriberStreamSource,
19766 channel_size: usize,
19767 flashblock_poll_interval: Duration,
19768 counters: &SubscriberRpcCounters,
19769) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>
19770where
19771 N: Network + 'static,
19772{
19773 #[cfg(not(feature = "reactive-ws"))]
19774 let _ = (provider, counters);
19775
19776 match source {
19777 SubscriberStreamSource::BasePendingLog { id, filter } => {
19778 #[cfg(feature = "reactive-ws")]
19779 {
19780 let source = SubscriberStreamSource::BasePendingLog {
19781 id,
19782 filter: filter.clone(),
19783 };
19784 let params = base_pending_log_filter(&filter)?;
19785 counters.record(
19786 SubscriberRpcCause::StreamSubscription,
19787 SubscriberRpcMethod::EthSubscribe,
19788 );
19789 let stream = provider
19790 .subscribe::<_, Log>(("pendingLogs", params))
19791 .channel_size(channel_size.max(1))
19792 .await
19793 .map_err(provider_error)?
19794 .into_stream()
19795 .map(move |log| SubscriberEvent::BasePendingLogTimed {
19796 source_id: id,
19797 log,
19798 timing: FlashblockIngressTiming::new(Instant::now()),
19799 });
19800 Ok(stream_with_termination(stream, source))
19801 }
19802 #[cfg(not(feature = "reactive-ws"))]
19803 {
19804 let _ = (id, filter, channel_size);
19805 Err(SubscriberError::Unsupported(
19806 "Base Flashblocks require the reactive-ws feature",
19807 ))
19808 }
19809 }
19810 SubscriberStreamSource::BaseFlashblocks => {
19811 #[cfg(feature = "reactive-ws")]
19812 {
19813 counters.record(
19814 SubscriberRpcCause::StreamSubscription,
19815 SubscriberRpcMethod::EthSubscribe,
19816 );
19817 let stream = provider
19818 .subscribe::<_, BaseFlashblockWirePayload>(("newFlashblocks",))
19819 .channel_size(channel_size.max(1))
19820 .await
19821 .map_err(provider_error)?
19822 .into_stream()
19823 .map(|payload| SubscriberEvent::BaseFlashblockTimed {
19824 payload,
19825 timing: FlashblockIngressTiming::new(Instant::now()),
19826 });
19827 Ok(stream_with_termination(
19828 stream,
19829 SubscriberStreamSource::BaseFlashblocks,
19830 ))
19831 }
19832 #[cfg(not(feature = "reactive-ws"))]
19833 {
19834 let _ = channel_size;
19835 Err(SubscriberError::Unsupported(
19836 "Base Flashblocks require the reactive-ws feature",
19837 ))
19838 }
19839 }
19840 SubscriberStreamSource::OpPendingFlashblocks => {
19841 let first_tick = tokio::time::Instant::now();
19842 let mut interval = tokio::time::interval_at(first_tick, flashblock_poll_interval);
19843 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
19844 let stream = stream::unfold(interval, |mut interval| async move {
19845 interval.tick().await;
19846 Some((
19847 SubscriberEvent::OpFlashblockTickTimed(FlashblockIngressTiming::new(
19848 Instant::now(),
19849 )),
19850 interval,
19851 ))
19852 });
19853 Ok(stream_with_termination(
19854 stream,
19855 SubscriberStreamSource::OpPendingFlashblocks,
19856 ))
19857 }
19858 source => Err(SubscriberError::InvalidConfig(match source {
19859 SubscriberStreamSource::PubSubLog { .. }
19860 | SubscriberStreamSource::CanonicalHeadPolling
19861 | SubscriberStreamSource::PubSubPendingHashes
19862 | SubscriberStreamSource::PubSubBlockHeaders
19863 | SubscriberStreamSource::PollingLog { .. }
19864 | SubscriberStreamSource::PollingPendingHashes => {
19865 "Flashblocks reconnect received a canonical source"
19866 }
19867 SubscriberStreamSource::BasePendingLog { .. }
19868 | SubscriberStreamSource::BaseFlashblocks
19869 | SubscriberStreamSource::OpPendingFlashblocks => unreachable!(),
19870 #[cfg(feature = "raw-flashblocks-json")]
19871 SubscriberStreamSource::ExternalFlashblockUpdates => {
19872 "Flashblocks reconnect cannot own an application-managed source"
19873 }
19874 })),
19875 }
19876}
19877
19878fn aggregate_interests<N: Network>(
19879 base: &[ReactiveInterest<N>],
19880 owned: &[OwnedSubscriberInterests<N>],
19881) -> Vec<ReactiveInterest<N>> {
19882 base.iter()
19883 .cloned()
19884 .chain(
19885 owned
19886 .iter()
19887 .flat_map(|entry| entry.interests.iter().cloned()),
19888 )
19889 .collect()
19890}
19891
19892fn stream_terminated_error(source: &SubscriberStreamSource) -> SubscriberError {
19893 SubscriberError::Provider(format!(
19894 "Alloy subscriber {} stream terminated before the subscriber was stopped",
19895 source.label()
19896 ))
19897}
19898
19899fn reconnect_attempts_exhausted(attempts: usize, config: &SubscriberReconnectConfig) -> bool {
19900 config
19901 .max_attempts
19902 .is_some_and(|max_attempts| attempts >= max_attempts)
19903}
19904
19905fn next_reconnect_delay(current: Duration, max: Duration) -> Duration {
19906 if current.is_zero() {
19907 return current;
19908 }
19909 current.checked_mul(2).unwrap_or(max).min(max)
19910}
19911
19912fn should_dedupe_record<N: Network>(record: &ReactiveInputRecord<N>) -> bool {
19913 match &record.input {
19914 ReactiveInput::Log(log) => {
19915 is_canonical_status(&record.context.chain_status) && !log.removed
19916 }
19917 ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_) => true,
19918 ReactiveInput::FullBlock(_) | ReactiveInput::PendingTx(_) => false,
19919 }
19920}
19921
19922#[cfg(test)]
19923mod subscriber_helper_tests {
19924 use super::*;
19925 use alloy_json_rpc::{RequestPacket, ResponsePacket};
19926 use alloy_provider::ProviderBuilder;
19927 use alloy_rpc_client::RpcClient;
19928 use alloy_transport::{TransportError, TransportFut, mock::Asserter};
19929 use std::task::{Context, Poll};
19930 use tower::Service;
19931
19932 #[derive(Clone, Debug)]
19933 struct NeverRespondingTransport;
19934
19935 impl Service<RequestPacket> for NeverRespondingTransport {
19936 type Response = ResponsePacket;
19937 type Error = TransportError;
19938 type Future = TransportFut<'static>;
19939
19940 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
19941 Poll::Ready(Ok(()))
19942 }
19943
19944 fn call(&mut self, _request: RequestPacket) -> Self::Future {
19945 Box::pin(futures::future::pending())
19946 }
19947 }
19948
19949 fn indexed_flashblock(transaction_hash: B256, state_root: B256) -> BaseFlashblockWirePayload {
19950 BaseFlashblockWirePayload::Indexed(BaseFlashblockPayload {
19951 payload_id: FixedBytes::repeat_byte(0x11),
19952 index: 0,
19953 base: Some(BaseFlashblockBase {
19954 parent_hash: B256::repeat_byte(100),
19955 block_number: 101,
19956 timestamp: 1_700_000_101,
19957 gas_limit: Some(30_000_000),
19958 base_fee_per_gas: Some(7),
19959 beneficiary: Some(Address::repeat_byte(0xcb)),
19960 prevrandao: Some(B256::repeat_byte(0x77)),
19961 }),
19962 diff: BaseFlashblockDiff {
19963 state_root,
19964 block_hash: B256::ZERO,
19965 transactions: vec![serde_json::Value::String(format!("{transaction_hash:#x}"))],
19966 transactions_root: None,
19967 },
19968 metadata: None,
19969 })
19970 }
19971
19972 fn base_flashblock_event(payload: BaseFlashblockWirePayload) -> SubscriberEvent<Ethereum> {
19973 SubscriberEvent::BaseFlashblockTimed {
19974 payload,
19975 timing: FlashblockIngressTiming::new(Instant::now()),
19976 }
19977 }
19978
19979 #[test]
19980 fn duplicate_flashblock_transaction_membership_is_rejected() {
19981 let transaction = format!("{:#x}", B256::repeat_byte(0x41));
19982 let transactions = vec![
19983 serde_json::Value::String(transaction.clone()),
19984 serde_json::Value::String(transaction),
19985 ];
19986 assert!(matches!(
19987 flashblock_transaction_hashes(&transactions),
19988 Err(SubscriberError::Provider(ref message)) if message.contains("duplicate")
19989 ));
19990 }
19991
19992 #[test]
19993 fn conflicting_duplicate_indexed_flashblock_is_rejected() {
19994 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19995 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19996 provider,
19997 SubscriberMode::PubSub,
19998 SubscriberConfig::default(),
19999 )
20000 .with_provider_ref(ProviderRef::new("base-paid", 7));
20001 subscriber.chain_id = Some(8_453);
20002
20003 subscriber
20004 .accept_base_flashblock(indexed_flashblock(
20005 B256::repeat_byte(0x41),
20006 B256::repeat_byte(0xa1),
20007 ))
20008 .expect("first indexed preview");
20009 assert!(matches!(
20010 subscriber.accept_base_flashblock(indexed_flashblock(
20011 B256::repeat_byte(0x42),
20012 B256::repeat_byte(0xa2),
20013 )),
20014 Err(SubscriberError::Provider(ref message))
20015 if message.contains("conflicting duplicate")
20016 ));
20017 }
20018
20019 #[tokio::test]
20020 async fn duplicate_index_with_changed_commitment_is_rejected() {
20021 let transaction = B256::repeat_byte(0x41);
20022 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20023 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20024 provider,
20025 SubscriberMode::PubSub,
20026 SubscriberConfig::default(),
20027 )
20028 .with_provider_ref(ProviderRef::new("base-paid", 7));
20029 subscriber.chain_id = Some(8_453);
20030
20031 subscriber
20032 .normalize_flashblock_event(base_flashblock_event(indexed_flashblock(
20033 transaction,
20034 B256::repeat_byte(0xa1),
20035 )))
20036 .await
20037 .expect("first indexed preview");
20038
20039 let BaseFlashblockWirePayload::Indexed(mut conflicting) =
20040 indexed_flashblock(transaction, B256::repeat_byte(0xa1))
20041 else {
20042 unreachable!()
20043 };
20044 conflicting.diff.state_root = B256::repeat_byte(0xbb);
20045 assert!(matches!(
20046 subscriber
20047 .normalize_flashblock_event(base_flashblock_event(
20048 BaseFlashblockWirePayload::Indexed(conflicting),
20049 ))
20050 .await,
20051 Err(SubscriberError::Provider(ref message))
20052 if message.contains("conflicting duplicate indexed Flashblock content")
20053 ));
20054 }
20055
20056 #[tokio::test]
20057 async fn indexed_gap_recovery_seeds_later_cumulative_membership() {
20058 let transaction_a = B256::repeat_byte(0x41);
20059 let transaction_b = B256::repeat_byte(0x42);
20060 let transaction_c = B256::repeat_byte(0x43);
20061 let transaction_d = B256::repeat_byte(0x44);
20062 let asserter = Asserter::new();
20063 asserter.push_success(&100_u64);
20064 let pending = rpc_block(101, B256::ZERO).with_transactions(
20065 alloy_network::primitives::BlockTransactions::Hashes(vec![
20066 transaction_a,
20067 transaction_b,
20068 transaction_c,
20069 ]),
20070 );
20071 asserter.push_success(&Some(pending));
20072 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
20073 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20074 provider,
20075 SubscriberMode::PubSub,
20076 SubscriberConfig::default(),
20077 )
20078 .with_provider_ref(ProviderRef::new("base-paid", 7));
20079 subscriber.chain_id = Some(8_453);
20080
20081 subscriber
20082 .normalize_flashblock_event(base_flashblock_event(indexed_flashblock(
20083 transaction_a,
20084 B256::repeat_byte(0xa1),
20085 )))
20086 .await
20087 .expect("index zero preview");
20088 let BaseFlashblockWirePayload::Indexed(mut gap) =
20089 indexed_flashblock(transaction_c, B256::repeat_byte(0xa3))
20090 else {
20091 unreachable!()
20092 };
20093 gap.index = 2;
20094 gap.base = None;
20095 gap.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
20096 subscriber
20097 .normalize_flashblock_event(base_flashblock_event(BaseFlashblockWirePayload::Indexed(
20098 gap,
20099 )))
20100 .await
20101 .expect("the missing index is recovered from pending state");
20102
20103 let BaseFlashblockWirePayload::Indexed(mut next) =
20104 indexed_flashblock(transaction_d, B256::repeat_byte(0xa4))
20105 else {
20106 unreachable!()
20107 };
20108 next.index = 3;
20109 next.base = None;
20110 next.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
20111 let (next, recover) = subscriber
20112 .accept_base_flashblock(BaseFlashblockWirePayload::Indexed(next))
20113 .expect("the next diff extends the recovered cumulative set");
20114 assert!(!recover);
20115 assert_eq!(
20116 next.transaction_hashes,
20117 vec![transaction_a, transaction_b, transaction_c, transaction_d]
20118 );
20119 }
20120
20121 #[tokio::test]
20122 async fn unrecoverable_indexed_gap_revokes_the_generation() {
20123 let asserter = Asserter::new();
20124 asserter.push_success(&100_u64);
20125 asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(0x64))));
20126 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
20127 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20128 provider,
20129 SubscriberMode::PubSub,
20130 SubscriberConfig {
20131 preconfirmations: PreconfirmationMode::Preferred,
20132 ..SubscriberConfig::default()
20133 },
20134 )
20135 .with_provider_ref(ProviderRef::new("base-paid", 7));
20136 subscriber.chain_id = Some(8_453);
20137
20138 subscriber
20139 .normalize_flashblock_event(base_flashblock_event(indexed_flashblock(
20140 B256::repeat_byte(0x41),
20141 B256::repeat_byte(0xa1),
20142 )))
20143 .await
20144 .expect("index zero preview");
20145 let BaseFlashblockWirePayload::Indexed(mut gap) =
20146 indexed_flashblock(B256::repeat_byte(0x43), B256::repeat_byte(0xa3))
20147 else {
20148 unreachable!()
20149 };
20150 gap.index = 2;
20151 gap.base = None;
20152 gap.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
20153 let event = subscriber
20154 .normalize_flashblock_event(base_flashblock_event(BaseFlashblockWirePayload::Indexed(
20155 gap,
20156 )))
20157 .await
20158 .expect("preferred mode fails closed without pending recovery")
20159 .expect("generation invalidation is observable");
20160 assert!(matches!(event, SubscriberEvent::FlashblockInvalidated));
20161 assert!(subscriber.latest_preconfirmation.is_none());
20162 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 8);
20163 }
20164
20165 #[test]
20166 fn base_flashblock_wire_decodes_cumulative_block_shape() {
20167 let payload: BaseFlashblockWirePayload = serde_json::from_str(
20168 r#"{
20169 "hash":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
20170 "number":"0x2ef403b",
20171 "parentHash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
20172 "stateRoot":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
20173 "timestamp":"0x6a68dd59",
20174 "transactions":[]
20175 }"#,
20176 )
20177 .expect("decode current Base newFlashblocks shape");
20178 let BaseFlashblockWirePayload::Block(payload) = payload else {
20179 panic!("expected cumulative block-shaped payload")
20180 };
20181 assert_eq!(payload.number, 49_233_979);
20182 assert_eq!(payload.timestamp, 1_785_257_305);
20183 assert_eq!(payload.hash, B256::repeat_byte(0xaa));
20184 assert_eq!(payload.parent_hash, B256::repeat_byte(0xbb));
20185 assert_eq!(payload.state_root, B256::repeat_byte(0xcc));
20186 }
20187
20188 #[tokio::test]
20189 async fn zero_hash_pending_log_waits_for_the_preview_containing_its_transaction() {
20190 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20191 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20192 provider,
20193 SubscriberMode::PubSub,
20194 SubscriberConfig::default(),
20195 )
20196 .with_provider_ref(ProviderRef::new("base-paid", 7));
20197 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
20198 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
20199 local_matcher: None,
20200 route_key: None,
20201 })];
20202 subscriber.interests = subscriber.base_interests.clone();
20203
20204 let first: BaseFlashblockWirePayload = serde_json::from_str(
20205 r#"{
20206 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
20207 "number":"0x65",
20208 "parentHash":"0x6464646464646464646464646464646464646464646464646464646464646464",
20209 "stateRoot":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
20210 "transactionsRoot":"0x1111111111111111111111111111111111111111111111111111111111111111",
20211 "timestamp":"0x6553f165",
20212 "transactions":["0x4141414141414141414141414141414141414141414141414141414141414141"]
20213 }"#,
20214 )
20215 .expect("decode first cumulative preview");
20216 subscriber
20217 .normalize_flashblock_event(base_flashblock_event(first))
20218 .await
20219 .expect("first preview is accepted");
20220
20221 let mut second_log = rpc_log(false);
20222 second_log.block_hash = Some(B256::ZERO);
20223 second_log.block_number = Some(102);
20224 second_log.block_timestamp = Some(1_700_000_102);
20225 second_log.transaction_hash = Some(B256::repeat_byte(0x42));
20226 second_log.transaction_index = Some(0);
20227 second_log.log_index = Some(0);
20228
20229 let pending_log_ingress = Instant::now() - Duration::from_millis(25);
20230 let before_preview = subscriber
20231 .normalize_flashblock_event(SubscriberEvent::BasePendingLogTimed {
20232 source_id: 0,
20233 log: second_log,
20234 timing: FlashblockIngressTiming::new(pending_log_ingress),
20235 })
20236 .await
20237 .expect("a zero-hash log for the next block must be buffered");
20238 assert!(before_preview.is_none());
20239
20240 let second: BaseFlashblockWirePayload = serde_json::from_str(
20241 r#"{
20242 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
20243 "number":"0x66",
20244 "parentHash":"0x6565656565656565656565656565656565656565656565656565656565656565",
20245 "stateRoot":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
20246 "transactionsRoot":"0x2222222222222222222222222222222222222222222222222222222222222222",
20247 "timestamp":"0x6553f166",
20248 "transactions":["0x4242424242424242424242424242424242424242424242424242424242424242"]
20249 }"#,
20250 )
20251 .expect("decode second cumulative preview");
20252 let event = subscriber
20253 .normalize_flashblock_event(base_flashblock_event(second))
20254 .await
20255 .expect("second preview is accepted")
20256 .expect("the matching buffered log is released");
20257 let SubscriberEvent::PreconfirmedLogs {
20258 flashblock,
20259 logs,
20260 timing,
20261 } = event
20262 else {
20263 panic!("expected a preconfirmed log batch")
20264 };
20265 assert_eq!(timing.source_ingress(), pending_log_ingress);
20266 assert_eq!(flashblock.block_number, 102);
20267 assert_ne!(flashblock.content_hash, B256::ZERO);
20268 assert_eq!(flashblock.partial_block_hash, None);
20269 assert_eq!(logs.len(), 1);
20270 assert_eq!(logs[0].transaction_hash, Some(B256::repeat_byte(0x42)));
20271 assert_eq!(logs[0].block_hash, Some(flashblock.content_hash));
20272 }
20273
20274 #[test]
20275 fn flashblock_endpoints_certify_canonical_heads_instead_of_trusting_newheads() {
20276 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20277 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20278 provider,
20279 SubscriberMode::PubSub,
20280 SubscriberConfig {
20281 preconfirmations: PreconfirmationMode::Required,
20282 ..SubscriberConfig::default()
20283 },
20284 )
20285 .with_provider_ref(ProviderRef::new("base-paid", 7));
20286 subscriber.chain_id = Some(8_453);
20287 subscriber.interests = vec![ReactiveInterest::Blocks(BlockInterest::default())];
20288
20289 let sources = subscriber.pubsub_stream_sources();
20290 assert!(
20291 sources
20292 .iter()
20293 .any(|source| matches!(source, SubscriberStreamSource::CanonicalHeadPolling))
20294 );
20295 assert!(
20296 !sources
20297 .iter()
20298 .any(|source| matches!(source, SubscriberStreamSource::PubSubBlockHeaders))
20299 );
20300 }
20301
20302 #[test]
20303 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
20304 fn external_flashblocks_keep_normal_canonical_pubsub_sources_on_any_chain() {
20305 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20306 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20307 provider,
20308 SubscriberMode::PubSub,
20309 SubscriberConfig {
20310 preconfirmations: PreconfirmationMode::Required,
20311 ..SubscriberConfig::default()
20312 },
20313 );
20314 subscriber
20315 .configure_external_flashblock_updates(ProviderRef::new("raw-json", 4))
20316 .expect("configure external source");
20317 subscriber.chain_id = Some(1);
20318 subscriber.base_interests = vec![
20319 ReactiveInterest::Blocks(BlockInterest::default()),
20320 log_interest_matching_rpc_log(),
20321 ];
20322 subscriber.interests = subscriber.base_interests.clone();
20323
20324 let pubsub = subscriber.pubsub_stream_sources();
20325 assert!(
20326 pubsub
20327 .iter()
20328 .any(|source| matches!(source, SubscriberStreamSource::PubSubBlockHeaders))
20329 );
20330 assert!(
20331 pubsub
20332 .iter()
20333 .any(|source| matches!(source, SubscriberStreamSource::PubSubLog { .. }))
20334 );
20335 assert!(pubsub.iter().all(|source| !matches!(
20336 source,
20337 SubscriberStreamSource::BaseFlashblocks
20338 | SubscriberStreamSource::BasePendingLog { .. }
20339 | SubscriberStreamSource::OpPendingFlashblocks
20340 | SubscriberStreamSource::CanonicalHeadPolling
20341 )));
20342 assert!(
20343 subscriber
20344 .polling_stream_sources()
20345 .iter()
20346 .all(|source| { !matches!(source, SubscriberStreamSource::OpPendingFlashblocks) })
20347 );
20348 assert!(
20349 subscriber
20350 .capabilities()
20351 .supports(SubscriberCapability::Preconfirmations)
20352 );
20353 }
20354
20355 #[test]
20356 #[cfg(feature = "raw-flashblocks-json")]
20357 fn external_flashblocks_are_rejected_when_preconfirmations_are_disabled() {
20358 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20359 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20360 provider,
20361 SubscriberMode::PubSub,
20362 SubscriberConfig::default(),
20363 );
20364 subscriber
20365 .configure_external_flashblock_updates(ProviderRef::new("raw-json", 4))
20366 .expect("configure external source");
20367 subscriber.chain_id = Some(1);
20368
20369 assert!(matches!(
20370 subscriber.validate_flashblocks_setup(),
20371 Err(SubscriberError::InvalidConfig(message))
20372 if message.contains("require preconfirmations")
20373 ));
20374 }
20375
20376 #[tokio::test]
20377 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
20378 async fn external_flashblocks_configuration_is_rejected_after_registration_starts() {
20379 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20380 let mut fresh = AlloySubscriber::<_, Ethereum>::new(
20381 provider,
20382 SubscriberMode::PubSub,
20383 SubscriberConfig {
20384 preconfirmations: PreconfirmationMode::Preferred,
20385 ..SubscriberConfig::default()
20386 },
20387 );
20388 fresh
20389 .configure_external_flashblock_updates(ProviderRef::new("raw-json", 4))
20390 .expect("construction-time external source");
20391
20392 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20393 let mut started = AlloySubscriber::<_, Ethereum>::new(
20394 provider,
20395 SubscriberMode::PubSub,
20396 SubscriberConfig {
20397 preconfirmations: PreconfirmationMode::Preferred,
20398 ..SubscriberConfig::default()
20399 },
20400 )
20401 .with_provider_ref(ProviderRef::new("canonical", 3));
20402 started.chain_id = Some(8_453);
20403 started
20404 .register_interests(&[log_interest_matching_rpc_log()])
20405 .await
20406 .expect("register canonical topology");
20407
20408 assert!(matches!(
20409 started.configure_external_flashblock_updates(ProviderRef::new("raw-json", 4)),
20410 Err(SubscriberError::InvalidConfig(message))
20411 if message.contains("before subscriber registration")
20412 ));
20413 }
20414
20415 #[tokio::test]
20416 #[cfg(feature = "raw-flashblocks-json")]
20417 async fn external_flashblocks_preflight_performs_no_flashblocks_rpc() {
20418 let asserter = Asserter::new();
20419 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20420 let source = ProviderRef::new("raw-json", 4);
20421 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20422 provider,
20423 SubscriberMode::PubSub,
20424 SubscriberConfig {
20425 preconfirmations: PreconfirmationMode::Required,
20426 ..SubscriberConfig::default()
20427 },
20428 );
20429 subscriber
20430 .configure_external_flashblock_updates(source.clone())
20431 .expect("configure external source");
20432 subscriber.chain_id = Some(1);
20433 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20434 subscriber.interests = subscriber.base_interests.clone();
20435 let desired = subscriber.pubsub_stream_sources();
20436 let mut streams = SubscriberStreams::new();
20437 for source in desired {
20438 streams.push(source, stream::pending().boxed());
20439 }
20440 subscriber.state = AlloySubscriberState::Active(streams);
20441 subscriber.sources_dirty = false;
20442
20443 let preflight = subscriber
20444 .establish_flashblocks_preflight(1)
20445 .await
20446 .expect("external source preflight");
20447 assert_eq!(preflight.provider(), &source);
20448 assert_eq!(preflight.delivery(), FlashblocksDelivery::ExternalUpdates);
20449 assert_eq!(preflight.pending_log_subscriptions(), 0);
20450 assert_eq!(subscriber.flashblocks_rpc_metrics().total_requests(), 0);
20451 assert!(asserter.read_q().is_empty());
20452 }
20453
20454 #[tokio::test]
20455 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
20456 async fn bounded_external_channel_survives_subscriber_move_and_closure_keeps_canonical_stream()
20457 {
20458 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20459 let source = ProviderRef::new("raw-json", 4);
20460 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20461 provider,
20462 SubscriberMode::PubSub,
20463 SubscriberConfig {
20464 preconfirmations: PreconfirmationMode::Preferred,
20465 ..SubscriberConfig::default()
20466 },
20467 );
20468 subscriber
20469 .configure_external_flashblock_updates(source.clone())
20470 .expect("configure external source");
20471 subscriber.chain_id = Some(1);
20472 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20473 subscriber.interests = subscriber.base_interests.clone();
20474 let filter = subscriber.log_stream_filters().remove(0);
20475 let source_id = subscriber.log_source_id(&filter);
20476 let mut streams = SubscriberStreams::new();
20477 streams.push(
20478 SubscriberStreamSource::PubSubLog {
20479 id: source_id,
20480 filter,
20481 },
20482 stream::pending().boxed(),
20483 );
20484 subscriber.state = AlloySubscriberState::Active(streams);
20485 subscriber.sources_dirty = false;
20486
20487 let sender = subscriber
20488 .open_external_flashblock_update_channel(2)
20489 .expect("bounded external queue");
20490 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
20491 let update_stream = subscriber
20492 .connect_source_stream(external.clone())
20493 .await
20494 .expect("attach receiver as subscriber source");
20495 subscriber.install_source_stream(external, update_stream);
20496 subscriber.sources_dirty = false;
20497
20498 let mut adapter = RawJsonFlashblocksAdapter::new(source);
20499 let frame = br#"{
20500 "payload_id":"0x1111111111111111",
20501 "index":0,
20502 "base":{
20503 "parent_hash":"0x0606060606060606060606060606060606060606060606060606060606060606",
20504 "block_number":"0x7",
20505 "timestamp":"0x6553f107"
20506 },
20507 "diff":{
20508 "state_root":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
20509 "block_hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
20510 "transactions":["0x01"]
20511 },
20512 "metadata":{
20513 "block_number":7,
20514 "receipts":{
20515 "0x5fe7f977e71dba2ea1a68e21057beebb9be2ac30c6410aa38d4f3fbe41dcffd2":{
20516 "logs":[{
20517 "address":"0x4242424242424242424242424242424242424242",
20518 "topics":["0x0101010101010101010101010101010101010101010101010101010101010101"],
20519 "data":"0x"
20520 }]
20521 }
20522 }
20523 }
20524 }"#;
20525 let update = adapter
20526 .ingest_json(frame)
20527 .expect("valid raw update")
20528 .expect("snapshot update");
20529 let valid_update = update.clone();
20530 let sending = {
20531 let sender = sender.clone();
20532 tokio::spawn(async move { sender.send(update).await })
20533 };
20534
20535 let preview = subscriber
20536 .next_scoped_batch()
20537 .await
20538 .expect("poll preview")
20539 .expect("preview batch");
20540 assert_eq!(preview.records().len(), 1);
20541 assert!(preview.records()[0].scope().is_preconfirmed());
20542 assert_eq!(
20543 preview.records()[0].context.source,
20544 InputSource::Flashblocks
20545 );
20546 assert!(subscriber.latest_preconfirmation.is_some());
20547 assert_eq!(sending.await.expect("sender task"), Ok(()));
20548
20549 let mut invalid_update = valid_update.clone();
20550 let FlashblockUpdate::Snapshot(snapshot) = &mut invalid_update else {
20551 unreachable!("fixture is a snapshot")
20552 };
20553 snapshot.logs[0].block_hash = Some(B256::repeat_byte(0xee));
20554 let rejecting = {
20555 let sender = sender.clone();
20556 tokio::spawn(async move { sender.send(invalid_update).await })
20557 };
20558 let rejected = subscriber
20559 .next_scoped_batch()
20560 .await
20561 .expect("preferred mode keeps polling")
20562 .expect("rejected update invalidation");
20563 assert!(rejected.preconfirmation_invalidated());
20564 assert!(rejected.records().is_empty());
20565 assert!(subscriber.latest_preconfirmation.is_none());
20566 assert_eq!(
20567 rejecting.await.expect("sender task"),
20568 Err(FlashblockUpdateChannelError::Rejected)
20569 );
20570 subscriber
20571 .ingest_flashblock_update(valid_update)
20572 .expect("rejected generation is ignored thereafter");
20573 assert!(subscriber.latest_preconfirmation.is_none());
20574
20575 let _reset = adapter
20576 .reset(ProviderRef::new("raw-json", 5))
20577 .expect("advance rejected source generation");
20578 let recovered_update = adapter
20579 .ingest_json(frame)
20580 .expect("valid replacement generation")
20581 .expect("replacement snapshot update");
20582 let recovering = {
20583 let sender = sender.clone();
20584 tokio::spawn(async move { sender.send(recovered_update).await })
20585 };
20586 let recovered = subscriber
20587 .next_scoped_batch()
20588 .await
20589 .expect("poll replacement generation")
20590 .expect("replacement preview batch");
20591 assert_eq!(recovered.records().len(), 1);
20592 assert!(matches!(
20593 &recovered.records()[0].context.chain_status,
20594 ChainStatus::Preconfirmed { flashblock }
20595 if flashblock.provider == ProviderRef::new("raw-json", 5)
20596 ));
20597 assert_eq!(recovering.await.expect("sender task"), Ok(()));
20598
20599 drop(sender);
20600 let invalidation = subscriber
20601 .next_scoped_batch()
20602 .await
20603 .expect("poll channel closure")
20604 .expect("closure invalidation");
20605 assert!(invalidation.preconfirmation_invalidated());
20606 assert!(invalidation.records().is_empty());
20607 assert!(subscriber.latest_preconfirmation.is_none());
20608 assert!(matches!(
20609 &subscriber.state,
20610 AlloySubscriberState::Active(streams)
20611 if streams.entries.iter().any(|entry| matches!(
20612 entry.source,
20613 SubscriberStreamSource::PubSubLog { id, .. } if id == source_id
20614 ))
20615 ));
20616 }
20617
20618 #[tokio::test]
20619 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
20620 async fn required_external_channel_closure_fails_the_subscriber_closed() {
20621 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20622 let source = ProviderRef::new("raw-json", 4);
20623 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20624 provider,
20625 SubscriberMode::PubSub,
20626 SubscriberConfig {
20627 preconfirmations: PreconfirmationMode::Required,
20628 ..SubscriberConfig::default()
20629 },
20630 );
20631 subscriber
20632 .configure_external_flashblock_updates(source)
20633 .expect("configure external source");
20634 subscriber.chain_id = Some(1);
20635 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20636 subscriber.interests = subscriber.base_interests.clone();
20637 subscriber.state = AlloySubscriberState::Active(SubscriberStreams::new());
20638 subscriber.sources_dirty = false;
20639
20640 let sender = subscriber
20641 .open_external_flashblock_update_channel(1)
20642 .expect("bounded external queue");
20643 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
20644 let update_stream = subscriber
20645 .connect_source_stream(external.clone())
20646 .await
20647 .expect("attach receiver as subscriber source");
20648 subscriber.install_source_stream(external, update_stream);
20649 subscriber.sources_dirty = false;
20650 drop(sender);
20651
20652 assert!(matches!(
20653 subscriber.next_scoped_batch().await,
20654 Err(SubscriberError::Provider(ref message))
20655 if message.contains("required external Flashblock update channel closed")
20656 ));
20657 }
20658
20659 #[tokio::test]
20660 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
20661 async fn required_external_channel_rejects_a_queued_malformed_update() {
20662 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20663 let source = ProviderRef::new("raw-json", 4);
20664 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20665 provider,
20666 SubscriberMode::PubSub,
20667 SubscriberConfig {
20668 preconfirmations: PreconfirmationMode::Required,
20669 ..SubscriberConfig::default()
20670 },
20671 );
20672 subscriber
20673 .configure_external_flashblock_updates(source.clone())
20674 .expect("configure external source");
20675 subscriber.chain_id = Some(1);
20676 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20677 subscriber.interests = subscriber.base_interests.clone();
20678 subscriber.state = AlloySubscriberState::Active(SubscriberStreams::new());
20679 subscriber.sources_dirty = false;
20680
20681 let sender = subscriber
20682 .open_external_flashblock_update_channel(1)
20683 .expect("bounded external queue");
20684 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
20685 let update_stream = subscriber
20686 .connect_source_stream(external.clone())
20687 .await
20688 .expect("attach receiver as subscriber source");
20689 subscriber.install_source_stream(external, update_stream);
20690 subscriber.sources_dirty = false;
20691
20692 let mut adapter = RawJsonFlashblocksAdapter::new(source);
20693 let mut update = adapter
20694 .ingest_json(
20695 br#"{
20696 "payload_id":"0x1111111111111111",
20697 "index":0,
20698 "base":{
20699 "parent_hash":"0x0606060606060606060606060606060606060606060606060606060606060606",
20700 "block_number":"0x7",
20701 "timestamp":"0x6553f107"
20702 },
20703 "diff":{
20704 "state_root":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
20705 "block_hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
20706 "transactions":[]
20707 },
20708 "metadata":{"block_number":7,"receipts":{}}
20709 }"#,
20710 )
20711 .expect("valid raw frame")
20712 .expect("snapshot update");
20713 let FlashblockUpdate::Snapshot(snapshot) = &mut update else {
20714 unreachable!("fixture is a snapshot")
20715 };
20716 snapshot.flashblock.content_hash = B256::ZERO;
20717 let sending = tokio::spawn(async move { sender.send(update).await });
20718
20719 assert!(matches!(
20720 subscriber.next_scoped_batch().await,
20721 Err(SubscriberError::Provider(ref message))
20722 if message.contains("content commitment is invalid")
20723 ));
20724 assert_eq!(
20725 sending.await.expect("sender task"),
20726 Err(FlashblockUpdateChannelError::Rejected)
20727 );
20728 }
20729
20730 #[tokio::test]
20731 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
20732 async fn bounded_external_channel_reports_capacity_rejection_and_accepts_a_new_generation() {
20733 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20734 let source = ProviderRef::new("raw-json", 4);
20735 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20736 provider,
20737 SubscriberMode::PubSub,
20738 SubscriberConfig {
20739 preconfirmations: PreconfirmationMode::Preferred,
20740 max_pending_records: 1,
20741 ..SubscriberConfig::default()
20742 },
20743 );
20744 subscriber
20745 .configure_external_flashblock_updates(source.clone())
20746 .expect("configure external source");
20747 subscriber.chain_id = Some(1);
20748 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20749 subscriber.interests = subscriber.base_interests.clone();
20750 subscriber.state = AlloySubscriberState::Active(SubscriberStreams::new());
20751 subscriber.sources_dirty = false;
20752
20753 let sender = subscriber
20754 .open_external_flashblock_update_channel(1)
20755 .expect("bounded external queue");
20756 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
20757 let update_stream = subscriber
20758 .connect_source_stream(external.clone())
20759 .await
20760 .expect("attach receiver as subscriber source");
20761 subscriber.install_source_stream(external, update_stream);
20762 subscriber.sources_dirty = false;
20763
20764 let first_frame = br#"{
20765 "payload_id":"0x1111111111111111",
20766 "index":0,
20767 "base":{
20768 "parent_hash":"0x0606060606060606060606060606060606060606060606060606060606060606",
20769 "block_number":"0x7",
20770 "timestamp":"0x6553f107"
20771 },
20772 "diff":{
20773 "state_root":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
20774 "block_hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
20775 "transactions":["0x01"]
20776 },
20777 "metadata":{
20778 "block_number":7,
20779 "receipts":{
20780 "0x5fe7f977e71dba2ea1a68e21057beebb9be2ac30c6410aa38d4f3fbe41dcffd2":{
20781 "logs":[{
20782 "address":"0x4242424242424242424242424242424242424242",
20783 "topics":["0x0101010101010101010101010101010101010101010101010101010101010101"],
20784 "data":"0x"
20785 }]
20786 }
20787 }
20788 }
20789 }"#;
20790 let second_frame = br#"{
20791 "payload_id":"0x1111111111111111",
20792 "index":1,
20793 "diff":{
20794 "state_root":"0xabababababababababababababababababababababababababababababababab",
20795 "block_hash":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
20796 "transactions":["0x02"]
20797 },
20798 "metadata":{
20799 "block_number":7,
20800 "receipts":{
20801 "0xf2ee15ea639b73fa3db9b34a245bdfa015c260c598b211bf05a1ecc4b3e3b4f2":{
20802 "logs":[
20803 {"address":"0x4444444444444444444444444444444444444444","topics":[],"data":"0x"},
20804 {"address":"0x4545454545454545454545454545454545454545","topics":[],"data":"0x"}
20805 ]
20806 }
20807 }
20808 }
20809 }"#;
20810 let mut adapter = RawJsonFlashblocksAdapter::new(source);
20811 let first = adapter
20812 .ingest_json(first_frame)
20813 .expect("valid first frame")
20814 .expect("first snapshot");
20815 let first_send = {
20816 let sender = sender.clone();
20817 tokio::spawn(async move { sender.send(first).await })
20818 };
20819 let first_batch = subscriber
20820 .next_scoped_batch()
20821 .await
20822 .expect("poll first preview")
20823 .expect("first preview batch");
20824 assert_eq!(first_batch.records().len(), 1);
20825 assert_eq!(first_send.await.expect("sender task"), Ok(()));
20826
20827 let oversized = adapter
20828 .ingest_json(second_frame)
20829 .expect("valid oversized delta")
20830 .expect("oversized standardized snapshot");
20831 let rejected_send = {
20832 let sender = sender.clone();
20833 tokio::spawn(async move { sender.send(oversized).await })
20834 };
20835 let invalidation = subscriber
20836 .next_scoped_batch()
20837 .await
20838 .expect("poll capacity rejection")
20839 .expect("capacity invalidation batch");
20840 assert!(invalidation.preconfirmation_invalidated());
20841 assert_eq!(
20842 rejected_send.await.expect("sender task"),
20843 Err(FlashblockUpdateChannelError::Rejected)
20844 );
20845 assert_eq!(subscriber.rejected_external_flashblock_generation, None);
20846
20847 let _ = adapter
20848 .reset(ProviderRef::new("raw-json", 5))
20849 .expect("advance after local capacity rejection");
20850 let recovered = adapter
20851 .ingest_json(first_frame)
20852 .expect("valid recovered frame")
20853 .expect("recovered snapshot");
20854 let recovered_send = {
20855 let sender = sender.clone();
20856 tokio::spawn(async move { sender.send(recovered).await })
20857 };
20858 let recovered_batch = subscriber
20859 .next_scoped_batch()
20860 .await
20861 .expect("poll recovered generation")
20862 .expect("recovered preview batch");
20863 assert_eq!(recovered_batch.records().len(), 1);
20864 assert!(matches!(
20865 &recovered_batch.records()[0].context.chain_status,
20866 ChainStatus::Preconfirmed { flashblock }
20867 if flashblock.provider == ProviderRef::new("raw-json", 5)
20868 ));
20869 assert_eq!(recovered_send.await.expect("sender task"), Ok(()));
20870 }
20871
20872 #[tokio::test]
20873 async fn certified_canonical_heads_are_deduplicated_and_reject_placeholder_hashes() {
20874 let asserter = Asserter::new();
20875 let certified = rpc_block(101, B256::repeat_byte(0x65));
20876 asserter.push_success(&Some(certified.clone()));
20877 asserter.push_success(&Some(certified));
20878 asserter.push_success(&Some(rpc_block(102, B256::ZERO)));
20879 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
20880 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20881 provider,
20882 SubscriberMode::PubSub,
20883 SubscriberConfig::default(),
20884 );
20885
20886 assert!(matches!(
20887 subscriber
20888 .fetch_certified_canonical_head()
20889 .await
20890 .expect("first certified head"),
20891 Some(SubscriberEvent::BlockHeader { .. })
20892 ));
20893 assert!(
20894 subscriber
20895 .fetch_certified_canonical_head()
20896 .await
20897 .expect("duplicate certified head")
20898 .is_none()
20899 );
20900 assert!(matches!(
20901 subscriber.fetch_certified_canonical_head().await,
20902 Err(SubscriberError::Provider(ref message))
20903 if message.contains("placeholder hash")
20904 ));
20905 }
20906
20907 #[tokio::test]
20908 async fn canonical_head_certification_times_out_a_silent_provider() {
20909 let provider =
20910 ProviderBuilder::new().connect_client(RpcClient::new(NeverRespondingTransport, true));
20911 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20912 provider,
20913 SubscriberMode::PubSub,
20914 SubscriberConfig {
20915 preconfirmations: PreconfirmationMode::Required,
20916 canonical_head_request_timeout: Duration::from_millis(10),
20917 ..SubscriberConfig::default()
20918 },
20919 );
20920 subscriber.chain_id = Some(8_453);
20921
20922 let result = tokio::time::timeout(
20923 Duration::from_millis(100),
20924 subscriber.fetch_certified_canonical_head(),
20925 )
20926 .await
20927 .expect("subscriber must bound a silent provider request");
20928 assert!(matches!(
20929 result,
20930 Err(SubscriberError::Provider(ref message))
20931 if message.contains("canonical head certification timed out")
20932 ));
20933 }
20934
20935 #[tokio::test]
20936 async fn optimism_canonical_head_is_the_exact_parent_of_pending() {
20937 let asserter = Asserter::new();
20938 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
20939 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20940 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20941 provider,
20942 SubscriberMode::PubSub,
20943 SubscriberConfig {
20944 preconfirmations: PreconfirmationMode::Required,
20945 ..SubscriberConfig::default()
20946 },
20947 );
20948 subscriber.chain_id = Some(10);
20949 subscriber.interests = vec![ReactiveInterest::Blocks(BlockInterest::default())];
20950
20951 let event = subscriber
20952 .fetch_certified_canonical_head()
20953 .await
20954 .expect("OP pending parent can be certified")
20955 .expect("the first certified parent is emitted");
20956 let SubscriberEvent::BlockHeader { header, .. } = event else {
20957 panic!("expected a certified canonical block header")
20958 };
20959 assert_eq!(header.number(), 100);
20960 assert_eq!(header.hash, B256::repeat_byte(0x64));
20961 assert_eq!(
20962 subscriber
20963 .flashblocks_rpc_metrics()
20964 .pending_block_requests(),
20965 1
20966 );
20967 assert_eq!(
20968 subscriber
20969 .flashblocks_rpc_metrics()
20970 .canonical_head_requests(),
20971 1
20972 );
20973 assert!(asserter.read_q().is_empty());
20974 }
20975
20976 #[test]
20977 fn optimism_uses_one_bounded_pending_state_stream() {
20978 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20979 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20980 provider,
20981 SubscriberMode::PubSub,
20982 SubscriberConfig {
20983 preconfirmations: PreconfirmationMode::Required,
20984 ..SubscriberConfig::default()
20985 },
20986 )
20987 .with_provider_ref(ProviderRef::new("op-paid", 11));
20988 subscriber.chain_id = Some(10);
20989 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
20990 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
20991 local_matcher: None,
20992 route_key: None,
20993 })];
20994 subscriber.interests = subscriber.base_interests.clone();
20995
20996 let sources = subscriber.pubsub_stream_sources();
20997 assert_eq!(
20998 sources
20999 .iter()
21000 .filter(|source| matches!(source, SubscriberStreamSource::OpPendingFlashblocks))
21001 .count(),
21002 1
21003 );
21004 assert!(sources.iter().all(|source| !matches!(
21005 source,
21006 SubscriberStreamSource::BaseFlashblocks | SubscriberStreamSource::BasePendingLog { .. }
21007 )));
21008 }
21009
21010 #[test]
21011 fn optimism_default_receipt_budget_reserves_every_fixed_sampler_method() {
21012 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21013 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21014 provider,
21015 SubscriberMode::PubSub,
21016 SubscriberConfig {
21017 preconfirmations: PreconfirmationMode::Required,
21018 ..SubscriberConfig::default()
21019 },
21020 );
21021 subscriber.chain_id = Some(10);
21022 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21023 subscriber.interests = subscriber.base_interests.clone();
21024
21025 assert_eq!(
21029 subscriber.pending_receipt_requests_per_second_capacity(),
21030 28
21031 );
21032 assert_eq!(subscriber.pending_receipt_requests_per_tick_capacity(), 7);
21033
21034 subscriber
21035 .interests
21036 .push(ReactiveInterest::Blocks(BlockInterest::default()));
21037 assert_eq!(
21038 subscriber.pending_receipt_requests_per_second_capacity(),
21039 24
21040 );
21041 assert_eq!(subscriber.pending_receipt_requests_per_tick_capacity(), 6);
21042 subscriber.interests.pop();
21043
21044 for _ in 0..4 {
21045 assert!(subscriber.reserve_flashblock_rpc_methods(3));
21046 assert_eq!(subscriber.pending_receipt_request_allowance(), 7);
21047 assert!(subscriber.reserve_flashblock_rpc_methods(7));
21048 }
21049 assert!(!subscriber.reserve_flashblock_rpc_methods(1));
21050 subscriber.reset_flashblock_tracking();
21051 assert!(
21052 !subscriber.reserve_flashblock_rpc_methods(1),
21053 "a reconnect must not reset an endpoint's rolling quota window"
21054 );
21055 }
21056
21057 #[test]
21058 fn flashblocks_config_rejects_a_zero_rpc_budget() {
21059 let config = SubscriberConfig {
21060 preconfirmations: PreconfirmationMode::Required,
21061 max_flashblock_rpc_requests_per_second: 0,
21062 ..SubscriberConfig::default()
21063 };
21064
21065 assert!(matches!(
21066 validate_subscriber_config(&config),
21067 Err(SubscriberError::InvalidConfig(
21068 "SubscriberConfig::max_flashblock_rpc_requests_per_second must be greater than zero"
21069 ))
21070 ));
21071 }
21072
21073 #[test]
21074 fn flashblocks_config_rejects_a_zero_canonical_head_request_timeout() {
21075 let config = SubscriberConfig {
21076 preconfirmations: PreconfirmationMode::Required,
21077 canonical_head_request_timeout: Duration::ZERO,
21078 ..SubscriberConfig::default()
21079 };
21080
21081 assert!(matches!(
21082 validate_subscriber_config(&config),
21083 Err(SubscriberError::InvalidConfig(
21084 "SubscriberConfig::canonical_head_request_timeout must be greater than zero"
21085 ))
21086 ));
21087 }
21088
21089 #[tokio::test]
21090 async fn optimism_preflight_rejects_a_budget_without_receipt_capacity() {
21091 let asserter = Asserter::new();
21092 asserter.push_success(&serde_json::json!(["flashblocksv1"]));
21093 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21094 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21095 provider,
21096 SubscriberMode::PubSub,
21097 SubscriberConfig {
21098 preconfirmations: PreconfirmationMode::Required,
21099 max_flashblock_rpc_requests_per_second: 15,
21102 ..SubscriberConfig::default()
21103 },
21104 )
21105 .with_provider_ref(ProviderRef::new("op-paid", 12));
21106 subscriber.chain_id = Some(10);
21107 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21108 subscriber.interests = subscriber.base_interests.clone();
21109 let desired = subscriber.pubsub_stream_sources();
21110 let mut streams = SubscriberStreams::new();
21111 for source in desired {
21112 streams.push(source, stream::pending().boxed());
21113 }
21114 subscriber.state = AlloySubscriberState::Active(streams);
21115 subscriber.sources_dirty = false;
21116 assert!(matches!(
21117 subscriber.establish_flashblocks_preflight(10).await,
21118 Err(SubscriberError::InvalidConfig(message))
21119 if message.contains("leaves no capacity for OP transaction receipts")
21120 ));
21121 assert!(asserter.read_q().is_empty());
21122 }
21123
21124 #[cfg(feature = "reactive-ws")]
21125 #[tokio::test]
21126 async fn flashblocks_preflight_proves_chain_and_both_subscription_lanes() {
21127 let asserter = Asserter::new();
21128 asserter.push_success(&serde_json::json!({"flashblocks": true}));
21129 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
21130 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21131 provider,
21132 SubscriberMode::PubSub,
21133 SubscriberConfig {
21134 preconfirmations: PreconfirmationMode::Required,
21135 ..SubscriberConfig::default()
21136 },
21137 )
21138 .with_provider_ref(ProviderRef::new("base-paid", 7));
21139 subscriber.chain_id = Some(8_453);
21140 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21141 subscriber.interests = subscriber.base_interests.clone();
21142 let desired = subscriber.pubsub_stream_sources();
21143 let mut streams = SubscriberStreams::new();
21144 for source in desired {
21145 streams.push(source, stream::pending().boxed());
21146 }
21147 subscriber.state = AlloySubscriberState::Active(streams);
21148 subscriber.sources_dirty = false;
21149
21150 let preflight = subscriber
21151 .establish_flashblocks_preflight(8_453)
21152 .await
21153 .expect("preflight succeeds");
21154
21155 assert_eq!(preflight.chain_id(), 8_453);
21156 assert_eq!(preflight.provider(), &ProviderRef::new("base-paid", 7));
21157 assert_eq!(
21158 preflight.delivery(),
21159 FlashblocksDelivery::NativeSubscriptions
21160 );
21161 assert_eq!(preflight.pending_log_subscriptions(), 1);
21162 assert_eq!(preflight.pending_log_filters(), 1);
21163 assert_eq!(
21164 preflight.advertised_capabilities(),
21165 Some(&serde_json::json!({"flashblocks": true}))
21166 );
21167 }
21168
21169 #[tokio::test]
21170 async fn optimism_preflight_probes_pending_state_without_native_subscriptions() {
21171 let asserter = Asserter::new();
21172 asserter.push_success(&serde_json::json!(["flashblocksv1"]));
21173 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
21174 asserter.push_success(&Vec::<Log>::new());
21175 asserter.push_success(&serde_json::json!([]));
21176 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21177 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21178 provider,
21179 SubscriberMode::PubSub,
21180 SubscriberConfig {
21181 preconfirmations: PreconfirmationMode::Required,
21182 ..SubscriberConfig::default()
21183 },
21184 )
21185 .with_provider_ref(ProviderRef::new("op-paid", 12));
21186 subscriber.chain_id = Some(10);
21187 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21188 subscriber.interests = subscriber.base_interests.clone();
21189 let desired = subscriber.pubsub_stream_sources();
21190 let mut streams = SubscriberStreams::new();
21191 for source in desired {
21192 streams.push(source, stream::pending().boxed());
21193 }
21194 subscriber.state = AlloySubscriberState::Active(streams);
21195 subscriber.sources_dirty = false;
21196
21197 let preflight = subscriber
21198 .establish_flashblocks_preflight(10)
21199 .await
21200 .expect("Optimism pending-state preflight succeeds");
21201
21202 assert_eq!(preflight.chain_id(), 10);
21203 assert_eq!(preflight.provider(), &ProviderRef::new("op-paid", 12));
21204 assert_eq!(
21205 preflight.delivery(),
21206 FlashblocksDelivery::PendingStatePolling
21207 );
21208 assert_eq!(preflight.pending_log_subscriptions(), 0);
21209 assert_eq!(preflight.pending_log_filters(), 1);
21210 assert_eq!(
21211 preflight.advertised_capabilities(),
21212 Some(&serde_json::json!(["flashblocksv1"]))
21213 );
21214 assert!(asserter.read_q().is_empty());
21215 }
21216
21217 #[test]
21218 fn optimism_full_pending_block_normalizes_op_transaction_types_to_hashes() {
21219 let transaction_hash = B256::repeat_byte(0x7e);
21220 let mut value = serde_json::to_value(rpc_block(101, B256::ZERO))
21221 .expect("serialize pending block fixture");
21222 value["transactions"] = serde_json::json!([{
21223 "type": "0x7e",
21224 "hash": transaction_hash,
21225 "sourceHash": B256::repeat_byte(0x11),
21226 "from": Address::repeat_byte(0x22),
21227 "to": Address::repeat_byte(0x33)
21228 }]);
21229
21230 let block = normalize_op_pending_block::<Ethereum>(value)
21231 .expect("OP-specific transaction bodies are reduced to hashes");
21232
21233 assert_eq!(
21234 block.transactions().as_hashes(),
21235 Some(&[transaction_hash][..])
21236 );
21237 }
21238
21239 #[tokio::test]
21240 async fn optimism_sampler_does_not_retry_malformed_pending_content() {
21241 let asserter = Asserter::new();
21242 let mut pending = serde_json::to_value(rpc_block(101, B256::ZERO))
21243 .expect("serialize pending block fixture");
21244 pending["transactions"] = serde_json::json!([{"type": "0x7e"}]);
21245 asserter.push_success(&Some(pending));
21246 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21247 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21248 provider,
21249 SubscriberMode::PubSub,
21250 SubscriberConfig {
21251 preconfirmations: PreconfirmationMode::Required,
21252 ..SubscriberConfig::default()
21253 },
21254 )
21255 .with_provider_ref(ProviderRef::new("op-paid", 12));
21256 subscriber.chain_id = Some(10);
21257
21258 let error = match subscriber
21259 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21260 .await
21261 {
21262 Err(error) => error,
21263 Ok(_) => panic!("malformed provider content must fail immediately"),
21264 };
21265 assert!(
21266 error
21267 .to_string()
21268 .contains("transaction is missing its hash")
21269 );
21270 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
21271 assert!(asserter.read_q().is_empty());
21272 }
21273
21274 #[tokio::test]
21275 async fn optimism_sampler_certifies_the_pending_block_by_exact_parent_hash() {
21276 let asserter = Asserter::new();
21277 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
21278 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
21279 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21280 provider,
21281 SubscriberMode::PubSub,
21282 SubscriberConfig {
21283 preconfirmations: PreconfirmationMode::Required,
21284 ..SubscriberConfig::default()
21285 },
21286 )
21287 .with_provider_ref(ProviderRef::new("op-paid", 12));
21288 subscriber.chain_id = Some(10);
21289
21290 assert!(
21291 subscriber
21292 .fetch_pending_flashblock(None)
21293 .await
21294 .expect("the exact parent certifies the pending payload")
21295 .is_some()
21296 );
21297 }
21298
21299 #[tokio::test]
21300 async fn optimism_sampler_rejects_a_nonconsecutive_pending_parent() {
21301 let asserter = Asserter::new();
21302 asserter.push_success(&Some(rpc_block(101, B256::ZERO)));
21303 asserter.push_success(&Some(rpc_block(99, B256::repeat_byte(0x64))));
21304 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21305 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21306 provider,
21307 SubscriberMode::PubSub,
21308 SubscriberConfig {
21309 preconfirmations: PreconfirmationMode::Required,
21310 ..SubscriberConfig::default()
21311 },
21312 )
21313 .with_provider_ref(ProviderRef::new("op-paid", 12));
21314 subscriber.chain_id = Some(10);
21315
21316 assert!(matches!(
21317 subscriber.fetch_pending_flashblock(None).await,
21318 Err(PendingFlashblockPollError::Integrity(SubscriberError::Provider(
21319 ref message
21320 ))) if message.contains("does not extend its exact certified parent")
21321 ));
21322 assert!(asserter.read_q().is_empty());
21323 }
21324
21325 #[tokio::test]
21326 async fn optimism_sampler_rechecks_unchanged_content_without_republishing_logs() {
21327 let asserter = Asserter::new();
21328 let pending = rpc_block(101, B256::ZERO);
21329 queue_op_pending(&asserter, pending.clone());
21330 asserter.push_success(&Vec::<Log>::new());
21331 queue_op_pending(&asserter, pending);
21332 asserter.push_success(&Vec::<Log>::new());
21333 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21334 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21335 provider,
21336 SubscriberMode::PubSub,
21337 SubscriberConfig {
21338 preconfirmations: PreconfirmationMode::Required,
21339 ..SubscriberConfig::default()
21340 },
21341 )
21342 .with_provider_ref(ProviderRef::new("op-paid", 12));
21343 subscriber.chain_id = Some(10);
21344 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21345 subscriber.interests = subscriber.base_interests.clone();
21346
21347 assert!(
21348 subscriber
21349 .fetch_pending_flashblock(None)
21350 .await
21351 .expect("first cumulative pending view")
21352 .is_some()
21353 );
21354 assert!(
21355 subscriber
21356 .fetch_pending_flashblock(None)
21357 .await
21358 .expect("duplicate cumulative pending view")
21359 .is_none()
21360 );
21361
21362 assert_eq!(
21363 subscriber.flashblocks_rpc_metrics(),
21364 FlashblocksRpcMetrics {
21365 capability_requests: 0,
21366 provider_pair_chain_requests: 0,
21367 canonical_head_requests: 2,
21368 pending_block_requests: 2,
21369 pending_log_requests: 2,
21370 pending_receipt_requests: 0,
21371 pending_receipts_completed: 0,
21372 pending_receipts_unavailable: 0,
21373 failed_requests: 0,
21374 raced_samples: 0,
21375 suppressed_canonical_head_polls: 0,
21376 }
21377 );
21378 assert!(asserter.read_q().is_empty());
21379 }
21380
21381 #[tokio::test]
21382 async fn optimism_sampler_rechecks_logs_for_an_unchanged_pending_view() {
21383 let asserter = Asserter::new();
21384 let transaction = B256::repeat_byte(0x42);
21385 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21386 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
21387 );
21388 let mut log = rpc_log(false);
21389 log.block_number = Some(101);
21390 log.block_hash = Some(B256::repeat_byte(0xa2));
21391 log.transaction_hash = Some(transaction);
21392 log.transaction_index = Some(0);
21393 log.log_index = Some(0);
21394 queue_op_pending(&asserter, pending.clone());
21395 asserter.push_success(&Vec::<Log>::new());
21396 asserter.push_success(&serde_json::Value::Null);
21397 queue_op_pending(&asserter, pending);
21398 asserter.push_success(&vec![log]);
21399 asserter.push_success(&serde_json::Value::Null);
21400 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21401 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21402 provider,
21403 SubscriberMode::PubSub,
21404 SubscriberConfig {
21405 preconfirmations: PreconfirmationMode::Required,
21406 ..SubscriberConfig::default()
21407 },
21408 )
21409 .with_provider_ref(ProviderRef::new("op-paid", 12));
21410 subscriber.chain_id = Some(10);
21411 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21412 subscriber.interests = subscriber.base_interests.clone();
21413
21414 assert!(matches!(
21415 subscriber
21416 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21417 .await
21418 .expect("first pending view is coherent"),
21419 Some(SubscriberEvent::FlashblockObserved)
21420 ));
21421 assert!(matches!(
21422 subscriber
21423 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21424 .await
21425 .expect("the unchanged view is checked again for lagging logs"),
21426 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
21427 ));
21428 assert!(asserter.read_q().is_empty());
21429 }
21430
21431 #[tokio::test]
21432 async fn optimism_sampler_hydrates_exact_receipts_when_filtered_logs_are_empty() {
21433 let asserter = Asserter::new();
21434 let transaction = B256::repeat_byte(0x42);
21435 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21436 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
21437 );
21438 let mut log = rpc_log(false);
21439 log.block_number = Some(101);
21440 log.block_hash = Some(B256::repeat_byte(0xa2));
21441 log.transaction_hash = Some(transaction);
21442 log.transaction_index = Some(0);
21443 log.log_index = Some(0);
21444 queue_op_pending(&asserter, pending);
21445 asserter.push_success(&Vec::<Log>::new());
21446 asserter.push_success(&serde_json::json!({
21447 "transactionHash": transaction,
21448 "logs": [log]
21449 }));
21450 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21451 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21452 provider,
21453 SubscriberMode::PubSub,
21454 SubscriberConfig {
21455 preconfirmations: PreconfirmationMode::Required,
21456 ..SubscriberConfig::default()
21457 },
21458 )
21459 .with_provider_ref(ProviderRef::new("op-paid", 12));
21460 subscriber.chain_id = Some(10);
21461 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21462 subscriber.interests = subscriber.base_interests.clone();
21463
21464 let event = subscriber
21465 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21466 .await
21467 .expect("pending receipt fallback succeeds");
21468 assert!(matches!(
21469 event,
21470 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
21471 ));
21472 assert_eq!(
21473 subscriber
21474 .flashblocks_rpc_metrics()
21475 .pending_receipt_requests(),
21476 1
21477 );
21478 assert!(asserter.read_q().is_empty());
21479 }
21480
21481 #[tokio::test]
21482 async fn optimism_receipt_hydration_is_bounded_and_resumes_on_the_next_tick() {
21483 let asserter = Asserter::new();
21484 let transaction_a = B256::repeat_byte(0x41);
21485 let transaction_b = B256::repeat_byte(0x42);
21486 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21487 alloy_network::primitives::BlockTransactions::Hashes(vec![
21488 transaction_a,
21489 transaction_b,
21490 ]),
21491 );
21492 let mut log = rpc_log(false);
21493 log.block_number = Some(101);
21494 log.transaction_hash = Some(transaction_b);
21495 log.transaction_index = Some(1);
21496 queue_op_pending(&asserter, pending.clone());
21497 asserter.push_success(&Vec::<Log>::new());
21498 asserter.push_success(&serde_json::json!({
21499 "transactionHash": transaction_a,
21500 "logs": []
21501 }));
21502 queue_op_pending(&asserter, pending);
21503 asserter.push_success(&Vec::<Log>::new());
21504 asserter.push_success(&serde_json::json!({
21505 "transactionHash": transaction_b,
21506 "logs": [log]
21507 }));
21508 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21509 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21510 provider,
21511 SubscriberMode::PubSub,
21512 SubscriberConfig {
21513 preconfirmations: PreconfirmationMode::Required,
21514 max_pending_transaction_receipts_per_tick: 1,
21515 ..SubscriberConfig::default()
21516 },
21517 )
21518 .with_provider_ref(ProviderRef::new("op-paid", 12));
21519 subscriber.chain_id = Some(10);
21520 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21521 subscriber.interests = subscriber.base_interests.clone();
21522
21523 assert!(matches!(
21524 subscriber
21525 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21526 .await
21527 .expect("the first bounded receipt is hydrated"),
21528 Some(SubscriberEvent::FlashblockObserved)
21529 ));
21530 assert!(matches!(
21531 subscriber
21532 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21533 .await
21534 .expect("the remaining receipt is hydrated on the next tick"),
21535 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
21536 ));
21537 assert_eq!(
21538 subscriber
21539 .flashblocks_rpc_metrics()
21540 .pending_receipt_requests(),
21541 2
21542 );
21543 assert_eq!(subscriber.preconfirmed_receipted_transactions.len(), 2);
21544 assert!(asserter.read_q().is_empty());
21545 }
21546
21547 #[tokio::test]
21548 async fn optimism_receipt_hydration_prioritizes_unattempted_hashes_over_null_retries() {
21549 let asserter = Asserter::new();
21550 let transaction_a = B256::repeat_byte(0x41);
21551 let transaction_b = B256::repeat_byte(0x42);
21552 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21553 alloy_network::primitives::BlockTransactions::Hashes(vec![
21554 transaction_a,
21555 transaction_b,
21556 ]),
21557 );
21558 let mut log = rpc_log(false);
21559 log.block_number = Some(101);
21560 log.transaction_hash = Some(transaction_b);
21561 log.transaction_index = Some(1);
21562 queue_op_pending(&asserter, pending.clone());
21563 asserter.push_success(&Vec::<Log>::new());
21564 asserter.push_success(&serde_json::Value::Null);
21565 queue_op_pending(&asserter, pending);
21566 asserter.push_success(&Vec::<Log>::new());
21567 asserter.push_success(&serde_json::json!({
21568 "transactionHash": transaction_b,
21569 "logs": [log]
21570 }));
21571 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21572 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21573 provider,
21574 SubscriberMode::PubSub,
21575 SubscriberConfig {
21576 preconfirmations: PreconfirmationMode::Required,
21577 max_pending_transaction_receipts_per_tick: 1,
21578 ..SubscriberConfig::default()
21579 },
21580 )
21581 .with_provider_ref(ProviderRef::new("op-paid", 12));
21582 subscriber.chain_id = Some(10);
21583 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21584 subscriber.interests = subscriber.base_interests.clone();
21585
21586 assert!(matches!(
21587 subscriber
21588 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21589 .await
21590 .expect("the first null receipt remains retryable"),
21591 Some(SubscriberEvent::FlashblockObserved)
21592 ));
21593 assert!(matches!(
21594 subscriber
21595 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21596 .await
21597 .expect("the next unattempted receipt is not starved"),
21598 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
21599 ));
21600 assert!(
21601 subscriber
21602 .preconfirmed_unavailable_receipts
21603 .contains(&transaction_a)
21604 );
21605 assert!(
21606 subscriber
21607 .preconfirmed_receipted_transactions
21608 .contains(&transaction_b)
21609 );
21610 assert!(asserter.read_q().is_empty());
21611 }
21612
21613 #[tokio::test]
21614 async fn optimism_receipt_batch_commits_dedupe_only_after_every_response_succeeds() {
21615 let asserter = Asserter::new();
21616 let transaction_a = B256::repeat_byte(0x41);
21617 let transaction_b = B256::repeat_byte(0x42);
21618 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21619 alloy_network::primitives::BlockTransactions::Hashes(vec![
21620 transaction_a,
21621 transaction_b,
21622 ]),
21623 );
21624 let mut log = rpc_log(false);
21625 log.block_number = Some(101);
21626 log.transaction_hash = Some(transaction_b);
21627 log.transaction_index = Some(1);
21628 queue_op_pending(&asserter, pending.clone());
21629 asserter.push_success(&Vec::<Log>::new());
21630 asserter.push_success(&serde_json::json!({
21631 "transactionHash": transaction_a,
21632 "logs": []
21633 }));
21634 asserter.push_failure_msg("receipt temporarily unavailable");
21635 queue_op_pending(&asserter, pending);
21636 asserter.push_success(&Vec::<Log>::new());
21637 asserter.push_success(&serde_json::json!({
21638 "transactionHash": transaction_a,
21639 "logs": []
21640 }));
21641 asserter.push_success(&serde_json::json!({
21642 "transactionHash": transaction_b,
21643 "logs": [log]
21644 }));
21645 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21646 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21647 provider,
21648 SubscriberMode::PubSub,
21649 SubscriberConfig {
21650 preconfirmations: PreconfirmationMode::Required,
21651 max_pending_transaction_receipts_per_tick: 2,
21652 max_consecutive_flashblock_poll_failures: 2,
21653 ..SubscriberConfig::default()
21654 },
21655 )
21656 .with_provider_ref(ProviderRef::new("op-paid", 12));
21657 subscriber.chain_id = Some(10);
21658 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21659 subscriber.interests = subscriber.base_interests.clone();
21660
21661 assert!(
21662 subscriber
21663 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21664 .await
21665 .expect("one failed receipt response remains retryable")
21666 .is_none()
21667 );
21668 assert!(subscriber.preconfirmed_receipted_transactions.is_empty());
21669 assert!(matches!(
21670 subscriber
21671 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21672 .await
21673 .expect("the complete batch is retried transactionally"),
21674 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
21675 ));
21676 assert_eq!(subscriber.preconfirmed_receipted_transactions.len(), 2);
21677 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 1);
21678 assert_eq!(
21679 subscriber
21680 .flashblocks_rpc_metrics()
21681 .pending_receipt_requests(),
21682 4
21683 );
21684 assert!(asserter.read_q().is_empty());
21685 }
21686
21687 #[tokio::test]
21688 async fn optimism_sampler_rejects_a_receipt_for_a_different_transaction() {
21689 let asserter = Asserter::new();
21690 let sampled_transaction = B256::repeat_byte(0x41);
21691 let advanced_transaction = B256::repeat_byte(0x42);
21692 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21693 alloy_network::primitives::BlockTransactions::Hashes(vec![sampled_transaction]),
21694 );
21695 let mut log = rpc_log(false);
21696 log.block_number = Some(101);
21697 log.transaction_hash = Some(advanced_transaction);
21698 queue_op_pending(&asserter, pending);
21699 asserter.push_success(&Vec::<Log>::new());
21700 asserter.push_success(&serde_json::json!({
21701 "transactionHash": advanced_transaction,
21702 "logs": [log]
21703 }));
21704 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21705 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21706 provider,
21707 SubscriberMode::PubSub,
21708 SubscriberConfig {
21709 preconfirmations: PreconfirmationMode::Required,
21710 ..SubscriberConfig::default()
21711 },
21712 )
21713 .with_provider_ref(ProviderRef::new("op-paid", 12));
21714 subscriber.chain_id = Some(10);
21715 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21716 subscriber.interests = subscriber.base_interests.clone();
21717
21718 let error = match subscriber
21719 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21720 .await
21721 {
21722 Err(error) => error,
21723 Ok(_) => panic!("a receipt for another transaction must fail closed"),
21724 };
21725 assert!(
21726 error
21727 .to_string()
21728 .contains("hash disagrees with its request")
21729 );
21730 assert!(asserter.read_q().is_empty());
21731 }
21732
21733 #[tokio::test]
21734 async fn optimism_sampler_revokes_then_recovers_from_a_regressive_pending_view() {
21735 let asserter = Asserter::new();
21736 let transaction_a = B256::repeat_byte(0x41);
21737 let transaction_b = B256::repeat_byte(0x42);
21738 let first = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21739 alloy_network::primitives::BlockTransactions::Hashes(vec![
21740 transaction_a,
21741 transaction_b,
21742 ]),
21743 );
21744 let regressive = rpc_block(101, B256::repeat_byte(0xa2)).with_transactions(
21745 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction_a]),
21746 );
21747 queue_op_pending(&asserter, first);
21748 asserter.push_success(&Vec::<Log>::new());
21749 asserter.push_success(&serde_json::Value::Null);
21750 asserter.push_success(&serde_json::Value::Null);
21751 queue_op_pending(&asserter, regressive.clone());
21752 queue_op_pending(&asserter, regressive);
21753 asserter.push_success(&Vec::<Log>::new());
21754 asserter.push_success(&serde_json::Value::Null);
21755 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21756 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21757 provider,
21758 SubscriberMode::PubSub,
21759 SubscriberConfig {
21760 preconfirmations: PreconfirmationMode::Required,
21761 ..SubscriberConfig::default()
21762 },
21763 )
21764 .with_provider_ref(ProviderRef::new("op-paid", 12));
21765 subscriber.chain_id = Some(10);
21766 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21767 subscriber.interests = subscriber.base_interests.clone();
21768
21769 assert!(
21770 subscriber
21771 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21772 .await
21773 .expect("first pending view is coherent")
21774 .is_some()
21775 );
21776 assert!(matches!(
21777 subscriber
21778 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21779 .await
21780 .expect("regression revokes instead of terminating the stream"),
21781 Some(SubscriberEvent::FlashblockInvalidated)
21782 ));
21783 assert!(subscriber.latest_preconfirmation.is_none());
21784 assert!(subscriber.pending_preconfirmation_invalidation);
21785 assert!(
21786 subscriber
21787 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21788 .await
21789 .expect("a later coherent view establishes a fresh snapshot")
21790 .is_some()
21791 );
21792 assert!(subscriber.latest_preconfirmation.is_some());
21793 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 12);
21794 assert!(asserter.read_q().is_empty());
21795 }
21796
21797 #[tokio::test]
21798 async fn optimism_new_quiet_payload_revokes_the_previous_snapshot() {
21799 let asserter = Asserter::new();
21800 let first = rpc_block(101, B256::repeat_byte(0xa1));
21801 let second = rpc_block(102, B256::repeat_byte(0xa2));
21802 queue_op_pending(&asserter, first);
21803 asserter.push_success(&Vec::<Log>::new());
21804 queue_op_pending(&asserter, second);
21805 asserter.push_success(&Vec::<Log>::new());
21806 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21807 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21808 provider,
21809 SubscriberMode::PubSub,
21810 SubscriberConfig {
21811 preconfirmations: PreconfirmationMode::Required,
21812 ..SubscriberConfig::default()
21813 },
21814 )
21815 .with_provider_ref(ProviderRef::new("op-paid", 12));
21816 subscriber.chain_id = Some(10);
21817 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21818 subscriber.interests = subscriber.base_interests.clone();
21819
21820 subscriber
21821 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21822 .await
21823 .expect("first quiet payload is observed");
21824 assert!(!subscriber.pending_preconfirmation_invalidation);
21825 subscriber
21826 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21827 .await
21828 .expect("replacement quiet payload is observed");
21829 assert!(subscriber.pending_preconfirmation_invalidation);
21830 assert_eq!(
21831 subscriber
21832 .latest_preconfirmation
21833 .as_ref()
21834 .map(|flashblock| flashblock.block_number),
21835 Some(102)
21836 );
21837 assert!(asserter.read_q().is_empty());
21838 }
21839
21840 #[tokio::test]
21841 async fn optimism_sampler_rejects_malformed_pending_receipts() {
21842 let asserter = Asserter::new();
21843 let transaction = B256::repeat_byte(0x42);
21844 let pending = rpc_block(101, B256::ZERO).with_transactions(
21845 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
21846 );
21847 queue_op_pending(&asserter, pending);
21848 asserter.push_success(&Vec::<Log>::new());
21849 asserter.push_success(&serde_json::json!({"transactionHash": transaction}));
21850 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21851 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21852 provider,
21853 SubscriberMode::PubSub,
21854 SubscriberConfig {
21855 preconfirmations: PreconfirmationMode::Required,
21856 ..SubscriberConfig::default()
21857 },
21858 )
21859 .with_provider_ref(ProviderRef::new("op-paid", 12));
21860 subscriber.chain_id = Some(10);
21861 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21862 subscriber.interests = subscriber.base_interests.clone();
21863
21864 let error = match subscriber
21865 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21866 .await
21867 {
21868 Err(error) => error,
21869 Ok(_) => panic!("malformed receipt content must fail closed"),
21870 };
21871 assert!(error.to_string().contains("missing its log array"));
21872 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
21873 assert!(asserter.read_q().is_empty());
21874 }
21875
21876 #[tokio::test]
21877 async fn optimism_sampler_retries_when_logs_advance_past_the_sampled_block() {
21878 let asserter = Asserter::new();
21879 let transaction_a = B256::repeat_byte(0x41);
21880 let transaction_b = B256::repeat_byte(0x42);
21881 let first = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21882 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction_a]),
21883 );
21884 let second = rpc_block(101, B256::repeat_byte(0xa2)).with_transactions(
21885 alloy_network::primitives::BlockTransactions::Hashes(vec![
21886 transaction_a,
21887 transaction_b,
21888 ]),
21889 );
21890 let mut log = rpc_log(false);
21891 log.block_number = Some(101);
21892 log.block_hash = Some(B256::repeat_byte(0xa2));
21893 log.transaction_hash = Some(transaction_b);
21894 log.transaction_index = Some(1);
21895 log.log_index = Some(0);
21896 queue_op_pending(&asserter, first);
21897 asserter.push_success(&vec![log.clone()]);
21898 asserter.push_success(&serde_json::Value::Null);
21899 queue_op_pending(&asserter, second);
21900 asserter.push_success(&vec![log.clone()]);
21901 asserter.push_success(&serde_json::Value::Null);
21902 asserter.push_success(&serde_json::json!({
21903 "transactionHash": transaction_b,
21904 "logs": [log]
21905 }));
21906 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21907 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21908 provider,
21909 SubscriberMode::PubSub,
21910 SubscriberConfig {
21911 preconfirmations: PreconfirmationMode::Required,
21912 ..SubscriberConfig::default()
21913 },
21914 )
21915 .with_provider_ref(ProviderRef::new("op-paid", 12));
21916 subscriber.chain_id = Some(10);
21917 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21918 subscriber.interests = subscriber.base_interests.clone();
21919
21920 assert!(
21921 subscriber
21922 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21923 .await
21924 .expect("a cross-request race remains retryable")
21925 .is_none()
21926 );
21927 assert!(
21928 subscriber
21929 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21930 .await
21931 .expect("the next coherent cumulative view is delivered")
21932 .is_some()
21933 );
21934 assert_eq!(subscriber.flashblocks_rpc_metrics().raced_samples(), 1);
21935 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
21936 assert!(asserter.read_q().is_empty());
21937 }
21938
21939 #[tokio::test]
21940 async fn optimism_sampler_uses_the_paired_pending_state_provider() {
21941 let stream_asserter = Asserter::new();
21942 let stream_provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
21943 let state_asserter = Asserter::new();
21944 queue_op_pending(&state_asserter, rpc_block(101, B256::ZERO));
21945 state_asserter.push_success(&Vec::<Log>::new());
21946 let state_provider = ProviderBuilder::new().connect_mocked_client(state_asserter.clone());
21947 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21948 stream_provider,
21949 SubscriberMode::PubSub,
21950 SubscriberConfig {
21951 preconfirmations: PreconfirmationMode::Required,
21952 ..SubscriberConfig::default()
21953 },
21954 )
21955 .with_provider_ref(ProviderRef::new("op-paid", 12))
21956 .with_flashblocks_state_provider(state_provider);
21957 subscriber.chain_id = Some(10);
21958 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21959 subscriber.interests = subscriber.base_interests.clone();
21960
21961 assert!(
21962 subscriber
21963 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21964 .await
21965 .expect("paired pending-state reads succeed")
21966 .is_some()
21967 );
21968 assert!(state_asserter.read_q().is_empty());
21969 assert!(stream_asserter.read_q().is_empty());
21970 }
21971
21972 #[tokio::test]
21973 async fn optimism_sampler_retries_an_isolated_provider_request_failure() {
21974 let asserter = Asserter::new();
21975 let pending = rpc_block(101, B256::ZERO);
21976 queue_op_pending(&asserter, pending.clone());
21977 asserter.push_failure_msg("temporarily unavailable");
21978 queue_op_pending(&asserter, pending);
21979 asserter.push_success(&Vec::<Log>::new());
21980 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21981 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21982 provider,
21983 SubscriberMode::PubSub,
21984 SubscriberConfig {
21985 preconfirmations: PreconfirmationMode::Required,
21986 max_consecutive_flashblock_poll_failures: 2,
21987 ..SubscriberConfig::default()
21988 },
21989 )
21990 .with_provider_ref(ProviderRef::new("op-paid", 12));
21991 subscriber.chain_id = Some(10);
21992 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21993 subscriber.interests = subscriber.base_interests.clone();
21994
21995 assert!(
21996 subscriber
21997 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21998 .await
21999 .expect("one request failure stays retryable")
22000 .is_none()
22001 );
22002 assert!(
22003 subscriber
22004 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
22005 .await
22006 .expect("the next cumulative view retries the missing logs")
22007 .is_some()
22008 );
22009 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 1);
22010 assert!(asserter.read_q().is_empty());
22011 }
22012
22013 #[tokio::test]
22014 async fn optimism_sampler_surfaces_sustained_provider_request_failures() {
22015 let asserter = Asserter::new();
22016 asserter.push_failure_msg("temporarily unavailable");
22017 asserter.push_failure_msg("still unavailable");
22018 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
22019 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22020 provider,
22021 SubscriberMode::PubSub,
22022 SubscriberConfig {
22023 preconfirmations: PreconfirmationMode::Required,
22024 max_consecutive_flashblock_poll_failures: 2,
22025 ..SubscriberConfig::default()
22026 },
22027 )
22028 .with_provider_ref(ProviderRef::new("op-paid", 12));
22029 subscriber.chain_id = Some(10);
22030
22031 assert!(
22032 subscriber
22033 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
22034 .await
22035 .expect("the first request failure stays retryable")
22036 .is_none()
22037 );
22038 let error = match subscriber
22039 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
22040 .await
22041 {
22042 Err(error) => error,
22043 Ok(_) => panic!("the configured consecutive-failure limit must fail closed"),
22044 };
22045 assert!(error.to_string().contains("still unavailable"));
22046 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 2);
22047 assert!(asserter.read_q().is_empty());
22048 }
22049
22050 #[tokio::test]
22051 async fn flashblocks_preflight_rejects_a_mismatched_chain_before_subscribing() {
22052 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22053 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22054 provider,
22055 SubscriberMode::PubSub,
22056 SubscriberConfig {
22057 preconfirmations: PreconfirmationMode::Required,
22058 ..SubscriberConfig::default()
22059 },
22060 )
22061 .with_provider_ref(ProviderRef::new("wrong-chain", 1));
22062 subscriber.chain_id = Some(10);
22063 subscriber.interests = vec![log_interest_matching_rpc_log()];
22064
22065 assert!(matches!(
22066 subscriber.establish_flashblocks_preflight(8_453).await,
22067 Err(SubscriberError::ChainMismatch {
22068 expected: 8_453,
22069 actual: 10
22070 })
22071 ));
22072 }
22073
22074 #[tokio::test]
22075 async fn optimism_preflight_rejects_a_mismatched_paired_provider() {
22076 let stream_asserter = Asserter::new();
22077 let stream_provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
22078 let state_asserter = Asserter::new();
22079 state_asserter.push_success(&serde_json::json!(["flashblocksv1"]));
22080 state_asserter.push_success(&8_453_u64);
22081 let state_provider = ProviderBuilder::new().connect_mocked_client(state_asserter.clone());
22082 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22083 stream_provider,
22084 SubscriberMode::PubSub,
22085 SubscriberConfig {
22086 preconfirmations: PreconfirmationMode::Required,
22087 ..SubscriberConfig::default()
22088 },
22089 )
22090 .with_provider_ref(ProviderRef::new("op-paid", 12))
22091 .with_flashblocks_state_provider(state_provider);
22092 subscriber.chain_id = Some(10);
22093 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
22094 subscriber.interests = subscriber.base_interests.clone();
22095 let desired = subscriber.pubsub_stream_sources();
22096 let mut streams = SubscriberStreams::new();
22097 for source in desired {
22098 streams.push(source, stream::pending().boxed());
22099 }
22100 subscriber.state = AlloySubscriberState::Active(streams);
22101 subscriber.sources_dirty = false;
22102 assert!(matches!(
22103 subscriber.establish_flashblocks_preflight(10).await,
22104 Err(SubscriberError::ChainMismatch {
22105 expected: 10,
22106 actual: 8_453
22107 })
22108 ));
22109 assert!(state_asserter.read_q().is_empty());
22110 assert!(stream_asserter.read_q().is_empty());
22111 }
22112
22113 #[test]
22114 fn unproven_parent_replacement_rewind_discards_every_unauthenticated_identity() {
22115 let parent = BlockRef {
22116 number: 79,
22117 hash: B256::repeat_byte(0x79),
22118 parent_hash: Some(B256::repeat_byte(0x78)),
22119 timestamp: Some(1_700_000_079),
22120 };
22121 let old_tip = BlockRef {
22122 number: 80,
22123 hash: B256::repeat_byte(0x80),
22124 parent_hash: Some(parent.hash),
22125 timestamp: Some(1_700_000_080),
22126 };
22127 let replacement = BlockRef {
22128 hash: B256::repeat_byte(0xe0),
22129 parent_hash: Some(B256::repeat_byte(0xdf)),
22130 ..old_tip
22131 };
22132 let mut state =
22133 CanonicalSequenceState::new(vec![parent, old_tip], Some(old_tip), Some(parent), None);
22134
22135 let rewind = apply_sequence_canonical_block(&mut state, &replacement, false)
22136 .expect("replacement metadata is structurally valid")
22137 .expect("unknown parent is an observable rewind");
22138
22139 assert_eq!(rewind.common_ancestor, None);
22140 assert_eq!(rewind.dropped, vec![parent, old_tip]);
22141 assert_eq!(state.retained_canonical_history(), &[replacement]);
22142 assert_eq!(state.coverage_head(), Some(&replacement));
22143 assert_eq!(state.safe_head(), None);
22144 assert_eq!(state.finalized_head(), None);
22145 }
22146
22147 #[test]
22148 fn handler_ids_are_non_empty_across_construction_and_deserialization() {
22149 assert_eq!(HandlerId::try_new("").unwrap_err(), HandlerIdError);
22150 let valid = HandlerId::try_new("owner-1").expect("non-empty id");
22151 let encoded = serde_json::to_string(&valid).expect("serialize id");
22152 assert_eq!(
22153 serde_json::from_str::<HandlerId>(&encoded).expect("deserialize valid id"),
22154 valid
22155 );
22156 assert!(serde_json::from_str::<HandlerId>(r#"""#).is_err());
22157 }
22158
22159 fn rpc_log(removed: bool) -> Log {
22160 Log {
22161 inner: alloy_primitives::Log::new_unchecked(
22162 Address::repeat_byte(0x42),
22163 vec![B256::repeat_byte(0x01)],
22164 Bytes::new(),
22165 ),
22166 block_hash: Some(B256::repeat_byte(0x02)),
22167 block_number: Some(7),
22168 block_timestamp: Some(1_700_000_000),
22169 transaction_hash: Some(B256::repeat_byte(0x03)),
22170 transaction_index: Some(4),
22171 log_index: Some(5),
22172 removed,
22173 }
22174 }
22175
22176 fn rpc_transaction(chain_id: Option<u64>) -> alloy_rpc_types_eth::Transaction {
22177 use alloy_consensus::SignableTransaction as _;
22178
22179 let envelope: alloy_consensus::TxEnvelope = alloy_consensus::TxLegacy {
22180 chain_id,
22181 ..Default::default()
22182 }
22183 .into_signed(alloy_primitives::Signature::test_signature())
22184 .into();
22185 alloy_rpc_types_eth::Transaction {
22186 inner: alloy_consensus::transaction::Recovered::new_unchecked(envelope, Address::ZERO),
22187 block_hash: None,
22188 block_number: None,
22189 transaction_index: None,
22190 effective_gas_price: None,
22191 }
22192 }
22193
22194 #[cfg(feature = "reactive-ws")]
22195 fn rpc_log_at(block_number: u64, transaction_index: u64, log_index: u64) -> Log {
22196 Log {
22197 inner: alloy_primitives::Log::new_unchecked(
22198 Address::repeat_byte(0x42),
22199 vec![B256::repeat_byte(0x01)],
22200 Bytes::new(),
22201 ),
22202 block_hash: Some(B256::repeat_byte(block_number as u8)),
22203 block_number: Some(block_number),
22204 block_timestamp: Some(1_700_000_000 + block_number),
22205 transaction_hash: Some(B256::repeat_byte(0x20 + transaction_index as u8)),
22206 transaction_index: Some(transaction_index),
22207 log_index: Some(log_index),
22208 removed: false,
22209 }
22210 }
22211
22212 fn rpc_block(number: u64, hash: B256) -> alloy_rpc_types_eth::Block {
22213 alloy_rpc_types_eth::Block::empty(alloy_rpc_types_eth::Header {
22214 hash,
22215 inner: alloy_consensus::Header {
22216 number,
22217 parent_hash: B256::repeat_byte(number.saturating_sub(1) as u8),
22218 timestamp: 1_700_000_000 + number,
22219 ..Default::default()
22220 },
22221 total_difficulty: None,
22222 size: None,
22223 })
22224 }
22225
22226 fn queue_op_pending(asserter: &Asserter, pending: alloy_rpc_types_eth::Block) {
22227 let parent = rpc_block(
22228 pending.header().number().saturating_sub(1),
22229 pending.header().parent_hash(),
22230 );
22231 asserter.push_success(&Some(pending));
22232 asserter.push_success(&Some(parent));
22233 }
22234
22235 #[tokio::test(flavor = "multi_thread")]
22236 #[cfg(feature = "reactive-ws")]
22237 async fn verified_log_context_fetches_and_caches_exact_parent_identity() {
22238 let stream_asserter = Asserter::new();
22239 let provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
22240 let verification_asserter = Asserter::new();
22241 verification_asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(7))));
22242 let verification_provider =
22243 ProviderBuilder::new().connect_mocked_client(verification_asserter.clone());
22244 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22245 provider,
22246 SubscriberMode::PubSub,
22247 SubscriberConfig {
22248 verify_log_block_context: true,
22249 ..SubscriberConfig::default()
22250 },
22251 )
22252 .with_log_verification_provider(verification_provider);
22253 let log = rpc_log_at(7, 0, 0);
22254
22255 subscriber
22256 .verify_log_block_context(&log)
22257 .await
22258 .expect("verify live log block");
22259 subscriber
22260 .verify_log_block_context(&log)
22261 .await
22262 .expect("reuse verified block cache");
22263 let record = subscriber.with_chain_id(log_input_record(log, InputSource::Subscription));
22264
22265 assert_eq!(
22266 record.context.block.expect("verified block").parent_hash,
22267 Some(B256::repeat_byte(6))
22268 );
22269 assert!(
22270 verification_asserter.read_q().is_empty(),
22271 "one provider lookup should verify every log in the same block"
22272 );
22273 assert!(
22274 stream_asserter.read_q().is_empty(),
22275 "verification must not use the high-volume stream provider"
22276 );
22277 }
22278
22279 #[tokio::test(flavor = "multi_thread")]
22280 async fn stream_with_termination_yields_terminal_source_marker() {
22281 let mut stream = stream_with_termination::<Ethereum, _>(
22282 stream::iter([SubscriberEvent::<Ethereum>::PendingHash(B256::repeat_byte(
22283 0xaa,
22284 ))]),
22285 SubscriberStreamSource::PubSubPendingHashes,
22286 );
22287
22288 assert!(matches!(
22289 stream.next().await,
22290 Some(SubscriberEvent::PendingHash(hash)) if hash == B256::repeat_byte(0xaa)
22291 ));
22292 assert!(matches!(
22293 stream.next().await,
22294 Some(SubscriberEvent::StreamTerminated(source)) if source.is_pubsub()
22295 ));
22296 assert!(stream.next().await.is_none());
22297 }
22298
22299 #[test]
22300 fn reconnect_delay_doubles_until_capped() {
22301 assert_eq!(
22302 next_reconnect_delay(Duration::from_millis(250), Duration::from_secs(1)),
22303 Duration::from_millis(500)
22304 );
22305 assert_eq!(
22306 next_reconnect_delay(Duration::from_millis(750), Duration::from_secs(1)),
22307 Duration::from_secs(1)
22308 );
22309 assert_eq!(
22310 next_reconnect_delay(Duration::ZERO, Duration::from_secs(1)),
22311 Duration::ZERO
22312 );
22313 }
22314
22315 #[test]
22316 fn canonical_logs_are_deduped_but_removed_logs_are_not() {
22317 let included = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
22318 let removed = log_input_record::<Ethereum>(rpc_log(true), InputSource::Subscription);
22319
22320 assert!(should_dedupe_record(&included));
22321 assert!(!should_dedupe_record(&removed));
22322 }
22323
22324 #[test]
22325 fn owner_reconcile_dedupe_rejects_conflicts_and_preserves_compatible_enrichment() {
22326 let set_context_timestamp = |record: &mut ReactiveInputRecord<Ethereum>,
22327 timestamp: Option<u64>| {
22328 record.context.block.as_mut().expect("block").timestamp = timestamp;
22329 match &mut record.context.chain_status {
22330 ChainStatus::Included { block, .. }
22331 | ChainStatus::Safe { block }
22332 | ChainStatus::Finalized { block }
22333 | ChainStatus::Reorged {
22334 dropped_from: block,
22335 } => block.timestamp = timestamp,
22336 ChainStatus::Pending | ChainStatus::Preconfirmed { .. } => {
22337 panic!("log record is canonical")
22338 }
22339 }
22340 };
22341
22342 let mut payload_only = log_input_record::<Ethereum>(rpc_log(false), InputSource::Backfill);
22343 let payload_timestamp = match &payload_only.input {
22344 ReactiveInput::Log(log) => log.block_timestamp.expect("timestamp"),
22345 _ => unreachable!(),
22346 };
22347 set_context_timestamp(&mut payload_only, None);
22348 let mut context_only = payload_only.clone();
22349 if let ReactiveInput::Log(log) = &mut context_only.input {
22350 log.block_timestamp = None;
22351 }
22352 set_context_timestamp(&mut context_only, Some(payload_timestamp + 1));
22353 assert!(matches!(
22354 dedupe_records(vec![payload_only, context_only]),
22355 Err(ReactiveError::InvalidInputRecord { .. })
22356 ));
22357
22358 let mut partial = log_input_record::<Ethereum>(rpc_log(false), InputSource::Backfill);
22359 if let ReactiveInput::Log(log) = &mut partial.input {
22360 log.block_timestamp = None;
22361 }
22362 set_context_timestamp(&mut partial, None);
22363 let complete = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
22364 let deduped =
22365 dedupe_records(vec![partial, complete]).expect("compatible metadata enriches");
22366 assert_eq!(deduped.len(), 1);
22367 deduped[0]
22368 .validated_identity()
22369 .expect("merged record remains coherent");
22370 let resolved = resolve_record_block_payload_metadata(
22371 &deduped[0],
22372 *canonical_record_block(&deduped[0]).expect("canonical"),
22373 )
22374 .expect("effective block");
22375 assert_eq!(resolved.timestamp, Some(payload_timestamp));
22376 }
22377
22378 #[test]
22379 fn full_block_bodies_are_never_suppressed_from_header_hash_alone() {
22380 use alloy_rpc_types_eth::{Block, Header};
22381
22382 let block_ref = BlockRef {
22383 number: 7,
22384 hash: B256::repeat_byte(0x77),
22385 parent_hash: Some(B256::repeat_byte(0x66)),
22386 timestamp: Some(1_700_000_007),
22387 };
22388 let block = Block::empty(Header {
22389 hash: block_ref.hash,
22390 inner: alloy_consensus::Header {
22391 number: block_ref.number,
22392 parent_hash: block_ref.parent_hash.expect("parent"),
22393 timestamp: block_ref.timestamp.expect("timestamp"),
22394 ..Default::default()
22395 },
22396 total_difficulty: None,
22397 size: None,
22398 });
22399 let record = ReactiveInputRecord::<Ethereum>::new(
22400 ReactiveInput::FullBlock(block),
22401 ReactiveContext {
22402 chain_id: Some(1),
22403 source: InputSource::Subscription,
22404 chain_status: ChainStatus::Included {
22405 block: block_ref,
22406 confirmations: 0,
22407 },
22408 block: Some(block_ref),
22409 transaction_index: None,
22410 log_index: None,
22411 },
22412 );
22413
22414 assert!(!record.is_payload_deduplicable());
22415 assert!(!record.same_deduplicable_payload(&record));
22416 let retained = dedupe_scoped_records(vec![
22417 (
22418 record.clone(),
22419 DeliveryAudience::All,
22420 DeliveryScope::Canonical,
22421 ),
22422 (record, DeliveryAudience::All, DeliveryScope::Canonical),
22423 ])
22424 .expect("non-deduplicable bodies are preserved, not treated as conflicts");
22425 assert_eq!(retained.len(), 2);
22426 }
22427
22428 #[test]
22429 fn hydrated_transaction_wrappers_reject_inclusion_and_chain_identity_conflicts() {
22430 let pending_context = ReactiveContext {
22431 chain_id: Some(1),
22432 source: InputSource::Batch,
22433 chain_status: ChainStatus::Pending,
22434 block: None,
22435 transaction_index: None,
22436 log_index: None,
22437 };
22438 let mut included_pending = rpc_transaction(Some(1));
22439 included_pending.block_hash = Some(B256::repeat_byte(0xaa));
22440 assert!(matches!(
22441 ReactiveInputRecord::<Ethereum>::new(
22442 ReactiveInput::PendingTx(included_pending),
22443 pending_context.clone(),
22444 )
22445 .validated_identity(),
22446 Err(ReactiveError::InvalidInputRecord { .. })
22447 ));
22448 assert!(matches!(
22449 ReactiveInputRecord::<Ethereum>::new(
22450 ReactiveInput::PendingTx(rpc_transaction(Some(2))),
22451 pending_context,
22452 )
22453 .validated_identity(),
22454 Err(ReactiveError::InvalidInputRecord { .. })
22455 ));
22456
22457 let block_ref = BlockRef {
22458 number: 8,
22459 hash: B256::repeat_byte(0x88),
22460 parent_hash: Some(B256::repeat_byte(0x77)),
22461 timestamp: Some(1_700_000_008),
22462 };
22463 let header = alloy_rpc_types_eth::Header {
22464 hash: block_ref.hash,
22465 inner: alloy_consensus::Header {
22466 number: block_ref.number,
22467 parent_hash: block_ref.parent_hash.expect("parent"),
22468 timestamp: block_ref.timestamp.expect("timestamp"),
22469 ..Default::default()
22470 },
22471 total_difficulty: None,
22472 size: None,
22473 };
22474 let context = ReactiveContext {
22475 chain_id: Some(1),
22476 source: InputSource::Batch,
22477 chain_status: ChainStatus::Included {
22478 block: block_ref,
22479 confirmations: 0,
22480 },
22481 block: Some(block_ref),
22482 transaction_index: None,
22483 log_index: None,
22484 };
22485 for transaction in [
22486 alloy_rpc_types_eth::Transaction {
22487 block_hash: Some(B256::repeat_byte(0xff)),
22488 ..rpc_transaction(Some(1))
22489 },
22490 alloy_rpc_types_eth::Transaction {
22491 block_hash: Some(block_ref.hash),
22492 block_number: Some(block_ref.number),
22493 transaction_index: Some(1),
22494 ..rpc_transaction(Some(1))
22495 },
22496 rpc_transaction(Some(2)),
22497 ] {
22498 let block = alloy_rpc_types_eth::Block::new(
22499 header.clone(),
22500 alloy_network::primitives::BlockTransactions::Full(vec![transaction]),
22501 );
22502 assert!(matches!(
22503 ReactiveInputRecord::<Ethereum>::new(
22504 ReactiveInput::FullBlock(block),
22505 context.clone(),
22506 )
22507 .validated_identity(),
22508 Err(ReactiveError::InvalidInputRecord { .. })
22509 ));
22510 }
22511 }
22512
22513 #[test]
22514 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
22515 fn compatibility_owner_backfill_and_live_overlap_split_exact_audiences() {
22516 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22517 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22518 provider,
22519 SubscriberMode::Auto,
22520 SubscriberConfig::default(),
22521 );
22522 let owner = HandlerId::new("compat-owner");
22523 subscriber
22524 .add_interest_owner(
22525 owner.clone(),
22526 &[ReactiveInterest::Logs(LogInterest {
22527 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
22528 local_matcher: None,
22529 route_key: None,
22530 })],
22531 )
22532 .unwrap();
22533 let log = rpc_log(false);
22534
22535 subscriber.enqueue_compat_owner_record(
22536 log_input_record(log.clone(), InputSource::Backfill),
22537 owner.clone(),
22538 );
22539 subscriber.enqueue_event(SubscriberEvent::Log {
22540 source_id: 0,
22541 log,
22542 #[cfg(feature = "ingress-timing")]
22543 timing: None,
22544 });
22545
22546 let batch = subscriber
22547 .drain_next_scoped_batch()
22548 .expect("owner catch-up and residual live copies");
22549 assert_eq!(batch.records.len(), 2);
22550 assert_eq!(
22551 batch.records[0].scope,
22552 SubscriberInputScope::OwnerOnlyHandlers {
22553 owners: vec![owner.clone()]
22554 }
22555 );
22556 assert_eq!(
22557 batch.records[1].scope,
22558 SubscriberInputScope::CanonicalResidual {
22559 owners: Vec::new(),
22560 excluded: vec![owner.clone()]
22561 }
22562 );
22563
22564 let reactive = batch.into_reactive_batch();
22565 assert_eq!(
22566 reactive.record_audience(0),
22567 Some(&DeliveryAudience::Owners(vec![owner.clone()]))
22568 );
22569 assert_eq!(
22570 reactive.record_delivery_scope(0),
22571 Some(DeliveryScope::OwnerCatchup)
22572 );
22573 assert_eq!(
22574 reactive.record_audience(1),
22575 Some(&DeliveryAudience::AllExcept(vec![owner]))
22576 );
22577 assert_eq!(
22578 reactive.record_delivery_scope(1),
22579 Some(DeliveryScope::Canonical)
22580 );
22581 }
22582
22583 #[test]
22584 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
22585 fn active_owner_replacement_commits_atomically_to_one_new_epoch() {
22586 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22587 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22588 provider,
22589 SubscriberMode::Auto,
22590 SubscriberConfig::default(),
22591 );
22592 let owner = HandlerId::new("replace-owner");
22593 let original = ReactiveInterest::Logs(LogInterest {
22594 provider_filter: Filter::new().address(Address::repeat_byte(0x41)),
22595 local_matcher: None,
22596 route_key: None,
22597 });
22598 let replacement_interest = ReactiveInterest::Logs(LogInterest {
22599 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
22600 local_matcher: None,
22601 route_key: None,
22602 });
22603 let active = subscriber
22604 .stage_interest_owner(owner.clone(), &[original], SubscriberOwnerStart::Live)
22605 .unwrap();
22606 assert!(subscriber.activate_interest_owner(&active));
22607 let replacement = subscriber
22608 .stage_interest_owner_replacement(
22609 owner,
22610 &[replacement_interest],
22611 SubscriberOwnerStart::Live,
22612 )
22613 .unwrap();
22614
22615 assert!(subscriber.commit_interest_owner_replacement(&active, &replacement));
22616 assert_eq!(subscriber.interest_owner_state(&active), None);
22617 assert_eq!(
22618 subscriber.interest_owner_state(&replacement),
22619 Some(SubscriberOwnerState::Active)
22620 );
22621 assert_eq!(subscriber.registered_interests().len(), 1);
22622 }
22623
22624 #[test]
22625 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
22626 fn compatibility_and_epoch_owner_lifecycles_cannot_mix() {
22627 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22628 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22629 provider,
22630 SubscriberMode::Auto,
22631 SubscriberConfig::default(),
22632 );
22633 let owner = HandlerId::new("one-lifecycle");
22634 let interest = ReactiveInterest::Logs(LogInterest {
22635 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
22636 local_matcher: None,
22637 route_key: None,
22638 });
22639 let epoch = subscriber
22640 .stage_interest_owner(
22641 owner.clone(),
22642 std::slice::from_ref(&interest),
22643 SubscriberOwnerStart::Live,
22644 )
22645 .expect("stage epoch owner");
22646
22647 assert!(matches!(
22648 subscriber.add_interest_owner(owner.clone(), std::slice::from_ref(&interest)),
22649 Err(SubscriberError::InvalidConfig(_))
22650 ));
22651 assert_eq!(
22652 subscriber.interest_owner_state(&epoch),
22653 Some(SubscriberOwnerState::Staged)
22654 );
22655 assert!(subscriber.abort_interest_owner(&epoch));
22656 subscriber
22657 .add_interest_owner(owner.clone(), std::slice::from_ref(&interest))
22658 .expect("compatibility owner after epoch abort");
22659 assert!(matches!(
22660 subscriber.stage_interest_owner_replacement(
22661 owner,
22662 std::slice::from_ref(&interest),
22663 SubscriberOwnerStart::Live,
22664 ),
22665 Err(SubscriberOwnerError::AlreadyRegistered(_))
22666 ));
22667 }
22668
22669 #[test]
22670 fn pending_record_overflow_is_sticky_and_fail_closed() {
22671 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22672 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22673 provider,
22674 SubscriberMode::Polling,
22675 SubscriberConfig {
22676 max_pending_records: 1,
22677 ..SubscriberConfig::default()
22678 },
22679 );
22680 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
22681 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
22682 local_matcher: None,
22683 route_key: None,
22684 })];
22685 subscriber.enqueue_event(SubscriberEvent::Log {
22686 source_id: 0,
22687 log: rpc_log(false),
22688 #[cfg(feature = "ingress-timing")]
22689 timing: None,
22690 });
22691 let mut second = rpc_log(false);
22692 second.log_index = Some(6);
22693 second.transaction_hash = Some(B256::repeat_byte(0x04));
22694 subscriber.enqueue_event(SubscriberEvent::Log {
22695 source_id: 0,
22696 log: second,
22697 #[cfg(feature = "ingress-timing")]
22698 timing: None,
22699 });
22700
22701 assert_eq!(subscriber.pending_records.len(), 1);
22702 assert!(matches!(
22703 subscriber.check_resource_error(),
22704 Err(SubscriberError::ResourceExhausted(_))
22705 ));
22706 subscriber.reset_delivery_state();
22707 assert!(subscriber.check_resource_error().is_ok());
22708 }
22709
22710 #[test]
22711 fn historical_log_payload_bytes_are_bounded_independently_of_log_count() {
22712 let baseline = rpc_log(false);
22713 let fixed_bytes =
22714 validate_backfill_resource_limits(std::slice::from_ref(&baseline), 1, usize::MAX)
22715 .expect("measure fixed log accounting");
22716 let mut large = baseline;
22717 large.inner = alloy_primitives::Log::new_unchecked(
22718 Address::repeat_byte(0x42),
22719 vec![B256::repeat_byte(0x01)],
22720 Bytes::from(vec![0u8; 256]),
22721 );
22722
22723 assert!(matches!(
22724 validate_backfill_resource_limits(&[large], 1, fixed_bytes + 255),
22725 Err(SubscriberError::ResourceExhausted(_))
22726 ));
22727 }
22728
22729 #[tokio::test(flavor = "multi_thread")]
22730 #[cfg(feature = "reactive-polling")]
22731 async fn reconcile_capacity_failure_does_not_publish_progress_or_partial_history() {
22732 use alloy_rpc_types_eth::{Block, Header};
22733
22734 let asserter = Asserter::new();
22735 let baseline = BlockRef {
22736 number: 6,
22737 hash: B256::repeat_byte(6),
22738 parent_hash: Some(B256::repeat_byte(5)),
22739 timestamp: Some(1_700_000_006),
22740 };
22741 let through = BlockRef {
22742 number: 7,
22743 hash: B256::repeat_byte(7),
22744 parent_hash: Some(baseline.hash),
22745 timestamp: Some(1_700_000_007),
22746 };
22747 let rpc_block = || -> Block {
22748 Block::empty(Header {
22749 hash: through.hash,
22750 inner: alloy_consensus::Header {
22751 number: through.number,
22752 parent_hash: through.parent_hash.expect("parent"),
22753 timestamp: through.timestamp.expect("timestamp"),
22754 ..Default::default()
22755 },
22756 total_difficulty: None,
22757 size: None,
22758 })
22759 };
22760 let mut historical = rpc_log(false);
22761 historical.block_hash = Some(through.hash);
22762 historical.block_timestamp = through.timestamp;
22763 asserter.push_success(&Some(rpc_block()));
22764 asserter.push_success(&vec![historical]);
22765 asserter.push_success(&Some(rpc_block()));
22766 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
22767 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22768 provider,
22769 SubscriberMode::Polling,
22770 SubscriberConfig {
22771 max_pending_records: 1,
22772 ..SubscriberConfig::default()
22773 },
22774 );
22775 subscriber.chain_id = Some(1);
22776 let interest = ReactiveInterest::Logs(LogInterest {
22777 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
22778 local_matcher: None,
22779 route_key: None,
22780 });
22781 let epoch = subscriber
22782 .stage_interest_owner(
22783 HandlerId::new("capacity-owner"),
22784 std::slice::from_ref(&interest),
22785 SubscriberOwnerStart::PostBlock(baseline),
22786 )
22787 .expect("stage owner");
22788 subscriber.sources_dirty = false;
22791 subscriber.state = AlloySubscriberState::Empty;
22792 subscriber.push_pending_record(SubscriberInputRecord {
22793 record: log_input_record(rpc_log(false), InputSource::Poll),
22794 scope: SubscriberInputScope::Canonical { owners: Vec::new() },
22795 preconfirmation_timing: None,
22796 #[cfg(feature = "ingress-timing")]
22797 canonical_timing: None,
22798 });
22799
22800 let error = subscriber
22801 .reconcile_interest_owner(&epoch, through)
22802 .await
22803 .expect_err("historical delivery cannot displace the queued live record");
22804 assert!(matches!(
22805 error,
22806 SubscriberOwnerError::Subscriber(SubscriberError::ResourceExhausted(_))
22807 ));
22808 assert!(subscriber.interest_owner_progress(&epoch).is_none());
22809 assert_eq!(subscriber.pending_records.len(), 1);
22810 assert!(matches!(
22811 subscriber.pending_records[0].scope,
22812 SubscriberInputScope::Canonical { .. }
22813 ));
22814 }
22815
22816 #[test]
22817 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
22818 fn lazy_backfill_queue_capacity_failure_is_atomic() {
22819 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22820 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22821 provider,
22822 SubscriberMode::Auto,
22823 SubscriberConfig {
22824 max_pending_backfills: 1,
22825 ..SubscriberConfig::default()
22826 },
22827 );
22828 let interest = |address| {
22829 ReactiveInterest::Logs(LogInterest {
22830 provider_filter: Filter::new().address(address),
22831 local_matcher: None,
22832 route_key: None,
22833 })
22834 };
22835 subscriber
22836 .add_interest_owner_with_backfill(
22837 HandlerId::new("owner-a"),
22838 &[interest(Address::repeat_byte(0x41))],
22839 SubscriberBackfill::from_block(10),
22840 )
22841 .expect("first queued backfill");
22842
22843 let error = subscriber
22844 .add_interest_owner_with_backfill(
22845 HandlerId::new("owner-b"),
22846 &[interest(Address::repeat_byte(0x42))],
22847 SubscriberBackfill::from_block(10),
22848 )
22849 .expect_err("second backfill must exceed capacity");
22850
22851 assert!(matches!(error, SubscriberError::ResourceExhausted(_)));
22852 assert!(
22853 subscriber
22854 .owner_interests(&HandlerId::new("owner-b"))
22855 .is_none()
22856 );
22857 assert_eq!(subscriber.pending_backfills.len(), 1);
22858 }
22859
22860 #[test]
22861 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
22862 fn exact_owner_replacement_is_atomic_and_removes_crash_stale_owners() {
22863 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22864 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22865 provider,
22866 SubscriberMode::Auto,
22867 SubscriberConfig {
22868 max_pending_backfills: 1,
22869 ..SubscriberConfig::default()
22870 },
22871 );
22872 let interest = |address| {
22873 ReactiveInterest::Logs(LogInterest {
22874 provider_filter: Filter::new().address(address),
22875 local_matcher: None,
22876 route_key: None,
22877 })
22878 };
22879 subscriber
22880 .add_interest_owner(
22881 HandlerId::new("crash-stale"),
22882 &[interest(Address::repeat_byte(0xee))],
22883 )
22884 .expect("seed stale owner");
22885 subscriber.base_interests = vec![interest(Address::repeat_byte(0xdd))];
22886 subscriber.rebuild_registered_interests();
22887 subscriber.push_pending_record(SubscriberInputRecord {
22888 record: log_input_record(rpc_log(false), InputSource::Poll),
22889 scope: SubscriberInputScope::Canonical { owners: Vec::new() },
22890 preconfirmation_timing: None,
22891 #[cfg(feature = "ingress-timing")]
22892 canonical_timing: None,
22893 });
22894 let baseline = BlockRef {
22895 number: 100,
22896 hash: B256::repeat_byte(100),
22897 parent_hash: Some(B256::repeat_byte(99)),
22898 timestamp: Some(1_700_000_100),
22899 };
22900 let backfill = SubscriberBackfill::after_canonical_block(baseline).expect("C + 1");
22901
22902 let error = subscriber
22903 .replace_interest_owners_with_global_backfill(
22904 vec![
22905 (
22906 HandlerId::new("pool-a"),
22907 vec![interest(Address::repeat_byte(0xa1))],
22908 ),
22909 (
22910 HandlerId::new("pool-b"),
22911 vec![ReactiveInterest::Logs(LogInterest {
22912 provider_filter: Filter::new()
22915 .address(Address::repeat_byte(0xb2))
22916 .from_block(7),
22917 local_matcher: None,
22918 route_key: None,
22919 })],
22920 ),
22921 ],
22922 backfill,
22923 )
22924 .expect_err("two backfills exceed atomic capacity");
22925 assert!(matches!(error, SubscriberError::ResourceExhausted(_)));
22926 assert!(
22927 subscriber
22928 .owner_interests(&HandlerId::new("crash-stale"))
22929 .is_some(),
22930 "failed replacement must preserve the prior topology"
22931 );
22932 assert!(
22933 subscriber
22934 .owner_interests(&HandlerId::new("pool-a"))
22935 .is_none()
22936 );
22937 assert_eq!(subscriber.base_interests.len(), 1);
22938 assert_eq!(subscriber.pending_records.len(), 1);
22939
22940 subscriber
22941 .replace_interest_owners_with_global_backfill(
22942 vec![(
22943 HandlerId::new("pool-a"),
22944 vec![interest(Address::repeat_byte(0xa1))],
22945 )],
22946 backfill,
22947 )
22948 .expect("replacement within capacity");
22949 assert!(
22950 subscriber
22951 .owner_interests(&HandlerId::new("crash-stale"))
22952 .is_none(),
22953 "successful exact replacement removes stale owners"
22954 );
22955 assert!(
22956 subscriber.base_interests.is_empty(),
22957 "successful exact replacement removes stale unowned interests"
22958 );
22959 assert!(
22960 subscriber.drain_next_scoped_batch().is_none(),
22961 "stale canonical delivery must not escape before C + 1 recovery"
22962 );
22963 assert!(
22964 subscriber
22965 .owner_interests(&HandlerId::new("pool-a"))
22966 .is_some()
22967 );
22968 assert_eq!(subscriber.pending_backfills.len(), 1);
22969 assert_eq!(subscriber.pending_backfills[0].backfill, backfill);
22970 assert!(
22971 subscriber.pending_backfills[0].owner.is_none(),
22972 "startup history must be global canonical catch-up, not owner-only"
22973 );
22974 }
22975
22976 #[test]
22977 fn exclusive_canonical_backfill_rejects_block_number_overflow() {
22978 let baseline = BlockRef {
22979 number: u64::MAX,
22980 hash: B256::repeat_byte(0xff),
22981 parent_hash: None,
22982 timestamp: None,
22983 };
22984 assert!(matches!(
22985 SubscriberBackfill::after_canonical_block(baseline),
22986 Err(SubscriberError::InvalidConfig(_))
22987 ));
22988 }
22989
22990 #[tokio::test(flavor = "multi_thread")]
22991 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
22992 async fn exclusive_canonical_backfill_validates_the_retained_baseline_hash() {
22993 let asserter = Asserter::new();
22994 asserter.push_success(&101u64);
22995 asserter.push_success(&Some(rpc_block(101, B256::repeat_byte(101))));
22996 asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(0xee))));
22997 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
22998 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22999 provider,
23000 SubscriberMode::Auto,
23001 SubscriberConfig::default(),
23002 );
23003 let baseline = BlockRef {
23004 number: 100,
23005 hash: B256::repeat_byte(0xaa),
23006 parent_hash: None,
23007 timestamp: None,
23008 };
23009 let backfill = SubscriberBackfill::after_canonical_block(baseline).expect("C + 1");
23010 subscriber
23011 .add_interest_owner_with_backfill(
23012 HandlerId::new("pool"),
23013 &[ReactiveInterest::Logs(LogInterest {
23014 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
23015 local_matcher: None,
23016 route_key: None,
23017 })],
23018 backfill,
23019 )
23020 .expect("queue post-baseline backfill");
23021
23022 let error = subscriber
23023 .drain_pending_backfills()
23024 .await
23025 .expect_err("provider branch differs at retained baseline");
23026 assert!(matches!(error, SubscriberError::InvalidBackfill(_)));
23027 assert_eq!(subscriber.pending_backfills.len(), 1);
23028 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 101);
23029 assert!(subscriber.pending_records.is_empty());
23030 }
23031
23032 #[tokio::test(flavor = "multi_thread")]
23033 #[cfg(feature = "reactive-ws")]
23034 async fn coordinated_multifilter_windows_are_globally_sorted_for_owner_and_canonical_delivery()
23035 {
23036 let asserter = Asserter::new();
23037 let retained = BlockRef {
23038 number: 10,
23039 hash: B256::repeat_byte(10),
23040 parent_hash: Some(B256::repeat_byte(9)),
23041 timestamp: Some(1_700_000_010),
23042 };
23043 let activation = BlockRef {
23044 number: 12,
23045 hash: B256::repeat_byte(12),
23046 parent_hash: Some(B256::repeat_byte(11)),
23047 timestamp: Some(1_700_000_012),
23048 };
23049
23050 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
23054 asserter.push_success(&vec![rpc_log_at(10, 2, 2)]);
23055 asserter.push_success(&vec![rpc_log_at(10, 1, 1)]);
23056 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
23057 asserter.push_success(&activation.number);
23058 asserter.push_success(&Some(rpc_block(activation.number, activation.hash)));
23059 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
23060 asserter.push_success(&vec![rpc_log_at(12, 2, 2)]);
23061 asserter.push_success(&vec![rpc_log_at(11, 1, 1)]);
23062 asserter.push_success(&Some(rpc_block(activation.number, activation.hash)));
23063 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
23064 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23065 provider,
23066 SubscriberMode::Auto,
23067 SubscriberConfig::default(),
23068 );
23069 let interests = (0..257)
23070 .map(|start| {
23071 ReactiveInterest::Logs(LogInterest {
23072 provider_filter: Filter::new()
23073 .address(Address::repeat_byte(0x42))
23074 .event_signature(B256::repeat_byte(0x01))
23075 .from_block(start),
23076 local_matcher: None,
23077 route_key: None,
23078 })
23079 })
23080 .collect::<Vec<_>>();
23081 subscriber
23082 .add_interest_owner_with_canonical_catchup(
23083 HandlerId::new("many-filters"),
23084 &interests,
23085 retained,
23086 )
23087 .expect("queue coordinated windows");
23088 assert_eq!(subscriber.pending_backfills.len(), 2);
23089 assert_eq!(subscriber.pending_backfills[0].filters.len(), 257);
23090 assert_eq!(subscriber.pending_backfills[1].filters.len(), 257);
23091
23092 subscriber
23093 .drain_pending_backfills()
23094 .await
23095 .expect("owner filter group");
23096 let owner = subscriber
23097 .drain_next_scoped_batch()
23098 .expect("owner ordered batch");
23099 assert_eq!(owner.records.len(), 2);
23100 assert_eq!(owner.records[0].record.context.transaction_index, Some(1));
23101 assert_eq!(owner.records[1].record.context.transaction_index, Some(2));
23102 assert!(
23103 owner.records.iter().all(|record| matches!(
23104 record.scope,
23105 SubscriberInputScope::OwnerOnlyHandlers { .. }
23106 ))
23107 );
23108
23109 subscriber
23110 .drain_pending_backfills()
23111 .await
23112 .expect("global filter group");
23113 let global = subscriber
23114 .drain_next_scoped_batch()
23115 .expect("global ordered batch");
23116 assert_eq!(global.records.len(), 2);
23117 assert_eq!(
23118 global.records[0].record.context.block.map(|b| b.number),
23119 Some(11)
23120 );
23121 assert_eq!(
23122 global.records[1].record.context.block.map(|b| b.number),
23123 Some(12)
23124 );
23125 assert!(
23126 global
23127 .records
23128 .iter()
23129 .all(|record| record.scope.is_canonical())
23130 );
23131 assert!(matches!(
23132 global.chain_controls.as_slice(),
23133 [ChainControl::Barrier {
23134 block: Some(block),
23135 ..
23136 }] if block == &activation
23137 ));
23138 }
23139
23140 #[tokio::test(flavor = "multi_thread")]
23141 #[cfg(feature = "reactive-ws")]
23142 async fn aborting_staged_epoch_purges_only_its_buffered_delivery() {
23143 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23144 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23145 provider,
23146 SubscriberMode::PubSub,
23147 SubscriberConfig::default(),
23148 );
23149 subscriber.chain_id = Some(1);
23150 let interest = ReactiveInterest::Logs(LogInterest {
23151 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
23152 local_matcher: None,
23153 route_key: None,
23154 });
23155 let owner_a = subscriber
23156 .stage_interest_owner(
23157 HandlerId::new("owner-a"),
23158 std::slice::from_ref(&interest),
23159 SubscriberOwnerStart::Live,
23160 )
23161 .unwrap();
23162 let owner_b = subscriber
23163 .stage_interest_owner(
23164 HandlerId::new("owner-b"),
23165 &[interest],
23166 SubscriberOwnerStart::Live,
23167 )
23168 .unwrap();
23169
23170 subscriber.enqueue_event(SubscriberEvent::Log {
23171 source_id: 0,
23172 log: rpc_log(false),
23173 #[cfg(feature = "ingress-timing")]
23174 timing: None,
23175 });
23176 assert!(subscriber.abort_interest_owner(&owner_a));
23177
23178 let batch = subscriber
23179 .next_scoped_batch()
23180 .await
23181 .unwrap()
23182 .expect("shared canonical delivery remains queued");
23183 assert_eq!(batch.records.len(), 1);
23184 assert_eq!(
23185 batch.records[0].scope,
23186 SubscriberInputScope::Canonical {
23187 owners: vec![owner_b]
23188 }
23189 );
23190 }
23191
23192 #[tokio::test(flavor = "multi_thread")]
23193 #[cfg(feature = "reactive-ws")]
23194 async fn owner_backfill_dedupe_never_suppresses_canonical_delivery() {
23195 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23196 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23197 provider,
23198 SubscriberMode::PubSub,
23199 SubscriberConfig::default(),
23200 );
23201 subscriber.chain_id = Some(1);
23202 let epoch = subscriber
23203 .stage_interest_owner(
23204 HandlerId::new("owner"),
23205 &[ReactiveInterest::Logs(LogInterest {
23206 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
23207 local_matcher: None,
23208 route_key: None,
23209 })],
23210 SubscriberOwnerStart::Live,
23211 )
23212 .unwrap();
23213 let log = rpc_log(false);
23214
23215 subscriber.enqueue_owner_record(
23216 log_input_record(log.clone(), InputSource::Backfill),
23217 epoch.clone(),
23218 );
23219 subscriber.enqueue_event(SubscriberEvent::Log {
23220 source_id: 0,
23221 log,
23222 #[cfg(feature = "ingress-timing")]
23223 timing: None,
23224 });
23225
23226 let batch = subscriber
23227 .next_scoped_batch()
23228 .await
23229 .unwrap()
23230 .expect("owner backfill and canonical live delivery");
23231 assert_eq!(batch.records.len(), 2);
23232 assert_eq!(
23233 batch.records[0].scope,
23234 SubscriberInputScope::OwnerOnly {
23235 owners: vec![epoch]
23236 }
23237 );
23238 assert_eq!(
23239 batch.records[1].scope,
23240 SubscriberInputScope::Canonical { owners: Vec::new() },
23241 "owner replay dedupe must not suppress the global live record"
23242 );
23243 }
23244
23245 #[tokio::test(flavor = "multi_thread")]
23246 #[cfg(feature = "reactive-polling")]
23247 async fn reconcile_fetch_drains_live_burst_beyond_output_batch_capacity() {
23248 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23249 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23250 provider,
23251 SubscriberMode::Polling,
23252 SubscriberConfig {
23253 max_batch_size: 2,
23254 ..SubscriberConfig::default()
23255 },
23256 );
23257 let interest = ReactiveInterest::Logs(LogInterest {
23258 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
23259 local_matcher: None,
23260 route_key: None,
23261 });
23262 let epoch = subscriber
23263 .stage_interest_owner(
23264 HandlerId::new("owner"),
23265 std::slice::from_ref(&interest),
23266 SubscriberOwnerStart::Live,
23267 )
23268 .unwrap();
23269 subscriber.sources_dirty = false;
23270
23271 let mut duplicate = rpc_log(false);
23272 duplicate.transaction_hash = Some(B256::repeat_byte(1));
23273 duplicate.log_index = Some(0);
23274 let events = (0u8..10).map(|index| {
23275 let mut log = rpc_log(false);
23276 log.transaction_hash = Some(B256::repeat_byte(index.saturating_add(1)));
23277 log.log_index = Some(index as u64);
23278 SubscriberEvent::Log {
23279 source_id: 0,
23280 log,
23281 #[cfg(feature = "ingress-timing")]
23282 timing: None,
23283 }
23284 });
23285 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
23286 let mut streams = SubscriberStreams::new();
23287 streams.push(
23288 SubscriberStreamSource::PollingLog { filter },
23289 stream::iter(events).boxed(),
23290 );
23291 subscriber.state = AlloySubscriberState::Active(streams);
23292
23293 let mut polls = 0usize;
23294 let fetched_duplicate = duplicate.clone();
23295 let fetch = poll_fn(move |cx| {
23296 polls += 1;
23297 if polls > 10 {
23298 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>(fetched_duplicate.clone()))
23299 } else {
23300 cx.waker().wake_by_ref();
23301 std::task::Poll::Pending
23302 }
23303 });
23304 let target_epochs = HashSet::from([epoch.clone()]);
23305 let fetched_duplicate = subscriber
23306 .drive_reconcile_fetch(fetch, &target_epochs)
23307 .await
23308 .unwrap();
23309 subscriber.enqueue_owner_record_for_owners_unmerged(
23310 log_input_record(fetched_duplicate, InputSource::Backfill),
23311 vec![epoch.clone()],
23312 );
23313 subscriber.promote_reconcile_owner_records(&target_epochs);
23314
23315 assert_eq!(subscriber.pending_records.len(), 20);
23316 assert!(subscriber.pending_records.iter().take(10).all(|record| {
23317 record.scope == SubscriberInputScope::Canonical { owners: Vec::new() }
23318 }));
23319 assert!(subscriber.pending_records.iter().skip(10).all(|record| {
23320 record.scope
23321 == SubscriberInputScope::OwnerOnly {
23322 owners: vec![epoch.clone()],
23323 }
23324 }));
23325 }
23326
23327 #[tokio::test(flavor = "multi_thread")]
23328 #[cfg(feature = "reactive-polling")]
23329 async fn reconcile_fetch_waits_for_provider_when_live_topology_is_empty() {
23330 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23331 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23332 provider,
23333 SubscriberMode::Polling,
23334 SubscriberConfig::default(),
23335 );
23336 subscriber.chain_id = Some(1);
23337 subscriber.sources_dirty = false;
23338 let mut first_poll = true;
23339 let fetch = poll_fn(move |cx| {
23340 if first_poll {
23341 first_poll = false;
23342 cx.waker().wake_by_ref();
23343 std::task::Poll::Pending
23344 } else {
23345 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>("certified"))
23346 }
23347 });
23348
23349 let result = subscriber
23350 .drive_reconcile_fetch(fetch, &HashSet::new())
23351 .await
23352 .expect("an empty live topology must not be mistaken for termination");
23353 assert_eq!(result, "certified");
23354 }
23355
23356 #[tokio::test(flavor = "multi_thread")]
23357 #[cfg(all(feature = "reactive-polling", feature = "reactive-ws"))]
23358 async fn successful_owner_reconcile_seeds_its_live_filter_reconnect_anchor() {
23359 use alloy_rpc_types_eth::{Block, Header};
23360
23361 let asserter = Asserter::new();
23362 let baseline = BlockRef {
23363 number: 100,
23364 hash: B256::repeat_byte(0x64),
23365 parent_hash: Some(B256::repeat_byte(0x63)),
23366 timestamp: Some(1_700_000_100),
23367 };
23368 let through = BlockRef {
23369 number: 101,
23370 hash: B256::repeat_byte(0x65),
23371 parent_hash: Some(baseline.hash),
23372 timestamp: Some(1_700_000_101),
23373 };
23374 let rpc_block = || -> Block {
23375 Block::empty(Header {
23376 hash: through.hash,
23377 inner: alloy_consensus::Header {
23378 number: through.number,
23379 parent_hash: through.parent_hash.unwrap(),
23380 timestamp: through.timestamp.unwrap(),
23381 ..Default::default()
23382 },
23383 total_difficulty: None,
23384 size: None,
23385 })
23386 };
23387 asserter.push_success(&Some(rpc_block()));
23388 asserter.push_success(&Vec::<Log>::new());
23389 asserter.push_success(&Some(rpc_block()));
23390 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
23391 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23392 provider,
23393 SubscriberMode::PubSub,
23394 SubscriberConfig::default(),
23395 );
23396 subscriber.chain_id = Some(1);
23397 let interest = ReactiveInterest::Logs(LogInterest {
23398 provider_filter: Filter::new().address(Address::repeat_byte(0xac)),
23399 local_matcher: None,
23400 route_key: None,
23401 });
23402 let epoch = subscriber
23403 .stage_interest_owner(
23404 HandlerId::new("reconnect-anchor"),
23405 std::slice::from_ref(&interest),
23406 SubscriberOwnerStart::PostBlock(baseline),
23407 )
23408 .unwrap();
23409 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
23410 let source = SubscriberStreamSource::PubSubLog {
23411 id: subscriber.log_source_id(&filter),
23412 filter: filter.clone(),
23413 };
23414 let mut streams = SubscriberStreams::new();
23415 streams.push(source, stream::pending().boxed());
23416 subscriber.state = AlloySubscriberState::Active(streams);
23417 subscriber.sources_dirty = false;
23418
23419 subscriber
23420 .reconcile_interest_owner(&epoch, through)
23421 .await
23422 .unwrap();
23423 assert_eq!(subscriber.log_anchor(&filter), Some(through.number));
23424 assert!(asserter.read_q().is_empty());
23425 }
23426
23427 #[tokio::test(flavor = "multi_thread")]
23428 #[cfg(feature = "reactive-polling")]
23429 async fn cancelled_reconcile_retains_hidden_owner_live_delivery_for_retry() {
23430 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23431 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23432 provider,
23433 SubscriberMode::Polling,
23434 SubscriberConfig::default(),
23435 );
23436 let interest = ReactiveInterest::Logs(LogInterest {
23437 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
23438 local_matcher: None,
23439 route_key: None,
23440 });
23441 let epoch = subscriber
23442 .stage_interest_owner(
23443 HandlerId::new("owner"),
23444 std::slice::from_ref(&interest),
23445 SubscriberOwnerStart::PostBlock(BlockRef {
23446 number: 100,
23447 hash: B256::repeat_byte(0x64),
23448 parent_hash: None,
23449 timestamp: None,
23450 }),
23451 )
23452 .unwrap();
23453 subscriber.sources_dirty = false;
23454
23455 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
23456 let event = SubscriberEvent::Log {
23457 source_id: 0,
23458 log: rpc_log(false),
23459 #[cfg(feature = "ingress-timing")]
23460 timing: None,
23461 };
23462 let mut streams = SubscriberStreams::new();
23463 streams.push(
23464 SubscriberStreamSource::PollingLog { filter },
23465 stream::once(async move { event })
23466 .chain(stream::pending())
23467 .boxed(),
23468 );
23469 subscriber.state = AlloySubscriberState::Active(streams);
23470
23471 let targets = HashSet::from([epoch.clone()]);
23472 {
23473 let fetch = futures::future::pending::<Result<(), SubscriberOwnerError>>();
23474 let drive = subscriber.drive_reconcile_fetch(fetch, &targets);
23475 futures::pin_mut!(drive);
23476 poll_fn(|cx| {
23477 assert!(drive.as_mut().poll(cx).is_pending());
23478 std::task::Poll::Ready(())
23479 })
23480 .await;
23481 }
23482
23483 assert_eq!(subscriber.pending_records.len(), 1);
23484 assert_eq!(
23485 subscriber.pending_records[0].scope,
23486 SubscriberInputScope::Canonical { owners: Vec::new() },
23487 "canonical delivery commits immediately at a cancellation-safe boundary"
23488 );
23489 assert_eq!(subscriber.pending_reconcile_owner_records.len(), 1);
23490
23491 subscriber
23492 .drive_reconcile_fetch(futures::future::ready(Ok(())), &targets)
23493 .await
23494 .unwrap();
23495 subscriber.promote_reconcile_owner_records(&targets);
23496 assert!(subscriber.pending_reconcile_owner_records.is_empty());
23497 assert_eq!(subscriber.pending_records.len(), 2);
23498 assert_eq!(
23499 subscriber.pending_records[0].scope,
23500 SubscriberInputScope::Canonical { owners: Vec::new() },
23501 "canonical delivery remains target-excluded"
23502 );
23503 assert_eq!(
23504 subscriber.pending_records[1].scope,
23505 SubscriberInputScope::OwnerOnly {
23506 owners: vec![epoch]
23507 },
23508 "retry commit appends hidden owner delivery after historical catch-up"
23509 );
23510 }
23511
23512 #[tokio::test(flavor = "multi_thread")]
23513 #[cfg(feature = "reactive-ws")]
23514 async fn control_cancellation_preserves_terminated_source_reconcile_intent() {
23515 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23516 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23517 provider,
23518 SubscriberMode::PubSub,
23519 SubscriberConfig {
23520 reconnect: SubscriberReconnectConfig {
23521 initial_delay: Duration::from_secs(60),
23522 ..SubscriberReconnectConfig::default()
23523 },
23524 ..SubscriberConfig::default()
23525 },
23526 );
23527 subscriber.chain_id = Some(1);
23528 let interest = ReactiveInterest::Logs(LogInterest {
23529 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
23530 local_matcher: None,
23531 route_key: None,
23532 });
23533 let epoch = subscriber
23534 .stage_interest_owner(
23535 HandlerId::new("owner"),
23536 std::slice::from_ref(&interest),
23537 SubscriberOwnerStart::PostBlock(BlockRef {
23538 number: 7,
23539 hash: B256::repeat_byte(0x07),
23540 parent_hash: Some(B256::repeat_byte(0x06)),
23541 timestamp: Some(1_700_000_007),
23542 }),
23543 )
23544 .unwrap();
23545 subscriber.sources_dirty = false;
23546 subscriber.stream_revision = 1;
23547 let entry = subscriber
23548 .owned_interests
23549 .iter_mut()
23550 .find(|entry| entry.epoch.as_ref() == Some(&epoch))
23551 .unwrap();
23552 entry.progress = Some(SubscriberOwnerProgress {
23553 owner: epoch.clone(),
23554 through: entry.baseline.unwrap(),
23555 });
23556 entry.progress_stream_revision = Some(1);
23557
23558 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
23559 let source = SubscriberStreamSource::PubSubLog {
23560 id: subscriber.log_source_id(&filter),
23561 filter,
23562 };
23563 let mut streams = SubscriberStreams::new();
23564 streams.push(
23565 source.clone(),
23566 stream::iter([SubscriberEvent::StreamTerminated(source)]).boxed(),
23567 );
23568 subscriber.state = AlloySubscriberState::Active(streams);
23569 let prior_revision = subscriber.stream_revision;
23570
23571 let mut first_poll = true;
23572 let control = poll_fn(move |cx| {
23573 if first_poll {
23574 first_poll = false;
23575 cx.waker().wake_by_ref();
23576 std::task::Poll::Pending
23577 } else {
23578 std::task::Poll::Ready("stop")
23579 }
23580 });
23581 futures::pin_mut!(control);
23582 let outcome = subscriber
23583 .next_scoped_batch_or(control.as_mut())
23584 .await
23585 .unwrap();
23586
23587 assert!(matches!(outcome, SubscriberDriverPoll::Control("stop")));
23588 assert!(subscriber.sources_dirty);
23589 assert!(subscriber.stream_revision > prior_revision);
23590 assert!(
23591 !subscriber.activate_interest_owner(&epoch),
23592 "progress certified against the terminated stream revision is stale"
23593 );
23594 }
23595
23596 #[tokio::test]
23597 #[cfg(feature = "reactive-ws")]
23598 async fn pubsub_sources_assign_stable_log_ids_before_shared_streams() {
23599 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23600 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23601 provider,
23602 SubscriberMode::PubSub,
23603 SubscriberConfig::default(),
23604 );
23605 subscriber.chain_id = Some(1);
23606 subscriber
23607 .register_interests(&[
23608 ReactiveInterest::Logs(LogInterest {
23609 provider_filter: Filter::new().address(Address::repeat_byte(0x01)),
23610 local_matcher: None,
23611 route_key: None,
23612 }),
23613 ReactiveInterest::Logs(LogInterest {
23614 provider_filter: Filter::new().address(Address::repeat_byte(0x02)),
23615 local_matcher: None,
23616 route_key: None,
23617 }),
23618 ReactiveInterest::PendingTransactions(PendingTxInterest::default()),
23619 ])
23620 .await
23621 .expect("register base interests");
23622
23623 let sources = subscriber.stream_sources().expect("stream sources");
23627 assert_eq!(sources.len(), 2);
23628 assert!(matches!(
23629 &sources[0],
23630 SubscriberStreamSource::PubSubLog { id: 0, .. }
23631 ));
23632 assert!(matches!(
23633 sources[1],
23634 SubscriberStreamSource::PubSubPendingHashes
23635 ));
23636
23637 let again = subscriber.stream_sources().expect("stream sources again");
23639 assert!(again[0].same_key(&sources[0]));
23640 }
23641
23642 #[tokio::test(flavor = "multi_thread")]
23643 #[cfg(feature = "reactive-ws")]
23644 async fn pubsub_stream_termination_attempts_reconnect_before_error() {
23645 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23646 let mut subscriber = AlloySubscriber::new(
23647 provider,
23648 SubscriberMode::PubSub,
23649 SubscriberConfig {
23650 reconnect: SubscriberReconnectConfig {
23651 initial_delay: Duration::ZERO,
23652 retry_delay: Duration::ZERO,
23653 max_delay: Duration::ZERO,
23654 max_attempts: Some(1),
23655 ..SubscriberReconnectConfig::default()
23656 },
23657 ..SubscriberConfig::default()
23658 },
23659 );
23660 subscriber.chain_id = Some(1);
23661 subscriber.interests = vec![ReactiveInterest::PendingTransactions(
23662 PendingTxInterest::default(),
23663 )];
23664
23665 let mut streams = SubscriberStreams::new();
23666 let source = SubscriberStreamSource::PubSubPendingHashes;
23667 streams.push(
23668 source,
23669 stream::once(async {
23670 SubscriberEvent::<Ethereum>::StreamTerminated(
23671 SubscriberStreamSource::PubSubPendingHashes,
23672 )
23673 })
23674 .boxed(),
23675 );
23676 subscriber.state = AlloySubscriberState::Active(streams);
23677
23678 let result = subscriber.next_batch().await;
23679 assert!(
23680 matches!(result, Err(SubscriberError::Provider(ref message)) if message.contains("reconnect failed after 1 attempt")),
23681 "terminated pubsub streams should attempt reconnect before surfacing failure: {result:?}"
23682 );
23683 }
23684
23685 #[tokio::test]
23686 #[cfg(feature = "reactive-ws")]
23687 async fn flashblock_stream_termination_invalidates_before_reconnect_io() {
23688 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23689 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23690 provider,
23691 SubscriberMode::PubSub,
23692 SubscriberConfig {
23693 preconfirmations: PreconfirmationMode::Required,
23694 ..SubscriberConfig::default()
23695 },
23696 )
23697 .with_provider_ref(ProviderRef::new("base-paid", 7));
23698 subscriber.chain_id = Some(8_453);
23699 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
23700 subscriber.interests = subscriber.base_interests.clone();
23701 subscriber.sources_dirty = false;
23702
23703 let preview: BaseFlashblockWirePayload = serde_json::from_str(
23704 r#"{
23705 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
23706 "number":"0x65",
23707 "parentHash":"0x6464646464646464646464646464646464646464646464646464646464646464",
23708 "stateRoot":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
23709 "transactionsRoot":"0x1111111111111111111111111111111111111111111111111111111111111111",
23710 "timestamp":"0x6553f165",
23711 "transactions":["0x4141414141414141414141414141414141414141414141414141414141414141"]
23712 }"#,
23713 )
23714 .unwrap();
23715 let (preview, _) = subscriber.accept_base_flashblock(preview).unwrap();
23716 subscriber.latest_preconfirmation = Some(preview);
23717
23718 let mut streams = SubscriberStreams::new();
23719 streams.push(
23720 SubscriberStreamSource::BaseFlashblocks,
23721 stream::once(async {
23722 SubscriberEvent::<Ethereum>::StreamTerminated(
23723 SubscriberStreamSource::BaseFlashblocks,
23724 )
23725 })
23726 .boxed(),
23727 );
23728 subscriber.state = AlloySubscriberState::Active(streams);
23729
23730 let batch = subscriber
23731 .next_scoped_batch()
23732 .await
23733 .expect("termination handling succeeds")
23734 .expect("invalidation is delivered");
23735 assert!(batch.preconfirmation_invalidated());
23736 assert!(subscriber.latest_preconfirmation.is_none());
23737 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 8);
23738 assert_eq!(subscriber.pending_flashblock_reconnects.len(), 2);
23739 assert!(
23740 subscriber
23741 .pending_flashblock_reconnect_sources
23742 .iter()
23743 .any(|source| matches!(source, SubscriberStreamSource::BaseFlashblocks))
23744 );
23745 assert!(
23746 subscriber
23747 .pending_flashblock_reconnect_sources
23748 .iter()
23749 .any(|source| matches!(source, SubscriberStreamSource::BasePendingLog { .. }))
23750 );
23751 let AlloySubscriberState::Active(streams) = &subscriber.state else {
23752 panic!("subscriber remains active while reconnect is pending")
23753 };
23754 assert!(
23755 streams
23756 .entries
23757 .iter()
23758 .all(|entry| !entry.source.is_flashblocks())
23759 );
23760 }
23761
23762 #[tokio::test]
23763 #[cfg(feature = "reactive-ws")]
23764 async fn preferred_initial_flashblock_rejection_retains_canonical_streams() {
23765 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23766 let filter = Filter::new().address(Address::repeat_byte(0x42));
23767 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23768 provider,
23769 SubscriberMode::PubSub,
23770 SubscriberConfig {
23771 preconfirmations: PreconfirmationMode::Preferred,
23772 reconnect: SubscriberReconnectConfig {
23773 enabled: false,
23774 ..SubscriberReconnectConfig::default()
23775 },
23776 ..SubscriberConfig::default()
23777 },
23778 )
23779 .with_provider_ref(ProviderRef::new("base-paid", 1));
23780 subscriber.chain_id = Some(8_453);
23781 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
23782 provider_filter: filter.clone(),
23783 local_matcher: None,
23784 route_key: None,
23785 })];
23786 subscriber.interests = subscriber.base_interests.clone();
23787 subscriber.log_source_ids.insert(filter.clone(), 0);
23788 subscriber.next_log_source_id = 1;
23789
23790 let canonical_source = SubscriberStreamSource::PubSubLog {
23791 id: 0,
23792 filter: filter.clone(),
23793 };
23794 let mut streams = SubscriberStreams::new();
23795 streams.push(
23796 canonical_source.clone(),
23797 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
23798 );
23799 subscriber.state = AlloySubscriberState::Active(streams);
23800 subscriber.sources_dirty = true;
23801
23802 subscriber
23803 .ensure_streams()
23804 .await
23805 .expect("preferred Flashblocks setup degrades to canonical-only");
23806 let AlloySubscriberState::Active(streams) = &subscriber.state else {
23807 panic!("canonical stream remains active")
23808 };
23809 assert!(streams.contains_source(&canonical_source));
23810 assert!(
23811 streams
23812 .entries
23813 .iter()
23814 .all(|entry| !entry.source.is_flashblocks())
23815 );
23816 assert!(subscriber.pending_flashblock_reconnects.is_empty());
23817 assert!(!subscriber.sources_dirty);
23818 }
23819
23820 #[tokio::test]
23821 #[cfg(feature = "reactive-ws")]
23822 async fn required_initial_flashblock_rejection_remains_fail_closed() {
23823 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23824 let filter = Filter::new().address(Address::repeat_byte(0x42));
23825 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23826 provider,
23827 SubscriberMode::PubSub,
23828 SubscriberConfig {
23829 preconfirmations: PreconfirmationMode::Required,
23830 reconnect: SubscriberReconnectConfig {
23831 enabled: false,
23832 ..SubscriberReconnectConfig::default()
23833 },
23834 ..SubscriberConfig::default()
23835 },
23836 )
23837 .with_provider_ref(ProviderRef::new("base-paid", 1));
23838 subscriber.chain_id = Some(8_453);
23839 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
23840 provider_filter: filter.clone(),
23841 local_matcher: None,
23842 route_key: None,
23843 })];
23844 subscriber.interests = subscriber.base_interests.clone();
23845 subscriber.log_source_ids.insert(filter.clone(), 0);
23846 subscriber.next_log_source_id = 1;
23847
23848 let canonical_source = SubscriberStreamSource::PubSubLog {
23849 id: 0,
23850 filter: filter.clone(),
23851 };
23852 let mut streams = SubscriberStreams::new();
23853 streams.push(
23854 canonical_source.clone(),
23855 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
23856 );
23857 subscriber.state = AlloySubscriberState::Active(streams);
23858 subscriber.sources_dirty = true;
23859
23860 let error = subscriber
23861 .ensure_streams()
23862 .await
23863 .expect_err("required Flashblocks setup must fail closed");
23864 assert!(matches!(error, SubscriberError::Provider(_)));
23865 let AlloySubscriberState::Active(streams) = &subscriber.state else {
23866 panic!("the already-connected canonical stream is retained")
23867 };
23868 assert!(streams.contains_source(&canonical_source));
23869 }
23870
23871 #[tokio::test]
23872 #[cfg(feature = "reactive-ws")]
23873 async fn preferred_flashblock_termination_preserves_canonical_delivery() {
23874 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23875 let filter = Filter::new().address(Address::repeat_byte(0x42));
23876 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23877 provider,
23878 SubscriberMode::PubSub,
23879 SubscriberConfig {
23880 preconfirmations: PreconfirmationMode::Preferred,
23881 reconnect: SubscriberReconnectConfig {
23882 enabled: false,
23883 ..SubscriberReconnectConfig::default()
23884 },
23885 ..SubscriberConfig::default()
23886 },
23887 )
23888 .with_provider_ref(ProviderRef::new("base-paid", 1));
23889 subscriber.chain_id = Some(8_453);
23890 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
23891 provider_filter: filter.clone(),
23892 local_matcher: None,
23893 route_key: None,
23894 })];
23895 subscriber.interests = subscriber.base_interests.clone();
23896 subscriber.log_source_ids.insert(filter.clone(), 0);
23897 subscriber.next_log_source_id = 1;
23898 subscriber.sources_dirty = false;
23899
23900 let mut streams = SubscriberStreams::new();
23901 streams.push(
23902 SubscriberStreamSource::BaseFlashblocks,
23903 stream::once(async {
23904 SubscriberEvent::<Ethereum>::StreamTerminated(
23905 SubscriberStreamSource::BaseFlashblocks,
23906 )
23907 })
23908 .boxed(),
23909 );
23910 streams.push(
23911 SubscriberStreamSource::PubSubLog {
23912 id: 0,
23913 filter: filter.clone(),
23914 },
23915 stream::once(async {
23916 SubscriberEvent::<Ethereum>::Log {
23917 #[cfg(feature = "ingress-timing")]
23918 timing: None,
23919 source_id: 0,
23920 log: rpc_log(false),
23921 }
23922 })
23923 .boxed(),
23924 );
23925 subscriber.state = AlloySubscriberState::Active(streams);
23926
23927 let invalidation = subscriber
23928 .next_scoped_batch()
23929 .await
23930 .expect("preferred termination does not fail")
23931 .expect("invalidation is delivered");
23932 assert!(invalidation.preconfirmation_invalidated());
23933
23934 let canonical = subscriber
23935 .next_scoped_batch()
23936 .await
23937 .expect("canonical stream remains healthy")
23938 .expect("canonical log is delivered");
23939 assert!(!canonical.preconfirmation_invalidated());
23940 assert_eq!(canonical.records().len(), 1);
23941 assert_eq!(
23942 canonical.records()[0].record.context.source,
23943 InputSource::Subscription
23944 );
23945 }
23946
23947 #[tokio::test]
23948 #[cfg(feature = "reactive-ws")]
23949 async fn preferred_flashblock_reconnect_exhaustion_preserves_canonical_delivery() {
23950 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23951 let filter = Filter::new().address(Address::repeat_byte(0x42));
23952 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23953 provider,
23954 SubscriberMode::PubSub,
23955 SubscriberConfig {
23956 preconfirmations: PreconfirmationMode::Preferred,
23957 reconnect: SubscriberReconnectConfig {
23958 enabled: false,
23959 ..SubscriberReconnectConfig::default()
23960 },
23961 ..SubscriberConfig::default()
23962 },
23963 )
23964 .with_provider_ref(ProviderRef::new("base-paid", 1));
23965 subscriber.chain_id = Some(8_453);
23966 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
23967 provider_filter: filter.clone(),
23968 local_matcher: None,
23969 route_key: None,
23970 })];
23971 subscriber.interests = subscriber.base_interests.clone();
23972 subscriber.log_source_ids.insert(filter.clone(), 0);
23973 subscriber.next_log_source_id = 1;
23974 subscriber.sources_dirty = false;
23975
23976 let canonical_source = SubscriberStreamSource::PubSubLog { id: 0, filter };
23977 let mut streams = SubscriberStreams::new();
23978 streams.push(
23979 canonical_source,
23980 stream::once(async {
23981 tokio::time::sleep(Duration::from_millis(1)).await;
23982 SubscriberEvent::<Ethereum>::Log {
23983 #[cfg(feature = "ingress-timing")]
23984 timing: None,
23985 source_id: 0,
23986 log: rpc_log(false),
23987 }
23988 })
23989 .boxed(),
23990 );
23991 subscriber.state = AlloySubscriberState::Active(streams);
23992
23993 let source = SubscriberStreamSource::BaseFlashblocks;
23994 subscriber
23995 .pending_flashblock_reconnect_sources
23996 .push(source.clone());
23997 subscriber
23998 .pending_flashblock_reconnects
23999 .push(Box::pin(async move {
24000 (
24001 source,
24002 Err(SubscriberError::Provider(
24003 "test reconnect window exhausted".to_owned(),
24004 )),
24005 )
24006 }));
24007
24008 let canonical = subscriber
24009 .next_scoped_batch()
24010 .await
24011 .expect("preferred reconnect exhaustion does not fail")
24012 .expect("canonical log is delivered");
24013 assert_eq!(canonical.records().len(), 1);
24014 assert!(subscriber.pending_flashblock_reconnects.is_empty());
24015 }
24016
24017 #[test]
24018 fn backfilled_logs_skip_recent_subscription_duplicates() {
24019 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24020 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24021 provider,
24022 SubscriberMode::PubSub,
24023 SubscriberConfig::default(),
24024 );
24025 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
24026 provider_filter: Filter::new()
24027 .address(Address::repeat_byte(0x42))
24028 .event_signature(B256::repeat_byte(0x01)),
24029 local_matcher: None,
24030 route_key: None,
24031 })];
24032
24033 let log = rpc_log(false);
24034 subscriber.enqueue_event(SubscriberEvent::Log {
24035 source_id: 0,
24036 log: log.clone(),
24037 #[cfg(feature = "ingress-timing")]
24038 timing: None,
24039 });
24040 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
24041 source_id: 0,
24042 logs: vec![log],
24043 });
24044
24045 assert_eq!(subscriber.pending_records.len(), 1);
24046 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
24047 assert_eq!(
24048 subscriber.pending_records[0].context.source,
24049 InputSource::Subscription
24050 );
24051 }
24052
24053 #[test]
24054 fn backfilled_logs_surface_with_backfill_source() {
24055 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24060 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24061 provider,
24062 SubscriberMode::PubSub,
24063 SubscriberConfig::default(),
24064 );
24065 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
24066 provider_filter: Filter::new()
24067 .address(Address::repeat_byte(0x42))
24068 .event_signature(B256::repeat_byte(0x01)),
24069 local_matcher: None,
24070 route_key: None,
24071 })];
24072
24073 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
24074 source_id: 0,
24075 logs: vec![rpc_log(false)],
24076 });
24077
24078 assert_eq!(subscriber.pending_records.len(), 1);
24079 assert_eq!(
24080 subscriber.pending_records[0].context.source,
24081 InputSource::Backfill
24082 );
24083 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
24084 }
24085
24086 #[test]
24087 #[cfg(feature = "reactive-ws")]
24088 fn owner_removal_preserves_delivery_and_dedupe_state() {
24089 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24090 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24091 provider,
24092 SubscriberMode::PubSub,
24093 SubscriberConfig::default(),
24094 );
24095 subscriber
24096 .add_interest_owner(
24097 HandlerId::new("pool-a"),
24098 &[ReactiveInterest::Logs(LogInterest {
24099 provider_filter: Filter::new()
24100 .address(Address::repeat_byte(0x42))
24101 .event_signature(B256::repeat_byte(0x01)),
24102 local_matcher: None,
24103 route_key: None,
24104 })],
24105 )
24106 .expect("register pool-a owner");
24107 subscriber
24108 .add_interest_owner(
24109 HandlerId::new("pool-b"),
24110 &[ReactiveInterest::Logs(LogInterest {
24111 provider_filter: Filter::new()
24112 .address(Address::repeat_byte(0x24))
24113 .event_signature(B256::repeat_byte(0x02)),
24114 local_matcher: None,
24115 route_key: None,
24116 })],
24117 )
24118 .expect("register pool-b owner");
24119
24120 let sources = subscriber.stream_sources().expect("stream sources");
24123 subscriber.enqueue_event(SubscriberEvent::Log {
24124 source_id: 0,
24125 log: rpc_log(false),
24126 #[cfg(feature = "ingress-timing")]
24127 timing: None,
24128 });
24129 let mut streams = SubscriberStreams::new();
24130 streams.push(
24131 sources[0].clone(),
24132 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
24133 );
24134 subscriber.state = AlloySubscriberState::Active(streams);
24135 assert_eq!(subscriber.pending_records.len(), 1);
24136 assert_eq!(subscriber.recent_input_refs.len(), 1);
24137 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
24138
24139 let removed = subscriber
24140 .remove_interest_owner(&HandlerId::new("pool-b"))
24141 .expect("pool-b should be removed");
24142
24143 assert_eq!(removed.len(), 1);
24144 assert_eq!(subscriber.pending_records.len(), 1);
24145 assert_eq!(subscriber.recent_input_refs.len(), 1);
24146 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
24147 assert!(
24148 subscriber
24149 .owner_interests(&HandlerId::new("pool-a"))
24150 .is_some()
24151 );
24152 assert!(
24153 subscriber
24154 .owner_interests(&HandlerId::new("pool-b"))
24155 .is_none()
24156 );
24157 assert_eq!(subscriber.registered_interests().len(), 1);
24158 }
24159
24160 #[test]
24161 #[cfg(feature = "reactive-ws")]
24162 fn owner_log_sources_fan_in_across_owners() {
24163 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24164 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24165 provider,
24166 SubscriberMode::PubSub,
24167 SubscriberConfig::default(),
24168 );
24169 subscriber
24170 .add_interest_owner(
24171 HandlerId::new("pool-a"),
24172 &[ReactiveInterest::Logs(LogInterest {
24173 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
24174 local_matcher: None,
24175 route_key: None,
24176 })],
24177 )
24178 .expect("register pool-a owner");
24179
24180 let initial_sources = subscriber.stream_sources().expect("initial sources");
24181 assert_eq!(initial_sources.len(), 1);
24182 let pool_a_source = initial_sources[0].clone();
24183 assert!(matches!(
24184 &pool_a_source,
24185 SubscriberStreamSource::PubSubLog { id: 0, .. }
24186 ));
24187
24188 subscriber
24189 .add_interest_owner(
24190 HandlerId::new("pool-b"),
24191 &[ReactiveInterest::Logs(LogInterest {
24192 provider_filter: Filter::new().address(Address::repeat_byte(0xb2)),
24193 local_matcher: None,
24194 route_key: None,
24195 })],
24196 )
24197 .expect("register pool-b owner");
24198
24199 let expanded_sources = subscriber.stream_sources().expect("expanded sources");
24200 assert_eq!(
24201 expanded_sources.len(),
24202 1,
24203 "compatible owner filters should share one provider subscription"
24204 );
24205 assert!(
24206 !expanded_sources[0].same_key(&pool_a_source),
24207 "the provider-facing superset changes while owner routing remains exact"
24208 );
24209
24210 subscriber
24211 .remove_interest_owner(&HandlerId::new("pool-b"))
24212 .expect("pool-b should be removed");
24213 let trimmed_sources = subscriber.stream_sources().expect("trimmed sources");
24214 assert_eq!(trimmed_sources.len(), 1);
24215 assert!(trimmed_sources[0].same_key(&pool_a_source));
24216 }
24217
24218 #[test]
24219 #[cfg(feature = "reactive-ws")]
24220 fn provider_log_fan_in_respects_address_ceiling() {
24221 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24222 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24223 provider,
24224 SubscriberMode::PubSub,
24225 SubscriberConfig {
24226 max_log_addresses_per_subscription: 2,
24227 ..SubscriberConfig::default()
24228 },
24229 );
24230 for index in 0..5 {
24231 subscriber
24232 .add_interest_owner(
24233 HandlerId::new(format!("pool-{index}")),
24234 &[log_interest_for(index + 1)],
24235 )
24236 .expect("register pool owner");
24237 }
24238
24239 let sources = subscriber.stream_sources().expect("stream sources");
24240 assert_eq!(sources.len(), 3);
24241 let mut address_counts: Vec<_> = sources
24242 .iter()
24243 .map(|source| match source {
24244 SubscriberStreamSource::PubSubLog { filter, .. } => filter.address.iter().count(),
24245 _ => panic!("expected log source"),
24246 })
24247 .collect();
24248 address_counts.sort_unstable();
24249 assert_eq!(address_counts, vec![1, 2, 2]);
24250 }
24251
24252 #[tokio::test(flavor = "multi_thread")]
24253 #[cfg(feature = "reactive-ws")]
24254 async fn owner_backfill_seeds_reconnect_anchor_before_live_log() {
24255 let asserter = Asserter::new();
24256 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
24257 asserter.push_success(&vec![rpc_log(false)]);
24258 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
24259 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
24260 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24261 provider,
24262 SubscriberMode::PubSub,
24263 SubscriberConfig::default(),
24264 );
24265 subscriber
24266 .add_interest_owner_with_backfill(
24267 HandlerId::new("pool-a"),
24268 &[ReactiveInterest::Logs(LogInterest {
24269 provider_filter: Filter::new()
24270 .address(Address::repeat_byte(0x42))
24271 .event_signature(B256::repeat_byte(0x01)),
24272 local_matcher: None,
24273 route_key: None,
24274 })],
24275 SubscriberBackfill::range(1, 7),
24276 )
24277 .expect("register pool-a with backfill");
24278
24279 subscriber
24280 .drain_pending_backfills()
24281 .await
24282 .expect("owner backfill should drain");
24283
24284 assert_eq!(subscriber.pending_records.len(), 1);
24285 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
24286 }
24287
24288 #[tokio::test(flavor = "multi_thread")]
24289 async fn subscriber_streams_poll_ready_sources_round_robin() {
24290 let first_hash = B256::repeat_byte(0x01);
24291 let second_hash = B256::repeat_byte(0x02);
24292 let mut streams = SubscriberStreams::new();
24293 streams.push(
24294 SubscriberStreamSource::PubSubPendingHashes,
24295 stream::iter([
24296 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
24297 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
24298 ])
24299 .boxed(),
24300 );
24301 streams.push(
24302 SubscriberStreamSource::PubSubBlockHeaders,
24303 stream::once(async move { SubscriberEvent::<Ethereum>::PendingHash(second_hash) })
24304 .boxed(),
24305 );
24306
24307 assert!(matches!(
24308 streams.next().await,
24309 Some(SubscriberEvent::PendingHash(hash)) if hash == first_hash
24310 ));
24311 assert!(matches!(
24312 streams.next().await,
24313 Some(SubscriberEvent::PendingHash(hash)) if hash == second_hash
24314 ));
24315 }
24316
24317 #[tokio::test(flavor = "multi_thread")]
24318 #[cfg(feature = "reactive-ws")]
24319 async fn owner_updates_ensure_streams_without_full_reset() {
24320 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24321 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24322 provider,
24323 SubscriberMode::PubSub,
24324 SubscriberConfig::default(),
24325 );
24326 subscriber.chain_id = Some(1);
24327 subscriber
24328 .register_interests(&[ReactiveInterest::PendingTransactions(
24329 PendingTxInterest::default(),
24330 )])
24331 .await
24332 .expect("register base pending interest");
24333 subscriber
24334 .add_interest_owner(
24335 HandlerId::new("headers"),
24336 &[ReactiveInterest::Blocks(BlockInterest::default())],
24337 )
24338 .expect("register header owner");
24339
24340 let mut streams = SubscriberStreams::new();
24341 streams.push(
24342 SubscriberStreamSource::PubSubPendingHashes,
24343 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
24344 );
24345 streams.push(
24346 SubscriberStreamSource::PubSubBlockHeaders,
24347 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
24348 );
24349 subscriber.state = AlloySubscriberState::Active(streams);
24350
24351 subscriber
24352 .remove_interest_owner(&HandlerId::new("headers"))
24353 .expect("header owner should be removed");
24354 assert!(matches!(
24355 &subscriber.state,
24356 AlloySubscriberState::Active(streams) if streams.len() == 2
24357 ));
24358
24359 subscriber
24360 .ensure_streams()
24361 .await
24362 .expect("pure removal reconciliation should not touch provider");
24363
24364 assert!(matches!(
24365 &subscriber.state,
24366 AlloySubscriberState::Active(streams)
24367 if streams.len() == 1
24368 && streams.contains_source(&SubscriberStreamSource::PubSubPendingHashes)
24369 && !streams.contains_source(&SubscriberStreamSource::PubSubBlockHeaders)
24370 ));
24371
24372 subscriber
24373 .add_interest_owner(
24374 HandlerId::new("headers"),
24375 &[ReactiveInterest::Blocks(BlockInterest::default())],
24376 )
24377 .expect("re-add header owner");
24378 assert!(matches!(
24379 &subscriber.state,
24380 AlloySubscriberState::Active(streams) if streams.len() == 1
24381 ));
24382 }
24383
24384 #[tokio::test(flavor = "multi_thread")]
24385 #[cfg(feature = "reactive-polling")]
24386 async fn ensure_streams_retains_each_successful_connection_across_later_failure() {
24387 let asserter = Asserter::new();
24388 asserter.push_success(&U256::from(1));
24389 asserter.push_failure_msg("second filter connection failed");
24390 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
24391 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24392 provider,
24393 SubscriberMode::Polling,
24394 SubscriberConfig {
24395 max_log_addresses_per_subscription: 1,
24396 ..SubscriberConfig::default()
24397 },
24398 );
24399 subscriber.chain_id = Some(1);
24400 subscriber
24401 .register_interests(&[log_interest_for(0x41), log_interest_for(0x42)])
24402 .await
24403 .expect("register two independently connected filters");
24404
24405 let error = subscriber
24406 .ensure_streams()
24407 .await
24408 .expect_err("second provider connection is forced to fail");
24409 assert!(matches!(error, SubscriberError::Provider(_)));
24410 assert!(subscriber.sources_dirty);
24411 let retained_streams = match &subscriber.state {
24412 AlloySubscriberState::Active(streams) => Some(streams.len()),
24413 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => None,
24414 };
24415 assert_eq!(
24416 retained_streams,
24417 Some(1),
24418 "first connection must survive later error {error:?}; revision {}",
24419 subscriber.stream_revision
24420 );
24421
24422 asserter.push_success(&U256::from(2));
24423 subscriber
24424 .ensure_streams()
24425 .await
24426 .expect("retry connects only the missing source");
24427 assert!(!subscriber.sources_dirty);
24428 assert!(matches!(
24429 &subscriber.state,
24430 AlloySubscriberState::Active(streams) if streams.len() == 2
24431 ));
24432 assert!(asserter.read_q().is_empty());
24433 }
24434
24435 #[tokio::test(flavor = "multi_thread")]
24436 #[cfg(feature = "reactive-ws")]
24437 async fn cancelled_post_install_backfill_is_retried_without_reconnecting() {
24438 let asserter = Asserter::new();
24439 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
24440 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24441 provider,
24442 SubscriberMode::PubSub,
24443 SubscriberConfig::default(),
24444 );
24445 subscriber.chain_id = Some(1);
24446 subscriber
24447 .register_interests(&[log_interest_for(0x43)])
24448 .await
24449 .expect("register log source");
24450 let source = subscriber
24451 .stream_sources()
24452 .expect("one desired source")
24453 .pop()
24454 .expect("log source");
24455 let SubscriberStreamSource::PubSubLog { id, .. } = source else {
24456 panic!("expected pubsub log source")
24457 };
24458 subscriber.last_seen_log_blocks.insert(id, 6);
24459
24460 {
24461 let source = SubscriberStreamSource::PubSubLog {
24462 id,
24463 filter: subscriber
24464 .log_stream_filters()
24465 .pop()
24466 .expect("provider filter"),
24467 };
24468 let interrupted = async {
24469 subscriber.install_source_stream(
24470 source.clone(),
24471 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
24472 );
24473 subscriber.queue_source_backfill(source);
24474 subscriber.sources_dirty = true;
24475 futures::future::pending::<()>().await;
24476 };
24477 futures::pin_mut!(interrupted);
24478 poll_fn(|cx| {
24479 assert!(interrupted.as_mut().poll(cx).is_pending());
24480 std::task::Poll::Ready(())
24481 })
24482 .await;
24483 }
24484
24485 assert_eq!(subscriber.pending_source_backfills.len(), 1);
24486 assert!(matches!(
24487 &subscriber.state,
24488 AlloySubscriberState::Active(streams) if streams.len() == 1
24489 ));
24490
24491 asserter.push_success(&7u64);
24492 asserter.push_success(&Vec::<Log>::new());
24493 subscriber
24494 .ensure_streams()
24495 .await
24496 .expect("retry completes only the pending historical window");
24497
24498 assert!(subscriber.pending_source_backfills.is_empty());
24499 assert!(!subscriber.sources_dirty);
24500 assert!(matches!(
24501 &subscriber.state,
24502 AlloySubscriberState::Active(streams) if streams.len() == 1
24503 ));
24504 assert!(asserter.read_q().is_empty());
24505 }
24506
24507 fn log_interest_matching_rpc_log() -> ReactiveInterest<Ethereum> {
24509 ReactiveInterest::Logs(LogInterest {
24510 provider_filter: Filter::new()
24511 .address(Address::repeat_byte(0x42))
24512 .event_signature(B256::repeat_byte(0x01)),
24513 local_matcher: None,
24514 route_key: None,
24515 })
24516 }
24517
24518 #[cfg(any(feature = "reactive-ws", feature = "reactive-polling"))]
24519 fn log_interest_for(address: u8) -> ReactiveInterest<Ethereum> {
24520 ReactiveInterest::Logs(LogInterest {
24521 provider_filter: Filter::new().address(Address::repeat_byte(address)),
24522 local_matcher: None,
24523 route_key: None,
24524 })
24525 }
24526
24527 #[tokio::test(flavor = "multi_thread")]
24530 #[cfg(feature = "reactive-ws")]
24531 async fn drain_backfill_retains_queue_entry_on_provider_error() {
24532 let asserter = Asserter::new();
24533 asserter.push_failure_msg("rate limited");
24534 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
24535 asserter.push_success(&vec![rpc_log(false)]);
24536 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
24537 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
24538 let mut subscriber = AlloySubscriber::new(
24539 provider,
24540 SubscriberMode::PubSub,
24541 SubscriberConfig::default(),
24542 );
24543 subscriber
24544 .add_interest_owner_with_backfill(
24545 HandlerId::new("pool"),
24546 &[log_interest_matching_rpc_log()],
24547 SubscriberBackfill::range(1, 7),
24548 )
24549 .expect("register owner with backfill");
24550 assert_eq!(subscriber.pending_backfills.len(), 1);
24551
24552 let first = subscriber.drain_pending_backfills().await;
24553 assert!(first.is_err(), "provider failure should surface");
24554 assert_eq!(
24555 subscriber.pending_backfills.len(),
24556 1,
24557 "failed fetch must leave the backfill queued for retry"
24558 );
24559 assert!(subscriber.pending_records.is_empty());
24560
24561 subscriber
24562 .drain_pending_backfills()
24563 .await
24564 .expect("retry should succeed");
24565 assert!(subscriber.pending_backfills.is_empty());
24566 assert_eq!(subscriber.pending_records.len(), 1);
24567 }
24568
24569 #[tokio::test(flavor = "multi_thread")]
24572 #[cfg(feature = "reactive-ws")]
24573 async fn drain_backfill_seeds_anchor_on_empty_window() {
24574 let asserter = Asserter::new();
24575 asserter.push_success(&Some(rpc_block(42, B256::repeat_byte(42))));
24576 asserter.push_success(&Vec::<Log>::new());
24577 asserter.push_success(&Some(rpc_block(42, B256::repeat_byte(42))));
24578 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
24579 let mut subscriber = AlloySubscriber::new(
24580 provider,
24581 SubscriberMode::PubSub,
24582 SubscriberConfig::default(),
24583 );
24584 subscriber
24585 .add_interest_owner_with_backfill(
24586 HandlerId::new("pool"),
24587 &[log_interest_matching_rpc_log()],
24588 SubscriberBackfill::range(1, 42),
24589 )
24590 .expect("register owner with backfill");
24591
24592 subscriber
24593 .drain_pending_backfills()
24594 .await
24595 .expect("empty backfill should drain");
24596
24597 assert!(subscriber.pending_records.is_empty());
24598 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
24599 .pop()
24600 .unwrap();
24601 assert_eq!(
24602 subscriber.log_anchor(&filter),
24603 Some(42),
24604 "empty window must still seed the anchor at its upper bound"
24605 );
24606 }
24607
24608 #[tokio::test(flavor = "multi_thread")]
24611 #[cfg(feature = "reactive-ws")]
24612 async fn drain_backfill_open_ended_resolves_head_and_seeds_anchor() {
24613 let asserter = Asserter::new();
24614 asserter.push_success(&100u64); asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(100))));
24616 asserter.push_success(&Vec::<Log>::new()); asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(100))));
24618 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
24619 let mut subscriber = AlloySubscriber::new(
24620 provider,
24621 SubscriberMode::PubSub,
24622 SubscriberConfig::default(),
24623 );
24624 subscriber
24625 .add_interest_owner_with_backfill(
24626 HandlerId::new("pool"),
24627 &[log_interest_matching_rpc_log()],
24628 SubscriberBackfill::from_block(10),
24629 )
24630 .expect("register owner with open-ended backfill");
24631
24632 subscriber
24633 .drain_pending_backfills()
24634 .await
24635 .expect("open-ended backfill should drain");
24636
24637 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
24638 .pop()
24639 .unwrap();
24640 assert_eq!(subscriber.log_anchor(&filter), Some(100));
24641 }
24642
24643 #[test]
24646 #[cfg(feature = "reactive-ws")]
24647 fn duplicate_filters_across_owners_map_to_single_source() {
24648 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24649 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24650 provider,
24651 SubscriberMode::PubSub,
24652 SubscriberConfig::default(),
24653 );
24654 subscriber
24655 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
24656 .expect("register pool-a");
24657 subscriber
24658 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xaa)])
24659 .expect("register pool-b with identical filter");
24660
24661 assert_eq!(
24662 subscriber.log_stream_filters().len(),
24663 1,
24664 "identical filters across owners must collapse to one"
24665 );
24666 let sources = subscriber.stream_sources().expect("stream sources");
24667 assert_eq!(sources.len(), 1);
24668 }
24669
24670 #[test]
24673 #[cfg(feature = "reactive-ws")]
24674 fn owner_removal_prunes_source_ids_and_anchors() {
24675 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24676 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24677 provider,
24678 SubscriberMode::PubSub,
24679 SubscriberConfig::default(),
24680 );
24681 subscriber
24682 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
24683 .expect("register pool-a");
24684 subscriber
24685 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xbb)])
24686 .expect("register pool-b");
24687
24688 let _ = subscriber.stream_sources().expect("stream sources");
24690 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
24691 let filter_b = log_filters(&[log_interest_for(0xbb)]).pop().unwrap();
24692 let id_a = subscriber.log_source_id(&filter_a);
24693 let id_b = subscriber.log_source_id(&filter_b);
24694 subscriber.last_seen_log_blocks.insert(id_a, 10);
24695 subscriber.last_seen_log_blocks.insert(id_b, 20);
24696 assert_eq!(
24697 subscriber.log_source_ids.len(),
24698 3,
24699 "one provider fan-in id plus two explicitly seeded logical ids"
24700 );
24701
24702 subscriber
24703 .remove_interest_owner(&HandlerId::new("pool-b"))
24704 .expect("remove pool-b");
24705
24706 assert_eq!(
24707 subscriber.log_source_ids.len(),
24708 1,
24709 "pool-b's filter id should be retired"
24710 );
24711 assert!(subscriber.log_source_ids.contains_key(&filter_a));
24712 assert_eq!(subscriber.last_seen_log_blocks.get(&id_a), Some(&10));
24713 assert_eq!(
24714 subscriber.last_seen_log_blocks.get(&id_b),
24715 None,
24716 "pool-b's anchor should be pruned"
24717 );
24718 }
24719
24720 #[test]
24725 #[cfg(feature = "reactive-ws")]
24726 fn owner_filter_growth_queues_continuity_backfill_from_prior_anchor() {
24727 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24728 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24729 provider,
24730 SubscriberMode::PubSub,
24731 SubscriberConfig::default(),
24732 );
24733 subscriber
24734 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
24735 .expect("register amm with pool A");
24736
24737 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
24740 let id_a = subscriber.log_source_id(&filter_a);
24741 subscriber.last_seen_log_blocks.insert(id_a, 50);
24742
24743 subscriber
24746 .add_interest_owner(
24747 HandlerId::new("amm"),
24748 &[log_interest_for(0xaa), log_interest_for(0xbb)],
24749 )
24750 .expect("grow amm to pools A+B");
24751
24752 assert_eq!(
24753 subscriber.pending_backfills.len(),
24754 1,
24755 "the changed merged filter should queue exactly one continuity backfill"
24756 );
24757 let queued = &subscriber.pending_backfills[0];
24758 assert_eq!(queued.owner, Some(HandlerId::new("amm")));
24759 assert_eq!(queued.backfill.start_block(), 50);
24760 assert_eq!(
24761 queued.backfill.end_block(),
24762 None,
24763 "continuity backfill runs open-ended to the current head"
24764 );
24765 }
24766
24767 #[test]
24770 #[cfg(feature = "reactive-ws")]
24771 fn unchanged_owner_filter_does_not_queue_continuity_backfill() {
24772 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24773 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24774 provider,
24775 SubscriberMode::PubSub,
24776 SubscriberConfig::default(),
24777 );
24778 subscriber
24779 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
24780 .expect("register amm");
24781 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
24782 let id_a = subscriber.log_source_id(&filter_a);
24783 subscriber.last_seen_log_blocks.insert(id_a, 50);
24784
24785 subscriber
24786 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
24787 .expect("re-register identical interests");
24788
24789 assert!(
24790 subscriber.pending_backfills.is_empty(),
24791 "an unchanged filter shape must not queue continuity backfill"
24792 );
24793 }
24794
24795 #[test]
24799 #[cfg(feature = "reactive-ws")]
24800 fn explicit_open_ended_backfill_below_anchor_suppresses_continuity() {
24801 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24802 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24803 provider,
24804 SubscriberMode::PubSub,
24805 SubscriberConfig::default(),
24806 );
24807 subscriber
24808 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
24809 .expect("register amm");
24810 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
24811 let id_a = subscriber.log_source_id(&filter_a);
24812 subscriber.last_seen_log_blocks.insert(id_a, 50);
24813
24814 subscriber
24816 .add_interest_owner_with_backfill(
24817 HandlerId::new("amm"),
24818 &[log_interest_for(0xaa), log_interest_for(0xbb)],
24819 SubscriberBackfill::from_block(10),
24820 )
24821 .expect("grow amm with explicit deep backfill");
24822
24823 assert_eq!(
24824 subscriber.pending_backfills.len(),
24825 1,
24826 "only the explicit backfill should be queued; continuity is subsumed"
24827 );
24828 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 10);
24829 }
24830
24831 #[tokio::test(flavor = "multi_thread")]
24834 #[cfg(feature = "reactive-ws")]
24835 async fn ensure_streams_is_noop_when_not_dirty() {
24836 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24837 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24838 provider,
24839 SubscriberMode::PubSub,
24840 SubscriberConfig::default(),
24841 );
24842 subscriber
24844 .add_interest_owner(
24845 HandlerId::new("headers"),
24846 &[ReactiveInterest::Blocks(BlockInterest::default())],
24847 )
24848 .expect("register header owner");
24849 subscriber.state = AlloySubscriberState::Empty;
24851 subscriber.sources_dirty = false;
24852
24853 subscriber
24854 .ensure_streams()
24855 .await
24856 .expect("clean reconcile must be a no-op");
24857
24858 assert!(
24859 matches!(subscriber.state, AlloySubscriberState::Empty),
24860 "not-dirty ensure_streams must not connect new sources"
24861 );
24862 }
24863}
24864
24865fn resolve_subscriber_transport(
24866 mode: SubscriberMode,
24867) -> Result<SubscriberTransport, SubscriberError> {
24868 match mode {
24869 SubscriberMode::PubSub => {
24870 #[cfg(feature = "reactive-ws")]
24871 {
24872 Ok(SubscriberTransport::PubSub)
24873 }
24874 #[cfg(not(feature = "reactive-ws"))]
24875 {
24876 Err(SubscriberError::Unsupported(
24877 "AlloySubscriber pubsub mode requires the reactive-ws feature",
24878 ))
24879 }
24880 }
24881 SubscriberMode::Polling => {
24882 #[cfg(feature = "reactive-polling")]
24883 {
24884 Ok(SubscriberTransport::Polling)
24885 }
24886 #[cfg(not(feature = "reactive-polling"))]
24887 {
24888 Err(SubscriberError::Unsupported(
24889 "AlloySubscriber polling mode requires the reactive-polling feature",
24890 ))
24891 }
24892 }
24893 SubscriberMode::Auto => resolve_auto_subscriber_transport(),
24894 }
24895}
24896
24897fn resolve_auto_subscriber_transport() -> Result<SubscriberTransport, SubscriberError> {
24898 #[cfg(feature = "reactive-ws")]
24899 {
24900 Ok(SubscriberTransport::PubSub)
24901 }
24902
24903 #[cfg(all(not(feature = "reactive-ws"), feature = "reactive-polling"))]
24904 {
24905 Ok(SubscriberTransport::Polling)
24906 }
24907
24908 #[cfg(not(any(feature = "reactive-ws", feature = "reactive-polling")))]
24909 {
24910 Err(SubscriberError::Unsupported(
24911 "AlloySubscriber requires either reactive-ws or reactive-polling",
24912 ))
24913 }
24914}
24915
24916fn validate_subscriber_config(config: &SubscriberConfig) -> Result<(), SubscriberError> {
24917 if config.preconfirmations != PreconfirmationMode::Disabled
24918 && config.canonical_head_poll_interval.is_zero()
24919 {
24920 return Err(SubscriberError::InvalidConfig(
24921 "SubscriberConfig::canonical_head_poll_interval must be greater than zero",
24922 ));
24923 }
24924 if config.preconfirmations != PreconfirmationMode::Disabled
24925 && config.canonical_head_request_timeout.is_zero()
24926 {
24927 return Err(SubscriberError::InvalidConfig(
24928 "SubscriberConfig::canonical_head_request_timeout must be greater than zero",
24929 ));
24930 }
24931 if config.preconfirmations != PreconfirmationMode::Disabled
24932 && config.flashblock_poll_interval.is_zero()
24933 {
24934 return Err(SubscriberError::InvalidConfig(
24935 "SubscriberConfig::flashblock_poll_interval must be greater than zero",
24936 ));
24937 }
24938 if config.preconfirmations != PreconfirmationMode::Disabled
24939 && config.max_consecutive_flashblock_poll_failures == 0
24940 {
24941 return Err(SubscriberError::InvalidConfig(
24942 "SubscriberConfig::max_consecutive_flashblock_poll_failures must be greater than zero",
24943 ));
24944 }
24945 if config.preconfirmations != PreconfirmationMode::Disabled
24946 && config.max_pending_transaction_receipts_per_tick == 0
24947 {
24948 return Err(SubscriberError::InvalidConfig(
24949 "SubscriberConfig::max_pending_transaction_receipts_per_tick must be greater than zero",
24950 ));
24951 }
24952 if config.preconfirmations != PreconfirmationMode::Disabled
24953 && config.max_flashblock_rpc_requests_per_second == 0
24954 {
24955 return Err(SubscriberError::InvalidConfig(
24956 "SubscriberConfig::max_flashblock_rpc_requests_per_second must be greater than zero",
24957 ));
24958 }
24959 if config.max_batch_size == 0 {
24960 return Err(SubscriberError::InvalidConfig(
24961 "SubscriberConfig::max_batch_size must be greater than zero",
24962 ));
24963 }
24964 if config.max_log_addresses_per_subscription == 0 {
24965 return Err(SubscriberError::InvalidConfig(
24966 "SubscriberConfig::max_log_addresses_per_subscription must be greater than zero",
24967 ));
24968 }
24969 if config.max_pending_records == 0 {
24970 return Err(SubscriberError::InvalidConfig(
24971 "SubscriberConfig::max_pending_records must be greater than zero",
24972 ));
24973 }
24974 if config.max_pending_backfills == 0 {
24975 return Err(SubscriberError::InvalidConfig(
24976 "SubscriberConfig::max_pending_backfills must be greater than zero",
24977 ));
24978 }
24979 if config.max_backfill_log_bytes == 0 {
24980 return Err(SubscriberError::InvalidConfig(
24981 "SubscriberConfig::max_backfill_log_bytes must be greater than zero",
24982 ));
24983 }
24984 if config.max_reconcile_requests_in_flight == 0 {
24985 return Err(SubscriberError::InvalidConfig(
24986 "SubscriberConfig::max_reconcile_requests_in_flight must be greater than zero",
24987 ));
24988 }
24989 if config.reconnect.enabled {
24990 if config.reconnect.retry_delay > config.reconnect.max_delay {
24991 return Err(SubscriberError::InvalidConfig(
24992 "SubscriberReconnectConfig::retry_delay must be less than or equal to max_delay",
24993 ));
24994 }
24995 if matches!(config.reconnect.max_attempts, Some(0)) {
24996 return Err(SubscriberError::InvalidConfig(
24997 "SubscriberReconnectConfig::max_attempts must be greater than zero when set",
24998 ));
24999 }
25000 }
25001 Ok(())
25002}
25003
25004fn validate_supported_interests<N: Network>(
25005 mode: SubscriberMode,
25006 config: &SubscriberConfig,
25007 interests: &[ReactiveInterest<N>],
25008) -> Result<(), SubscriberError> {
25009 let transport = resolve_subscriber_transport(mode)?;
25010
25011 for interest in interests {
25012 match interest {
25013 ReactiveInterest::Logs(_) => {}
25014 ReactiveInterest::PendingTransactions(interest)
25015 if !config.hydrate_pending_transactions && interest.matches_hash_only() => {}
25016 ReactiveInterest::PendingTransactions(_) => {
25017 return Err(SubscriberError::Unsupported(
25018 "AlloySubscriber currently supports pending transaction hash interests only (full pending-tx hydration is unimplemented)",
25019 ));
25020 }
25021 ReactiveInterest::Blocks(interest) => match (transport, interest.mode) {
25022 (SubscriberTransport::PubSub, BlockInterestMode::Header) => {}
25023 (_, BlockInterestMode::FullBlock) => {
25024 return Err(SubscriberError::Unsupported(
25025 "AlloySubscriber full block streams are not implemented in this transport slice",
25026 ));
25027 }
25028 (SubscriberTransport::Polling, BlockInterestMode::Header) => {
25029 return Err(SubscriberError::Unsupported(
25030 "AlloySubscriber polling block streams are not implemented in this transport slice",
25031 ));
25032 }
25033 },
25034 }
25035 }
25036
25037 Ok(())
25038}
25039
25040fn log_filters<N: Network>(interests: &[ReactiveInterest<N>]) -> Vec<Filter> {
25041 let mut filters = Vec::new();
25042 for interest in interests {
25043 if let ReactiveInterest::Logs(interest) = interest {
25044 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
25045 }
25046 }
25047 filters
25048}
25049
25050fn needs_header_block_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
25051 interests.iter().any(|interest| {
25052 matches!(
25053 interest,
25054 ReactiveInterest::Blocks(BlockInterest {
25055 mode: BlockInterestMode::Header,
25056 })
25057 )
25058 })
25059}
25060
25061fn needs_pending_hash_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
25062 interests.iter().any(|interest| {
25063 matches!(
25064 interest,
25065 ReactiveInterest::PendingTransactions(interest) if interest.matches_hash_only()
25066 )
25067 })
25068}
25069
25070fn log_matches_any_interest<N: Network>(log: &Log, interests: &[ReactiveInterest<N>]) -> bool {
25071 interests.iter().any(|interest| {
25072 matches!(
25073 interest,
25074 ReactiveInterest::Logs(interest) if interest.matches(log)
25075 )
25076 })
25077}
25078
25079fn validate_owner_backfill_logs(
25080 logs: &[Log],
25081 from_block: u64,
25082 through: &BlockRef,
25083) -> Result<(), SubscriberOwnerError> {
25084 for log in logs {
25085 if log.removed {
25086 return Err(SubscriberOwnerError::InvalidBackfillLog(
25087 "removed log in canonical catch-up",
25088 ));
25089 }
25090 let number = log
25091 .block_number
25092 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
25093 "log missing block number",
25094 ))?;
25095 let hash = log
25096 .block_hash
25097 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
25098 "log missing block hash",
25099 ))?;
25100 log.transaction_hash
25101 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
25102 "log missing transaction hash",
25103 ))?;
25104 log.transaction_index
25105 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
25106 "log missing transaction index",
25107 ))?;
25108 log.log_index
25109 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
25110 "log missing log index",
25111 ))?;
25112 if number < from_block || number > through.number {
25113 return Err(SubscriberOwnerError::InvalidBackfillLog(
25114 "log outside requested block range",
25115 ));
25116 }
25117 if number == through.number && hash != through.hash {
25118 return Err(SubscriberOwnerError::InvalidBackfillLog(
25119 "target-block log hash mismatch",
25120 ));
25121 }
25122 }
25123 Ok(())
25124}
25125
25126fn validate_backfill_resource_limits(
25127 logs: &[Log],
25128 max_logs: usize,
25129 max_log_bytes: usize,
25130) -> Result<usize, SubscriberError> {
25131 if logs.len() > max_logs {
25132 return Err(SubscriberError::ResourceExhausted(format!(
25133 "historical response returned {} logs, above the configured limit of {max_logs}",
25134 logs.len()
25135 )));
25136 }
25137 let bytes = logs.iter().fold(0usize, |total, log| {
25138 let fixed = 20usize + (32 * 3) + (8 * 4) + 1;
25142 total
25143 .saturating_add(fixed)
25144 .saturating_add(log.topics().len().saturating_mul(32))
25145 .saturating_add(log.inner.data.data.len())
25146 });
25147 if bytes > max_log_bytes {
25148 return Err(SubscriberError::ResourceExhausted(format!(
25149 "historical response retained approximately {bytes} log bytes, above the configured limit of {max_log_bytes}"
25150 )));
25151 }
25152 Ok(bytes)
25153}
25154
25155async fn fetch_provider_block_ref<P, N>(
25156 provider: &P,
25157 number: u64,
25158 counters: &SubscriberRpcCounters,
25159 cause: SubscriberRpcCause,
25160) -> Result<BlockRef, SubscriberError>
25161where
25162 P: Provider<N> + Send + Sync,
25163 N: Network,
25164{
25165 counters.record(cause, SubscriberRpcMethod::EthGetBlockByNumber);
25166 let block = provider
25167 .get_block_by_number(BlockNumberOrTag::Number(number))
25168 .await
25169 .map_err(provider_error)?
25170 .ok_or_else(|| {
25171 SubscriberError::InvalidBackfill(format!(
25172 "canonical target block {number} is unavailable"
25173 ))
25174 })?;
25175 let header = block.header();
25176 Ok(BlockRef {
25177 number: header.number(),
25178 hash: header.hash(),
25179 parent_hash: Some(header.parent_hash()),
25180 timestamp: Some(header.timestamp()),
25181 })
25182}
25183
25184fn block_ref_satisfies_expected(actual: &BlockRef, expected: &BlockRef) -> bool {
25185 actual.number == expected.number
25186 && actual.hash == expected.hash
25187 && optional_metadata_compatible(actual.parent_hash.as_ref(), expected.parent_hash.as_ref())
25188 && optional_metadata_compatible(actual.timestamp.as_ref(), expected.timestamp.as_ref())
25189}
25190
25191fn validate_owner_backfill_log_set(logs: &[Log]) -> Result<(), SubscriberOwnerError> {
25192 let mut positions = HashMap::new();
25193 let mut block_hashes = HashMap::new();
25194 let mut transaction_hashes = HashMap::new();
25195 let mut transaction_positions = HashMap::new();
25196 let mut ordering = BTreeMap::<u64, Vec<(u64, u64)>>::new();
25197 for log in logs {
25198 let number = log
25199 .block_number
25200 .expect("individual owner catch-up logs are validated before set validation");
25201 let block_hash = log
25202 .block_hash
25203 .expect("individual owner catch-up logs are validated before set validation");
25204 let transaction_hash = log
25205 .transaction_hash
25206 .expect("individual owner catch-up logs are validated before set validation");
25207 let transaction_index = log
25208 .transaction_index
25209 .expect("individual owner catch-up logs are validated before set validation");
25210 let log_index = log
25211 .log_index
25212 .expect("individual owner catch-up logs are validated before set validation");
25213 if block_hashes
25214 .insert(number, block_hash)
25215 .is_some_and(|prior| prior != block_hash)
25216 {
25217 return Err(SubscriberOwnerError::InvalidBackfillLog(
25218 "conflicting block identity in canonical catch-up",
25219 ));
25220 }
25221 if let Some(previous) = positions.insert((number, log_index), log)
25222 && previous != log
25223 {
25224 return Err(SubscriberOwnerError::InvalidBackfillLog(
25225 "conflicting logs at one canonical block position",
25226 ));
25227 }
25228 let conflicting_transaction = transaction_hashes
25229 .insert((number, transaction_index), transaction_hash)
25230 .is_some_and(|prior| prior != transaction_hash)
25231 || transaction_positions
25232 .insert((number, transaction_hash), transaction_index)
25233 .is_some_and(|prior| prior != transaction_index);
25234 if conflicting_transaction {
25235 return Err(SubscriberOwnerError::InvalidBackfillLog(
25236 "conflicting transaction identity at one canonical block position",
25237 ));
25238 }
25239 ordering
25240 .entry(number)
25241 .or_default()
25242 .push((log_index, transaction_index));
25243 }
25244 for positions in ordering.values_mut() {
25245 positions.sort_unstable();
25246 if positions.windows(2).any(|pair| pair[0].1 > pair[1].1) {
25247 return Err(SubscriberOwnerError::InvalidBackfillLog(
25248 "transaction and log positions disagree on canonical order",
25249 ));
25250 }
25251 }
25252 Ok(())
25253}
25254
25255fn merged_owner_reconcile_filters<N: Network>(
25256 plans: &[SubscriberOwnerReconcilePlan<N>],
25257 through: u64,
25258) -> Vec<SubscriberOwnerReconcileFilter> {
25259 let mut by_start = BTreeMap::<u64, Vec<Filter>>::new();
25260 for plan in plans.iter().filter(|plan| plan.from_block <= through) {
25261 let filters = by_start.entry(plan.from_block).or_default();
25262 filters.extend(
25263 log_filters(&plan.interests)
25264 .into_iter()
25265 .map(|filter| filter.from_block(plan.from_block).to_block(through)),
25266 );
25267 }
25268
25269 let mut chunks = Vec::new();
25270 for (from_block, filters) in by_start {
25271 for filters in filters.chunks(OWNER_RECONCILE_FILTERS_PER_CHUNK) {
25272 let mut merged = Vec::new();
25273 for filter in filters {
25274 merge_log_subscription_filter(&mut merged, filter);
25275 }
25276 chunks.extend(
25277 merged
25278 .into_iter()
25279 .map(|filter| SubscriberOwnerReconcileFilter { filter, from_block }),
25280 );
25281 }
25282 }
25283 chunks
25284}
25285
25286fn merged_lazy_backfill_filters(
25287 filters: &[Filter],
25288 from_block: u64,
25289 through: u64,
25290) -> Vec<SubscriberOwnerReconcileFilter> {
25291 let mut requests = Vec::new();
25292 for filters in filters.chunks(OWNER_RECONCILE_FILTERS_PER_CHUNK) {
25293 let mut merged = Vec::new();
25294 for filter in filters {
25295 merge_log_subscription_filter(
25296 &mut merged,
25297 &filter.clone().from_block(from_block).to_block(through),
25298 );
25299 }
25300 requests.extend(
25301 merged
25302 .into_iter()
25303 .map(|filter| SubscriberOwnerReconcileFilter { filter, from_block }),
25304 );
25305 }
25306 requests
25307}
25308
25309fn lazy_backfill_error(error: SubscriberOwnerError) -> SubscriberError {
25310 match error {
25311 SubscriberOwnerError::Subscriber(error) => error,
25312 error => SubscriberError::InvalidBackfill(error.to_string()),
25313 }
25314}
25315
25316fn global_backfill_barrier(backfill: SubscriberBackfill, certified: BlockRef) -> ChainControl {
25317 let mut id = b"alloy-global-backfill-v1".to_vec();
25318 id.extend_from_slice(&backfill.start_block().to_be_bytes());
25319 id.extend_from_slice(&certified.number.to_be_bytes());
25320 id.extend_from_slice(certified.hash.as_slice());
25321 ChainControl::Barrier {
25322 id,
25323 block: Some(certified),
25324 }
25325}
25326
25327async fn fetch_owner_catchup<P, N>(
25328 provider: P,
25329 filters: Vec<SubscriberOwnerReconcileFilter>,
25330 retained: Vec<BlockRef>,
25331 through: BlockRef,
25332 options: SubscriberOwnerCatchupOptions,
25333 counters: Arc<SubscriberRpcCounters>,
25334) -> Result<SubscriberOwnerCatchup, SubscriberOwnerError>
25335where
25336 P: Provider<N> + Send + Sync,
25337 N: Network,
25338{
25339 let counters = counters.as_ref();
25340 if !options.target_preverified {
25341 let _ =
25342 verify_provider_reconcile_target::<P, N>(&provider, &through, counters, options.cause)
25343 .await?;
25344 }
25345 let mut certified_positions = HashSet::new();
25346 for position in retained {
25347 let target_certifies_position = position == through
25348 || (position.number.checked_add(1) == Some(through.number)
25349 && through.parent_hash == Some(position.hash));
25350 if !target_certifies_position && certified_positions.insert(position) {
25351 let _ = verify_provider_reconcile_target::<P, N>(
25352 &provider,
25353 &position,
25354 counters,
25355 options.cause,
25356 )
25357 .await?;
25358 }
25359 }
25360 let mut logs = Vec::new();
25361 let mut total_log_bytes = 0usize;
25362 let requests = stream::iter(filters.into_iter().map(|filter| {
25363 let provider = &provider;
25364 async move {
25365 counters.record(options.cause, SubscriberRpcMethod::EthGetLogs);
25366 let logs = provider
25367 .get_logs(&filter.filter)
25368 .await
25369 .map_err(provider_error)?;
25370 Ok::<_, SubscriberOwnerError>((filter.from_block, logs))
25371 }
25372 }))
25373 .buffer_unordered(options.max_requests_in_flight);
25374 futures::pin_mut!(requests);
25375 while let Some(result) = requests.next().await {
25376 let (from_block, fetched) = result?;
25377 let fetched_bytes =
25378 validate_backfill_resource_limits(&fetched, options.max_logs, options.max_log_bytes)?;
25379 validate_owner_backfill_logs(&fetched, from_block, &through)?;
25380 if logs.len().saturating_add(fetched.len()) > options.max_logs {
25381 return Err(SubscriberError::ResourceExhausted(format!(
25382 "bulk reconcile returned more than {} logs",
25383 options.max_logs
25384 ))
25385 .into());
25386 }
25387 total_log_bytes = total_log_bytes.saturating_add(fetched_bytes);
25388 if total_log_bytes > options.max_log_bytes {
25389 return Err(SubscriberError::ResourceExhausted(format!(
25390 "bulk reconcile retained approximately {total_log_bytes} log bytes, above the configured limit of {}",
25391 options.max_log_bytes
25392 ))
25393 .into());
25394 }
25395 logs.extend(fetched);
25396 }
25397 validate_owner_backfill_log_set(&logs)?;
25398 let certified =
25399 verify_provider_reconcile_target::<P, N>(&provider, &through, counters, options.cause)
25400 .await?;
25401 Ok(SubscriberOwnerCatchup { logs, certified })
25402}
25403
25404async fn verify_provider_reconcile_target<P, N>(
25405 provider: &P,
25406 expected: &BlockRef,
25407 counters: &SubscriberRpcCounters,
25408 cause: SubscriberRpcCause,
25409) -> Result<BlockRef, SubscriberOwnerError>
25410where
25411 P: Provider<N> + Send + Sync,
25412 N: Network,
25413{
25414 counters.record(cause, SubscriberRpcMethod::EthGetBlockByNumber);
25415 let block = provider
25416 .get_block_by_number(BlockNumberOrTag::Number(expected.number))
25417 .await
25418 .map_err(provider_error)?
25419 .ok_or(SubscriberOwnerError::BlockUnavailable(expected.number))?;
25420 let header = block.header();
25421 let actual = BlockRef {
25422 number: header.number(),
25423 hash: header.hash(),
25424 parent_hash: Some(header.parent_hash()),
25425 timestamp: Some(header.timestamp()),
25426 };
25427 let exact_parent = expected
25428 .parent_hash
25429 .is_none_or(|parent| Some(parent) == actual.parent_hash);
25430 let exact_timestamp = expected
25431 .timestamp
25432 .is_none_or(|timestamp| Some(timestamp) == actual.timestamp);
25433 if actual.number != expected.number
25434 || actual.hash != expected.hash
25435 || !exact_parent
25436 || !exact_timestamp
25437 {
25438 return Err(SubscriberOwnerError::BlockMismatch {
25439 expected_number: expected.number,
25440 expected_hash: expected.hash,
25441 actual_number: actual.number,
25442 actual_hash: actual.hash,
25443 });
25444 }
25445 Ok(actual)
25446}
25447
25448fn log_input_record<N: Network>(log: Log, source: InputSource) -> ReactiveInputRecord<N> {
25449 let context = log_reactive_context(&log);
25450 ReactiveInputRecord::new(
25451 ReactiveInput::Log(log),
25452 ReactiveContext { source, ..context },
25453 )
25454}
25455
25456fn preconfirmed_log_input_record<N: Network>(
25457 log: Log,
25458 flashblock: FlashblockRef,
25459) -> ReactiveInputRecord<N> {
25460 let block = flashblock.block_ref();
25461 let provider = flashblock.provider.clone();
25462 ReactiveInputRecord::new(
25463 ReactiveInput::Log(log.clone()),
25464 ReactiveContext {
25465 chain_id: None,
25466 source: InputSource::Flashblocks,
25467 chain_status: ChainStatus::Preconfirmed {
25468 flashblock: Arc::new(flashblock),
25469 },
25470 block: Some(block),
25471 transaction_index: log.transaction_index,
25472 log_index: log.log_index,
25473 },
25474 )
25475 .with_provider(provider)
25476}
25477
25478fn log_reactive_context(log: &Log) -> ReactiveContext {
25479 let block = match (log.block_hash, log.block_number) {
25480 (Some(hash), Some(number)) => Some(BlockRef {
25481 number,
25482 hash,
25483 parent_hash: None,
25484 timestamp: log.block_timestamp,
25485 }),
25486 _ => None,
25487 };
25488
25489 let chain_status = match (&block, log.removed) {
25490 (Some(block), true) => ChainStatus::Reorged {
25491 dropped_from: *block,
25492 },
25493 (Some(block), false) => ChainStatus::Included {
25494 block: *block,
25495 confirmations: 0,
25496 },
25497 (None, _) => ChainStatus::Pending,
25498 };
25499
25500 ReactiveContext {
25501 chain_id: None,
25502 source: InputSource::Poll,
25503 chain_status,
25504 block,
25505 transaction_index: log.transaction_index,
25506 log_index: log.log_index,
25507 }
25508}
25509
25510fn block_header_input_record<N>(header: N::HeaderResponse) -> ReactiveInputRecord<N>
25511where
25512 N: Network,
25513{
25514 let block = BlockRef {
25515 number: header.number(),
25516 hash: HeaderResponseTrait::hash(&header),
25517 parent_hash: Some(header.parent_hash()),
25518 timestamp: Some(header.timestamp()),
25519 };
25520 ReactiveInputRecord::new(
25521 ReactiveInput::BlockHeader(header),
25522 ReactiveContext {
25523 chain_id: None,
25524 source: InputSource::Subscription,
25525 chain_status: ChainStatus::Included {
25526 block,
25527 confirmations: 0,
25528 },
25529 block: Some(block),
25530 transaction_index: None,
25531 log_index: None,
25532 },
25533 )
25534}
25535
25536fn pending_hash_input_record<N: Network>(
25537 hash: B256,
25538 source: InputSource,
25539) -> ReactiveInputRecord<N> {
25540 ReactiveInputRecord::new(
25541 ReactiveInput::PendingTxHash(hash),
25542 ReactiveContext {
25543 chain_id: None,
25544 source,
25545 chain_status: ChainStatus::Pending,
25546 block: None,
25547 transaction_index: None,
25548 log_index: None,
25549 },
25550 )
25551}
25552
25553#[cfg(feature = "reactive-ws")]
25554fn base_pending_log_filter(filter: &Filter) -> Result<serde_json::Value, SubscriberError> {
25555 let encoded = serde_json::to_value(filter)
25556 .map_err(|error| SubscriberError::Provider(error.to_string()))?;
25557 let serde_json::Value::Object(mut fields) = encoded else {
25558 return Err(SubscriberError::Provider(
25559 "Alloy log filter did not serialize as an object".into(),
25560 ));
25561 };
25562 fields.retain(|key, _| key == "address" || key == "topics");
25563 Ok(serde_json::Value::Object(fields))
25564}
25565
25566fn provider_error(error: impl fmt::Display) -> SubscriberError {
25567 SubscriberError::Provider(error.to_string())
25568}
25569
25570#[derive(Debug, thiserror::Error)]
25572#[non_exhaustive]
25573pub enum SubscriberError {
25574 #[error("{0}")]
25576 InvalidConfig(&'static str),
25577 #[error("{0}")]
25579 Unsupported(&'static str),
25580 #[error("subscriber chain mismatch: expected {expected}, got {actual}")]
25582 ChainMismatch {
25583 expected: u64,
25585 actual: u64,
25587 },
25588 #[error("provider error: {0}")]
25590 Provider(String),
25591 #[error("invalid canonical backfill: {0}")]
25594 InvalidBackfill(String),
25595 #[error("subscriber resource limit exceeded: {0}")]
25597 ResourceExhausted(String),
25598}
25599
25600#[cfg(test)]
25605mod canonical_head_rpc_stats_tests {
25606 use super::*;
25607 use alloy_provider::ProviderBuilder;
25608 use alloy_rpc_types_eth::{Block, Header as RpcHeader};
25609 use alloy_transport::mock::Asserter;
25610
25611 fn sealed_head() -> Block {
25612 Block::empty(RpcHeader {
25613 hash: B256::repeat_byte(0x65),
25614 inner: alloy_consensus::Header {
25615 number: 101,
25616 parent_hash: B256::repeat_byte(0x64),
25617 timestamp: 1_700_000_101,
25618 ..alloy_consensus::Header::default()
25619 },
25620 total_difficulty: None,
25621 size: None,
25622 })
25623 }
25624
25625 #[tokio::test]
25630 async fn unchanged_head_still_counts_its_certification_request() {
25631 let asserter = Asserter::new();
25632 asserter.push_success(&Some(sealed_head()));
25633 asserter.push_success(&Some(sealed_head()));
25634 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
25635 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
25636 provider,
25637 SubscriberMode::PubSub,
25638 SubscriberConfig::default(),
25639 );
25640 subscriber.chain_id = Some(8_453);
25641
25642 let first = subscriber
25643 .fetch_certified_canonical_head()
25644 .await
25645 .expect("first certification succeeds");
25646 assert!(
25647 matches!(first, Some(SubscriberEvent::BlockHeader { .. })),
25648 "a newly certified head is delivered"
25649 );
25650
25651 let second = subscriber
25652 .fetch_certified_canonical_head()
25653 .await
25654 .expect("second certification succeeds");
25655 assert!(
25656 second.is_none(),
25657 "an unchanged head produces no event to deliver"
25658 );
25659
25660 let stats = subscriber.rpc_stats();
25661 assert_eq!(
25662 stats.get(
25663 SubscriberRpcCause::CanonicalHeadCertification,
25664 SubscriberRpcMethod::EthGetBlockByNumber,
25665 ),
25666 2,
25667 "both polls are billed even though only one advanced the head"
25668 );
25669 assert_eq!(stats.total(), 2, "certification is the only request issued");
25670 assert_eq!(
25671 subscriber
25672 .flashblocks_rpc_metrics()
25673 .canonical_head_requests(),
25674 2,
25675 "the attributed counter agrees with the Flashblocks-scoped one"
25676 );
25677 assert!(asserter.read_q().is_empty());
25678 }
25679}
25680
25681#[cfg(all(test, feature = "reactive-ws"))]
25689mod stream_gap_tests {
25690 use super::*;
25691 use alloy_provider::ProviderBuilder;
25692 use alloy_transport::mock::Asserter;
25693 use serde_json::value::RawValue;
25694
25695 fn raw_json(value: &serde_json::Value) -> Box<RawValue> {
25696 RawValue::from_string(value.to_string()).expect("valid JSON")
25697 }
25698
25699 fn wire_log(block_number: u64, log_index: u64) -> Box<RawValue> {
25700 raw_json(&serde_json::json!({
25701 "address": "0x0000000000000000000000000000000000000077",
25702 "topics": ["0x1111111111111111111111111111111111111111111111111111111111111111"],
25703 "data": "0x",
25704 "blockHash": format!("0x{:064x}", block_number),
25705 "blockNumber": format!("0x{block_number:x}"),
25706 "transactionHash": format!("0x{:064x}", 0x20 + log_index),
25707 "transactionIndex": format!("0x{log_index:x}"),
25708 "logIndex": format!("0x{log_index:x}"),
25709 "removed": false,
25710 }))
25711 }
25712
25713 fn log_source(id: usize) -> SubscriberStreamSource {
25714 SubscriberStreamSource::PubSubLog {
25715 id,
25716 filter: Filter::new().address(Address::repeat_byte(0x77)),
25717 }
25718 }
25719
25720 fn wired_subscription(
25723 capacity: usize,
25724 ) -> (
25725 tokio::sync::broadcast::Sender<Box<RawValue>>,
25726 alloy_pubsub::Subscription<Log>,
25727 ) {
25728 let (tx, rx) = tokio::sync::broadcast::channel(capacity);
25729 let raw = alloy_pubsub::RawSubscription {
25730 rx,
25731 local_id: B256::repeat_byte(0x5b),
25732 };
25733 (tx, raw.into_typed())
25734 }
25735
25736 fn gap_stream(
25737 subscription: alloy_pubsub::Subscription<Log>,
25738 source: SubscriberStreamSource,
25739 counters: Arc<SubscriberStreamGapCounters>,
25740 ) -> BoxStream<'static, SubscriberEvent<Ethereum>> {
25741 gap_observing_stream(
25742 subscription,
25743 source,
25744 counters,
25745 #[cfg(feature = "ingress-timing")]
25746 None,
25747 |log, #[cfg(feature = "ingress-timing")] timing| SubscriberEvent::Log {
25748 source_id: 0,
25749 log,
25750 #[cfg(feature = "ingress-timing")]
25751 timing,
25752 },
25753 )
25754 }
25755
25756 #[cfg(feature = "ingress-timing")]
25757 #[tokio::test]
25758 async fn receipt_lookup_disabled_preserves_delivery_without_timing() {
25759 let counters = Arc::new(SubscriberStreamGapCounters::default());
25760 let (tx, subscription) = wired_subscription(8);
25761 tx.send(wire_log(100, 0)).expect("receiver alive");
25762 let mut stream = gap_stream(subscription, log_source(0), counters);
25763 let Some(SubscriberEvent::Log { log, timing, .. }) = stream.next().await else {
25764 panic!("canonical log expected");
25765 };
25766 assert_eq!(log.block_number, Some(100));
25767 assert!(timing.is_none());
25768 }
25769
25770 #[cfg(feature = "ingress-timing")]
25771 #[tokio::test]
25772 async fn consumed_raw_subscription_preserves_transport_clock_without_rewriting_payload() {
25773 let counters = Arc::new(SubscriberStreamGapCounters::default());
25774 let (tx, subscription) = wired_subscription(8);
25775 let raw = wire_log(100, 0);
25776 let expected = raw.get().to_owned();
25777 let received = Instant::now() - Duration::from_millis(30);
25778 let receipt = TransportReceipt::new(B256::repeat_byte(7), received, received).unwrap();
25779 let lookup: TransportReceiptLookup = Arc::new(move |raw| {
25780 assert_eq!(raw, expected);
25781 Some(receipt)
25782 });
25783 tx.send(raw).expect("receiver alive");
25784 let mut stream = gap_observing_stream(
25785 subscription,
25786 log_source(0),
25787 counters,
25788 Some(lookup),
25789 |log, timing| SubscriberEvent::<Ethereum>::Log {
25790 source_id: 0,
25791 log,
25792 timing,
25793 },
25794 );
25795 let Some(SubscriberEvent::Log {
25796 #[cfg(feature = "ingress-timing")]
25797 timing: Some(timing),
25798 log,
25799 ..
25800 }) = stream.next().await
25801 else {
25802 panic!("timed canonical log expected");
25803 };
25804 assert_eq!(log.block_number, Some(100));
25805 assert_eq!(timing.receipt(), receipt);
25806 assert!(timing.dequeued().duration_since(received) >= Duration::from_millis(30));
25807 assert!(timing.decoded() >= timing.dequeued());
25808 }
25809
25810 #[tokio::test]
25814 async fn overflowing_channel_reports_the_gap_instead_of_skipping_it() {
25815 let counters = Arc::new(SubscriberStreamGapCounters::default());
25816 let (tx, subscription) = wired_subscription(2);
25817 for index in 0..5 {
25819 tx.send(wire_log(100 + index, index))
25820 .expect("receiver alive");
25821 }
25822 let mut stream = gap_stream(subscription, log_source(0), Arc::clone(&counters));
25823
25824 let first = stream.next().await.expect("an event is produced");
25825 let SubscriberEvent::StreamGap { source, gap } = first else {
25826 panic!("expected the dropped notifications to surface as a gap, got a delivery");
25827 };
25828 assert!(
25829 matches!(source, SubscriberStreamSource::PubSubLog { id: 0, .. }),
25830 "the gap must name the source that lost data"
25831 );
25832 assert_eq!(gap, SubscriberStreamGap::Lagged { skipped: 3 });
25833 assert_eq!(gap.skipped(), Some(3));
25834
25835 assert!(matches!(
25837 stream.next().await,
25838 Some(SubscriberEvent::Log { .. })
25839 ));
25840 assert_eq!(counters.snapshot().lagged_notifications(), 3);
25841 assert_eq!(counters.snapshot().undecodable_notifications(), 0);
25842 }
25843
25844 #[tokio::test]
25847 async fn undecodable_notification_fails_closed_as_a_gap() {
25848 let counters = Arc::new(SubscriberStreamGapCounters::default());
25849 let (tx, subscription) = wired_subscription(8);
25850 tx.send(raw_json(&serde_json::json!({"not": "a log"})))
25851 .expect("receiver alive");
25852 tx.send(wire_log(101, 0)).expect("receiver alive");
25853 let mut stream = gap_stream(subscription, log_source(0), Arc::clone(&counters));
25854
25855 assert_eq!(
25856 stream.next().await.map(|event| matches!(
25857 event,
25858 SubscriberEvent::StreamGap {
25859 gap: SubscriberStreamGap::Undecodable,
25860 ..
25861 }
25862 )),
25863 Some(true),
25864 );
25865 assert!(matches!(
25866 stream.next().await,
25867 Some(SubscriberEvent::Log { .. })
25868 ));
25869 let stats = counters.snapshot();
25870 assert_eq!(stats.undecodable_notifications(), 1);
25871 assert_eq!(stats.lagged_notifications(), 0);
25872 assert_eq!(stats.total_gaps(), 1);
25873 }
25874
25875 #[tokio::test]
25878 async fn closed_channel_terminates_the_stream_for_reconnect() {
25879 let counters = Arc::new(SubscriberStreamGapCounters::default());
25880 let (tx, subscription) = wired_subscription(8);
25881 tx.send(wire_log(101, 0)).expect("receiver alive");
25882 drop(tx);
25883 let mut stream = gap_stream(subscription, log_source(0), Arc::clone(&counters));
25884
25885 assert!(matches!(
25886 stream.next().await,
25887 Some(SubscriberEvent::Log { .. })
25888 ));
25889 assert!(
25890 matches!(
25891 stream.next().await,
25892 Some(SubscriberEvent::StreamTerminated(
25893 SubscriberStreamSource::PubSubLog { id: 0, .. }
25894 ))
25895 ),
25896 "a closed subscription must terminate, not report a gap"
25897 );
25898 assert_eq!(counters.snapshot().total_gaps(), 0);
25899 }
25900
25901 fn mocked_subscriber(
25902 asserter: Asserter,
25903 ) -> AlloySubscriber<impl alloy_provider::Provider<Ethereum> + Clone, Ethereum> {
25904 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
25905 AlloySubscriber::new(
25906 provider,
25907 SubscriberMode::PubSub,
25908 SubscriberConfig::default(),
25909 )
25910 }
25911
25912 #[tokio::test]
25916 async fn log_gap_refetches_the_missed_window_and_attributes_it() {
25917 let asserter = Asserter::new();
25918 asserter.push_success(&U256::from(104)); asserter.push_success(&vec![
25920 serde_json::from_str::<Log>(wire_log(103, 0).get()).expect("log"),
25921 ]);
25922 let mut subscriber = mocked_subscriber(asserter.clone());
25923 subscriber.chain_id = Some(1);
25924 subscriber.last_seen_log_blocks.insert(0, 102);
25926
25927 let event = subscriber
25928 .recover_stream_gap(&log_source(0), SubscriberStreamGap::Lagged { skipped: 2 })
25929 .await
25930 .expect("a bounded window is recoverable");
25931
25932 assert!(
25933 matches!(
25934 event,
25935 Some(SubscriberEvent::BackfilledLogs { source_id: 0, ref logs }) if logs.len() == 1
25936 ),
25937 "the missed window is delivered as backfill"
25938 );
25939 let stats = subscriber.rpc_stats();
25940 assert_eq!(
25941 stats.by_cause(SubscriberRpcCause::GapBackfill),
25942 2,
25943 "one head read plus one bounded eth_getLogs"
25944 );
25945 assert_eq!(
25946 stats.by_cause(SubscriberRpcCause::ReconnectBackfill),
25947 0,
25948 "a live-stream gap is not reconnect churn"
25949 );
25950 assert_eq!(subscriber.stream_gap_stats().log_gaps_healed(), 1);
25951 assert!(asserter.read_q().is_empty());
25952 }
25953
25954 #[tokio::test]
25957 async fn log_gap_without_a_delivery_anchor_fails_closed() {
25958 let mut subscriber = mocked_subscriber(Asserter::new());
25959 subscriber.chain_id = Some(1);
25960
25961 let Err(error) = subscriber
25962 .recover_stream_gap(&log_source(0), SubscriberStreamGap::Lagged { skipped: 9 })
25963 .await
25964 else {
25965 panic!("an unbounded gap must not be silently ignored");
25966 };
25967
25968 assert!(
25969 matches!(&error, SubscriberError::Provider(message)
25970 if message.contains("delivery anchor") && message.contains("lagged(9)")),
25971 "the error must name the cause and the loss: {error}"
25972 );
25973 assert_eq!(subscriber.stream_gap_stats().log_gaps_healed(), 0);
25974 }
25975
25976 #[tokio::test]
25979 async fn header_gap_is_counted_without_spending_a_request() {
25980 let asserter = Asserter::new();
25981 let mut subscriber = mocked_subscriber(asserter.clone());
25982 subscriber.chain_id = Some(1);
25983
25984 let event = subscriber
25985 .recover_stream_gap(
25986 &SubscriberStreamSource::PubSubBlockHeaders,
25987 SubscriberStreamGap::Lagged { skipped: 1 },
25988 )
25989 .await
25990 .expect("a header gap is not fatal");
25991
25992 assert!(event.is_none(), "no synthetic header is fabricated");
25993 assert_eq!(subscriber.stream_gap_stats().header_gaps(), 1);
25994 assert_eq!(
25995 subscriber.rpc_stats().total(),
25996 0,
25997 "the lineage walk already covers this; refetching would duplicate it"
25998 );
25999 assert!(asserter.read_q().is_empty());
26000 }
26001
26002 #[tokio::test]
26005 async fn preconfirmation_gap_discards_the_speculative_snapshot() {
26006 let mut subscriber = mocked_subscriber(Asserter::new());
26007 subscriber.chain_id = Some(8_453);
26008
26009 let event = subscriber
26010 .recover_stream_gap(
26011 &SubscriberStreamSource::BasePendingLog {
26012 id: 0,
26013 filter: Filter::new(),
26014 },
26015 SubscriberStreamGap::Undecodable,
26016 )
26017 .await
26018 .expect("a preview gap is recoverable by discarding it");
26019
26020 assert!(
26021 matches!(event, Some(SubscriberEvent::FlashblockInvalidated)),
26022 "the incomplete preview must be invalidated, not delivered"
26023 );
26024 assert_eq!(subscriber.stream_gap_stats().preconfirmation_gaps(), 1);
26025 assert_eq!(subscriber.rpc_stats().total(), 0);
26026 }
26027
26028 #[tokio::test]
26030 async fn resetting_gap_stats_opens_a_new_window() {
26031 let counters = Arc::new(SubscriberStreamGapCounters::default());
26032 counters.record_gap(SubscriberStreamGap::Lagged { skipped: 4 });
26033 counters.record_gap(SubscriberStreamGap::Undecodable);
26034 counters.record_header_gap();
26035 assert_eq!(counters.snapshot().total_gaps(), 5);
26036
26037 counters.reset();
26038
26039 assert_eq!(counters.snapshot(), SubscriberStreamGapStats::default());
26040 }
26041
26042 #[test]
26044 fn gap_labels_are_stable() {
26045 assert_eq!(
26046 SubscriberStreamGap::Lagged { skipped: 7 }.as_str(),
26047 "lagged"
26048 );
26049 assert_eq!(SubscriberStreamGap::Undecodable.as_str(), "undecodable");
26050 assert_eq!(
26051 SubscriberStreamGap::Lagged { skipped: 7 }.to_string(),
26052 "lagged(7)"
26053 );
26054 assert_eq!(SubscriberRpcCause::GapBackfill.as_str(), "gap_backfill");
26055 assert_eq!(SubscriberStreamGap::Undecodable.skipped(), None);
26056 }
26057}
26058
26059#[cfg(all(test, feature = "reactive-ws"))]
26063mod log_coverage_attestation_tests {
26064 use super::*;
26065 use alloy_provider::ProviderBuilder;
26066 use alloy_rpc_types_eth::Header as RpcHeader;
26067 use alloy_transport::mock::Asserter;
26068
26069 fn header_record(number: u64) -> ReactiveInputRecord<Ethereum> {
26070 block_header_input_record::<Ethereum>(RpcHeader {
26071 hash: B256::repeat_byte(number as u8),
26072 inner: alloy_consensus::Header {
26073 number,
26074 parent_hash: B256::repeat_byte((number - 1) as u8),
26075 timestamp: 1_700_000_000 + number,
26076 ..alloy_consensus::Header::default()
26077 },
26078 total_difficulty: None,
26079 size: None,
26080 })
26081 }
26082
26083 fn subscriber(
26084 mode: SubscriberMode,
26085 with_log_interest: bool,
26086 ) -> AlloySubscriber<impl alloy_provider::Provider<Ethereum> + Clone, Ethereum> {
26087 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
26088 let mut subscriber = AlloySubscriber::new(provider, mode, SubscriberConfig::default());
26089 if with_log_interest {
26090 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
26091 provider_filter: Filter::new().address(Address::repeat_byte(0x77)),
26092 local_matcher: None,
26093 route_key: None,
26094 })];
26095 }
26096 subscriber
26097 }
26098
26099 fn queued_coverage(
26100 subscriber: &mut AlloySubscriber<impl alloy_provider::Provider<Ethereum> + Clone, Ethereum>,
26101 ) -> Vec<u64> {
26102 subscriber.queue_log_coverage_attestation();
26103 subscriber
26104 .pending_chain_controls
26105 .drain(..)
26106 .filter_map(|control| match control {
26107 ChainControl::LogCoverage(block) => Some(block.number),
26108 _ => None,
26109 })
26110 .collect()
26111 }
26112
26113 #[test]
26114 fn attestation_advances_with_observed_canonical_headers() {
26115 let mut subscriber = subscriber(SubscriberMode::PubSub, true);
26116
26117 subscriber.note_attestable_canonical_block(&header_record(101));
26118 assert_eq!(queued_coverage(&mut subscriber), vec![101]);
26119
26120 assert!(queued_coverage(&mut subscriber).is_empty());
26122
26123 subscriber.note_attestable_canonical_block(&header_record(102));
26124 assert_eq!(queued_coverage(&mut subscriber), vec![102]);
26125 }
26126
26127 #[test]
26132 fn a_detected_gap_withdraws_the_pending_attestation() {
26133 let mut subscriber = subscriber(SubscriberMode::PubSub, true);
26134 subscriber.note_attestable_canonical_block(&header_record(101));
26135
26136 subscriber.reset_log_attestation();
26137
26138 assert!(
26139 queued_coverage(&mut subscriber).is_empty(),
26140 "a withdrawn candidate must not be attested"
26141 );
26142
26143 subscriber.note_attestable_canonical_block(&header_record(102));
26145 assert_eq!(queued_coverage(&mut subscriber), vec![102]);
26146 }
26147
26148 #[test]
26151 fn attestation_never_regresses_after_a_gap() {
26152 let mut subscriber = subscriber(SubscriberMode::PubSub, true);
26153 subscriber.note_attestable_canonical_block(&header_record(105));
26154 assert_eq!(queued_coverage(&mut subscriber), vec![105]);
26155
26156 subscriber.reset_log_attestation();
26157 subscriber.note_attestable_canonical_block(&header_record(103));
26158
26159 assert!(
26160 queued_coverage(&mut subscriber).is_empty(),
26161 "an older block must never be attested after a newer one"
26162 );
26163 }
26164
26165 #[test]
26168 #[cfg(feature = "reactive-polling")]
26169 fn polling_transport_neither_claims_nor_emits_the_attestation() {
26170 let mut subscriber = subscriber(SubscriberMode::Polling, true);
26171 assert!(!subscriber.attests_log_coverage());
26172
26173 subscriber.note_attestable_canonical_block(&header_record(101));
26174
26175 assert!(queued_coverage(&mut subscriber).is_empty());
26176 assert!(
26177 !EventSubscriber::capabilities(&subscriber)
26178 .supports(SubscriberCapability::LogCoverageAttestation)
26179 );
26180 }
26181
26182 #[test]
26183 fn pubsub_transport_claims_the_capability() {
26184 let subscriber = subscriber(SubscriberMode::PubSub, true);
26185 assert!(
26186 EventSubscriber::capabilities(&subscriber)
26187 .supports(SubscriberCapability::LogCoverageAttestation)
26188 );
26189 }
26190
26191 #[test]
26194 fn subscriber_without_log_interests_stays_silent() {
26195 let mut subscriber = subscriber(SubscriberMode::PubSub, false);
26196 assert!(!subscriber.attests_log_coverage());
26197
26198 subscriber.note_attestable_canonical_block(&header_record(101));
26199
26200 assert!(queued_coverage(&mut subscriber).is_empty());
26201 }
26202}
26203
26204#[cfg(all(test, feature = "reactive-ws"))]
26210mod canonical_head_suppression_tests {
26211 use super::*;
26212 use alloy_provider::ProviderBuilder;
26213 use alloy_rpc_types_eth::{Block, Header as RpcHeader};
26214 use alloy_transport::mock::Asserter;
26215
26216 fn sealed_head(number: u64) -> Block {
26217 Block::empty(RpcHeader {
26218 hash: B256::repeat_byte(number as u8),
26219 inner: alloy_consensus::Header {
26220 number,
26221 parent_hash: B256::repeat_byte((number - 1) as u8),
26222 timestamp: 1_700_000_000 + number,
26223 ..alloy_consensus::Header::default()
26224 },
26225 total_difficulty: None,
26226 size: None,
26227 })
26228 }
26229
26230 fn subscriber(
26231 asserter: Asserter,
26232 ) -> AlloySubscriber<impl alloy_provider::Provider<Ethereum> + Clone, Ethereum> {
26233 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
26234 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
26235 provider,
26236 SubscriberMode::PubSub,
26237 SubscriberConfig::default(),
26238 );
26239 subscriber.chain_id = Some(8_453);
26240 subscriber.interests = vec![ReactiveInterest::Blocks(BlockInterest::default())];
26241 subscriber
26242 }
26243
26244 #[tokio::test]
26248 async fn a_tick_inside_the_window_after_a_certification_costs_nothing() {
26249 let asserter = Asserter::new();
26250 asserter.push_success(&Some(sealed_head(101)));
26251 let mut subscriber = subscriber(asserter.clone());
26252
26253 let certified = subscriber
26255 .certify_canonical_head_on_sealed_block()
26256 .await
26257 .expect("certification succeeds");
26258 assert!(matches!(
26259 certified,
26260 Some(SubscriberEvent::BlockHeader { .. })
26261 ));
26262
26263 assert!(subscriber.canonical_head_certification_is_current());
26265 let suppressed = subscriber
26266 .certify_canonical_head_on_sealed_block()
26267 .await
26268 .expect("a suppressed certification is not an error");
26269
26270 assert!(suppressed.is_none());
26271 assert_eq!(
26272 subscriber.rpc_stats().get(
26273 SubscriberRpcCause::CanonicalHeadCertification,
26274 SubscriberRpcMethod::EthGetBlockByNumber,
26275 ),
26276 1,
26277 "only the first certification may spend a request"
26278 );
26279 assert!(
26280 asserter.read_q().is_empty(),
26281 "the suppressed call must not consume a queued response"
26282 );
26283 }
26284
26285 #[tokio::test]
26289 async fn certification_resumes_once_the_window_lapses() {
26290 let asserter = Asserter::new();
26291 asserter.push_success(&Some(sealed_head(101)));
26292 asserter.push_success(&Some(sealed_head(102)));
26293 let mut subscriber = subscriber(asserter.clone());
26294
26295 let _ = subscriber
26296 .certify_canonical_head_on_sealed_block()
26297 .await
26298 .expect("first certification");
26299
26300 subscriber.last_canonical_head_certification = Some(
26302 Instant::now()
26303 - subscriber.config.canonical_head_poll_interval
26304 - Duration::from_millis(1),
26305 );
26306 assert!(!subscriber.canonical_head_certification_is_current());
26307
26308 let second = subscriber
26309 .certify_canonical_head_on_sealed_block()
26310 .await
26311 .expect("second certification");
26312
26313 assert!(matches!(second, Some(SubscriberEvent::BlockHeader { .. })));
26314 assert_eq!(
26315 subscriber
26316 .rpc_stats()
26317 .by_cause(SubscriberRpcCause::CanonicalHeadCertification),
26318 2
26319 );
26320 assert!(asserter.read_q().is_empty());
26321 }
26322
26323 #[tokio::test]
26326 async fn without_a_header_interest_nothing_is_certified() {
26327 let asserter = Asserter::new();
26328 let mut subscriber = subscriber(asserter.clone());
26329 subscriber.interests = Vec::new();
26330
26331 let certified = subscriber
26332 .certify_canonical_head_on_sealed_block()
26333 .await
26334 .expect("no interest is not an error");
26335
26336 assert!(certified.is_none());
26337 assert_eq!(subscriber.rpc_stats().total(), 0);
26338 assert!(asserter.read_q().is_empty());
26339 }
26340}