1use std::{
15 any::Any,
16 borrow::Cow,
17 collections::{HashMap, HashSet, VecDeque},
18 fmt,
19 future::Future,
20 hash::Hash,
21 marker::PhantomData,
22 num::NonZeroU64,
23 pin::Pin,
24 sync::{
25 Arc,
26 atomic::{AtomicU64, Ordering},
27 },
28 time::Duration,
29};
30
31use alloy_consensus::{BlockHeader as _, Transaction as _};
32use alloy_eips::BlockId;
33use alloy_network::{
34 Ethereum, Network,
35 primitives::{
36 BlockResponse as _, HeaderResponse as HeaderResponseTrait,
37 TransactionResponse as TransactionResponseTrait,
38 },
39};
40use alloy_primitives::{Address, B256, Bytes, U256};
41use alloy_provider::Provider;
42use alloy_rpc_types_eth::{Filter, FilterSet, Log};
43#[cfg(any(feature = "reactive-ws", feature = "reactive-polling", test))]
44use futures::{StreamExt, stream};
45use futures::{future::poll_fn, stream::BoxStream};
46
47use crate::{
48 cache::{AccountProof, BlockStateDiff, EvmCache},
49 errors::{BlockContextError, StorageFetchResult},
50 events::{EventDecoder, StateView},
51 freshness::FreshnessRegistry,
52 state_update::{AccountPatch, PurgeScope, StateDiff, StateUpdate},
53};
54
55#[derive(Clone, Debug, PartialEq, Eq)]
57pub enum ReactiveInput<N: Network = Ethereum> {
58 Log(Log),
60 BlockHeader(N::HeaderResponse),
62 FullBlock(N::BlockResponse),
64 PendingTxHash(B256),
66 PendingTx(N::TransactionResponse),
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct ReactiveContext {
73 pub chain_id: Option<u64>,
75 pub source: InputSource,
77 pub chain_status: ChainStatus,
79 pub block: Option<BlockRef>,
81 pub transaction_index: Option<u64>,
83 pub log_index: Option<u64>,
85}
86
87#[derive(Clone, Debug, PartialEq, Eq, Hash)]
89pub struct BlockRef {
90 pub number: u64,
92 pub hash: B256,
94 pub parent_hash: Option<B256>,
96 pub timestamp: Option<u64>,
98}
99
100#[derive(Clone, Debug, PartialEq, Eq)]
102pub enum ChainStatus {
103 Pending,
105 Included {
107 block: BlockRef,
109 confirmations: u64,
111 },
112 Safe {
114 block: BlockRef,
116 },
117 Finalized {
119 block: BlockRef,
121 },
122 Reorged {
124 dropped_from: BlockRef,
126 },
127}
128
129#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
131pub enum InputSource {
132 Batch,
134 Subscription,
136 Poll,
138 Backfill,
140 Synthetic,
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
146pub enum InputRef {
147 Log {
149 chain_id: Option<u64>,
151 block_hash: B256,
153 transaction_hash: B256,
155 log_index: u64,
157 },
158 PendingTx {
160 chain_id: Option<u64>,
162 hash: B256,
164 },
165 Block {
167 chain_id: Option<u64>,
169 hash: B256,
171 number: u64,
173 },
174}
175
176#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
178pub enum StateEffectQuality {
179 ExactFromInput,
181 AppliedWithPendingResync,
183 ResyncedAuthoritatively,
185 RequiresRepair,
187 NoStateEffect,
189}
190
191#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
193pub struct HandlerId(String);
194
195impl HandlerId {
196 pub fn new(id: impl Into<String>) -> Self {
198 Self(id.into())
199 }
200
201 pub fn as_str(&self) -> &str {
203 &self.0
204 }
205}
206
207impl fmt::Display for HandlerId {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 self.0.fmt(f)
210 }
211}
212
213#[derive(Clone, Debug, PartialEq, Eq, Hash)]
215pub struct ReportTag {
216 pub key: String,
218 pub value: String,
220}
221
222impl ReportTag {
223 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
225 Self {
226 key: key.into(),
227 value: value.into(),
228 }
229 }
230}
231
232#[derive(Clone)]
234pub struct HookSignal {
235 pub namespace: Cow<'static, str>,
237 pub kind: Cow<'static, str>,
239 pub labels: Vec<ReportTag>,
241 pub payload: Option<Arc<dyn Any + Send + Sync>>,
243}
244
245impl fmt::Debug for HookSignal {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 f.debug_struct("HookSignal")
248 .field("namespace", &self.namespace)
249 .field("kind", &self.kind)
250 .field("labels", &self.labels)
251 .field("payload", &self.payload.as_ref().map(|_| "<payload>"))
252 .finish()
253 }
254}
255
256#[derive(Clone, Debug)]
258pub enum ReactiveEffect {
259 StateUpdate(StateUpdate),
261 Resync(ResyncRequest),
263 Invalidate(InvalidationRequest),
265 Hook(HookSignal),
267 Speculative(SpeculativeRequest),
269}
270
271#[derive(Clone, Debug)]
273pub struct HandlerOutcome {
274 pub effects: Vec<ReactiveEffect>,
276 pub quality: StateEffectQuality,
278 pub tags: Vec<ReportTag>,
280}
281
282impl HandlerOutcome {
283 pub fn empty(quality: StateEffectQuality) -> Self {
285 Self {
286 effects: Vec::new(),
287 quality,
288 tags: Vec::new(),
289 }
290 }
291}
292
293#[derive(Clone, Debug)]
295pub struct ReactiveInputRecord<N: Network = Ethereum> {
296 pub input: ReactiveInput<N>,
298 pub context: ReactiveContext,
300}
301
302impl<N: Network> ReactiveInputRecord<N> {
303 pub fn new(input: ReactiveInput<N>, context: ReactiveContext) -> Self {
305 Self { input, context }
306 }
307
308 pub fn input_ref(&self) -> InputRef {
310 input_ref(&self.input, &self.context)
311 }
312}
313
314#[derive(Clone, Debug)]
316pub struct ReactiveInputBatch<N: Network = Ethereum> {
317 records: Vec<ReactiveInputRecord<N>>,
318}
319
320impl<N: Network> ReactiveInputBatch<N> {
321 pub fn new(records: Vec<ReactiveInputRecord<N>>) -> Self {
323 Self { records }
324 }
325
326 pub fn records(&self) -> &[ReactiveInputRecord<N>] {
328 &self.records
329 }
330
331 pub fn into_records(self) -> Vec<ReactiveInputRecord<N>> {
333 self.records
334 }
335}
336
337pub trait ReactiveHandler<N: Network = Ethereum>: Send + Sync {
339 fn id(&self) -> HandlerId;
341
342 fn interests(&self) -> Vec<ReactiveInterest<N>>;
344
345 fn handle(
347 &self,
348 ctx: &ReactiveContext,
349 input: &ReactiveInput<N>,
350 state: &dyn StateView,
351 ) -> Result<HandlerOutcome, HandlerError>;
352}
353
354pub trait ReactiveHook<N: Network = Ethereum>: Send + Sync {
356 fn on_report(&self, report: Arc<ReactiveReport<N>>);
358}
359
360#[allow(clippy::large_enum_variant)]
362#[derive(Clone)]
363pub enum ReactiveInterest<N: Network = Ethereum> {
364 Logs(LogInterest),
366 Blocks(BlockInterest),
368 PendingTransactions(PendingTxInterest<N>),
370}
371
372impl<N: Network> fmt::Debug for ReactiveInterest<N> {
373 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374 match self {
375 Self::Logs(interest) => f.debug_tuple("Logs").field(interest).finish(),
376 Self::Blocks(interest) => f.debug_tuple("Blocks").field(interest).finish(),
377 Self::PendingTransactions(interest) => f
378 .debug_tuple("PendingTransactions")
379 .field(interest)
380 .finish(),
381 }
382 }
383}
384
385#[derive(Clone)]
387pub struct LogInterest {
388 pub provider_filter: Filter,
390 pub local_matcher: Option<Arc<dyn LogMatcher>>,
392 pub route_key: Option<RouteKeySpec>,
394}
395
396impl LogInterest {
397 pub fn matches(&self, log: &Log) -> bool {
399 self.provider_filter.rpc_matches(log)
400 && self
401 .local_matcher
402 .as_ref()
403 .is_none_or(|matcher| matcher.matches(log))
404 }
405
406 pub fn route_key(&self, log: &Log) -> Option<RouteKey> {
408 self.route_key.as_ref().and_then(|spec| spec.extract(log))
409 }
410}
411
412impl fmt::Debug for LogInterest {
413 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
414 f.debug_struct("LogInterest")
415 .field("provider_filter", &self.provider_filter)
416 .field(
417 "local_matcher",
418 &self.local_matcher.as_ref().map(|_| "<matcher>"),
419 )
420 .field("route_key", &self.route_key)
421 .finish()
422 }
423}
424
425pub trait LogMatcher: Send + Sync {
427 fn matches(&self, log: &Log) -> bool;
429}
430
431#[derive(Clone)]
433pub enum RouteKeySpec {
434 EmitterAddress,
436 Topic {
438 index: usize,
440 },
441 DataSlice {
443 offset: usize,
445 len: usize,
447 },
448 Custom(Arc<dyn RouteKeyExtractor>),
450}
451
452impl RouteKeySpec {
453 pub fn extract(&self, log: &Log) -> Option<RouteKey> {
455 match self {
456 Self::EmitterAddress => Some(RouteKey::Address(log.address())),
457 Self::Topic { index } => log.topics().get(*index).copied().map(RouteKey::Bytes32),
458 Self::DataSlice { offset, len } => {
459 let data = log.inner.data.data.as_ref();
460 let end = offset.checked_add(*len)?;
461 data.get(*offset..end)
462 .map(|bytes| RouteKey::Bytes(bytes.to_vec()))
463 }
464 Self::Custom(extractor) => extractor.extract(log),
465 }
466 }
467}
468
469impl fmt::Debug for RouteKeySpec {
470 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
471 match self {
472 Self::EmitterAddress => f.write_str("EmitterAddress"),
473 Self::Topic { index } => f.debug_struct("Topic").field("index", index).finish(),
474 Self::DataSlice { offset, len } => f
475 .debug_struct("DataSlice")
476 .field("offset", offset)
477 .field("len", len)
478 .finish(),
479 Self::Custom(_) => f.write_str("Custom(<extractor>)"),
480 }
481 }
482}
483
484pub trait RouteKeyExtractor: Send + Sync {
486 fn extract(&self, log: &Log) -> Option<RouteKey>;
488}
489
490#[derive(Clone, Debug, PartialEq, Eq, Hash)]
492pub enum RouteKey {
493 Address(Address),
495 Bytes32(B256),
497 Bytes(Vec<u8>),
499}
500
501#[derive(Clone, Debug, PartialEq, Eq)]
503pub struct ReactiveLogRoute {
504 pub handler_id: HandlerId,
506 pub route_key: Option<RouteKey>,
508}
509
510#[derive(Clone, Debug, PartialEq, Eq, Hash)]
512pub struct BlockInterest {
513 pub mode: BlockInterestMode,
515}
516
517impl Default for BlockInterest {
518 fn default() -> Self {
519 Self {
520 mode: BlockInterestMode::Header,
521 }
522 }
523}
524
525#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
527pub enum BlockInterestMode {
528 Header,
530 FullBlock,
532}
533
534#[derive(Clone)]
536pub struct PendingTxInterest<N: Network = Ethereum> {
537 pub full_transactions: bool,
539 pub from: AddressMatcher,
541 pub to: AddressMatcher,
543 pub selectors: SelectorMatcher,
545 pub local_matcher: Option<Arc<dyn PendingTxMatcher<N>>>,
547}
548
549impl<N: Network> Default for PendingTxInterest<N> {
550 fn default() -> Self {
551 Self {
552 full_transactions: false,
553 from: AddressMatcher::Any,
554 to: AddressMatcher::Any,
555 selectors: SelectorMatcher::Any,
556 local_matcher: None,
557 }
558 }
559}
560
561impl<N: Network> PendingTxInterest<N> {
562 fn matches_hash_only(&self) -> bool {
563 !self.full_transactions
564 && self.from.is_any()
565 && self.to.is_any()
566 && self.selectors.is_any()
567 && self.local_matcher.is_none()
568 }
569
570 fn matches_tx(&self, tx: &N::TransactionResponse) -> bool {
571 self.from.matches(tx.from())
572 && self.to.matches_option(tx.to())
573 && self.selectors.matches(tx.input())
574 && self
575 .local_matcher
576 .as_ref()
577 .is_none_or(|matcher| matcher.matches(tx))
578 }
579}
580
581impl<N: Network> fmt::Debug for PendingTxInterest<N> {
582 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
583 f.debug_struct("PendingTxInterest")
584 .field("full_transactions", &self.full_transactions)
585 .field("from", &self.from)
586 .field("to", &self.to)
587 .field("selectors", &self.selectors)
588 .field(
589 "local_matcher",
590 &self.local_matcher.as_ref().map(|_| "<matcher>"),
591 )
592 .finish()
593 }
594}
595
596#[derive(Clone, Debug, PartialEq, Eq, Hash)]
598pub enum AddressMatcher {
599 Any,
601 Exact(Address),
603 AnyOf(Vec<Address>),
605}
606
607impl AddressMatcher {
608 pub fn is_any(&self) -> bool {
610 matches!(self, Self::Any)
611 }
612
613 pub fn matches(&self, address: Address) -> bool {
615 match self {
616 Self::Any => true,
617 Self::Exact(expected) => *expected == address,
618 Self::AnyOf(addresses) => addresses.contains(&address),
619 }
620 }
621
622 pub fn matches_option(&self, address: Option<Address>) -> bool {
624 match (self, address) {
625 (Self::Any, _) => true,
626 (_, Some(address)) => self.matches(address),
627 _ => false,
628 }
629 }
630}
631
632#[derive(Clone, Debug, PartialEq, Eq, Hash)]
634pub enum SelectorMatcher {
635 Any,
637 AnyOf(Vec<[u8; 4]>),
639}
640
641impl SelectorMatcher {
642 pub fn is_any(&self) -> bool {
644 matches!(self, Self::Any)
645 }
646
647 pub fn matches(&self, input: &Bytes) -> bool {
649 match self {
650 Self::Any => true,
651 Self::AnyOf(selectors) => input
652 .get(..4)
653 .and_then(|bytes| bytes.try_into().ok())
654 .is_some_and(|selector| selectors.contains(&selector)),
655 }
656 }
657}
658
659pub trait PendingTxMatcher<N: Network = Ethereum>: Send + Sync {
661 fn matches(&self, tx: &N::TransactionResponse) -> bool;
663}
664
665#[derive(Clone, Debug)]
683#[non_exhaustive]
684pub enum TrackingPolicy {
685 Slots {
690 slots: Vec<U256>,
692 },
693 WholeAccount,
698 Scalars,
703}
704
705#[derive(Clone, Copy, Debug, PartialEq, Eq)]
718pub enum RootGateCadence {
719 EveryNBlocks(NonZeroU64),
723 Disabled,
725}
726
727impl RootGateCadence {
728 pub fn every_n_blocks(n: u64) -> Self {
730 Self::EveryNBlocks(NonZeroU64::new(n.max(1)).expect("clamped to at least 1"))
731 }
732}
733
734impl Default for RootGateCadence {
735 fn default() -> Self {
739 Self::every_n_blocks(16)
740 }
741}
742
743#[derive(Clone, Debug)]
750struct TrackedRoot {
751 last_root: B256,
752 last_block: u64,
753 balance: U256,
754 nonce: u64,
755 code_hash: B256,
756}
757
758#[derive(Clone, Debug, PartialEq, Eq)]
760pub struct ResyncRequest {
761 pub id: ResyncId,
763 pub reason: ResyncReason,
765 pub block: ResyncBlock,
767 pub targets: Vec<ResyncTarget>,
769 pub priority: ResyncPriority,
771}
772
773#[derive(Clone, Debug, PartialEq, Eq, Hash)]
775pub struct ResyncId(String);
776
777impl ResyncId {
778 pub fn new(id: impl Into<String>) -> Self {
780 Self(id.into())
781 }
782}
783
784#[derive(Clone, Debug, PartialEq, Eq, Hash)]
786#[non_exhaustive]
787pub enum ResyncReason {
788 HandlerRequested,
790 SkippedStateEffect,
792 MissedBlockRange,
799 RootMoved,
809 Custom(String),
811}
812
813#[derive(Clone, Debug, PartialEq, Eq, Hash)]
815pub enum ResyncBlock {
816 Latest,
818 Safe,
820 Finalized,
822 Number(u64),
824 Hash {
826 number: u64,
828 hash: B256,
830 require_canonical: bool,
832 },
833}
834
835#[derive(Clone, Debug, PartialEq, Eq, Hash)]
837pub enum ResyncTarget {
838 StorageSlot {
840 address: Address,
842 slot: U256,
844 },
845 StorageSlots {
847 address: Address,
849 slots: Vec<U256>,
851 },
852 Account {
854 address: Address,
856 fields: AccountFieldMask,
858 },
859}
860
861#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
863pub struct AccountFieldMask {
864 pub balance: bool,
866 pub nonce: bool,
868 pub code: bool,
870}
871
872#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
874pub enum ResyncPriority {
875 Low,
877 #[default]
879 Normal,
880 High,
882}
883
884#[derive(Clone, Debug, PartialEq, Eq)]
886pub struct InvalidationRequest {
887 pub scope: PurgeScope,
889 pub address: Address,
891 pub reason: InvalidationReason,
893}
894
895#[derive(Clone, Debug, PartialEq, Eq, Hash)]
897pub enum InvalidationReason {
898 HandlerRequested,
900 Reorg,
902 Custom(String),
904}
905
906#[derive(Clone, Debug, PartialEq, Eq)]
908pub struct SpeculativeRequest {
909 pub id: SpeculativeId,
911 pub input_ref: InputRef,
913 pub labels: Vec<ReportTag>,
915}
916
917#[derive(Clone, Debug, PartialEq, Eq, Hash)]
919pub struct SpeculativeId(String);
920
921impl SpeculativeId {
922 pub fn new(id: impl Into<String>) -> Self {
924 Self(id.into())
925 }
926}
927
928#[derive(Clone, Debug, PartialEq, Eq)]
930pub struct ReactiveConfig {
931 pub hook_backpressure: HookBackpressure,
936 pub journal_depth: usize,
949}
950
951impl Default for ReactiveConfig {
952 fn default() -> Self {
953 Self {
954 hook_backpressure: HookBackpressure::Block,
955 journal_depth: 64,
956 }
957 }
958}
959
960#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
962pub enum HookBackpressure {
963 Block,
965 DropNewest,
967 DropOldest,
969 Error,
971}
972
973#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
981#[non_exhaustive]
982pub enum CacheHealth {
983 #[default]
985 Healthy,
986 Degraded {
990 since_block: u64,
992 },
993 Unhealthy {
996 since_block: u64,
998 },
999}
1000
1001#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1008#[non_exhaustive]
1009pub struct CacheMetricsSnapshot {
1010 pub deep_reorgs: u64,
1013 pub reorgs_recovered: u64,
1015 pub resync_requests: u64,
1017 pub resync_failures: u64,
1019 pub missed_ranges: u64,
1021 pub coverage_gaps: u64,
1023 pub pending_contamination: u64,
1025 pub stale_verdicts: u64,
1027}
1028
1029#[derive(Debug, Default)]
1035struct CacheMetrics {
1036 deep_reorgs: AtomicU64,
1037 reorgs_recovered: AtomicU64,
1038 resync_requests: AtomicU64,
1039 resync_failures: AtomicU64,
1040 missed_ranges: AtomicU64,
1041 coverage_gaps: AtomicU64,
1042 pending_contamination: AtomicU64,
1043 stale_verdicts: AtomicU64,
1044}
1045
1046impl CacheMetrics {
1047 fn snapshot(&self) -> CacheMetricsSnapshot {
1048 CacheMetricsSnapshot {
1049 deep_reorgs: self.deep_reorgs.load(Ordering::Relaxed),
1050 reorgs_recovered: self.reorgs_recovered.load(Ordering::Relaxed),
1051 resync_requests: self.resync_requests.load(Ordering::Relaxed),
1052 resync_failures: self.resync_failures.load(Ordering::Relaxed),
1053 missed_ranges: self.missed_ranges.load(Ordering::Relaxed),
1054 coverage_gaps: self.coverage_gaps.load(Ordering::Relaxed),
1055 pending_contamination: self.pending_contamination.load(Ordering::Relaxed),
1056 stale_verdicts: self.stale_verdicts.load(Ordering::Relaxed),
1057 }
1058 }
1059}
1060
1061#[derive(Clone, Debug)]
1063#[non_exhaustive]
1064pub enum ReactiveReport<N: Network = Ethereum> {
1065 Input(InputReport<N>),
1067 Decoded(DecodedReport<N>),
1069 Applied(AppliedReport<N>),
1071 Resynced(ResyncReport),
1073 BlockCommitted(BlockReport<N>),
1075 Reorg(ReorgReport<N>),
1077 MissedBlockRange(MissedRangeReport<N>),
1080 Health(HealthReport<N>),
1082 CoverageGap(CoverageGapReport<N>),
1085 Error(ReactiveErrorReport<N>),
1087}
1088
1089#[derive(Clone, Debug)]
1091pub struct InputReport<N: Network = Ethereum> {
1092 pub input_ref: InputRef,
1094 pub context: ReactiveContext,
1096 pub _network: PhantomData<N>,
1098}
1099
1100#[derive(Clone, Debug)]
1102pub struct DecodedReport<N: Network = Ethereum> {
1103 pub input_ref: InputRef,
1105 pub handler_ids: Vec<HandlerId>,
1107 pub _network: PhantomData<N>,
1109}
1110
1111#[derive(Clone, Debug)]
1113pub struct AppliedReport<N: Network = Ethereum> {
1114 pub input_ref: InputRef,
1116 pub handler_id: HandlerId,
1118 pub quality: StateEffectQuality,
1120 pub tags: Vec<ReportTag>,
1122 pub diff: StateDiff,
1124 pub state_updates: Vec<StateUpdate>,
1126 pub invalidations: Vec<InvalidationRequest>,
1128 pub resyncs: Vec<ResyncRequest>,
1130 pub speculative: Vec<SpeculativeRequest>,
1132 pub hook_signals: Vec<HookSignal>,
1134 pub _network: PhantomData<N>,
1136}
1137
1138#[derive(Clone, Debug, Default, PartialEq, Eq)]
1142pub struct ResyncReport {
1143 pub requested: Vec<ResyncRequest>,
1145 pub state_updates: Vec<StateUpdate>,
1147 pub diff: StateDiff,
1149 pub failed: Vec<ResyncFailure>,
1151}
1152
1153#[derive(Clone, Debug, PartialEq, Eq)]
1155pub struct ResyncFailure {
1156 pub request_id: ResyncId,
1158 pub block: ResyncBlock,
1160 pub target: ResyncTarget,
1162 pub kind: ResyncFailureKind,
1164 pub message: String,
1166}
1167
1168#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1170#[non_exhaustive]
1171pub enum ResyncFailureKind {
1172 MissingStorageFetcher,
1174 StorageFetchFailed,
1176 StorageFetchOmitted,
1178 MissingAccountFetcher,
1180 AccountFetchFailed,
1182 AccountFetchOmitted,
1184}
1185
1186#[derive(Clone, Debug)]
1188pub struct BlockReport<N: Network = Ethereum> {
1189 pub block: Option<BlockRef>,
1191 pub inputs: Vec<InputRef>,
1193 pub _network: PhantomData<N>,
1195}
1196
1197#[derive(Clone, Debug)]
1208pub struct ReorgReport<N: Network = Ethereum> {
1209 pub dropped: Option<BlockRef>,
1211 pub dropped_blocks: Vec<BlockRef>,
1213 pub dropped_inputs: Vec<InputRef>,
1215 pub rollback_updates: Vec<StateUpdate>,
1217 pub rollback_diff: StateDiff,
1219 pub purge_updates: Vec<StateUpdate>,
1221 pub purge_diff: StateDiff,
1223 pub canceled_resyncs: Vec<ResyncRequest>,
1225 pub reason: ReorgReason,
1227 pub _network: PhantomData<N>,
1229}
1230
1231#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1233pub enum ReorgReason {
1234 RemovedLog,
1236 ReorgedInput,
1238 ParentMismatch,
1240}
1241
1242#[derive(Clone, Debug)]
1250pub struct MissedRangeReport<N: Network = Ethereum> {
1251 pub from: u64,
1253 pub to: u64,
1255 pub block: u64,
1257 pub _network: PhantomData<N>,
1259}
1260
1261#[derive(Clone, Debug)]
1264pub struct HealthReport<N: Network = Ethereum> {
1265 pub from: CacheHealth,
1267 pub to: CacheHealth,
1269 pub block: Option<u64>,
1271 pub _network: PhantomData<N>,
1273}
1274
1275#[derive(Clone, Debug)]
1288pub struct CoverageGapReport<N: Network = Ethereum> {
1289 pub address: Address,
1291 pub block: u64,
1293 pub _network: PhantomData<N>,
1295}
1296
1297#[derive(Clone, Debug)]
1300pub struct ReactiveErrorReport<N: Network = Ethereum> {
1301 pub input_ref: Option<InputRef>,
1303 pub message: String,
1305 pub _network: PhantomData<N>,
1307}
1308
1309#[derive(Clone, Debug)]
1312pub struct ReactiveBatchReport<N: Network = Ethereum> {
1313 pub applied: Vec<AppliedReport<N>>,
1315 pub resyncs: Vec<ResyncRequest>,
1317 pub speculative: Vec<SpeculativeRequest>,
1319 pub reports: Vec<Arc<ReactiveReport<N>>>,
1321}
1322
1323impl<N: Network> Default for ReactiveBatchReport<N> {
1324 fn default() -> Self {
1325 Self {
1326 applied: Vec::new(),
1327 resyncs: Vec::new(),
1328 speculative: Vec::new(),
1329 reports: Vec::new(),
1330 }
1331 }
1332}
1333
1334#[derive(Clone, Debug, PartialEq, Eq)]
1336pub struct HandlerError {
1337 message: String,
1338}
1339
1340impl HandlerError {
1341 pub fn new(message: impl Into<String>) -> Self {
1343 Self {
1344 message: message.into(),
1345 }
1346 }
1347}
1348
1349impl fmt::Display for HandlerError {
1350 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1351 self.message.fmt(f)
1352 }
1353}
1354
1355impl std::error::Error for HandlerError {}
1356
1357impl From<String> for HandlerError {
1358 fn from(message: String) -> Self {
1359 Self::new(message)
1360 }
1361}
1362
1363impl From<&str> for HandlerError {
1364 fn from(message: &str) -> Self {
1365 Self::new(message)
1366 }
1367}
1368
1369#[derive(Debug, thiserror::Error)]
1371pub enum ReactiveError {
1372 #[error("handler `{handler_id}` failed: {source}")]
1374 HandlerFailed {
1375 handler_id: HandlerId,
1377 source: HandlerError,
1379 },
1380 #[error(
1382 "conflicting effects for input {input_ref:?} on target {target:?}: `{first}` vs `{second}`"
1383 )]
1384 ConflictingEffects {
1385 input_ref: Box<InputRef>,
1387 target: Box<EffectTarget>,
1389 first: HandlerId,
1391 second: HandlerId,
1393 },
1394 #[error(
1396 "pending input {input_ref:?} emitted invalid canonical effect `{effect_kind}` from `{handler_id}`"
1397 )]
1398 InvalidPendingEffect {
1399 input_ref: Box<InputRef>,
1401 handler_id: HandlerId,
1403 effect_kind: &'static str,
1405 },
1406 #[error(transparent)]
1408 Register(#[from] RegisterError),
1409}
1410
1411#[derive(Debug, thiserror::Error)]
1413pub enum RegisterError {
1414 #[error("handler id `{0}` is already registered")]
1416 DuplicateHandler(HandlerId),
1417}
1418
1419#[derive(Debug, thiserror::Error)]
1422pub enum ReactiveEngineRegisterError {
1423 #[error(transparent)]
1425 Register(#[from] RegisterError),
1426 #[error(transparent)]
1428 Subscriber(#[from] SubscriberError),
1429}
1430
1431#[derive(Debug, thiserror::Error)]
1434pub enum ReactiveEngineError {
1435 #[error(transparent)]
1437 Subscriber(#[from] SubscriberError),
1438 #[error(transparent)]
1440 Runtime(#[from] ReactiveError),
1441}
1442
1443#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1445pub enum EffectTarget {
1446 StorageSlot {
1448 address: Address,
1450 slot: U256,
1452 },
1453 AccountBalance {
1455 address: Address,
1457 },
1458 AccountNonce {
1460 address: Address,
1462 },
1463 AccountCode {
1465 address: Address,
1467 },
1468 MaskedStorageSlot {
1470 address: Address,
1472 slot: U256,
1474 mask: U256,
1476 },
1477}
1478
1479#[derive(Clone, Debug, PartialEq, Eq)]
1480enum AbsoluteValue {
1481 U256(U256),
1482 U64(u64),
1483 Bytes(Bytes),
1484}
1485
1486pub struct ReactiveRuntime<N: Network = Ethereum> {
1488 registry: ReactiveRegistry<N>,
1489 hooks: Vec<Arc<dyn ReactiveHook<N>>>,
1490 config: ReactiveConfig,
1491 journal: VecDeque<BlockJournal<N>>,
1492 pending_resyncs: Vec<ResyncRequest>,
1493 health: CacheHealth,
1494 metrics: CacheMetrics,
1495 freshness: Option<FreshnessRegistry>,
1504 tracking: HashMap<Address, TrackingPolicy>,
1508 tracked_roots: HashMap<Address, TrackedRoot>,
1511 root_gate_cadence: RootGateCadence,
1513 last_gate_block: Option<u64>,
1517 touched_since_gate: HashSet<Address>,
1523}
1524
1525#[derive(Clone, Debug)]
1526struct BlockJournal<N: Network = Ethereum> {
1527 block: BlockRef,
1528 inputs: Vec<InputRef>,
1529 applied: Vec<AppliedReport<N>>,
1530 resynced: Vec<ResyncReport>,
1531}
1532
1533pub struct ReactiveRegistry<N: Network = Ethereum> {
1541 handlers: Vec<RegisteredHandler<N>>,
1542}
1543
1544struct RegisteredHandler<N: Network = Ethereum> {
1545 id: HandlerId,
1546 handler: Arc<dyn ReactiveHandler<N>>,
1547 interests: Vec<ReactiveInterest<N>>,
1548}
1549
1550impl<N: Network> Default for ReactiveRegistry<N> {
1551 fn default() -> Self {
1552 Self::new()
1553 }
1554}
1555
1556impl<N: Network> ReactiveRegistry<N> {
1557 pub fn new() -> Self {
1559 Self {
1560 handlers: Vec::new(),
1561 }
1562 }
1563
1564 pub fn register_handler(
1569 &mut self,
1570 handler: Arc<dyn ReactiveHandler<N>>,
1571 ) -> Result<(), RegisterError> {
1572 let id = handler.id();
1573 if self.handlers.iter().any(|registered| registered.id == id) {
1574 return Err(RegisterError::DuplicateHandler(id));
1575 }
1576 let interests = handler.interests();
1577 self.handlers.push(RegisteredHandler {
1578 id,
1579 handler,
1580 interests,
1581 });
1582 Ok(())
1583 }
1584
1585 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
1591 let index = self
1592 .handlers
1593 .iter()
1594 .position(|registered| ®istered.id == id)?;
1595 Some(self.handlers.remove(index).handler)
1596 }
1597
1598 pub fn contains_handler(&self, id: &HandlerId) -> bool {
1600 self.handlers.iter().any(|registered| ®istered.id == id)
1601 }
1602
1603 pub fn handler_ids(&self) -> Vec<HandlerId> {
1605 self.handlers
1606 .iter()
1607 .map(|registered| registered.id.clone())
1608 .collect()
1609 }
1610
1611 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
1613 self.handlers
1614 .iter()
1615 .find(|registered| ®istered.id == id)
1616 .map(|registered| registered.interests.as_slice())
1617 }
1618
1619 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
1621 self.handlers
1622 .iter()
1623 .flat_map(|handler| handler.interests.clone())
1624 .collect()
1625 }
1626
1627 pub fn log_subscription_filters(&self) -> Vec<Filter> {
1634 let mut filters = Vec::new();
1635 for interest in self.log_interests() {
1636 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
1637 }
1638 filters
1639 }
1640
1641 pub fn route_log(&self, log: &Log) -> Vec<ReactiveLogRoute> {
1647 self.handlers
1648 .iter()
1649 .filter_map(|handler| handler.route_log(log))
1650 .collect()
1651 }
1652
1653 fn handlers(&self) -> &[RegisteredHandler<N>] {
1654 &self.handlers
1655 }
1656
1657 fn log_interests(&self) -> impl Iterator<Item = &LogInterest> {
1658 self.handlers.iter().flat_map(|handler| {
1659 handler
1660 .interests
1661 .iter()
1662 .filter_map(|interest| match interest {
1663 ReactiveInterest::Logs(interest) => Some(interest),
1664 ReactiveInterest::Blocks(_) | ReactiveInterest::PendingTransactions(_) => None,
1665 })
1666 })
1667 }
1668}
1669
1670impl<N: Network> ReactiveRuntime<N> {
1671 pub fn new(config: ReactiveConfig) -> Self {
1673 Self {
1674 registry: ReactiveRegistry::new(),
1675 hooks: Vec::new(),
1676 config,
1677 journal: VecDeque::new(),
1678 pending_resyncs: Vec::new(),
1679 health: CacheHealth::Healthy,
1680 metrics: CacheMetrics::default(),
1681 freshness: None,
1682 tracking: HashMap::new(),
1683 tracked_roots: HashMap::new(),
1684 root_gate_cadence: RootGateCadence::default(),
1685 last_gate_block: None,
1686 touched_since_gate: HashSet::new(),
1687 }
1688 }
1689
1690 pub fn track_account(&mut self, address: Address, policy: TrackingPolicy) {
1704 self.tracking.insert(address, policy);
1705 self.tracked_roots.remove(&address);
1706 }
1707
1708 pub fn untrack_account(&mut self, address: Address) -> bool {
1712 self.tracked_roots.remove(&address);
1713 self.tracking.remove(&address).is_some()
1714 }
1715
1716 pub fn set_root_gate_cadence(&mut self, cadence: RootGateCadence) {
1724 self.root_gate_cadence = cadence;
1725 self.last_gate_block = None;
1726 self.touched_since_gate.clear();
1727 }
1728
1729 pub fn root_gate_cadence(&self) -> RootGateCadence {
1731 self.root_gate_cadence
1732 }
1733
1734 pub fn enable_freshness_stamping(&mut self) {
1746 if self.freshness.is_none() {
1747 self.freshness = Some(FreshnessRegistry::new());
1748 }
1749 }
1750
1751 pub fn freshness(&self) -> Option<&FreshnessRegistry> {
1756 self.freshness.as_ref()
1757 }
1758
1759 pub fn freshness_mut(&mut self) -> Option<&mut FreshnessRegistry> {
1764 self.freshness.as_mut()
1765 }
1766
1767 pub fn health(&self) -> CacheHealth {
1769 self.health
1770 }
1771
1772 pub fn metrics(&self) -> CacheMetricsSnapshot {
1774 self.metrics.snapshot()
1775 }
1776
1777 pub fn reset_health(&mut self) {
1787 self.health = CacheHealth::Healthy;
1788 }
1789
1790 fn escalate_trust(&mut self, block: u64) -> Option<Arc<ReactiveReport<N>>> {
1804 let to = match self.health {
1805 CacheHealth::Healthy => CacheHealth::Degraded { since_block: block },
1806 CacheHealth::Degraded { .. } => CacheHealth::Unhealthy { since_block: block },
1807 CacheHealth::Unhealthy { .. } => return None,
1808 };
1809 self.transition_health(to, Some(block))
1810 }
1811
1812 fn transition_health(
1820 &mut self,
1821 to: CacheHealth,
1822 block: Option<u64>,
1823 ) -> Option<Arc<ReactiveReport<N>>> {
1824 if to == self.health {
1825 return None;
1826 }
1827 let from = self.health;
1828 self.health = to;
1829 Some(Arc::new(ReactiveReport::Health(HealthReport {
1830 from,
1831 to,
1832 block,
1833 _network: PhantomData,
1834 })))
1835 }
1836
1837 pub fn register_handler(
1839 &mut self,
1840 handler: Arc<dyn ReactiveHandler<N>>,
1841 ) -> Result<(), RegisterError> {
1842 self.registry.register_handler(handler)
1843 }
1844
1845 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
1853 self.registry.unregister_handler(id)
1854 }
1855
1856 pub fn contains_handler(&self, id: &HandlerId) -> bool {
1858 self.registry.contains_handler(id)
1859 }
1860
1861 pub fn handler_ids(&self) -> Vec<HandlerId> {
1863 self.registry.handler_ids()
1864 }
1865
1866 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
1868 self.registry.handler_interests(id)
1869 }
1870
1871 pub fn last_canonical_block(&self) -> Option<BlockRef> {
1881 self.journal.back().map(|entry| entry.block.clone())
1882 }
1883
1884 pub fn pending_resyncs(&self) -> &[ResyncRequest] {
1893 &self.pending_resyncs
1894 }
1895
1896 pub fn cancel_pending_resyncs(&mut self, address: Address) -> Vec<ResyncRequest> {
1911 let mut cancelled = Vec::new();
1912 self.pending_resyncs.retain_mut(|request| {
1913 let (matching, remaining): (Vec<_>, Vec<_>) = request
1914 .targets
1915 .drain(..)
1916 .partition(|target| resync_target_address(target) == address);
1917 request.targets = remaining;
1918 if !matching.is_empty() {
1919 cancelled.push(ResyncRequest {
1920 id: request.id.clone(),
1921 reason: request.reason.clone(),
1922 block: request.block.clone(),
1923 targets: matching,
1924 priority: request.priority,
1925 });
1926 }
1927 !request.targets.is_empty()
1928 });
1929 cancelled
1930 }
1931
1932 pub fn register_hook(&mut self, hook: Arc<dyn ReactiveHook<N>>) -> Result<(), RegisterError> {
1934 self.hooks.push(hook);
1935 Ok(())
1936 }
1937
1938 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
1940 self.registry.interests()
1941 }
1942
1943 pub fn ingest_batch(
1945 &mut self,
1946 cache: &mut EvmCache,
1947 batch: ReactiveInputBatch<N>,
1948 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
1949 let batch_report = self.ingest_batch_direct(cache, batch)?;
1950 self.dispatch_reports(&batch_report.reports);
1951 let _ = &self.config;
1952 Ok(batch_report)
1953 }
1954
1955 pub fn ingest_batch_with_resync(
1966 &mut self,
1967 cache: &mut EvmCache,
1968 batch: ReactiveInputBatch<N>,
1969 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
1970 let mut batch_report = self.ingest_batch_direct(cache, batch)?;
1971
1972 if !batch_report.resyncs.is_empty() {
1973 let resync_report = execute_resync_requests(cache, &batch_report.resyncs);
1974 let unique_requests = resync_report
1978 .requested
1979 .iter()
1980 .map(|request| &request.id)
1981 .collect::<HashSet<_>>()
1982 .len();
1983 self.metrics
1984 .resync_requests
1985 .fetch_add(unique_requests as u64, Ordering::Relaxed);
1986 self.metrics
1987 .resync_failures
1988 .fetch_add(resync_report.failed.len() as u64, Ordering::Relaxed);
1989 self.remove_pending_resyncs(batch_report.resyncs.iter().map(|request| &request.id));
1990 self.record_journal_resync(&resync_report);
1991 batch_report
1992 .reports
1993 .push(Arc::new(ReactiveReport::Resynced(resync_report)));
1994 }
1995
1996 self.dispatch_reports(&batch_report.reports);
1997 let _ = &self.config;
1998 Ok(batch_report)
1999 }
2000
2001 fn ingest_batch_direct(
2002 &mut self,
2003 cache: &mut EvmCache,
2004 batch: ReactiveInputBatch<N>,
2005 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
2006 let records = sort_records(dedupe_records(batch.into_records()));
2007
2008 let mut batch_report = ReactiveBatchReport::default();
2009 let mut reports_to_dispatch = Vec::new();
2010 let mut touched_addrs: HashSet<Address> = HashSet::new();
2015 let mut canonical_batch_block: Option<u64> = None;
2016
2017 for record in records {
2018 let input_ref = record.input_ref();
2019 reports_to_dispatch.push(Arc::new(ReactiveReport::Input(InputReport {
2020 input_ref,
2021 context: record.context.clone(),
2022 _network: PhantomData,
2023 })));
2024
2025 if let Some(reorg_report) =
2026 self.recover_for_canonical_input(cache, &record, &mut reports_to_dispatch)
2027 {
2028 self.metrics
2029 .reorgs_recovered
2030 .fetch_add(1, Ordering::Relaxed);
2031 remove_canceled_resyncs_from_batch(
2032 &mut batch_report.resyncs,
2033 &reorg_report.canceled_resyncs,
2034 );
2035 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
2036 }
2037
2038 if let Some(reorg_report) =
2039 self.recover_for_reorged_input(cache, &record, &mut reports_to_dispatch)
2040 {
2041 self.metrics
2042 .reorgs_recovered
2043 .fetch_add(1, Ordering::Relaxed);
2044 remove_canceled_resyncs_from_batch(
2045 &mut batch_report.resyncs,
2046 &reorg_report.canceled_resyncs,
2047 );
2048 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
2049 continue;
2050 }
2051
2052 if let Some(block) = canonical_record_block(&record) {
2053 canonical_batch_block = Some(block.number);
2056 self.record_journal_input(block, input_ref);
2057 }
2058
2059 if let Some(Err(err)) = advance_block_for_canonical_record(cache, &record) {
2063 reports_to_dispatch.push(Arc::new(ReactiveReport::Error(ReactiveErrorReport {
2064 input_ref: Some(input_ref),
2065 message: err.to_string(),
2066 _network: PhantomData,
2067 })));
2068 }
2069
2070 let executions = self.execute_handlers(cache, &record, input_ref)?;
2071 if executions.is_empty() {
2072 continue;
2073 }
2074
2075 reports_to_dispatch.push(Arc::new(ReactiveReport::Decoded(DecodedReport {
2076 input_ref,
2077 handler_ids: executions
2078 .iter()
2079 .map(|execution| execution.handler_id.clone())
2080 .collect(),
2081 _network: PhantomData,
2082 })));
2083
2084 detect_conflicts(input_ref, &executions)?;
2085
2086 let canonical_block_number = canonical_record_block(&record).map(|block| block.number);
2092
2093 for execution in executions {
2094 let diff = if execution.state_updates.is_empty() {
2095 StateDiff::default()
2096 } else {
2097 cache.apply_updates(&execution.state_updates)
2098 };
2099
2100 batch_report
2101 .resyncs
2102 .extend(execution.resyncs.iter().cloned());
2103 self.pending_resyncs
2104 .extend(execution.resyncs.iter().cloned());
2105 batch_report
2106 .speculative
2107 .extend(execution.speculative.iter().cloned());
2108
2109 let applied = AppliedReport {
2110 input_ref,
2111 handler_id: execution.handler_id,
2112 quality: execution.quality,
2113 tags: execution.tags,
2114 diff,
2115 state_updates: execution.state_updates,
2116 invalidations: execution.invalidations,
2117 resyncs: execution.resyncs,
2118 speculative: execution.speculative,
2119 hook_signals: execution.hook_signals,
2120 _network: PhantomData,
2121 };
2122 if let (Some(number), Some(registry)) =
2131 (canonical_block_number, self.freshness.as_mut())
2132 {
2133 for change in &applied.diff.slots {
2134 registry.valid_through_slot(change.address, change.slot, number);
2135 }
2136 }
2137
2138 collect_diff_addresses(&applied.diff, &mut touched_addrs);
2146
2147 let report = Arc::new(ReactiveReport::Applied(applied.clone()));
2148 reports_to_dispatch.push(report);
2149 if let Some(block) = canonical_record_block(&record) {
2150 self.record_journal_applied(block, applied.clone());
2151 }
2152 batch_report.applied.push(applied);
2153 }
2154 }
2155
2156 if self.root_gate_runnable(cache) {
2166 self.touched_since_gate
2167 .extend(touched_addrs.iter().copied());
2168 if self.root_gate_due(canonical_batch_block) {
2169 let accumulated = std::mem::take(&mut self.touched_since_gate);
2170 self.run_root_gate(
2171 cache,
2172 canonical_batch_block,
2173 &accumulated,
2174 &mut batch_report.resyncs,
2175 &mut reports_to_dispatch,
2176 );
2177 self.last_gate_block = canonical_batch_block;
2178 }
2179 } else {
2180 self.touched_since_gate.clear();
2187 }
2188
2189 batch_report.reports = reports_to_dispatch;
2190 Ok(batch_report)
2191 }
2192
2193 fn root_gate_runnable(&self, cache: &EvmCache) -> bool {
2198 if matches!(self.root_gate_cadence, RootGateCadence::Disabled) {
2199 return false;
2200 }
2201 let has_gated_targets = self
2202 .tracking
2203 .values()
2204 .any(|policy| !matches!(policy, TrackingPolicy::Slots { .. }));
2205 has_gated_targets && cache.account_proof_fetcher().is_some()
2206 }
2207
2208 fn root_gate_due(&self, canonical_block: Option<u64>) -> bool {
2212 let Some(block) = canonical_block else {
2213 return false;
2214 };
2215 match self.root_gate_cadence {
2216 RootGateCadence::Disabled => false,
2217 RootGateCadence::EveryNBlocks(n) => match self.last_gate_block {
2218 None => true,
2219 Some(last) => block >= last.saturating_add(n.get()),
2220 },
2221 }
2222 }
2223
2224 fn run_root_gate(
2250 &mut self,
2251 cache: &EvmCache,
2252 canonical_block: Option<u64>,
2253 touched: &HashSet<Address>,
2254 resyncs: &mut Vec<ResyncRequest>,
2255 reports: &mut Vec<Arc<ReactiveReport<N>>>,
2256 ) {
2257 if self.tracking.is_empty() {
2258 return;
2259 }
2260 let Some(block) = canonical_block else {
2261 return;
2262 };
2263 let Some(fetcher) = cache.account_proof_fetcher().cloned() else {
2264 return;
2265 };
2266
2267 let mut targets: Vec<(Address, bool)> = self
2270 .tracking
2271 .iter()
2272 .filter_map(|(address, policy)| match policy {
2273 TrackingPolicy::Slots { .. } => None,
2274 TrackingPolicy::WholeAccount => Some((*address, true)),
2275 TrackingPolicy::Scalars => Some((*address, false)),
2276 })
2277 .collect();
2278 if targets.is_empty() {
2279 return;
2280 }
2281 targets.sort_by_key(|(address, _)| *address);
2282
2283 let block_id = BlockId::number(block);
2284 let mut probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
2289 targets
2290 .iter()
2291 .map(|&(address, _)| (address, vec![]))
2292 .collect(),
2293 block_id,
2294 )
2295 .into_iter()
2296 .collect();
2297 for (address, whole_account) in targets {
2298 let Some(Ok(proof)) = probes.remove(&address) else {
2299 continue;
2302 };
2303
2304 let baseline = self.tracked_roots.get(&address).cloned();
2305 let Some(baseline) = baseline else {
2306 self.adopt_root(address, block, &proof);
2308 continue;
2309 };
2310
2311 if block <= baseline.last_block {
2316 continue;
2317 }
2318
2319 if whole_account {
2320 if proof.storage_hash == baseline.last_root {
2321 continue;
2323 }
2324 if !touched.contains(&address) {
2326 reports.push(Arc::new(ReactiveReport::CoverageGap(CoverageGapReport {
2328 address,
2329 block,
2330 _network: PhantomData,
2331 })));
2332 self.metrics.coverage_gaps.fetch_add(1, Ordering::Relaxed);
2333 resyncs.push(root_moved_account_resync(
2334 address,
2335 block,
2336 AccountFieldMask {
2337 balance: true,
2338 nonce: true,
2339 code: true,
2340 },
2341 ));
2342 }
2343 self.adopt_root(address, block, &proof);
2345 } else {
2346 let balance_moved = proof.balance != baseline.balance;
2349 let nonce_moved = proof.nonce != baseline.nonce;
2350 let code_moved = proof.code_hash != baseline.code_hash;
2351 if (balance_moved || nonce_moved || code_moved) && !touched.contains(&address) {
2352 resyncs.push(root_moved_account_resync(
2353 address,
2354 block,
2355 AccountFieldMask {
2356 balance: balance_moved,
2357 nonce: nonce_moved,
2358 code: code_moved,
2359 },
2360 ));
2361 }
2362 self.adopt_root(address, block, &proof);
2363 }
2364 }
2365 }
2366
2367 fn adopt_root(&mut self, address: Address, block: u64, proof: &AccountProof) {
2369 self.tracked_roots.insert(
2370 address,
2371 TrackedRoot {
2372 last_root: proof.storage_hash,
2373 last_block: block,
2374 balance: proof.balance,
2375 nonce: proof.nonce,
2376 code_hash: proof.code_hash,
2377 },
2378 );
2379 }
2380
2381 fn execute_handlers(
2382 &self,
2383 cache: &EvmCache,
2384 record: &ReactiveInputRecord<N>,
2385 input_ref: InputRef,
2386 ) -> Result<Vec<HandlerExecution>, ReactiveError> {
2387 let mut executions = Vec::new();
2388 for registered in self.registry.handlers() {
2389 if !registered.matches(&record.input) {
2390 continue;
2391 }
2392
2393 let outcome = registered
2394 .handler
2395 .handle(&record.context, &record.input, cache)
2396 .map_err(|source| ReactiveError::HandlerFailed {
2397 handler_id: registered.id.clone(),
2398 source,
2399 })?;
2400
2401 if let Err(error) =
2402 validate_effects(input_ref, &record.context, ®istered.id, &outcome.effects)
2403 {
2404 if matches!(error, ReactiveError::InvalidPendingEffect { .. }) {
2405 self.metrics
2406 .pending_contamination
2407 .fetch_add(1, Ordering::Relaxed);
2408 }
2409 return Err(error);
2410 }
2411 executions.push(HandlerExecution::from_outcome(
2412 registered.id.clone(),
2413 input_ref,
2414 outcome,
2415 ));
2416 }
2417 Ok(executions)
2418 }
2419
2420 fn dispatch_reports(&self, reports: &[Arc<ReactiveReport<N>>]) {
2421 for report in reports {
2422 for hook in &self.hooks {
2423 hook.on_report(report.clone());
2424 }
2425 }
2426 }
2427
2428 fn recover_for_canonical_input(
2429 &mut self,
2430 cache: &mut EvmCache,
2431 record: &ReactiveInputRecord<N>,
2432 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
2433 ) -> Option<ReorgReport<N>> {
2434 let block = canonical_record_block(record)?;
2435 let latest = self.journal.back()?.block.clone();
2436
2437 if self
2438 .journal
2439 .iter()
2440 .any(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
2441 {
2442 return None;
2443 }
2444
2445 if block.number == latest.number.saturating_add(1) && block.parent_hash == Some(latest.hash)
2446 {
2447 return None;
2448 }
2449
2450 if block.number > latest.number.saturating_add(1) {
2451 self.metrics.missed_ranges.fetch_add(1, Ordering::Relaxed);
2456 health_reports.extend(self.escalate_trust(block.number));
2457 health_reports.push(Arc::new(ReactiveReport::MissedBlockRange(
2458 MissedRangeReport {
2459 from: latest.number + 1,
2460 to: block.number - 1,
2461 block: block.number,
2462 _network: PhantomData,
2463 },
2464 )));
2465 return None;
2466 }
2467
2468 let dropped = if let Some(parent_hash) = block.parent_hash {
2469 if let Some(parent_index) = self
2470 .journal
2471 .iter()
2472 .rposition(|entry| entry.block.hash == parent_hash)
2473 {
2474 self.drain_journal_after(parent_index)
2475 } else {
2476 health_reports.extend(self.warn_under_recovery(block.number));
2477 self.drain_journal_from_number(block.number)
2478 }
2479 } else {
2480 health_reports.extend(self.warn_under_recovery(block.number));
2481 self.drain_journal_from_number(block.number)
2482 };
2483
2484 self.recover_dropped_journals(cache, dropped, ReorgReason::ParentMismatch)
2485 }
2486
2487 fn recover_for_reorged_input(
2488 &mut self,
2489 cache: &mut EvmCache,
2490 record: &ReactiveInputRecord<N>,
2491 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
2492 ) -> Option<ReorgReport<N>> {
2493 let (dropped_block, reason) = reorg_signal_block(record)?;
2494 let dropped = if let Some(index) = self
2495 .journal
2496 .iter()
2497 .position(|entry| entry.block.hash == dropped_block.hash)
2498 {
2499 self.drain_journal_from(index)
2500 } else {
2501 health_reports.extend(self.warn_under_recovery(dropped_block.number));
2502 self.drain_journal_from_number(dropped_block.number)
2503 };
2504
2505 if dropped.is_empty() {
2506 let canceled_resyncs =
2507 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
2508 if canceled_resyncs.is_empty() {
2509 return None;
2510 }
2511 return Some(ReorgReport {
2512 dropped: Some(dropped_block.clone()),
2513 dropped_blocks: vec![dropped_block],
2514 dropped_inputs: Vec::new(),
2515 rollback_updates: Vec::new(),
2516 rollback_diff: StateDiff::default(),
2517 purge_updates: Vec::new(),
2518 purge_diff: StateDiff::default(),
2519 canceled_resyncs,
2520 reason,
2521 _network: PhantomData,
2522 });
2523 }
2524
2525 self.recover_dropped_journals(cache, dropped, reason)
2526 }
2527
2528 fn warn_under_recovery(&mut self, reorg_number: u64) -> Option<Arc<ReactiveReport<N>>> {
2540 let oldest_journaled = self.journal.front().map(|entry| entry.block.number);
2541 tracing::warn!(
2542 reorg_block = reorg_number,
2543 oldest_journaled = ?oldest_journaled,
2544 journal_depth = self.config.journal_depth,
2545 "reactive reorg recovery is incomplete: the reorged block is no longer \
2546 in the journal, so effects from blocks aged out of the journal are \
2547 neither rolled back nor purged (the freshness/validation loop is the \
2548 backstop). Increase ReactiveConfig::journal_depth to recover deeper \
2549 reorgs precisely."
2550 );
2551
2552 self.metrics.deep_reorgs.fetch_add(1, Ordering::Relaxed);
2553
2554 self.escalate_trust(reorg_number)
2555 }
2556
2557 fn record_journal_input(&mut self, block: &BlockRef, input_ref: InputRef) {
2558 let entry = self.journal_entry_mut(block);
2559 if !entry.inputs.contains(&input_ref) {
2560 entry.inputs.push(input_ref);
2561 }
2562 self.trim_journal();
2563 }
2564
2565 fn record_journal_applied(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
2566 self.journal_entry_mut(block).applied.push(applied);
2567 self.trim_journal();
2568 }
2569
2570 fn record_journal_resync(&mut self, report: &ResyncReport) {
2571 if report.diff.is_empty() {
2572 return;
2573 }
2574 let Some(block) = single_hash_pinned_resync_block(report) else {
2575 return;
2576 };
2577 self.journal_entry_mut(&block).resynced.push(report.clone());
2578 self.trim_journal();
2579 }
2580
2581 fn journal_entry_mut(&mut self, block: &BlockRef) -> &mut BlockJournal<N> {
2582 if let Some(index) = self
2583 .journal
2584 .iter()
2585 .position(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
2586 {
2587 return &mut self.journal[index];
2588 }
2589
2590 self.journal.push_back(BlockJournal {
2591 block: block.clone(),
2592 inputs: Vec::new(),
2593 applied: Vec::new(),
2594 resynced: Vec::new(),
2595 });
2596 let index = self.journal.len() - 1;
2597 &mut self.journal[index]
2598 }
2599
2600 fn trim_journal(&mut self) {
2601 if self.config.journal_depth == 0 {
2602 self.journal.clear();
2603 return;
2604 }
2605 while self.journal.len() > self.config.journal_depth {
2606 self.journal.pop_front();
2607 }
2608 }
2609
2610 fn drain_journal_after(&mut self, index: usize) -> Vec<BlockJournal<N>> {
2611 self.journal.drain((index + 1)..).collect()
2612 }
2613
2614 fn drain_journal_from(&mut self, index: usize) -> Vec<BlockJournal<N>> {
2615 self.journal.drain(index..).collect()
2616 }
2617
2618 fn drain_journal_from_number(&mut self, number: u64) -> Vec<BlockJournal<N>> {
2619 let Some(index) = self
2620 .journal
2621 .iter()
2622 .position(|entry| entry.block.number >= number)
2623 else {
2624 return Vec::new();
2625 };
2626 self.drain_journal_from(index)
2627 }
2628
2629 fn recover_dropped_journals(
2630 &mut self,
2631 cache: &mut EvmCache,
2632 dropped: Vec<BlockJournal<N>>,
2633 reason: ReorgReason,
2634 ) -> Option<ReorgReport<N>> {
2635 if dropped.is_empty() {
2636 return None;
2637 }
2638
2639 let dropped_blocks: Vec<_> = dropped.iter().map(|entry| entry.block.clone()).collect();
2640 let dropped_inputs: Vec<_> = dropped
2641 .iter()
2642 .flat_map(|entry| entry.inputs.iter().copied())
2643 .collect();
2644 let canceled_resyncs = self.cancel_resyncs_for_dropped_blocks(&dropped_blocks);
2645 let purge_scopes = purge_scopes_for_dropped_journals(&dropped);
2646 let rollback_updates = rollback_updates_for_dropped_journals(&dropped, &purge_scopes);
2647 let purge_updates: Vec<_> = purge_scopes
2648 .iter()
2649 .map(|(address, scope)| StateUpdate::purge(*address, scope.clone()))
2650 .collect();
2651
2652 let rollback_diff = if rollback_updates.is_empty() {
2653 StateDiff::default()
2654 } else {
2655 cache.apply_updates(&rollback_updates)
2656 };
2657 let purge_diff = if purge_updates.is_empty() {
2658 StateDiff::default()
2659 } else {
2660 cache.apply_updates(&purge_updates)
2661 };
2662
2663 Some(ReorgReport {
2664 dropped: dropped_blocks.first().cloned(),
2665 dropped_blocks,
2666 dropped_inputs,
2667 rollback_updates,
2668 rollback_diff,
2669 purge_updates,
2670 purge_diff,
2671 canceled_resyncs,
2672 reason,
2673 _network: PhantomData,
2674 })
2675 }
2676
2677 fn cancel_resyncs_for_dropped_blocks(
2678 &mut self,
2679 dropped_blocks: &[BlockRef],
2680 ) -> Vec<ResyncRequest> {
2681 let mut canceled = Vec::new();
2682 self.pending_resyncs.retain(|request| {
2683 let should_cancel = resync_request_targets_dropped_block(request, dropped_blocks);
2684 if should_cancel {
2685 canceled.push(request.clone());
2686 }
2687 !should_cancel
2688 });
2689 canceled
2690 }
2691
2692 fn remove_pending_resyncs<'a>(&mut self, ids: impl IntoIterator<Item = &'a ResyncId>) {
2693 let ids: HashSet<_> = ids.into_iter().cloned().collect();
2694 self.pending_resyncs
2695 .retain(|request| !ids.contains(&request.id));
2696 }
2697}
2698
2699fn collect_diff_addresses(diff: &StateDiff, into: &mut HashSet<Address>) {
2705 into.extend(diff.slots.iter().map(|change| change.address));
2706 into.extend(diff.accounts.iter().map(|change| change.address));
2707 into.extend(diff.purged.iter().map(|purge| purge.address));
2708 into.extend(diff.skipped.iter().map(|skipped| skipped.address));
2709 into.extend(diff.skipped_balances.iter().map(|skipped| skipped.address));
2710 into.extend(diff.skipped_masks.iter().map(|skipped| skipped.address));
2711 into.extend(diff.skipped_accounts.iter().map(|skipped| skipped.address));
2712}
2713
2714fn root_moved_account_resync(
2719 address: Address,
2720 block: u64,
2721 fields: AccountFieldMask,
2722) -> ResyncRequest {
2723 ResyncRequest {
2724 id: ResyncId::new(format!("root-moved:{address:#x}:{block}")),
2725 reason: ResyncReason::RootMoved,
2726 block: ResyncBlock::Number(block),
2727 targets: vec![ResyncTarget::Account { address, fields }],
2728 priority: ResyncPriority::Normal,
2729 }
2730}
2731
2732fn canonical_record_block<N: Network>(record: &ReactiveInputRecord<N>) -> Option<&BlockRef> {
2733 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
2734 return None;
2735 }
2736 if is_canonical_status(&record.context.chain_status) {
2737 return context_block_ref(&record.context);
2738 }
2739 None
2740}
2741
2742fn advance_block_for_canonical_record<N: Network>(
2751 cache: &mut EvmCache,
2752 record: &ReactiveInputRecord<N>,
2753) -> Option<Result<(), BlockContextError>> {
2754 if !is_canonical_status(&record.context.chain_status) {
2755 return None;
2756 }
2757 match &record.input {
2758 ReactiveInput::BlockHeader(header) => Some(cache.advance_block(header)),
2759 ReactiveInput::FullBlock(block) => Some(cache.advance_block(block.header())),
2760 _ => None,
2761 }
2762}
2763
2764fn context_block_ref(ctx: &ReactiveContext) -> Option<&BlockRef> {
2765 match &ctx.chain_status {
2766 ChainStatus::Included { block, .. }
2767 | ChainStatus::Safe { block }
2768 | ChainStatus::Finalized { block } => Some(block),
2769 ChainStatus::Reorged { dropped_from } => Some(dropped_from),
2770 ChainStatus::Pending => ctx.block.as_ref(),
2771 }
2772}
2773
2774fn reorg_signal_block<N: Network>(
2775 record: &ReactiveInputRecord<N>,
2776) -> Option<(BlockRef, ReorgReason)> {
2777 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
2778 return block_ref_from_record(record).map(|block| (block, ReorgReason::RemovedLog));
2779 }
2780
2781 if let ChainStatus::Reorged { dropped_from } = &record.context.chain_status {
2782 return Some((dropped_from.clone(), ReorgReason::ReorgedInput));
2783 }
2784
2785 None
2786}
2787
2788fn block_ref_from_record<N: Network>(record: &ReactiveInputRecord<N>) -> Option<BlockRef> {
2789 context_block_ref(&record.context)
2790 .cloned()
2791 .or_else(|| match &record.input {
2792 ReactiveInput::Log(log) => Some(BlockRef {
2793 number: log.block_number?,
2794 hash: log.block_hash?,
2795 parent_hash: None,
2796 timestamp: log.block_timestamp,
2797 }),
2798 ReactiveInput::BlockHeader(header) => Some(BlockRef {
2799 number: header.number(),
2800 hash: header.hash(),
2801 parent_hash: Some(header.parent_hash()),
2802 timestamp: Some(header.timestamp()),
2803 }),
2804 ReactiveInput::FullBlock(block) => {
2805 let header = block.header();
2806 Some(BlockRef {
2807 number: header.number(),
2808 hash: header.hash(),
2809 parent_hash: Some(header.parent_hash()),
2810 timestamp: Some(header.timestamp()),
2811 })
2812 }
2813 ReactiveInput::PendingTxHash(_) | ReactiveInput::PendingTx(_) => None,
2814 })
2815}
2816
2817fn remove_canceled_resyncs_from_batch(
2818 resyncs: &mut Vec<ResyncRequest>,
2819 canceled: &[ResyncRequest],
2820) {
2821 if canceled.is_empty() {
2822 return;
2823 }
2824 let canceled_ids: HashSet<_> = canceled.iter().map(|request| request.id.clone()).collect();
2825 resyncs.retain(|request| !canceled_ids.contains(&request.id));
2826}
2827
2828fn resync_target_address(target: &ResyncTarget) -> Address {
2829 match target {
2830 ResyncTarget::StorageSlot { address, .. }
2831 | ResyncTarget::StorageSlots { address, .. }
2832 | ResyncTarget::Account { address, .. } => *address,
2833 }
2834}
2835
2836fn resync_request_targets_dropped_block(
2837 request: &ResyncRequest,
2838 dropped_blocks: &[BlockRef],
2839) -> bool {
2840 let ResyncBlock::Hash { number, hash, .. } = &request.block else {
2841 return false;
2842 };
2843 dropped_blocks
2844 .iter()
2845 .any(|block| block.hash == *hash && block.number == *number)
2846}
2847
2848fn single_hash_pinned_resync_block(report: &ResyncReport) -> Option<BlockRef> {
2849 let first = report.requested.first()?.block.clone();
2850 if !report
2851 .requested
2852 .iter()
2853 .all(|request| request.block == first)
2854 {
2855 return None;
2856 }
2857
2858 let ResyncBlock::Hash { number, hash, .. } = first else {
2859 return None;
2860 };
2861
2862 Some(BlockRef {
2863 number,
2864 hash,
2865 parent_hash: None,
2866 timestamp: None,
2867 })
2868}
2869
2870fn purge_scopes_for_dropped_journals<N: Network>(
2871 dropped: &[BlockJournal<N>],
2872) -> Vec<(Address, PurgeScope)> {
2873 let mut scopes: Vec<(Address, PurgeScope)> = Vec::new();
2874 for entry in dropped.iter().rev() {
2875 for resynced in entry.resynced.iter().rev() {
2876 merge_purge_scopes_for_diff(&mut scopes, &resynced.diff);
2877 }
2878 for applied in entry.applied.iter().rev() {
2879 merge_purge_scopes_for_diff(&mut scopes, &applied.diff);
2880 }
2881 }
2882 scopes
2883}
2884
2885fn rollback_updates_for_dropped_journals<N: Network>(
2886 dropped: &[BlockJournal<N>],
2887 purge_scopes: &[(Address, PurgeScope)],
2888) -> Vec<StateUpdate> {
2889 let purge_addresses: HashSet<_> = purge_scopes
2890 .iter()
2891 .map(|(address, _scope)| *address)
2892 .collect();
2893 let mut updates = Vec::new();
2894 for entry in dropped.iter().rev() {
2895 for resynced in entry.resynced.iter().rev() {
2896 push_rollback_updates_for_diff(&mut updates, &resynced.diff, &purge_addresses);
2897 }
2898 for applied in entry.applied.iter().rev() {
2899 push_rollback_updates_for_diff(&mut updates, &applied.diff, &purge_addresses);
2900 }
2901 }
2902 updates
2903}
2904
2905fn merge_purge_scopes_for_diff(scopes: &mut Vec<(Address, PurgeScope)>, diff: &StateDiff) {
2906 for change in &diff.accounts {
2907 merge_purge_scope(scopes, change.address, PurgeScope::Account);
2908 }
2909 for record in &diff.purged {
2910 merge_purge_scope(scopes, record.address, record.scope.clone());
2911 }
2912}
2913
2914fn push_rollback_updates_for_diff(
2915 updates: &mut Vec<StateUpdate>,
2916 diff: &StateDiff,
2917 purge_addresses: &HashSet<Address>,
2918) {
2919 for change in diff.slots.iter().rev() {
2920 if purge_addresses.contains(&change.address) {
2921 continue;
2922 }
2923 updates.push(StateUpdate::slot(change.address, change.slot, change.old));
2924 }
2925}
2926
2927fn merge_purge_scope(scopes: &mut Vec<(Address, PurgeScope)>, address: Address, scope: PurgeScope) {
2928 if let Some((_existing_address, existing_scope)) = scopes
2929 .iter_mut()
2930 .find(|(existing_address, _scope)| *existing_address == address)
2931 {
2932 *existing_scope = merged_purge_scope(existing_scope.clone(), scope);
2933 } else {
2934 scopes.push((address, scope));
2935 }
2936}
2937
2938fn merged_purge_scope(left: PurgeScope, right: PurgeScope) -> PurgeScope {
2939 match (left, right) {
2940 (PurgeScope::Account, _) | (_, PurgeScope::Account) => PurgeScope::Account,
2941 (PurgeScope::AllStorage, _) | (_, PurgeScope::AllStorage) => PurgeScope::AllStorage,
2942 (PurgeScope::Slots(mut left), PurgeScope::Slots(right)) => {
2943 for slot in right {
2944 if !left.contains(&slot) {
2945 left.push(slot);
2946 }
2947 }
2948 PurgeScope::Slots(left)
2949 }
2950 }
2951}
2952
2953#[derive(Clone, Debug)]
2954struct StorageFetchSlot {
2955 address: Address,
2956 slot: U256,
2957 origins: Vec<StorageFetchOrigin>,
2958}
2959
2960#[derive(Clone, Debug)]
2961struct StorageFetchOrigin {
2962 request_id: ResyncId,
2963 target: ResyncTarget,
2964}
2965
2966#[derive(Clone, Debug)]
2967struct StorageFetchGroup {
2968 block: ResyncBlock,
2969 slots: Vec<StorageFetchSlot>,
2970 seen: HashSet<(Address, U256)>,
2971}
2972
2973#[derive(Clone, Debug)]
2976struct AccountResyncTarget {
2977 request_id: ResyncId,
2978 block: ResyncBlock,
2979 address: Address,
2980 fields: AccountFieldMask,
2981}
2982
2983fn resolve_trace_resyncs(
2984 cache: &EvmCache,
2985 storage_groups: &mut Vec<StorageFetchGroup>,
2986 account_targets: &mut Vec<AccountResyncTarget>,
2987 state_updates: &mut Vec<StateUpdate>,
2988) {
2989 let Some(fetcher) = cache.block_state_diff_fetcher().cloned() else {
2990 return;
2991 };
2992
2993 let mut blocks = Vec::new();
2994 let mut seen = HashSet::new();
2995 for block in storage_groups
2996 .iter()
2997 .map(|group| group.block.clone())
2998 .chain(account_targets.iter().map(|target| target.block.clone()))
2999 {
3000 if seen.insert(block.clone()) {
3001 blocks.push(block);
3002 }
3003 }
3004
3005 let mut traces = HashMap::new();
3006 for block in blocks {
3007 match (fetcher)(resync_block_to_block_id(&block)) {
3008 Ok(diff) => {
3009 traces.insert(block, diff);
3010 }
3011 Err(error) => {
3012 tracing::debug!(
3013 block = ?block,
3014 error = %error,
3015 "block trace resync source failed; falling back to point resync"
3016 );
3017 }
3018 }
3019 }
3020
3021 for group in storage_groups.iter_mut() {
3022 let Some(trace) = traces.get(&group.block) else {
3023 continue;
3024 };
3025 group.slots.retain(|slot| {
3026 if let Some(value) = trace_storage_value(trace, slot.address, slot.slot) {
3027 state_updates.push(StateUpdate::slot(slot.address, slot.slot, value));
3028 return false;
3029 }
3030 cache
3031 .cached_storage_value(slot.address, slot.slot)
3032 .is_none()
3033 });
3034 group.seen = group
3035 .slots
3036 .iter()
3037 .map(|slot| (slot.address, slot.slot))
3038 .collect();
3039 }
3040 storage_groups.retain(|group| !group.slots.is_empty());
3041
3042 let mut unresolved_accounts = Vec::new();
3043 for mut account in account_targets.drain(..) {
3044 let Some(trace) = traces.get(&account.block) else {
3045 unresolved_accounts.push(account);
3046 continue;
3047 };
3048 let Some(trace_account) = trace
3049 .accounts
3050 .iter()
3051 .find(|diff| diff.address == account.address)
3052 else {
3053 unresolved_accounts.push(account);
3054 continue;
3055 };
3056
3057 let mut patch = AccountPatch::default();
3058 let mut unresolved = AccountFieldMask::default();
3059 if account.fields.balance {
3060 if let Some(balance) = trace_account.balance {
3061 patch = patch.balance(balance);
3062 } else {
3063 unresolved.balance = true;
3064 }
3065 }
3066 if account.fields.nonce {
3067 if let Some(nonce) = trace_account.nonce {
3068 patch = patch.nonce(nonce);
3069 } else {
3070 unresolved.nonce = true;
3071 }
3072 }
3073 if account.fields.code {
3074 if let Some(code) = &trace_account.code {
3075 patch = patch.code(code.clone());
3076 } else {
3077 unresolved.code = true;
3078 }
3079 }
3080
3081 if patch.balance.is_some() || patch.nonce.is_some() || patch.code.is_some() {
3082 state_updates.push(StateUpdate::account_upsert(account.address, patch));
3083 }
3084 if !account_field_mask_empty(unresolved) {
3085 account.fields = unresolved;
3086 unresolved_accounts.push(account);
3087 }
3088 }
3089 *account_targets = unresolved_accounts;
3090}
3091
3092fn trace_storage_value(trace: &BlockStateDiff, address: Address, slot: U256) -> Option<U256> {
3093 trace
3094 .accounts
3095 .iter()
3096 .find(|account| account.address == address)
3097 .and_then(|account| {
3098 account
3099 .storage
3100 .iter()
3101 .find(|entry| entry.slot == slot)
3102 .map(|entry| entry.value)
3103 })
3104}
3105
3106fn account_field_mask_empty(mask: AccountFieldMask) -> bool {
3107 !mask.balance && !mask.nonce && !mask.code
3108}
3109
3110fn execute_resync_requests(cache: &mut EvmCache, requests: &[ResyncRequest]) -> ResyncReport {
3111 let mut failed = Vec::new();
3112 let mut storage_groups: Vec<StorageFetchGroup> = Vec::new();
3113 let mut account_targets: Vec<AccountResyncTarget> = Vec::new();
3114
3115 for request in requests {
3116 for target in &request.targets {
3117 match target {
3118 ResyncTarget::StorageSlot { address, slot } => {
3119 push_storage_resync_slot(
3120 &mut storage_groups,
3121 &request.id,
3122 &request.block,
3123 *address,
3124 *slot,
3125 );
3126 }
3127 ResyncTarget::StorageSlots { address, slots } => {
3128 for slot in slots {
3129 push_storage_resync_slot(
3130 &mut storage_groups,
3131 &request.id,
3132 &request.block,
3133 *address,
3134 *slot,
3135 );
3136 }
3137 }
3138 ResyncTarget::Account { address, fields } => {
3139 account_targets.push(AccountResyncTarget {
3140 request_id: request.id.clone(),
3141 block: request.block.clone(),
3142 address: *address,
3143 fields: *fields,
3144 });
3145 }
3146 }
3147 }
3148 }
3149
3150 let mut state_updates = Vec::new();
3151 resolve_trace_resyncs(
3152 cache,
3153 &mut storage_groups,
3154 &mut account_targets,
3155 &mut state_updates,
3156 );
3157
3158 if !storage_groups.is_empty() {
3159 if let Some(fetcher) = cache.storage_batch_fetcher().cloned() {
3160 for group in storage_groups {
3161 let block = group.block.clone();
3162 let fetches: Vec<(Address, U256)> = group
3163 .slots
3164 .iter()
3165 .map(|slot| (slot.address, slot.slot))
3166 .collect();
3167 let results = (fetcher)(fetches, resync_block_to_block_id(&block));
3168 let mut pending: HashMap<(Address, U256), StorageFetchSlot> = group
3169 .slots
3170 .iter()
3171 .cloned()
3172 .map(|slot| ((slot.address, slot.slot), slot))
3173 .collect();
3174
3175 for (address, slot, fetched) in results {
3176 let Some(requested_slot) = pending.remove(&(address, slot)) else {
3177 continue;
3178 };
3179 match fetched {
3180 Ok(value) => state_updates.push(StateUpdate::slot(address, slot, value)),
3181 Err(error) => {
3182 let message = error.to_string();
3183 push_resync_failures(
3184 &mut failed,
3185 &block,
3186 requested_slot.origins,
3187 ResyncFailureKind::StorageFetchFailed,
3188 message,
3189 );
3190 }
3191 }
3192 }
3193
3194 for requested_slot in group.slots {
3195 if pending
3196 .remove(&(requested_slot.address, requested_slot.slot))
3197 .is_some()
3198 {
3199 push_resync_failures(
3200 &mut failed,
3201 &block,
3202 requested_slot.origins,
3203 ResyncFailureKind::StorageFetchOmitted,
3204 "storage batch fetcher did not return a value for slot".to_string(),
3205 );
3206 }
3207 }
3208 }
3209 } else {
3210 for group in storage_groups {
3211 let block = group.block.clone();
3212 for slot in group.slots {
3213 push_resync_failures(
3214 &mut failed,
3215 &block,
3216 slot.origins,
3217 ResyncFailureKind::MissingStorageFetcher,
3218 "storage resync requires a storage batch fetcher".to_string(),
3219 );
3220 }
3221 }
3222 }
3223 }
3224
3225 if !account_targets.is_empty() {
3226 if let Some(fetcher) = cache.account_proof_fetcher().cloned() {
3227 let mut groups: Vec<(BlockId, Vec<_>)> = Vec::new();
3233 for account in account_targets {
3234 let block_id = resync_block_to_block_id(&account.block);
3235 match groups
3236 .iter_mut()
3237 .find(|(group_block, _)| *group_block == block_id)
3238 {
3239 Some((_, group)) => group.push(account),
3240 None => groups.push((block_id, vec![account])),
3241 }
3242 }
3243 for (block_id, group) in groups {
3244 let probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
3245 group
3246 .iter()
3247 .map(|account| (account.address, vec![]))
3248 .collect(),
3249 block_id,
3250 )
3251 .into_iter()
3252 .collect();
3253 for account in group {
3254 match probes.get(&account.address).cloned() {
3258 Some(Ok(proof)) => {
3259 let mut patch = AccountPatch::default();
3264 if account.fields.balance {
3265 patch = patch.balance(proof.balance);
3266 }
3267 if account.fields.nonce {
3268 patch = patch.nonce(proof.nonce);
3269 }
3270 state_updates.push(StateUpdate::account_upsert(account.address, patch));
3276 }
3277 Some(Err(error)) => {
3278 failed.push(ResyncFailure {
3279 request_id: account.request_id,
3280 block: account.block,
3281 target: ResyncTarget::Account {
3282 address: account.address,
3283 fields: account.fields,
3284 },
3285 kind: ResyncFailureKind::AccountFetchFailed,
3286 message: error.to_string(),
3287 });
3288 }
3289 None => {
3290 failed.push(ResyncFailure {
3291 request_id: account.request_id,
3292 block: account.block,
3293 target: ResyncTarget::Account {
3294 address: account.address,
3295 fields: account.fields,
3296 },
3297 kind: ResyncFailureKind::AccountFetchOmitted,
3298 message:
3299 "account proof fetcher did not return a result for address"
3300 .to_string(),
3301 });
3302 }
3303 }
3304 }
3305 }
3306 } else {
3307 for account in account_targets {
3308 failed.push(ResyncFailure {
3309 request_id: account.request_id,
3310 block: account.block,
3311 target: ResyncTarget::Account {
3312 address: account.address,
3313 fields: account.fields,
3314 },
3315 kind: ResyncFailureKind::MissingAccountFetcher,
3316 message: "account resync requires an account proof fetcher".to_string(),
3317 });
3318 }
3319 }
3320 }
3321
3322 let diff = if state_updates.is_empty() {
3323 StateDiff::default()
3324 } else {
3325 cache.apply_updates(&state_updates)
3326 };
3327
3328 ResyncReport {
3329 requested: requests.to_vec(),
3330 state_updates,
3331 diff,
3332 failed,
3333 }
3334}
3335
3336fn push_resync_failures(
3337 failed: &mut Vec<ResyncFailure>,
3338 block: &ResyncBlock,
3339 origins: Vec<StorageFetchOrigin>,
3340 kind: ResyncFailureKind,
3341 message: String,
3342) {
3343 for origin in origins {
3344 failed.push(ResyncFailure {
3345 request_id: origin.request_id,
3346 block: block.clone(),
3347 target: origin.target,
3348 kind,
3349 message: message.clone(),
3350 });
3351 }
3352}
3353
3354fn push_storage_resync_slot(
3355 groups: &mut Vec<StorageFetchGroup>,
3356 request_id: &ResyncId,
3357 block: &ResyncBlock,
3358 address: Address,
3359 slot: U256,
3360) {
3361 let group_index = if let Some(index) = groups.iter().position(|group| group.block == *block) {
3362 index
3363 } else {
3364 groups.push(StorageFetchGroup {
3365 block: block.clone(),
3366 slots: Vec::new(),
3367 seen: HashSet::new(),
3368 });
3369 groups.len() - 1
3370 };
3371
3372 let group = &mut groups[group_index];
3373 let origin = StorageFetchOrigin {
3374 request_id: request_id.clone(),
3375 target: ResyncTarget::StorageSlot { address, slot },
3376 };
3377 if group.seen.insert((address, slot)) {
3378 group.slots.push(StorageFetchSlot {
3379 address,
3380 slot,
3381 origins: vec![origin],
3382 });
3383 } else if let Some(existing) = group
3384 .slots
3385 .iter_mut()
3386 .find(|existing| existing.address == address && existing.slot == slot)
3387 {
3388 existing.origins.push(origin);
3389 }
3390}
3391
3392fn resync_block_to_block_id(block: &ResyncBlock) -> BlockId {
3393 match block {
3394 ResyncBlock::Latest => BlockId::latest(),
3395 ResyncBlock::Safe => BlockId::safe(),
3396 ResyncBlock::Finalized => BlockId::finalized(),
3397 ResyncBlock::Number(number) => BlockId::number(*number),
3398 ResyncBlock::Hash {
3399 number: _,
3400 hash,
3401 require_canonical,
3402 } => BlockId::from((*hash, Some(*require_canonical))),
3403 }
3404}
3405
3406impl<N: Network> RegisteredHandler<N> {
3407 fn matches(&self, input: &ReactiveInput<N>) -> bool {
3408 self.interests
3409 .iter()
3410 .any(|interest| interest_matches(interest, input))
3411 }
3412
3413 fn route_log(&self, log: &Log) -> Option<ReactiveLogRoute> {
3414 self.interests.iter().find_map(|interest| match interest {
3415 ReactiveInterest::Logs(interest) if interest.matches(log) => Some(ReactiveLogRoute {
3416 handler_id: self.id.clone(),
3417 route_key: interest.route_key(log),
3418 }),
3419 ReactiveInterest::Logs(_)
3420 | ReactiveInterest::Blocks(_)
3421 | ReactiveInterest::PendingTransactions(_) => None,
3422 })
3423 }
3424}
3425
3426fn merge_log_subscription_filter(filters: &mut Vec<Filter>, next: &Filter) {
3427 if let Some(existing) = filters
3428 .iter_mut()
3429 .find(|existing| existing.block_option == next.block_option)
3430 {
3431 merge_filter_set(&mut existing.address, &next.address);
3432 for (existing_topic, next_topic) in existing.topics.iter_mut().zip(next.topics.iter()) {
3433 merge_filter_set(existing_topic, next_topic);
3434 }
3435 } else {
3436 filters.push(next.clone());
3437 }
3438}
3439
3440fn merge_filter_set<T: Clone + Eq + Hash>(target: &mut FilterSet<T>, source: &FilterSet<T>) {
3441 if target.is_empty() {
3442 return;
3443 }
3444 if source.is_empty() {
3445 *target = FilterSet::default();
3446 return;
3447 }
3448 for value in source.iter() {
3449 target.insert(value.clone());
3450 }
3451}
3452
3453#[derive(Clone, Debug)]
3454struct HandlerExecution {
3455 handler_id: HandlerId,
3456 quality: StateEffectQuality,
3457 tags: Vec<ReportTag>,
3458 state_updates: Vec<StateUpdate>,
3459 invalidations: Vec<InvalidationRequest>,
3460 resyncs: Vec<ResyncRequest>,
3461 speculative: Vec<SpeculativeRequest>,
3462 hook_signals: Vec<HookSignal>,
3463}
3464
3465impl HandlerExecution {
3466 fn from_outcome(handler_id: HandlerId, input_ref: InputRef, outcome: HandlerOutcome) -> Self {
3467 let mut state_updates = Vec::new();
3468 let mut invalidations = Vec::new();
3469 let mut resyncs = Vec::new();
3470 let mut speculative = Vec::new();
3471 let mut hook_signals = Vec::new();
3472
3473 for effect in outcome.effects {
3474 match effect {
3475 ReactiveEffect::StateUpdate(update) => state_updates.push(update),
3476 ReactiveEffect::Invalidate(invalidation) => {
3477 state_updates.push(StateUpdate::purge(
3478 invalidation.address,
3479 invalidation.scope.clone(),
3480 ));
3481 invalidations.push(invalidation);
3482 }
3483 ReactiveEffect::Resync(request) => resyncs.push(request),
3484 ReactiveEffect::Hook(signal) => hook_signals.push(signal),
3485 ReactiveEffect::Speculative(mut request) => {
3486 request.input_ref = input_ref;
3487 speculative.push(request);
3488 }
3489 }
3490 }
3491
3492 Self {
3493 handler_id,
3494 quality: outcome.quality,
3495 tags: outcome.tags,
3496 state_updates,
3497 invalidations,
3498 resyncs,
3499 speculative,
3500 hook_signals,
3501 }
3502 }
3503}
3504
3505fn dedupe_records<N: Network>(records: Vec<ReactiveInputRecord<N>>) -> Vec<ReactiveInputRecord<N>> {
3506 let mut seen = HashSet::new();
3507 let mut deduped = Vec::with_capacity(records.len());
3508 for record in records {
3509 if seen.insert(record.input_ref()) {
3510 deduped.push(record);
3511 }
3512 }
3513 deduped
3514}
3515
3516fn sort_records<N: Network>(records: Vec<ReactiveInputRecord<N>>) -> Vec<ReactiveInputRecord<N>> {
3517 let mut indexed: Vec<(usize, ReactiveInputRecord<N>)> =
3518 records.into_iter().enumerate().collect();
3519 indexed.sort_by_key(|(index, record)| record_sort_key(*index, record));
3520 indexed.into_iter().map(|(_, record)| record).collect()
3521}
3522
3523fn record_sort_key<N: Network>(index: usize, record: &ReactiveInputRecord<N>) -> RecordSortKey {
3524 if let ReactiveInput::Log(log) = &record.input
3525 && is_canonical_status(&record.context.chain_status)
3526 && !log.removed
3527 {
3528 return RecordSortKey {
3529 class: 0,
3530 block_number: log
3531 .block_number
3532 .or(record.context.block.as_ref().map(|block| block.number))
3533 .unwrap_or(u64::MAX),
3534 transaction_index: log
3535 .transaction_index
3536 .or(record.context.transaction_index)
3537 .unwrap_or(u64::MAX),
3538 log_index: log
3539 .log_index
3540 .or(record.context.log_index)
3541 .unwrap_or(u64::MAX),
3542 original_index: index,
3543 };
3544 }
3545
3546 RecordSortKey {
3547 class: 1,
3548 block_number: 0,
3549 transaction_index: 0,
3550 log_index: 0,
3551 original_index: index,
3552 }
3553}
3554
3555#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
3556struct RecordSortKey {
3557 class: u8,
3558 block_number: u64,
3559 transaction_index: u64,
3560 log_index: u64,
3561 original_index: usize,
3562}
3563
3564fn interest_matches<N: Network>(interest: &ReactiveInterest<N>, input: &ReactiveInput<N>) -> bool {
3565 match (interest, input) {
3566 (ReactiveInterest::Logs(interest), ReactiveInput::Log(log)) => interest.matches(log),
3567 (
3568 ReactiveInterest::Blocks(BlockInterest {
3569 mode: BlockInterestMode::Header,
3570 }),
3571 ReactiveInput::BlockHeader(_),
3572 ) => true,
3573 (
3574 ReactiveInterest::Blocks(BlockInterest {
3575 mode: BlockInterestMode::FullBlock,
3576 }),
3577 ReactiveInput::FullBlock(_),
3578 ) => true,
3579 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTxHash(_)) => {
3580 interest.matches_hash_only()
3581 }
3582 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTx(tx)) => {
3583 interest.matches_tx(tx)
3584 }
3585 _ => false,
3586 }
3587}
3588
3589fn validate_effects(
3590 input_ref: InputRef,
3591 ctx: &ReactiveContext,
3592 handler_id: &HandlerId,
3593 effects: &[ReactiveEffect],
3594) -> Result<(), ReactiveError> {
3595 let pending = matches!(ctx.chain_status, ChainStatus::Pending)
3596 || matches!(input_ref, InputRef::PendingTx { .. });
3597 if !pending {
3598 return Ok(());
3599 }
3600
3601 for effect in effects {
3602 let effect_kind = match effect {
3603 ReactiveEffect::StateUpdate(_) => Some("state_update"),
3604 ReactiveEffect::Invalidate(_) => Some("invalidate"),
3605 ReactiveEffect::Resync(_) => Some("resync"),
3606 ReactiveEffect::Hook(_) | ReactiveEffect::Speculative(_) => None,
3607 };
3608 if let Some(effect_kind) = effect_kind {
3609 return Err(ReactiveError::InvalidPendingEffect {
3610 input_ref: Box::new(input_ref),
3611 handler_id: handler_id.clone(),
3612 effect_kind,
3613 });
3614 }
3615 }
3616 Ok(())
3617}
3618
3619fn detect_conflicts(
3620 input_ref: InputRef,
3621 executions: &[HandlerExecution],
3622) -> Result<(), ReactiveError> {
3623 let mut writes: HashMap<EffectTarget, (AbsoluteValue, HandlerId)> = HashMap::new();
3624 for execution in executions {
3625 for update in &execution.state_updates {
3626 for (target, value) in absolute_writes(update) {
3627 if let Some((previous_value, previous_handler)) = writes.get(&target) {
3628 if previous_value != &value {
3629 return Err(ReactiveError::ConflictingEffects {
3630 input_ref: Box::new(input_ref),
3631 target: Box::new(target),
3632 first: previous_handler.clone(),
3633 second: execution.handler_id.clone(),
3634 });
3635 }
3636 } else {
3637 writes.insert(target, (value, execution.handler_id.clone()));
3638 }
3639 }
3640 }
3641 }
3642 Ok(())
3643}
3644
3645fn absolute_writes(update: &StateUpdate) -> Vec<(EffectTarget, AbsoluteValue)> {
3646 match update {
3647 StateUpdate::Slot {
3648 address,
3649 slot,
3650 value,
3651 } => vec![(
3652 EffectTarget::StorageSlot {
3653 address: *address,
3654 slot: *slot,
3655 },
3656 AbsoluteValue::U256(*value),
3657 )],
3658 StateUpdate::SlotMasked {
3659 address,
3660 slot,
3661 mask,
3662 value,
3663 } => vec![(
3664 EffectTarget::MaskedStorageSlot {
3665 address: *address,
3666 slot: *slot,
3667 mask: *mask,
3668 },
3669 AbsoluteValue::U256(*value),
3670 )],
3671 StateUpdate::Account { address, patch } | StateUpdate::AccountUpsert { address, patch } => {
3672 account_patch_writes(*address, patch)
3673 }
3674 StateUpdate::SlotDelta { .. }
3675 | StateUpdate::BalanceDelta { .. }
3676 | StateUpdate::Purge { .. } => Vec::new(),
3677 }
3678}
3679
3680fn account_patch_writes(
3681 address: Address,
3682 patch: &AccountPatch,
3683) -> Vec<(EffectTarget, AbsoluteValue)> {
3684 let mut writes = Vec::new();
3685 if let Some(balance) = patch.balance {
3686 writes.push((
3687 EffectTarget::AccountBalance { address },
3688 AbsoluteValue::U256(balance),
3689 ));
3690 }
3691 if let Some(nonce) = patch.nonce {
3692 writes.push((
3693 EffectTarget::AccountNonce { address },
3694 AbsoluteValue::U64(nonce),
3695 ));
3696 }
3697 if let Some(code) = &patch.code {
3698 writes.push((
3699 EffectTarget::AccountCode { address },
3700 AbsoluteValue::Bytes(code.clone()),
3701 ));
3702 }
3703 writes
3704}
3705
3706fn input_ref<N: Network>(input: &ReactiveInput<N>, ctx: &ReactiveContext) -> InputRef {
3707 match input {
3708 ReactiveInput::Log(log) => InputRef::Log {
3709 chain_id: ctx.chain_id,
3710 block_hash: log
3711 .block_hash
3712 .or(ctx.block.as_ref().map(|block| block.hash))
3713 .unwrap_or_default(),
3714 transaction_hash: log.transaction_hash.unwrap_or_default(),
3715 log_index: log.log_index.or(ctx.log_index).unwrap_or_default(),
3716 },
3717 ReactiveInput::PendingTxHash(hash) => InputRef::PendingTx {
3718 chain_id: ctx.chain_id,
3719 hash: *hash,
3720 },
3721 ReactiveInput::PendingTx(tx) => InputRef::PendingTx {
3722 chain_id: ctx.chain_id,
3723 hash: tx.tx_hash(),
3724 },
3725 ReactiveInput::BlockHeader(header) => InputRef::Block {
3726 chain_id: ctx.chain_id,
3727 hash: header.hash(),
3728 number: header.number(),
3729 },
3730 ReactiveInput::FullBlock(block) => {
3731 let header = block.header();
3732 InputRef::Block {
3733 chain_id: ctx.chain_id,
3734 hash: header.hash(),
3735 number: header.number(),
3736 }
3737 }
3738 }
3739}
3740
3741fn is_canonical_status(status: &ChainStatus) -> bool {
3742 matches!(
3743 status,
3744 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. }
3745 )
3746}
3747
3748pub struct EventDecoderHandler {
3750 id: HandlerId,
3751 decoder: Arc<dyn EventDecoder>,
3752 interest: LogInterest,
3753}
3754
3755impl EventDecoderHandler {
3756 pub fn new(id: HandlerId, decoder: Arc<dyn EventDecoder>, interest: LogInterest) -> Self {
3758 Self {
3759 id,
3760 decoder,
3761 interest,
3762 }
3763 }
3764}
3765
3766impl<N: Network> ReactiveHandler<N> for EventDecoderHandler {
3767 fn id(&self) -> HandlerId {
3768 self.id.clone()
3769 }
3770
3771 fn interests(&self) -> Vec<ReactiveInterest<N>> {
3772 vec![ReactiveInterest::Logs(self.interest.clone())]
3773 }
3774
3775 fn handle(
3776 &self,
3777 _ctx: &ReactiveContext,
3778 input: &ReactiveInput<N>,
3779 state: &dyn StateView,
3780 ) -> Result<HandlerOutcome, HandlerError> {
3781 let ReactiveInput::Log(log) = input else {
3782 return Ok(HandlerOutcome::empty(StateEffectQuality::NoStateEffect));
3783 };
3784
3785 Ok(HandlerOutcome {
3786 effects: self
3787 .decoder
3788 .decode(&log.inner, state)
3789 .into_iter()
3790 .map(ReactiveEffect::StateUpdate)
3791 .collect(),
3792 quality: StateEffectQuality::ExactFromInput,
3793 tags: Vec::new(),
3794 })
3795 }
3796}
3797
3798pub trait EventSubscriber<N: Network = Ethereum>: Send {
3800 fn register_interests(
3806 &mut self,
3807 interests: &[ReactiveInterest<N>],
3808 ) -> Result<(), SubscriberError>;
3809
3810 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N>;
3812}
3813
3814pub type SubscriberNextBatch<'a, N> = Pin<
3816 Box<dyn Future<Output = Result<Option<ReactiveInputBatch<N>>, SubscriberError>> + Send + 'a>,
3817>;
3818
3819#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
3821pub enum SubscriberMode {
3822 #[default]
3828 Auto,
3829 PubSub,
3831 Polling,
3833}
3834
3835#[derive(Clone, Debug, PartialEq, Eq)]
3837pub struct SubscriberConfig {
3838 pub hydrate_pending_transactions: bool,
3840 pub max_batch_size: usize,
3842 pub reconnect: SubscriberReconnectConfig,
3844}
3845
3846impl Default for SubscriberConfig {
3847 fn default() -> Self {
3848 Self {
3849 hydrate_pending_transactions: false,
3850 max_batch_size: 1024,
3851 reconnect: SubscriberReconnectConfig::default(),
3852 }
3853 }
3854}
3855
3856#[derive(Clone, Debug, PartialEq, Eq)]
3862pub struct SubscriberReconnectConfig {
3863 pub enabled: bool,
3865 pub initial_delay: Duration,
3867 pub retry_delay: Duration,
3870 pub max_delay: Duration,
3872 pub max_attempts: Option<usize>,
3874 pub dedupe_window: usize,
3877}
3878
3879impl Default for SubscriberReconnectConfig {
3880 fn default() -> Self {
3881 Self {
3882 enabled: true,
3883 initial_delay: Duration::ZERO,
3884 retry_delay: Duration::from_millis(250),
3885 max_delay: Duration::from_secs(30),
3886 max_attempts: Some(3),
3887 dedupe_window: 4096,
3888 }
3889 }
3890}
3891
3892#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3902pub struct SubscriberBackfill {
3903 from_block: u64,
3904 to_block: Option<u64>,
3905}
3906
3907impl SubscriberBackfill {
3908 pub fn range(from_block: u64, to_block: u64) -> Self {
3910 Self {
3911 from_block,
3912 to_block: Some(to_block),
3913 }
3914 }
3915
3916 pub fn from_block(from_block: u64) -> Self {
3918 Self {
3919 from_block,
3920 to_block: None,
3921 }
3922 }
3923
3924 pub fn start_block(&self) -> u64 {
3926 self.from_block
3927 }
3928
3929 pub fn end_block(&self) -> Option<u64> {
3931 self.to_block
3932 }
3933}
3934
3935pub trait InterestOwnerSubscriber<N: Network = Ethereum>: EventSubscriber<N> {
3947 fn add_interest_owner(
3949 &mut self,
3950 owner: HandlerId,
3951 interests: &[ReactiveInterest<N>],
3952 ) -> Result<(), SubscriberError>;
3953
3954 fn add_interest_owner_with_backfill(
3956 &mut self,
3957 owner: HandlerId,
3958 interests: &[ReactiveInterest<N>],
3959 backfill: SubscriberBackfill,
3960 ) -> Result<(), SubscriberError>;
3961
3962 fn remove_interest_owner(&mut self, owner: &HandlerId) -> Option<Vec<ReactiveInterest<N>>>;
3964
3965 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]>;
3967}
3968
3969pub struct ReactiveEngine<S, N: Network = Ethereum> {
4008 runtime: ReactiveRuntime<N>,
4009 subscriber: S,
4010}
4011
4012impl<S, N> ReactiveEngine<S, N>
4013where
4014 N: Network,
4015 S: EventSubscriber<N>,
4016{
4017 pub fn new(runtime: ReactiveRuntime<N>, subscriber: S) -> Self {
4019 Self {
4020 runtime,
4021 subscriber,
4022 }
4023 }
4024
4025 pub fn into_parts(self) -> (ReactiveRuntime<N>, S) {
4027 (self.runtime, self.subscriber)
4028 }
4029
4030 pub fn runtime(&self) -> &ReactiveRuntime<N> {
4032 &self.runtime
4033 }
4034
4035 pub fn runtime_mut(&mut self) -> &mut ReactiveRuntime<N> {
4037 &mut self.runtime
4038 }
4039
4040 pub fn subscriber(&self) -> &S {
4042 &self.subscriber
4043 }
4044
4045 pub fn subscriber_mut(&mut self) -> &mut S {
4047 &mut self.subscriber
4048 }
4049
4050 pub fn next_batch(&mut self) -> SubscriberNextBatch<'_, N> {
4052 self.subscriber.next_batch()
4053 }
4054
4055 pub fn ingest_batch(
4058 &mut self,
4059 cache: &mut EvmCache,
4060 batch: ReactiveInputBatch<N>,
4061 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4062 self.runtime.ingest_batch(cache, batch)
4063 }
4064
4065 pub fn ingest_batch_with_resync(
4069 &mut self,
4070 cache: &mut EvmCache,
4071 batch: ReactiveInputBatch<N>,
4072 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4073 self.runtime.ingest_batch_with_resync(cache, batch)
4074 }
4075
4076 pub async fn next_ingest(
4079 &mut self,
4080 cache: &mut EvmCache,
4081 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
4082 let Some(batch) = self.subscriber.next_batch().await? else {
4083 return Ok(None);
4084 };
4085 Ok(Some(self.runtime.ingest_batch(cache, batch)?))
4086 }
4087
4088 pub async fn next_ingest_with_resync(
4092 &mut self,
4093 cache: &mut EvmCache,
4094 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
4095 let Some(batch) = self.subscriber.next_batch().await? else {
4096 return Ok(None);
4097 };
4098 Ok(Some(self.runtime.ingest_batch_with_resync(cache, batch)?))
4099 }
4100}
4101
4102impl<S, N> ReactiveEngine<S, N>
4103where
4104 N: Network,
4105 S: InterestOwnerSubscriber<N>,
4106{
4107 pub fn register_handler(
4125 &mut self,
4126 handler: Arc<dyn ReactiveHandler<N>>,
4127 ) -> Result<(), ReactiveEngineRegisterError> {
4128 let backfill = self
4129 .runtime
4130 .last_canonical_block()
4131 .map(|block| SubscriberBackfill::from_block(block.number));
4132 self.register_handler_inner(handler, backfill)
4133 }
4134
4135 pub fn register_handler_with_backfill(
4141 &mut self,
4142 handler: Arc<dyn ReactiveHandler<N>>,
4143 backfill: SubscriberBackfill,
4144 ) -> Result<(), ReactiveEngineRegisterError> {
4145 self.register_handler_inner(handler, Some(backfill))
4146 }
4147
4148 pub fn register_handler_live_only(
4154 &mut self,
4155 handler: Arc<dyn ReactiveHandler<N>>,
4156 ) -> Result<(), ReactiveEngineRegisterError> {
4157 self.register_handler_inner(handler, None)
4158 }
4159
4160 fn register_handler_inner(
4161 &mut self,
4162 handler: Arc<dyn ReactiveHandler<N>>,
4163 backfill: Option<SubscriberBackfill>,
4164 ) -> Result<(), ReactiveEngineRegisterError> {
4165 let id = handler.id();
4166 self.runtime.register_handler(handler)?;
4167 let interests = self
4168 .runtime
4169 .handler_interests(&id)
4170 .expect("handler was just registered")
4171 .to_vec();
4172
4173 let subscribed = match backfill {
4174 Some(backfill) => {
4175 self.subscriber
4176 .add_interest_owner_with_backfill(id.clone(), &interests, backfill)
4177 }
4178 None => self.subscriber.add_interest_owner(id.clone(), &interests),
4179 };
4180 if let Err(error) = subscribed {
4181 self.runtime.unregister_handler(&id);
4182 return Err(error.into());
4183 }
4184
4185 Ok(())
4186 }
4187
4188 pub fn sync_handler_interests(&mut self) -> Result<(), SubscriberError> {
4204 for id in self.runtime.handler_ids() {
4205 let interests = self
4206 .runtime
4207 .handler_interests(&id)
4208 .map(<[ReactiveInterest<N>]>::to_vec)
4209 .unwrap_or_default();
4210 self.subscriber.add_interest_owner(id, &interests)?;
4211 }
4212 Ok(())
4213 }
4214
4215 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
4239 self.subscriber.remove_interest_owner(id);
4240 self.runtime.unregister_handler(id)
4241 }
4242}
4243
4244pub struct AlloySubscriber<P, N: Network = Ethereum> {
4256 provider: P,
4257 mode: SubscriberMode,
4258 config: SubscriberConfig,
4259 base_interests: Vec<ReactiveInterest<N>>,
4260 owned_interests: Vec<OwnedSubscriberInterests<N>>,
4261 interests: Vec<ReactiveInterest<N>>,
4262 log_source_ids: HashMap<Filter, usize>,
4267 next_log_source_id: usize,
4268 pending_backfills: VecDeque<QueuedSubscriberBackfill>,
4269 sources_dirty: bool,
4272 state: AlloySubscriberState<N>,
4273 pending_records: VecDeque<ReactiveInputRecord<N>>,
4274 last_seen_log_blocks: HashMap<usize, u64>,
4275 recent_input_refs: VecDeque<InputRef>,
4276 recent_input_ref_set: HashSet<InputRef>,
4277 _network: PhantomData<N>,
4278}
4279
4280struct OwnedSubscriberInterests<N: Network = Ethereum> {
4281 owner: HandlerId,
4282 interests: Vec<ReactiveInterest<N>>,
4283}
4284
4285struct QueuedSubscriberBackfill {
4286 owner: HandlerId,
4287 filter: Filter,
4288 backfill: SubscriberBackfill,
4289}
4290
4291#[cfg(feature = "reactive-ws")]
4297fn ensure_ring_crypto_provider() {
4298 use std::sync::Once;
4299 static INSTALL: Once = Once::new();
4300 INSTALL.call_once(|| {
4301 let _ = rustls::crypto::ring::default_provider().install_default();
4302 });
4303}
4304
4305impl<P, N: Network> AlloySubscriber<P, N> {
4306 pub fn new(provider: P, mode: SubscriberMode, config: SubscriberConfig) -> Self {
4308 #[cfg(feature = "reactive-ws")]
4309 ensure_ring_crypto_provider();
4310 Self {
4311 provider,
4312 mode,
4313 config,
4314 base_interests: Vec::new(),
4315 owned_interests: Vec::new(),
4316 interests: Vec::new(),
4317 log_source_ids: HashMap::new(),
4318 next_log_source_id: 0,
4319 pending_backfills: VecDeque::new(),
4320 sources_dirty: true,
4321 state: AlloySubscriberState::Uninitialized,
4322 pending_records: VecDeque::new(),
4323 last_seen_log_blocks: HashMap::new(),
4324 recent_input_refs: VecDeque::new(),
4325 recent_input_ref_set: HashSet::new(),
4326 _network: PhantomData,
4327 }
4328 }
4329
4330 pub fn provider(&self) -> &P {
4332 &self.provider
4333 }
4334
4335 pub fn mode(&self) -> SubscriberMode {
4337 self.mode
4338 }
4339
4340 pub fn config(&self) -> &SubscriberConfig {
4342 &self.config
4343 }
4344
4345 pub fn registered_interests(&self) -> &[ReactiveInterest<N>] {
4347 &self.interests
4348 }
4349
4350 pub fn add_interest_owner(
4367 &mut self,
4368 owner: HandlerId,
4369 interests: &[ReactiveInterest<N>],
4370 ) -> Result<(), SubscriberError> {
4371 self.set_interest_owner(owner, interests, None)
4372 }
4373
4374 pub fn add_interest_owner_with_backfill(
4387 &mut self,
4388 owner: HandlerId,
4389 interests: &[ReactiveInterest<N>],
4390 backfill: SubscriberBackfill,
4391 ) -> Result<(), SubscriberError> {
4392 self.set_interest_owner(owner, interests, Some(backfill))
4393 }
4394
4395 pub fn remove_interest_owner(&mut self, owner: &HandlerId) -> Option<Vec<ReactiveInterest<N>>> {
4404 let index = self
4405 .owned_interests
4406 .iter()
4407 .position(|entry| &entry.owner == owner)?;
4408 let removed = self.owned_interests.remove(index).interests;
4409 self.pending_backfills
4410 .retain(|backfill| &backfill.owner != owner);
4411 self.rebuild_registered_interests();
4412 self.retire_unreferenced_filters();
4413 self.sources_dirty = true;
4414 Some(removed)
4415 }
4416
4417 pub fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
4419 self.owned_interests
4420 .iter()
4421 .find(|entry| &entry.owner == owner)
4422 .map(|entry| entry.interests.as_slice())
4423 }
4424
4425 fn set_interest_owner(
4426 &mut self,
4427 owner: HandlerId,
4428 interests: &[ReactiveInterest<N>],
4429 backfill: Option<SubscriberBackfill>,
4430 ) -> Result<(), SubscriberError> {
4431 validate_subscriber_config(&self.config)?;
4432
4433 let mut next_owned = self.clone_owned_interests();
4434 match next_owned.iter_mut().find(|entry| entry.owner == owner) {
4435 Some(entry) => entry.interests = interests.to_vec(),
4436 None => next_owned.push(OwnedSubscriberInterests {
4437 owner: owner.clone(),
4438 interests: interests.to_vec(),
4439 }),
4440 }
4441 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
4442 validate_supported_interests(self.mode, &self.config, &next_registered)?;
4443
4444 let previous_filters: Vec<Filter> = self
4451 .owner_interests(&owner)
4452 .map(log_filters)
4453 .unwrap_or_default();
4454 let continuity_anchor: Option<u64> = previous_filters
4455 .iter()
4456 .filter_map(|filter| self.log_anchor(filter))
4457 .min();
4458
4459 self.owned_interests = next_owned;
4460 self.interests = next_registered;
4461 self.retire_unreferenced_filters();
4462 self.sources_dirty = true;
4463
4464 self.pending_backfills
4467 .retain(|queued| queued.owner != owner);
4468 for filter in log_filters(interests) {
4469 if let Some(backfill) = backfill {
4470 self.pending_backfills.push_back(QueuedSubscriberBackfill {
4471 owner: owner.clone(),
4472 filter: filter.clone(),
4473 backfill,
4474 });
4475 }
4476
4477 let unchanged = previous_filters.contains(&filter);
4482 let explicit_covers = backfill.is_some_and(|explicit| {
4483 explicit.end_block().is_none()
4484 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
4485 });
4486 if let Some(anchor) = continuity_anchor
4487 && !unchanged
4488 && !explicit_covers
4489 {
4490 self.pending_backfills.push_back(QueuedSubscriberBackfill {
4491 owner: owner.clone(),
4492 filter,
4493 backfill: SubscriberBackfill::from_block(anchor),
4494 });
4495 }
4496 }
4497 Ok(())
4498 }
4499
4500 fn clone_owned_interests(&self) -> Vec<OwnedSubscriberInterests<N>> {
4501 self.owned_interests
4502 .iter()
4503 .map(|entry| OwnedSubscriberInterests {
4504 owner: entry.owner.clone(),
4505 interests: entry.interests.clone(),
4506 })
4507 .collect()
4508 }
4509
4510 fn rebuild_registered_interests(&mut self) {
4511 self.interests = aggregate_interests(&self.base_interests, &self.owned_interests);
4512 }
4513
4514 fn log_anchor(&self, filter: &Filter) -> Option<u64> {
4517 let id = self.log_source_ids.get(filter)?;
4518 self.last_seen_log_blocks.get(id).copied()
4519 }
4520
4521 #[allow(clippy::mutable_key_type)]
4529 fn log_stream_filters(&self) -> Vec<Filter> {
4530 let mut filters = log_filters(&self.base_interests);
4531 for entry in &self.owned_interests {
4532 filters.extend(log_filters(&entry.interests));
4533 }
4534 let mut seen = HashSet::new();
4535 filters.retain(|filter| seen.insert(filter.clone()));
4536 filters
4537 }
4538
4539 #[allow(clippy::mutable_key_type)]
4546 fn retire_unreferenced_filters(&mut self) {
4547 let live: HashSet<Filter> = self.log_stream_filters().into_iter().collect();
4548 self.log_source_ids
4549 .retain(|filter, _| live.contains(filter));
4550 let live_ids: HashSet<usize> = self.log_source_ids.values().copied().collect();
4551 self.last_seen_log_blocks
4552 .retain(|id, _| live_ids.contains(id));
4553 }
4554
4555 fn drain_next_batch(&mut self) -> Option<ReactiveInputBatch<N>> {
4556 if self.pending_records.is_empty() {
4557 return None;
4558 }
4559
4560 let len = self.config.max_batch_size.min(self.pending_records.len());
4561 let records = self.pending_records.drain(..len).collect();
4562 Some(ReactiveInputBatch::new(records))
4563 }
4564
4565 fn reset_delivery_state(&mut self) {
4566 self.pending_records.clear();
4567 self.last_seen_log_blocks.clear();
4568 self.recent_input_refs.clear();
4569 self.recent_input_ref_set.clear();
4570 self.pending_backfills.clear();
4571 self.log_source_ids.clear();
4572 self.next_log_source_id = 0;
4573 self.sources_dirty = true;
4574 }
4575}
4576
4577impl<P, N> InterestOwnerSubscriber<N> for AlloySubscriber<P, N>
4578where
4579 P: Provider<N> + Send + Sync,
4580 N: Network + 'static,
4581 N::HeaderResponse: Send + 'static,
4582{
4583 fn add_interest_owner(
4584 &mut self,
4585 owner: HandlerId,
4586 interests: &[ReactiveInterest<N>],
4587 ) -> Result<(), SubscriberError> {
4588 AlloySubscriber::add_interest_owner(self, owner, interests)
4589 }
4590
4591 fn add_interest_owner_with_backfill(
4592 &mut self,
4593 owner: HandlerId,
4594 interests: &[ReactiveInterest<N>],
4595 backfill: SubscriberBackfill,
4596 ) -> Result<(), SubscriberError> {
4597 AlloySubscriber::add_interest_owner_with_backfill(self, owner, interests, backfill)
4598 }
4599
4600 fn remove_interest_owner(&mut self, owner: &HandlerId) -> Option<Vec<ReactiveInterest<N>>> {
4601 AlloySubscriber::remove_interest_owner(self, owner)
4602 }
4603
4604 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
4605 AlloySubscriber::owner_interests(self, owner)
4606 }
4607}
4608
4609enum AlloySubscriberState<N: Network> {
4610 Uninitialized,
4611 Active(SubscriberStreams<N>),
4612 Empty,
4613}
4614
4615struct SubscriberStreams<N: Network> {
4616 entries: Vec<SubscriberStreamEntry<N>>,
4617 next_index: usize,
4618}
4619
4620struct SubscriberStreamEntry<N: Network> {
4621 source: SubscriberStreamSource,
4622 stream: BoxStream<'static, SubscriberEvent<N>>,
4623}
4624
4625impl<N: Network> SubscriberStreams<N> {
4626 fn new() -> Self {
4627 Self {
4628 entries: Vec::new(),
4629 next_index: 0,
4630 }
4631 }
4632
4633 fn is_empty(&self) -> bool {
4634 self.entries.is_empty()
4635 }
4636
4637 fn push(
4638 &mut self,
4639 source: SubscriberStreamSource,
4640 stream: BoxStream<'static, SubscriberEvent<N>>,
4641 ) {
4642 self.entries.push(SubscriberStreamEntry { source, stream });
4643 }
4644
4645 #[cfg(all(test, feature = "reactive-ws"))]
4646 fn len(&self) -> usize {
4647 self.entries.len()
4648 }
4649
4650 fn contains_source(&self, source: &SubscriberStreamSource) -> bool {
4651 self.entries
4652 .iter()
4653 .any(|entry| entry.source.same_key(source))
4654 }
4655
4656 fn retain_sources(&mut self, sources: &[SubscriberStreamSource]) {
4657 self.entries
4658 .retain(|entry| sources.iter().any(|source| entry.source.same_key(source)));
4659 self.normalize_next_index();
4660 }
4661
4662 fn normalize_next_index(&mut self) {
4663 if self.entries.is_empty() {
4664 self.next_index = 0;
4665 } else if self.next_index >= self.entries.len() {
4666 self.next_index %= self.entries.len();
4667 }
4668 }
4669
4670 async fn next(&mut self) -> Option<SubscriberEvent<N>> {
4671 poll_fn(|cx| {
4672 self.normalize_next_index();
4673 if self.entries.is_empty() {
4674 return std::task::Poll::Ready(None);
4675 }
4676
4677 let mut index = self.next_index;
4678 let mut checked = 0usize;
4679 while checked < self.entries.len() {
4680 if index >= self.entries.len() {
4681 index = 0;
4682 }
4683 match self.entries[index].stream.as_mut().poll_next(cx) {
4684 std::task::Poll::Ready(Some(event)) => {
4685 if matches!(event, SubscriberEvent::StreamTerminated(_)) {
4686 self.entries.remove(index);
4687 self.next_index = if self.entries.is_empty() {
4688 0
4689 } else {
4690 index % self.entries.len()
4691 };
4692 } else {
4693 self.next_index = (index + 1) % self.entries.len();
4694 }
4695 return std::task::Poll::Ready(Some(event));
4696 }
4697 std::task::Poll::Ready(None) => {
4698 self.entries.remove(index);
4699 if self.entries.is_empty() {
4700 self.next_index = 0;
4701 return std::task::Poll::Ready(None);
4702 }
4703 }
4704 std::task::Poll::Pending => {
4705 checked += 1;
4706 index += 1;
4707 }
4708 }
4709 }
4710
4711 if self.entries.is_empty() {
4712 std::task::Poll::Ready(None)
4713 } else {
4714 self.next_index = index % self.entries.len();
4715 std::task::Poll::Pending
4716 }
4717 })
4718 .await
4719 }
4720}
4721
4722#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4723#[allow(dead_code)]
4724enum SubscriberTransport {
4725 PubSub,
4726 Polling,
4727}
4728
4729#[derive(Clone, Debug)]
4730enum SubscriberStreamSource {
4731 PubSubLog { id: usize, filter: Filter },
4732 PubSubPendingHashes,
4733 PubSubBlockHeaders,
4734 PollingLog { filter: Filter },
4735 PollingPendingHashes,
4736}
4737
4738impl SubscriberStreamSource {
4739 fn label(&self) -> &'static str {
4740 match self {
4741 Self::PubSubLog { .. } => "pubsub log",
4742 Self::PubSubPendingHashes => "pubsub pending transaction hash",
4743 Self::PubSubBlockHeaders => "pubsub block header",
4744 Self::PollingLog { .. } => "polling log",
4745 Self::PollingPendingHashes => "polling pending transaction hash",
4746 }
4747 }
4748
4749 fn is_pubsub(&self) -> bool {
4750 matches!(
4751 self,
4752 Self::PubSubLog { .. } | Self::PubSubPendingHashes | Self::PubSubBlockHeaders
4753 )
4754 }
4755
4756 fn same_key(&self, other: &Self) -> bool {
4757 match (self, other) {
4758 (Self::PubSubLog { filter: left, .. }, Self::PubSubLog { filter: right, .. })
4759 | (Self::PollingLog { filter: left }, Self::PollingLog { filter: right }) => {
4760 left == right
4761 }
4762 (Self::PubSubPendingHashes, Self::PubSubPendingHashes)
4763 | (Self::PubSubBlockHeaders, Self::PubSubBlockHeaders)
4764 | (Self::PollingPendingHashes, Self::PollingPendingHashes) => true,
4765 _ => false,
4766 }
4767 }
4768}
4769
4770#[allow(dead_code)]
4771enum SubscriberEvent<N: Network> {
4772 Log { source_id: usize, log: Log },
4773 BackfilledLogs { source_id: usize, logs: Vec<Log> },
4774 Logs(Vec<Log>),
4775 BlockHeader(N::HeaderResponse),
4776 PendingHash(B256),
4777 PendingHashes(Vec<B256>),
4778 StreamTerminated(SubscriberStreamSource),
4779}
4780
4781impl<P, N> EventSubscriber<N> for AlloySubscriber<P, N>
4782where
4783 P: Provider<N> + Send + Sync,
4784 N: Network + 'static,
4785 N::HeaderResponse: Send + 'static,
4786{
4787 fn register_interests(
4788 &mut self,
4789 interests: &[ReactiveInterest<N>],
4790 ) -> Result<(), SubscriberError> {
4791 validate_subscriber_config(&self.config)?;
4792 validate_supported_interests(self.mode, &self.config, interests)?;
4793
4794 self.base_interests = interests.to_vec();
4795 self.owned_interests.clear();
4796 self.rebuild_registered_interests();
4797 self.reset_delivery_state();
4798 self.state = AlloySubscriberState::Uninitialized;
4799 Ok(())
4800 }
4801
4802 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N> {
4803 Box::pin(async {
4804 if let Some(batch) = self.drain_next_batch() {
4805 return Ok(Some(batch));
4806 }
4807
4808 self.drain_pending_backfills().await?;
4809 if let Some(batch) = self.drain_next_batch() {
4810 return Ok(Some(batch));
4811 }
4812
4813 self.ensure_streams().await?;
4814 if let Some(batch) = self.drain_next_batch() {
4815 return Ok(Some(batch));
4816 }
4817
4818 if self.interests.is_empty() {
4819 return Ok(None);
4820 }
4821
4822 loop {
4823 let Some(event) = self.next_event().await? else {
4824 return Ok(None);
4825 };
4826
4827 self.enqueue_event(event);
4828 if let Some(batch) = self.drain_next_batch() {
4829 return Ok(Some(batch));
4830 }
4831 }
4832 })
4833 }
4834}
4835
4836impl<P, N> AlloySubscriber<P, N>
4837where
4838 P: Provider<N> + Send + Sync,
4839 N: Network + 'static,
4840 N::HeaderResponse: Send + 'static,
4841{
4842 async fn ensure_streams(&mut self) -> Result<(), SubscriberError> {
4857 if !self.sources_dirty {
4858 return Ok(());
4859 }
4860 if matches!(self.state, AlloySubscriberState::Uninitialized) && self.interests.is_empty() {
4864 return Ok(());
4865 }
4866
4867 let desired = self.stream_sources()?;
4868 let missing: Vec<SubscriberStreamSource> = match &self.state {
4869 AlloySubscriberState::Active(streams) => desired
4870 .iter()
4871 .filter(|source| !streams.contains_source(source))
4872 .cloned()
4873 .collect(),
4874 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => desired.clone(),
4875 };
4876
4877 let mut connected = Vec::new();
4878 for source in missing {
4879 let stream = self.connect_source_stream(source.clone()).await?;
4880 if let Some(event) = self.backfill_reconnected_source(&source).await? {
4885 self.enqueue_event(event);
4886 }
4887 connected.push((source, stream));
4888 }
4889
4890 match &mut self.state {
4891 AlloySubscriberState::Active(streams) => {
4892 streams.retain_sources(&desired);
4893 for (source, stream) in connected {
4894 streams.push(source, stream);
4895 }
4896 if streams.is_empty() {
4897 self.state = AlloySubscriberState::Empty;
4898 }
4899 }
4900 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
4901 let mut streams = SubscriberStreams::new();
4902 for (source, stream) in connected {
4903 streams.push(source, stream);
4904 }
4905 self.state = if streams.is_empty() {
4906 AlloySubscriberState::Empty
4907 } else {
4908 AlloySubscriberState::Active(streams)
4909 };
4910 }
4911 }
4912
4913 self.sources_dirty = false;
4914 Ok(())
4915 }
4916
4917 async fn drain_pending_backfills(&mut self) -> Result<(), SubscriberError> {
4929 while let Some(queued) = self.pending_backfills.front() {
4930 if self.owner_interests(&queued.owner).is_none() {
4932 self.pending_backfills.pop_front();
4933 continue;
4934 }
4935 let filter = queued.filter.clone();
4936 let backfill = queued.backfill;
4937
4938 let to_block = match backfill.end_block() {
4939 Some(to_block) => to_block,
4940 None => self
4941 .provider
4942 .get_block_number()
4943 .await
4944 .map_err(provider_error)?,
4945 };
4946 if to_block < backfill.start_block() {
4947 self.pending_backfills.pop_front();
4950 continue;
4951 }
4952
4953 let range = filter
4954 .clone()
4955 .from_block(backfill.start_block())
4956 .to_block(to_block);
4957 let logs = self
4958 .provider
4959 .get_logs(&range)
4960 .await
4961 .map_err(provider_error)?;
4962
4963 self.pending_backfills.pop_front();
4966 let source_id = self.log_source_id(&filter);
4967 self.enqueue_backfilled_logs(logs, Some(source_id));
4968 let anchor = self
4969 .last_seen_log_blocks
4970 .entry(source_id)
4971 .or_insert(to_block);
4972 *anchor = (*anchor).max(to_block);
4973
4974 if !self.pending_records.is_empty() {
4975 break;
4976 }
4977 }
4978 Ok(())
4979 }
4980
4981 fn stream_sources(&mut self) -> Result<Vec<SubscriberStreamSource>, SubscriberError> {
4982 match resolve_subscriber_transport(self.mode)? {
4983 SubscriberTransport::PubSub => Ok(self.pubsub_stream_sources()),
4984 SubscriberTransport::Polling => Ok(self.polling_stream_sources()),
4985 }
4986 }
4987
4988 fn pubsub_stream_sources(&mut self) -> Vec<SubscriberStreamSource> {
4989 let mut sources = Vec::new();
4990
4991 for filter in self.log_stream_filters() {
4992 let id = self.log_source_id(&filter);
4993 sources.push(SubscriberStreamSource::PubSubLog { id, filter });
4994 }
4995
4996 if needs_pending_hash_stream(&self.interests) {
4997 sources.push(SubscriberStreamSource::PubSubPendingHashes);
4998 }
4999
5000 if needs_header_block_stream(&self.interests) {
5001 sources.push(SubscriberStreamSource::PubSubBlockHeaders);
5002 }
5003
5004 sources
5005 }
5006
5007 fn polling_stream_sources(&self) -> Vec<SubscriberStreamSource> {
5008 let mut sources = Vec::new();
5009
5010 for filter in self.log_stream_filters() {
5011 sources.push(SubscriberStreamSource::PollingLog { filter });
5012 }
5013
5014 if needs_pending_hash_stream(&self.interests) {
5015 sources.push(SubscriberStreamSource::PollingPendingHashes);
5016 }
5017
5018 sources
5019 }
5020
5021 fn log_source_id(&mut self, filter: &Filter) -> usize {
5022 if let Some(id) = self.log_source_ids.get(filter) {
5023 return *id;
5024 }
5025
5026 let id = self.next_log_source_id;
5027 self.next_log_source_id = self.next_log_source_id.saturating_add(1);
5028 self.log_source_ids.insert(filter.clone(), id);
5029 id
5030 }
5031
5032 async fn connect_source_stream(
5033 &mut self,
5034 source: SubscriberStreamSource,
5035 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
5036 match source {
5037 SubscriberStreamSource::PubSubLog { id, filter } => {
5038 self.connect_pubsub_log_stream(id, filter).await
5039 }
5040 SubscriberStreamSource::PubSubPendingHashes => {
5041 self.connect_pubsub_pending_hash_stream().await
5042 }
5043 SubscriberStreamSource::PubSubBlockHeaders => {
5044 self.connect_pubsub_block_header_stream().await
5045 }
5046 SubscriberStreamSource::PollingLog { filter } => {
5047 self.connect_polling_log_stream(filter).await
5048 }
5049 SubscriberStreamSource::PollingPendingHashes => {
5050 self.connect_polling_pending_hash_stream().await
5051 }
5052 }
5053 }
5054
5055 async fn connect_pubsub_log_stream(
5056 &mut self,
5057 id: usize,
5058 filter: Filter,
5059 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
5060 #[cfg(feature = "reactive-ws")]
5061 {
5062 let source = SubscriberStreamSource::PubSubLog {
5063 id,
5064 filter: filter.clone(),
5065 };
5066 let stream = self
5067 .provider
5068 .subscribe_logs(&filter)
5069 .channel_size(self.config.max_batch_size.max(1))
5070 .await
5071 .map_err(provider_error)?
5072 .into_stream()
5073 .map(move |log| SubscriberEvent::Log { source_id: id, log });
5074 Ok(stream_with_termination(stream, source))
5075 }
5076
5077 #[cfg(not(feature = "reactive-ws"))]
5078 {
5079 let _ = (id, filter);
5080 Err(SubscriberError::Unsupported(
5081 "AlloySubscriber pubsub mode requires the reactive-ws feature",
5082 ))
5083 }
5084 }
5085
5086 async fn connect_pubsub_pending_hash_stream(
5087 &mut self,
5088 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
5089 #[cfg(feature = "reactive-ws")]
5090 {
5091 let stream = self
5092 .provider
5093 .subscribe_pending_transactions()
5094 .channel_size(self.config.max_batch_size.max(1))
5095 .await
5096 .map_err(provider_error)?
5097 .into_stream()
5098 .map(SubscriberEvent::PendingHash);
5099 Ok(stream_with_termination(
5100 stream,
5101 SubscriberStreamSource::PubSubPendingHashes,
5102 ))
5103 }
5104
5105 #[cfg(not(feature = "reactive-ws"))]
5106 {
5107 Err(SubscriberError::Unsupported(
5108 "AlloySubscriber pubsub mode requires the reactive-ws feature",
5109 ))
5110 }
5111 }
5112
5113 async fn connect_pubsub_block_header_stream(
5114 &mut self,
5115 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
5116 #[cfg(feature = "reactive-ws")]
5117 {
5118 let stream = self
5119 .provider
5120 .subscribe_blocks()
5121 .channel_size(self.config.max_batch_size.max(1))
5122 .await
5123 .map_err(provider_error)?
5124 .into_stream()
5125 .map(SubscriberEvent::BlockHeader);
5126 Ok(stream_with_termination(
5127 stream,
5128 SubscriberStreamSource::PubSubBlockHeaders,
5129 ))
5130 }
5131
5132 #[cfg(not(feature = "reactive-ws"))]
5133 {
5134 Err(SubscriberError::Unsupported(
5135 "AlloySubscriber pubsub mode requires the reactive-ws feature",
5136 ))
5137 }
5138 }
5139
5140 async fn connect_polling_log_stream(
5141 &mut self,
5142 filter: Filter,
5143 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
5144 #[cfg(feature = "reactive-polling")]
5145 {
5146 let source = SubscriberStreamSource::PollingLog {
5147 filter: filter.clone(),
5148 };
5149 let stream = self
5150 .provider
5151 .watch_logs(&filter)
5152 .await
5153 .map_err(provider_error)?
5154 .with_channel_size(self.config.max_batch_size.max(1))
5155 .into_stream()
5156 .map(SubscriberEvent::Logs);
5157 Ok(stream_with_termination(stream, source))
5158 }
5159
5160 #[cfg(not(feature = "reactive-polling"))]
5161 {
5162 let _ = filter;
5163 Err(SubscriberError::Unsupported(
5164 "AlloySubscriber polling mode requires the reactive-polling feature",
5165 ))
5166 }
5167 }
5168
5169 async fn connect_polling_pending_hash_stream(
5170 &mut self,
5171 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
5172 #[cfg(feature = "reactive-polling")]
5173 {
5174 let stream = self
5175 .provider
5176 .watch_pending_transactions()
5177 .await
5178 .map_err(provider_error)?
5179 .with_channel_size(self.config.max_batch_size.max(1))
5180 .into_stream()
5181 .map(SubscriberEvent::PendingHashes);
5182 Ok(stream_with_termination(
5183 stream,
5184 SubscriberStreamSource::PollingPendingHashes,
5185 ))
5186 }
5187
5188 #[cfg(not(feature = "reactive-polling"))]
5189 {
5190 Err(SubscriberError::Unsupported(
5191 "AlloySubscriber polling mode requires the reactive-polling feature",
5192 ))
5193 }
5194 }
5195
5196 async fn next_event(&mut self) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
5197 loop {
5198 let event = match &mut self.state {
5199 AlloySubscriberState::Active(streams) => streams.next().await,
5200 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
5201 return Ok(None);
5202 }
5203 };
5204
5205 let Some(event) = event else {
5206 return Err(SubscriberError::Provider(
5207 "Alloy subscriber streams terminated before the subscriber was stopped"
5208 .to_owned(),
5209 ));
5210 };
5211
5212 match event {
5213 SubscriberEvent::StreamTerminated(source) => {
5214 if let Some(backfill_event) = self.reconnect_source_stream(source).await? {
5215 return Ok(Some(backfill_event));
5216 }
5217 }
5218 event => return Ok(Some(event)),
5219 }
5220 }
5221 }
5222
5223 fn enqueue_event(&mut self, event: SubscriberEvent<N>) {
5224 match event {
5225 SubscriberEvent::Log { source_id, log } => {
5226 if log_matches_any_interest(&log, &self.interests) {
5227 let record = log_input_record(log, InputSource::Subscription);
5228 self.note_log_block(source_id, &record);
5229 self.enqueue_record(record);
5230 }
5231 }
5232 SubscriberEvent::BackfilledLogs { source_id, logs } => {
5233 self.enqueue_backfilled_logs(logs, Some(source_id));
5234 }
5235 SubscriberEvent::Logs(logs) => self.pending_records.extend(
5236 logs.into_iter()
5237 .filter(|log| log_matches_any_interest(log, &self.interests))
5238 .map(|log| log_input_record(log, InputSource::Poll)),
5239 ),
5240 SubscriberEvent::BlockHeader(header) => {
5241 if needs_header_block_stream(&self.interests) {
5242 let record = block_header_input_record::<N>(header);
5243 self.enqueue_record(record);
5244 }
5245 }
5246 SubscriberEvent::PendingHash(hash) => {
5247 let record = pending_hash_input_record::<N>(hash, InputSource::Subscription);
5248 self.enqueue_record(record);
5249 }
5250 SubscriberEvent::PendingHashes(hashes) => self.pending_records.extend(
5251 hashes
5252 .into_iter()
5253 .map(|hash| pending_hash_input_record::<N>(hash, InputSource::Poll)),
5254 ),
5255 SubscriberEvent::StreamTerminated(_) => {}
5256 }
5257 }
5258
5259 fn enqueue_backfilled_logs(&mut self, logs: Vec<Log>, source_id: Option<usize>) {
5260 for log in logs {
5261 if log_matches_any_interest(&log, &self.interests) {
5262 let record = log_input_record(log, InputSource::Backfill);
5263 if let Some(source_id) = source_id {
5264 self.note_log_block(source_id, &record);
5265 }
5266 self.enqueue_record(record);
5267 }
5268 }
5269 }
5270
5271 async fn reconnect_source_stream(
5272 &mut self,
5273 source: SubscriberStreamSource,
5274 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
5275 if !source.is_pubsub() {
5276 return Err(stream_terminated_error(&source));
5277 }
5278
5279 if !self.config.reconnect.enabled {
5280 return Err(SubscriberError::Provider(format!(
5281 "Alloy subscriber {} stream terminated and reconnect is disabled",
5282 source.label()
5283 )));
5284 }
5285
5286 let mut attempts = 0usize;
5287 let mut delay = self.config.reconnect.initial_delay;
5288 let mut retry_delay = self.config.reconnect.retry_delay;
5289
5290 loop {
5291 attempts = attempts.saturating_add(1);
5292 if !delay.is_zero() {
5293 tokio::time::sleep(delay).await;
5294 }
5295
5296 match self.reconnect_source_once(source.clone()).await {
5297 Ok(backfill_event) => return Ok(backfill_event),
5298 Err(error) if reconnect_attempts_exhausted(attempts, &self.config.reconnect) => {
5299 return Err(SubscriberError::Provider(format!(
5300 "Alloy subscriber {} stream terminated and reconnect failed after {attempts} attempt(s): {error}",
5301 source.label()
5302 )));
5303 }
5304 Err(error) => {
5305 tracing::warn!(
5306 stream = source.label(),
5307 attempts,
5308 error = %error,
5309 "Alloy subscriber reconnect attempt failed"
5310 );
5311 delay = retry_delay;
5312 retry_delay =
5313 next_reconnect_delay(retry_delay, self.config.reconnect.max_delay);
5314 }
5315 }
5316 }
5317 }
5318
5319 async fn reconnect_source_once(
5320 &mut self,
5321 source: SubscriberStreamSource,
5322 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
5323 let stream = self.connect_source_stream(source.clone()).await?;
5324 let backfill_event = self.backfill_reconnected_source(&source).await?;
5325
5326 match &mut self.state {
5327 AlloySubscriberState::Active(streams) => streams.push(source, stream),
5328 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
5329 return Err(SubscriberError::Provider(
5330 "Alloy subscriber state changed before reconnect completed".to_owned(),
5331 ));
5332 }
5333 }
5334
5335 Ok(backfill_event)
5336 }
5337
5338 async fn backfill_reconnected_source(
5339 &mut self,
5340 source: &SubscriberStreamSource,
5341 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
5342 let SubscriberStreamSource::PubSubLog { id, filter } = source else {
5343 return Ok(None);
5344 };
5345 let Some(from_block) = self.last_seen_log_blocks.get(id).copied() else {
5346 return Ok(None);
5347 };
5348
5349 let latest = self
5350 .provider
5351 .get_block_number()
5352 .await
5353 .map_err(provider_error)?;
5354 if latest < from_block {
5355 return Ok(None);
5356 }
5357
5358 let logs = self
5359 .provider
5360 .get_logs(&filter.clone().from_block(from_block).to_block(latest))
5361 .await
5362 .map_err(provider_error)?;
5363 Ok(Some(SubscriberEvent::BackfilledLogs {
5364 source_id: *id,
5365 logs,
5366 }))
5367 }
5368
5369 fn note_log_block(&mut self, source_id: usize, record: &ReactiveInputRecord<N>) {
5370 if let Some(block) = record.context.block.as_ref() {
5371 self.last_seen_log_blocks.insert(source_id, block.number);
5372 }
5373 }
5374
5375 fn enqueue_record(&mut self, record: ReactiveInputRecord<N>) {
5376 if self.should_skip_recent_duplicate(&record) {
5377 return;
5378 }
5379 self.remember_record(&record);
5380 self.pending_records.push_back(record);
5381 }
5382
5383 fn should_skip_recent_duplicate(&self, record: &ReactiveInputRecord<N>) -> bool {
5384 if !should_dedupe_record(record) {
5385 return false;
5386 }
5387 self.recent_input_ref_set.contains(&record.input_ref())
5388 }
5389
5390 fn remember_record(&mut self, record: &ReactiveInputRecord<N>) {
5391 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
5392 return;
5393 }
5394
5395 let input_ref = record.input_ref();
5396 if !self.recent_input_ref_set.insert(input_ref) {
5397 return;
5398 }
5399 self.recent_input_refs.push_back(input_ref);
5400
5401 while self.recent_input_refs.len() > self.config.reconnect.dedupe_window {
5402 if let Some(evicted) = self.recent_input_refs.pop_front() {
5403 self.recent_input_ref_set.remove(&evicted);
5404 }
5405 }
5406 }
5407}
5408
5409#[cfg(any(feature = "reactive-ws", feature = "reactive-polling", test))]
5410fn stream_with_termination<N, S>(
5411 stream: S,
5412 source: SubscriberStreamSource,
5413) -> BoxStream<'static, SubscriberEvent<N>>
5414where
5415 N: Network + 'static,
5416 S: futures::Stream<Item = SubscriberEvent<N>> + Send + 'static,
5417{
5418 stream
5419 .chain(stream::once(async move {
5420 SubscriberEvent::StreamTerminated(source)
5421 }))
5422 .boxed()
5423}
5424
5425fn aggregate_interests<N: Network>(
5426 base: &[ReactiveInterest<N>],
5427 owned: &[OwnedSubscriberInterests<N>],
5428) -> Vec<ReactiveInterest<N>> {
5429 base.iter()
5430 .cloned()
5431 .chain(
5432 owned
5433 .iter()
5434 .flat_map(|entry| entry.interests.iter().cloned()),
5435 )
5436 .collect()
5437}
5438
5439fn stream_terminated_error(source: &SubscriberStreamSource) -> SubscriberError {
5440 SubscriberError::Provider(format!(
5441 "Alloy subscriber {} stream terminated before the subscriber was stopped",
5442 source.label()
5443 ))
5444}
5445
5446fn reconnect_attempts_exhausted(attempts: usize, config: &SubscriberReconnectConfig) -> bool {
5447 config
5448 .max_attempts
5449 .is_some_and(|max_attempts| attempts >= max_attempts)
5450}
5451
5452fn next_reconnect_delay(current: Duration, max: Duration) -> Duration {
5453 if current.is_zero() {
5454 return current;
5455 }
5456 current.checked_mul(2).unwrap_or(max).min(max)
5457}
5458
5459fn should_dedupe_record<N: Network>(record: &ReactiveInputRecord<N>) -> bool {
5460 match &record.input {
5461 ReactiveInput::Log(log) => {
5462 is_canonical_status(&record.context.chain_status) && !log.removed
5463 }
5464 ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_) => true,
5465 ReactiveInput::FullBlock(_) | ReactiveInput::PendingTx(_) => false,
5466 }
5467}
5468
5469#[cfg(test)]
5470mod subscriber_helper_tests {
5471 use super::*;
5472 use alloy_provider::ProviderBuilder;
5473 use alloy_transport::mock::Asserter;
5474
5475 fn rpc_log(removed: bool) -> Log {
5476 Log {
5477 inner: alloy_primitives::Log::new_unchecked(
5478 Address::repeat_byte(0x42),
5479 vec![B256::repeat_byte(0x01)],
5480 Bytes::new(),
5481 ),
5482 block_hash: Some(B256::repeat_byte(0x02)),
5483 block_number: Some(7),
5484 block_timestamp: Some(1_700_000_000),
5485 transaction_hash: Some(B256::repeat_byte(0x03)),
5486 transaction_index: Some(4),
5487 log_index: Some(5),
5488 removed,
5489 }
5490 }
5491
5492 #[tokio::test(flavor = "multi_thread")]
5493 async fn stream_with_termination_yields_terminal_source_marker() {
5494 let mut stream = stream_with_termination::<Ethereum, _>(
5495 stream::iter([SubscriberEvent::<Ethereum>::PendingHash(B256::repeat_byte(
5496 0xaa,
5497 ))]),
5498 SubscriberStreamSource::PubSubPendingHashes,
5499 );
5500
5501 assert!(matches!(
5502 stream.next().await,
5503 Some(SubscriberEvent::PendingHash(hash)) if hash == B256::repeat_byte(0xaa)
5504 ));
5505 assert!(matches!(
5506 stream.next().await,
5507 Some(SubscriberEvent::StreamTerminated(source)) if source.is_pubsub()
5508 ));
5509 assert!(stream.next().await.is_none());
5510 }
5511
5512 #[test]
5513 fn reconnect_delay_doubles_until_capped() {
5514 assert_eq!(
5515 next_reconnect_delay(Duration::from_millis(250), Duration::from_secs(1)),
5516 Duration::from_millis(500)
5517 );
5518 assert_eq!(
5519 next_reconnect_delay(Duration::from_millis(750), Duration::from_secs(1)),
5520 Duration::from_secs(1)
5521 );
5522 assert_eq!(
5523 next_reconnect_delay(Duration::ZERO, Duration::from_secs(1)),
5524 Duration::ZERO
5525 );
5526 }
5527
5528 #[test]
5529 fn canonical_logs_are_deduped_but_removed_logs_are_not() {
5530 let included = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
5531 let removed = log_input_record::<Ethereum>(rpc_log(true), InputSource::Subscription);
5532
5533 assert!(should_dedupe_record(&included));
5534 assert!(!should_dedupe_record(&removed));
5535 }
5536
5537 #[test]
5538 #[cfg(feature = "reactive-ws")]
5539 fn pubsub_sources_assign_stable_log_ids_before_shared_streams() {
5540 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
5541 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
5542 provider,
5543 SubscriberMode::PubSub,
5544 SubscriberConfig::default(),
5545 );
5546 subscriber
5547 .register_interests(&[
5548 ReactiveInterest::Logs(LogInterest {
5549 provider_filter: Filter::new().address(Address::repeat_byte(0x01)),
5550 local_matcher: None,
5551 route_key: None,
5552 }),
5553 ReactiveInterest::Logs(LogInterest {
5554 provider_filter: Filter::new().address(Address::repeat_byte(0x02)),
5555 local_matcher: None,
5556 route_key: None,
5557 }),
5558 ReactiveInterest::PendingTransactions(PendingTxInterest::default()),
5559 ])
5560 .expect("register base interests");
5561
5562 let sources = subscriber.stream_sources().expect("stream sources");
5566 assert_eq!(sources.len(), 2);
5567 assert!(matches!(
5568 &sources[0],
5569 SubscriberStreamSource::PubSubLog { id: 0, .. }
5570 ));
5571 assert!(matches!(
5572 sources[1],
5573 SubscriberStreamSource::PubSubPendingHashes
5574 ));
5575
5576 let again = subscriber.stream_sources().expect("stream sources again");
5578 assert!(again[0].same_key(&sources[0]));
5579 }
5580
5581 #[tokio::test(flavor = "multi_thread")]
5582 #[cfg(feature = "reactive-ws")]
5583 async fn pubsub_stream_termination_attempts_reconnect_before_error() {
5584 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
5585 let mut subscriber = AlloySubscriber::new(
5586 provider,
5587 SubscriberMode::PubSub,
5588 SubscriberConfig {
5589 reconnect: SubscriberReconnectConfig {
5590 initial_delay: Duration::ZERO,
5591 retry_delay: Duration::ZERO,
5592 max_delay: Duration::ZERO,
5593 max_attempts: Some(1),
5594 ..SubscriberReconnectConfig::default()
5595 },
5596 ..SubscriberConfig::default()
5597 },
5598 );
5599 subscriber.interests = vec![ReactiveInterest::PendingTransactions(
5600 PendingTxInterest::default(),
5601 )];
5602
5603 let mut streams = SubscriberStreams::new();
5604 let source = SubscriberStreamSource::PubSubPendingHashes;
5605 streams.push(
5606 source,
5607 stream::once(async {
5608 SubscriberEvent::<Ethereum>::StreamTerminated(
5609 SubscriberStreamSource::PubSubPendingHashes,
5610 )
5611 })
5612 .boxed(),
5613 );
5614 subscriber.state = AlloySubscriberState::Active(streams);
5615
5616 let result = subscriber.next_batch().await;
5617 assert!(
5618 matches!(result, Err(SubscriberError::Provider(ref message)) if message.contains("reconnect failed after 1 attempt")),
5619 "terminated pubsub streams should attempt reconnect before surfacing failure: {result:?}"
5620 );
5621 }
5622
5623 #[test]
5624 fn backfilled_logs_skip_recent_subscription_duplicates() {
5625 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
5626 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
5627 provider,
5628 SubscriberMode::PubSub,
5629 SubscriberConfig::default(),
5630 );
5631 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
5632 provider_filter: Filter::new()
5633 .address(Address::repeat_byte(0x42))
5634 .event_signature(B256::repeat_byte(0x01)),
5635 local_matcher: None,
5636 route_key: None,
5637 })];
5638
5639 let log = rpc_log(false);
5640 subscriber.enqueue_event(SubscriberEvent::Log {
5641 source_id: 0,
5642 log: log.clone(),
5643 });
5644 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
5645 source_id: 0,
5646 logs: vec![log],
5647 });
5648
5649 assert_eq!(subscriber.pending_records.len(), 1);
5650 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
5651 assert_eq!(
5652 subscriber.pending_records[0].context.source,
5653 InputSource::Subscription
5654 );
5655 }
5656
5657 #[test]
5658 fn backfilled_logs_surface_with_backfill_source() {
5659 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
5664 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
5665 provider,
5666 SubscriberMode::PubSub,
5667 SubscriberConfig::default(),
5668 );
5669 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
5670 provider_filter: Filter::new()
5671 .address(Address::repeat_byte(0x42))
5672 .event_signature(B256::repeat_byte(0x01)),
5673 local_matcher: None,
5674 route_key: None,
5675 })];
5676
5677 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
5678 source_id: 0,
5679 logs: vec![rpc_log(false)],
5680 });
5681
5682 assert_eq!(subscriber.pending_records.len(), 1);
5683 assert_eq!(
5684 subscriber.pending_records[0].context.source,
5685 InputSource::Backfill
5686 );
5687 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
5688 }
5689
5690 #[test]
5691 #[cfg(feature = "reactive-ws")]
5692 fn owner_removal_preserves_delivery_and_dedupe_state() {
5693 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
5694 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
5695 provider,
5696 SubscriberMode::PubSub,
5697 SubscriberConfig::default(),
5698 );
5699 subscriber
5700 .add_interest_owner(
5701 HandlerId::new("pool-a"),
5702 &[ReactiveInterest::Logs(LogInterest {
5703 provider_filter: Filter::new()
5704 .address(Address::repeat_byte(0x42))
5705 .event_signature(B256::repeat_byte(0x01)),
5706 local_matcher: None,
5707 route_key: None,
5708 })],
5709 )
5710 .expect("register pool-a owner");
5711 subscriber
5712 .add_interest_owner(
5713 HandlerId::new("pool-b"),
5714 &[ReactiveInterest::Logs(LogInterest {
5715 provider_filter: Filter::new()
5716 .address(Address::repeat_byte(0x24))
5717 .event_signature(B256::repeat_byte(0x02)),
5718 local_matcher: None,
5719 route_key: None,
5720 })],
5721 )
5722 .expect("register pool-b owner");
5723
5724 let _ = subscriber.stream_sources().expect("stream sources");
5727 subscriber.enqueue_event(SubscriberEvent::Log {
5728 source_id: 0,
5729 log: rpc_log(false),
5730 });
5731 assert_eq!(subscriber.pending_records.len(), 1);
5732 assert_eq!(subscriber.recent_input_refs.len(), 1);
5733 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
5734
5735 let removed = subscriber
5736 .remove_interest_owner(&HandlerId::new("pool-b"))
5737 .expect("pool-b should be removed");
5738
5739 assert_eq!(removed.len(), 1);
5740 assert_eq!(subscriber.pending_records.len(), 1);
5741 assert_eq!(subscriber.recent_input_refs.len(), 1);
5742 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
5743 assert!(
5744 subscriber
5745 .owner_interests(&HandlerId::new("pool-a"))
5746 .is_some()
5747 );
5748 assert!(
5749 subscriber
5750 .owner_interests(&HandlerId::new("pool-b"))
5751 .is_none()
5752 );
5753 assert_eq!(subscriber.registered_interests().len(), 1);
5754 }
5755
5756 #[test]
5757 #[cfg(feature = "reactive-ws")]
5758 fn owner_log_sources_do_not_merge_across_owners() {
5759 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
5760 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
5761 provider,
5762 SubscriberMode::PubSub,
5763 SubscriberConfig::default(),
5764 );
5765 subscriber
5766 .add_interest_owner(
5767 HandlerId::new("pool-a"),
5768 &[ReactiveInterest::Logs(LogInterest {
5769 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
5770 local_matcher: None,
5771 route_key: None,
5772 })],
5773 )
5774 .expect("register pool-a owner");
5775
5776 let initial_sources = subscriber.stream_sources().expect("initial sources");
5777 assert_eq!(initial_sources.len(), 1);
5778 let pool_a_source = initial_sources[0].clone();
5779 assert!(matches!(
5780 &pool_a_source,
5781 SubscriberStreamSource::PubSubLog { id: 0, .. }
5782 ));
5783
5784 subscriber
5785 .add_interest_owner(
5786 HandlerId::new("pool-b"),
5787 &[ReactiveInterest::Logs(LogInterest {
5788 provider_filter: Filter::new().address(Address::repeat_byte(0xb2)),
5789 local_matcher: None,
5790 route_key: None,
5791 })],
5792 )
5793 .expect("register pool-b owner");
5794
5795 let expanded_sources = subscriber.stream_sources().expect("expanded sources");
5796 assert_eq!(expanded_sources.len(), 2);
5797 assert!(
5798 expanded_sources
5799 .iter()
5800 .any(|source| source.same_key(&pool_a_source)),
5801 "adding pool-b should not rewrite pool-a's stream source"
5802 );
5803
5804 subscriber
5805 .remove_interest_owner(&HandlerId::new("pool-b"))
5806 .expect("pool-b should be removed");
5807 let trimmed_sources = subscriber.stream_sources().expect("trimmed sources");
5808 assert_eq!(trimmed_sources.len(), 1);
5809 assert!(trimmed_sources[0].same_key(&pool_a_source));
5810 }
5811
5812 #[tokio::test(flavor = "multi_thread")]
5813 #[cfg(feature = "reactive-ws")]
5814 async fn owner_backfill_seeds_reconnect_anchor_before_live_log() {
5815 let asserter = Asserter::new();
5816 asserter.push_success(&vec![rpc_log(false)]);
5817 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
5818 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
5819 provider,
5820 SubscriberMode::PubSub,
5821 SubscriberConfig::default(),
5822 );
5823 subscriber
5824 .add_interest_owner_with_backfill(
5825 HandlerId::new("pool-a"),
5826 &[ReactiveInterest::Logs(LogInterest {
5827 provider_filter: Filter::new()
5828 .address(Address::repeat_byte(0x42))
5829 .event_signature(B256::repeat_byte(0x01)),
5830 local_matcher: None,
5831 route_key: None,
5832 })],
5833 SubscriberBackfill::range(1, 7),
5834 )
5835 .expect("register pool-a with backfill");
5836
5837 subscriber
5838 .drain_pending_backfills()
5839 .await
5840 .expect("owner backfill should drain");
5841
5842 assert_eq!(subscriber.pending_records.len(), 1);
5843 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
5844 }
5845
5846 #[tokio::test(flavor = "multi_thread")]
5847 async fn subscriber_streams_poll_ready_sources_round_robin() {
5848 let first_hash = B256::repeat_byte(0x01);
5849 let second_hash = B256::repeat_byte(0x02);
5850 let mut streams = SubscriberStreams::new();
5851 streams.push(
5852 SubscriberStreamSource::PubSubPendingHashes,
5853 stream::iter([
5854 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
5855 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
5856 ])
5857 .boxed(),
5858 );
5859 streams.push(
5860 SubscriberStreamSource::PubSubBlockHeaders,
5861 stream::once(async move { SubscriberEvent::<Ethereum>::PendingHash(second_hash) })
5862 .boxed(),
5863 );
5864
5865 assert!(matches!(
5866 streams.next().await,
5867 Some(SubscriberEvent::PendingHash(hash)) if hash == first_hash
5868 ));
5869 assert!(matches!(
5870 streams.next().await,
5871 Some(SubscriberEvent::PendingHash(hash)) if hash == second_hash
5872 ));
5873 }
5874
5875 #[tokio::test(flavor = "multi_thread")]
5876 #[cfg(feature = "reactive-ws")]
5877 async fn owner_updates_ensure_streams_without_full_reset() {
5878 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
5879 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
5880 provider,
5881 SubscriberMode::PubSub,
5882 SubscriberConfig::default(),
5883 );
5884 subscriber
5885 .register_interests(&[ReactiveInterest::PendingTransactions(
5886 PendingTxInterest::default(),
5887 )])
5888 .expect("register base pending interest");
5889 subscriber
5890 .add_interest_owner(
5891 HandlerId::new("headers"),
5892 &[ReactiveInterest::Blocks(BlockInterest::default())],
5893 )
5894 .expect("register header owner");
5895
5896 let mut streams = SubscriberStreams::new();
5897 streams.push(
5898 SubscriberStreamSource::PubSubPendingHashes,
5899 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
5900 );
5901 streams.push(
5902 SubscriberStreamSource::PubSubBlockHeaders,
5903 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
5904 );
5905 subscriber.state = AlloySubscriberState::Active(streams);
5906
5907 subscriber
5908 .remove_interest_owner(&HandlerId::new("headers"))
5909 .expect("header owner should be removed");
5910 assert!(matches!(
5911 &subscriber.state,
5912 AlloySubscriberState::Active(streams) if streams.len() == 2
5913 ));
5914
5915 subscriber
5916 .ensure_streams()
5917 .await
5918 .expect("pure removal reconciliation should not touch provider");
5919
5920 assert!(matches!(
5921 &subscriber.state,
5922 AlloySubscriberState::Active(streams)
5923 if streams.len() == 1
5924 && streams.contains_source(&SubscriberStreamSource::PubSubPendingHashes)
5925 && !streams.contains_source(&SubscriberStreamSource::PubSubBlockHeaders)
5926 ));
5927
5928 subscriber
5929 .add_interest_owner(
5930 HandlerId::new("headers"),
5931 &[ReactiveInterest::Blocks(BlockInterest::default())],
5932 )
5933 .expect("re-add header owner");
5934 assert!(matches!(
5935 &subscriber.state,
5936 AlloySubscriberState::Active(streams) if streams.len() == 1
5937 ));
5938 }
5939
5940 #[cfg(feature = "reactive-ws")]
5942 fn log_interest_matching_rpc_log() -> ReactiveInterest<Ethereum> {
5943 ReactiveInterest::Logs(LogInterest {
5944 provider_filter: Filter::new()
5945 .address(Address::repeat_byte(0x42))
5946 .event_signature(B256::repeat_byte(0x01)),
5947 local_matcher: None,
5948 route_key: None,
5949 })
5950 }
5951
5952 #[cfg(feature = "reactive-ws")]
5953 fn log_interest_for(address: u8) -> ReactiveInterest<Ethereum> {
5954 ReactiveInterest::Logs(LogInterest {
5955 provider_filter: Filter::new().address(Address::repeat_byte(address)),
5956 local_matcher: None,
5957 route_key: None,
5958 })
5959 }
5960
5961 #[tokio::test(flavor = "multi_thread")]
5964 #[cfg(feature = "reactive-ws")]
5965 async fn drain_backfill_retains_queue_entry_on_provider_error() {
5966 let asserter = Asserter::new();
5967 asserter.push_failure_msg("rate limited");
5968 asserter.push_success(&vec![rpc_log(false)]);
5969 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
5970 let mut subscriber = AlloySubscriber::new(
5971 provider,
5972 SubscriberMode::PubSub,
5973 SubscriberConfig::default(),
5974 );
5975 subscriber
5976 .add_interest_owner_with_backfill(
5977 HandlerId::new("pool"),
5978 &[log_interest_matching_rpc_log()],
5979 SubscriberBackfill::range(1, 7),
5980 )
5981 .expect("register owner with backfill");
5982 assert_eq!(subscriber.pending_backfills.len(), 1);
5983
5984 let first = subscriber.drain_pending_backfills().await;
5985 assert!(first.is_err(), "provider failure should surface");
5986 assert_eq!(
5987 subscriber.pending_backfills.len(),
5988 1,
5989 "failed fetch must leave the backfill queued for retry"
5990 );
5991 assert!(subscriber.pending_records.is_empty());
5992
5993 subscriber
5994 .drain_pending_backfills()
5995 .await
5996 .expect("retry should succeed");
5997 assert!(subscriber.pending_backfills.is_empty());
5998 assert_eq!(subscriber.pending_records.len(), 1);
5999 }
6000
6001 #[tokio::test(flavor = "multi_thread")]
6004 #[cfg(feature = "reactive-ws")]
6005 async fn drain_backfill_seeds_anchor_on_empty_window() {
6006 let asserter = Asserter::new();
6007 asserter.push_success(&Vec::<Log>::new());
6008 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
6009 let mut subscriber = AlloySubscriber::new(
6010 provider,
6011 SubscriberMode::PubSub,
6012 SubscriberConfig::default(),
6013 );
6014 subscriber
6015 .add_interest_owner_with_backfill(
6016 HandlerId::new("pool"),
6017 &[log_interest_matching_rpc_log()],
6018 SubscriberBackfill::range(1, 42),
6019 )
6020 .expect("register owner with backfill");
6021
6022 subscriber
6023 .drain_pending_backfills()
6024 .await
6025 .expect("empty backfill should drain");
6026
6027 assert!(subscriber.pending_records.is_empty());
6028 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
6029 .pop()
6030 .unwrap();
6031 assert_eq!(
6032 subscriber.log_anchor(&filter),
6033 Some(42),
6034 "empty window must still seed the anchor at its upper bound"
6035 );
6036 }
6037
6038 #[tokio::test(flavor = "multi_thread")]
6041 #[cfg(feature = "reactive-ws")]
6042 async fn drain_backfill_open_ended_resolves_head_and_seeds_anchor() {
6043 let asserter = Asserter::new();
6044 asserter.push_success(&100u64); asserter.push_success(&Vec::<Log>::new()); let provider = ProviderBuilder::new().connect_mocked_client(asserter);
6047 let mut subscriber = AlloySubscriber::new(
6048 provider,
6049 SubscriberMode::PubSub,
6050 SubscriberConfig::default(),
6051 );
6052 subscriber
6053 .add_interest_owner_with_backfill(
6054 HandlerId::new("pool"),
6055 &[log_interest_matching_rpc_log()],
6056 SubscriberBackfill::from_block(10),
6057 )
6058 .expect("register owner with open-ended backfill");
6059
6060 subscriber
6061 .drain_pending_backfills()
6062 .await
6063 .expect("open-ended backfill should drain");
6064
6065 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
6066 .pop()
6067 .unwrap();
6068 assert_eq!(subscriber.log_anchor(&filter), Some(100));
6069 }
6070
6071 #[test]
6074 #[cfg(feature = "reactive-ws")]
6075 fn duplicate_filters_across_owners_map_to_single_source() {
6076 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
6077 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
6078 provider,
6079 SubscriberMode::PubSub,
6080 SubscriberConfig::default(),
6081 );
6082 subscriber
6083 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
6084 .expect("register pool-a");
6085 subscriber
6086 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xaa)])
6087 .expect("register pool-b with identical filter");
6088
6089 assert_eq!(
6090 subscriber.log_stream_filters().len(),
6091 1,
6092 "identical filters across owners must collapse to one"
6093 );
6094 let sources = subscriber.stream_sources().expect("stream sources");
6095 assert_eq!(sources.len(), 1);
6096 }
6097
6098 #[test]
6101 #[cfg(feature = "reactive-ws")]
6102 fn owner_removal_prunes_source_ids_and_anchors() {
6103 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
6104 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
6105 provider,
6106 SubscriberMode::PubSub,
6107 SubscriberConfig::default(),
6108 );
6109 subscriber
6110 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
6111 .expect("register pool-a");
6112 subscriber
6113 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xbb)])
6114 .expect("register pool-b");
6115
6116 let _ = subscriber.stream_sources().expect("stream sources");
6118 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
6119 let filter_b = log_filters(&[log_interest_for(0xbb)]).pop().unwrap();
6120 let id_a = subscriber.log_source_id(&filter_a);
6121 let id_b = subscriber.log_source_id(&filter_b);
6122 subscriber.last_seen_log_blocks.insert(id_a, 10);
6123 subscriber.last_seen_log_blocks.insert(id_b, 20);
6124 assert_eq!(subscriber.log_source_ids.len(), 2);
6125
6126 subscriber
6127 .remove_interest_owner(&HandlerId::new("pool-b"))
6128 .expect("remove pool-b");
6129
6130 assert_eq!(
6131 subscriber.log_source_ids.len(),
6132 1,
6133 "pool-b's filter id should be retired"
6134 );
6135 assert!(subscriber.log_source_ids.contains_key(&filter_a));
6136 assert_eq!(subscriber.last_seen_log_blocks.get(&id_a), Some(&10));
6137 assert_eq!(
6138 subscriber.last_seen_log_blocks.get(&id_b),
6139 None,
6140 "pool-b's anchor should be pruned"
6141 );
6142 }
6143
6144 #[test]
6149 #[cfg(feature = "reactive-ws")]
6150 fn owner_filter_growth_queues_continuity_backfill_from_prior_anchor() {
6151 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
6152 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
6153 provider,
6154 SubscriberMode::PubSub,
6155 SubscriberConfig::default(),
6156 );
6157 subscriber
6158 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
6159 .expect("register amm with pool A");
6160
6161 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
6164 let id_a = subscriber.log_source_id(&filter_a);
6165 subscriber.last_seen_log_blocks.insert(id_a, 50);
6166
6167 subscriber
6170 .add_interest_owner(
6171 HandlerId::new("amm"),
6172 &[log_interest_for(0xaa), log_interest_for(0xbb)],
6173 )
6174 .expect("grow amm to pools A+B");
6175
6176 assert_eq!(
6177 subscriber.pending_backfills.len(),
6178 1,
6179 "the changed merged filter should queue exactly one continuity backfill"
6180 );
6181 let queued = &subscriber.pending_backfills[0];
6182 assert_eq!(queued.owner, HandlerId::new("amm"));
6183 assert_eq!(queued.backfill.start_block(), 50);
6184 assert_eq!(
6185 queued.backfill.end_block(),
6186 None,
6187 "continuity backfill runs open-ended to the current head"
6188 );
6189 }
6190
6191 #[test]
6194 #[cfg(feature = "reactive-ws")]
6195 fn unchanged_owner_filter_does_not_queue_continuity_backfill() {
6196 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
6197 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
6198 provider,
6199 SubscriberMode::PubSub,
6200 SubscriberConfig::default(),
6201 );
6202 subscriber
6203 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
6204 .expect("register amm");
6205 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
6206 let id_a = subscriber.log_source_id(&filter_a);
6207 subscriber.last_seen_log_blocks.insert(id_a, 50);
6208
6209 subscriber
6210 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
6211 .expect("re-register identical interests");
6212
6213 assert!(
6214 subscriber.pending_backfills.is_empty(),
6215 "an unchanged filter shape must not queue continuity backfill"
6216 );
6217 }
6218
6219 #[test]
6223 #[cfg(feature = "reactive-ws")]
6224 fn explicit_open_ended_backfill_below_anchor_suppresses_continuity() {
6225 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
6226 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
6227 provider,
6228 SubscriberMode::PubSub,
6229 SubscriberConfig::default(),
6230 );
6231 subscriber
6232 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
6233 .expect("register amm");
6234 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
6235 let id_a = subscriber.log_source_id(&filter_a);
6236 subscriber.last_seen_log_blocks.insert(id_a, 50);
6237
6238 subscriber
6240 .add_interest_owner_with_backfill(
6241 HandlerId::new("amm"),
6242 &[log_interest_for(0xaa), log_interest_for(0xbb)],
6243 SubscriberBackfill::from_block(10),
6244 )
6245 .expect("grow amm with explicit deep backfill");
6246
6247 assert_eq!(
6248 subscriber.pending_backfills.len(),
6249 1,
6250 "only the explicit backfill should be queued; continuity is subsumed"
6251 );
6252 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 10);
6253 }
6254
6255 #[tokio::test(flavor = "multi_thread")]
6258 #[cfg(feature = "reactive-ws")]
6259 async fn ensure_streams_is_noop_when_not_dirty() {
6260 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
6261 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
6262 provider,
6263 SubscriberMode::PubSub,
6264 SubscriberConfig::default(),
6265 );
6266 subscriber
6268 .add_interest_owner(
6269 HandlerId::new("headers"),
6270 &[ReactiveInterest::Blocks(BlockInterest::default())],
6271 )
6272 .expect("register header owner");
6273 subscriber.state = AlloySubscriberState::Empty;
6275 subscriber.sources_dirty = false;
6276
6277 subscriber
6278 .ensure_streams()
6279 .await
6280 .expect("clean reconcile must be a no-op");
6281
6282 assert!(
6283 matches!(subscriber.state, AlloySubscriberState::Empty),
6284 "not-dirty ensure_streams must not connect new sources"
6285 );
6286 }
6287}
6288
6289fn resolve_subscriber_transport(
6290 mode: SubscriberMode,
6291) -> Result<SubscriberTransport, SubscriberError> {
6292 match mode {
6293 SubscriberMode::PubSub => {
6294 #[cfg(feature = "reactive-ws")]
6295 {
6296 Ok(SubscriberTransport::PubSub)
6297 }
6298 #[cfg(not(feature = "reactive-ws"))]
6299 {
6300 Err(SubscriberError::Unsupported(
6301 "AlloySubscriber pubsub mode requires the reactive-ws feature",
6302 ))
6303 }
6304 }
6305 SubscriberMode::Polling => {
6306 #[cfg(feature = "reactive-polling")]
6307 {
6308 Ok(SubscriberTransport::Polling)
6309 }
6310 #[cfg(not(feature = "reactive-polling"))]
6311 {
6312 Err(SubscriberError::Unsupported(
6313 "AlloySubscriber polling mode requires the reactive-polling feature",
6314 ))
6315 }
6316 }
6317 SubscriberMode::Auto => resolve_auto_subscriber_transport(),
6318 }
6319}
6320
6321fn resolve_auto_subscriber_transport() -> Result<SubscriberTransport, SubscriberError> {
6322 #[cfg(feature = "reactive-ws")]
6323 {
6324 Ok(SubscriberTransport::PubSub)
6325 }
6326
6327 #[cfg(all(not(feature = "reactive-ws"), feature = "reactive-polling"))]
6328 {
6329 Ok(SubscriberTransport::Polling)
6330 }
6331
6332 #[cfg(not(any(feature = "reactive-ws", feature = "reactive-polling")))]
6333 {
6334 Err(SubscriberError::Unsupported(
6335 "AlloySubscriber requires either reactive-ws or reactive-polling",
6336 ))
6337 }
6338}
6339
6340fn validate_subscriber_config(config: &SubscriberConfig) -> Result<(), SubscriberError> {
6341 if config.max_batch_size == 0 {
6342 return Err(SubscriberError::InvalidConfig(
6343 "SubscriberConfig::max_batch_size must be greater than zero",
6344 ));
6345 }
6346 if config.reconnect.enabled {
6347 if config.reconnect.retry_delay > config.reconnect.max_delay {
6348 return Err(SubscriberError::InvalidConfig(
6349 "SubscriberReconnectConfig::retry_delay must be less than or equal to max_delay",
6350 ));
6351 }
6352 if matches!(config.reconnect.max_attempts, Some(0)) {
6353 return Err(SubscriberError::InvalidConfig(
6354 "SubscriberReconnectConfig::max_attempts must be greater than zero when set",
6355 ));
6356 }
6357 }
6358 Ok(())
6359}
6360
6361fn validate_supported_interests<N: Network>(
6362 mode: SubscriberMode,
6363 config: &SubscriberConfig,
6364 interests: &[ReactiveInterest<N>],
6365) -> Result<(), SubscriberError> {
6366 let transport = resolve_subscriber_transport(mode)?;
6367
6368 for interest in interests {
6369 match interest {
6370 ReactiveInterest::Logs(_) => {}
6371 ReactiveInterest::PendingTransactions(interest)
6372 if !config.hydrate_pending_transactions && interest.matches_hash_only() => {}
6373 ReactiveInterest::PendingTransactions(_) => {
6374 return Err(SubscriberError::Unsupported(
6375 "AlloySubscriber currently supports pending transaction hash interests only (full pending-tx hydration is unimplemented)",
6376 ));
6377 }
6378 ReactiveInterest::Blocks(interest) => match (transport, interest.mode) {
6379 (SubscriberTransport::PubSub, BlockInterestMode::Header) => {}
6380 (_, BlockInterestMode::FullBlock) => {
6381 return Err(SubscriberError::Unsupported(
6382 "AlloySubscriber full block streams are not implemented in this transport slice",
6383 ));
6384 }
6385 (SubscriberTransport::Polling, BlockInterestMode::Header) => {
6386 return Err(SubscriberError::Unsupported(
6387 "AlloySubscriber polling block streams are not implemented in this transport slice",
6388 ));
6389 }
6390 },
6391 }
6392 }
6393
6394 Ok(())
6395}
6396
6397fn log_filters<N: Network>(interests: &[ReactiveInterest<N>]) -> Vec<Filter> {
6398 let mut filters = Vec::new();
6399 for interest in interests {
6400 if let ReactiveInterest::Logs(interest) = interest {
6401 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
6402 }
6403 }
6404 filters
6405}
6406
6407fn needs_header_block_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
6408 interests.iter().any(|interest| {
6409 matches!(
6410 interest,
6411 ReactiveInterest::Blocks(BlockInterest {
6412 mode: BlockInterestMode::Header,
6413 })
6414 )
6415 })
6416}
6417
6418fn needs_pending_hash_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
6419 interests.iter().any(|interest| {
6420 matches!(
6421 interest,
6422 ReactiveInterest::PendingTransactions(interest) if interest.matches_hash_only()
6423 )
6424 })
6425}
6426
6427fn log_matches_any_interest<N: Network>(log: &Log, interests: &[ReactiveInterest<N>]) -> bool {
6428 interests.iter().any(|interest| {
6429 matches!(
6430 interest,
6431 ReactiveInterest::Logs(interest) if interest.matches(log)
6432 )
6433 })
6434}
6435
6436fn log_input_record<N: Network>(log: Log, source: InputSource) -> ReactiveInputRecord<N> {
6437 let context = log_reactive_context(&log);
6438 ReactiveInputRecord::new(
6439 ReactiveInput::Log(log),
6440 ReactiveContext { source, ..context },
6441 )
6442}
6443
6444fn log_reactive_context(log: &Log) -> ReactiveContext {
6445 let block = match (log.block_hash, log.block_number) {
6446 (Some(hash), Some(number)) => Some(BlockRef {
6447 number,
6448 hash,
6449 parent_hash: None,
6450 timestamp: log.block_timestamp,
6451 }),
6452 _ => None,
6453 };
6454
6455 let chain_status = match (&block, log.removed) {
6456 (Some(block), true) => ChainStatus::Reorged {
6457 dropped_from: block.clone(),
6458 },
6459 (Some(block), false) => ChainStatus::Included {
6460 block: block.clone(),
6461 confirmations: 0,
6462 },
6463 (None, _) => ChainStatus::Pending,
6464 };
6465
6466 ReactiveContext {
6467 chain_id: None,
6468 source: InputSource::Poll,
6469 chain_status,
6470 block,
6471 transaction_index: log.transaction_index,
6472 log_index: log.log_index,
6473 }
6474}
6475
6476fn block_header_input_record<N>(header: N::HeaderResponse) -> ReactiveInputRecord<N>
6477where
6478 N: Network,
6479{
6480 let block = BlockRef {
6481 number: header.number(),
6482 hash: HeaderResponseTrait::hash(&header),
6483 parent_hash: Some(header.parent_hash()),
6484 timestamp: Some(header.timestamp()),
6485 };
6486 ReactiveInputRecord::new(
6487 ReactiveInput::BlockHeader(header),
6488 ReactiveContext {
6489 chain_id: None,
6490 source: InputSource::Subscription,
6491 chain_status: ChainStatus::Included {
6492 block: block.clone(),
6493 confirmations: 0,
6494 },
6495 block: Some(block),
6496 transaction_index: None,
6497 log_index: None,
6498 },
6499 )
6500}
6501
6502fn pending_hash_input_record<N: Network>(
6503 hash: B256,
6504 source: InputSource,
6505) -> ReactiveInputRecord<N> {
6506 ReactiveInputRecord::new(
6507 ReactiveInput::PendingTxHash(hash),
6508 ReactiveContext {
6509 chain_id: None,
6510 source,
6511 chain_status: ChainStatus::Pending,
6512 block: None,
6513 transaction_index: None,
6514 log_index: None,
6515 },
6516 )
6517}
6518
6519fn provider_error(error: impl fmt::Display) -> SubscriberError {
6520 SubscriberError::Provider(error.to_string())
6521}
6522
6523#[derive(Debug, thiserror::Error)]
6525pub enum SubscriberError {
6526 #[error("{0}")]
6528 InvalidConfig(&'static str),
6529 #[error("{0}")]
6531 Unsupported(&'static str),
6532 #[error("provider error: {0}")]
6534 Provider(String),
6535}