1use std::{
15 any::Any,
16 borrow::Cow,
17 collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
18 fmt,
19 future::Future,
20 hash::Hash,
21 marker::PhantomData,
22 num::NonZeroU64,
23 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, BlockNumberOrTag};
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::{
46 future::{Either, poll_fn, select, try_join_all},
47 stream::BoxStream,
48};
49
50use crate::{
51 cache::{AccountProof, BlockStateDiff, EvmCache},
52 errors::{BlockContextError, StorageFetchResult},
53 events::{EventDecoder, StateView},
54 freshness::FreshnessRegistry,
55 state_update::{AccountPatch, PurgeScope, StateDiff, StateUpdate},
56};
57
58#[derive(Clone, Debug, PartialEq, Eq)]
60pub enum ReactiveInput<N: Network = Ethereum> {
61 Log(Log),
63 BlockHeader(N::HeaderResponse),
65 FullBlock(N::BlockResponse),
67 PendingTxHash(B256),
69 PendingTx(N::TransactionResponse),
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct ReactiveContext {
76 pub chain_id: Option<u64>,
78 pub source: InputSource,
80 pub chain_status: ChainStatus,
82 pub block: Option<BlockRef>,
84 pub transaction_index: Option<u64>,
86 pub log_index: Option<u64>,
88}
89
90#[derive(Clone, Debug, PartialEq, Eq, Hash)]
92pub struct BlockRef {
93 pub number: u64,
95 pub hash: B256,
97 pub parent_hash: Option<B256>,
99 pub timestamp: Option<u64>,
101}
102
103#[derive(Clone, Debug, PartialEq, Eq)]
105pub enum ChainStatus {
106 Pending,
108 Included {
110 block: BlockRef,
112 confirmations: u64,
114 },
115 Safe {
117 block: BlockRef,
119 },
120 Finalized {
122 block: BlockRef,
124 },
125 Reorged {
127 dropped_from: BlockRef,
129 },
130}
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
134pub enum InputSource {
135 Batch,
137 Subscription,
139 Poll,
141 Backfill,
143 Synthetic,
145}
146
147#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
149pub enum InputRef {
150 Log {
152 chain_id: Option<u64>,
154 block_hash: B256,
156 transaction_hash: B256,
158 log_index: u64,
160 },
161 PendingTx {
163 chain_id: Option<u64>,
165 hash: B256,
167 },
168 Block {
170 chain_id: Option<u64>,
172 hash: B256,
174 number: u64,
176 },
177}
178
179#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
181pub enum StateEffectQuality {
182 ExactFromInput,
184 AppliedWithPendingResync,
186 ResyncedAuthoritatively,
188 RequiresRepair,
190 NoStateEffect,
192}
193
194#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
196pub struct HandlerId(String);
197
198impl HandlerId {
199 pub fn new(id: impl Into<String>) -> Self {
201 Self(id.into())
202 }
203
204 pub fn as_str(&self) -> &str {
206 &self.0
207 }
208}
209
210impl fmt::Display for HandlerId {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 self.0.fmt(f)
213 }
214}
215
216#[derive(Clone, Debug, PartialEq, Eq, Hash)]
218pub struct ReportTag {
219 pub key: String,
221 pub value: String,
223}
224
225impl ReportTag {
226 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
228 Self {
229 key: key.into(),
230 value: value.into(),
231 }
232 }
233}
234
235#[derive(Clone)]
237pub struct HookSignal {
238 pub namespace: Cow<'static, str>,
240 pub kind: Cow<'static, str>,
242 pub labels: Vec<ReportTag>,
244 pub payload: Option<Arc<dyn Any + Send + Sync>>,
246}
247
248impl fmt::Debug for HookSignal {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 f.debug_struct("HookSignal")
251 .field("namespace", &self.namespace)
252 .field("kind", &self.kind)
253 .field("labels", &self.labels)
254 .field("payload", &self.payload.as_ref().map(|_| "<payload>"))
255 .finish()
256 }
257}
258
259#[derive(Clone, Debug)]
261pub enum ReactiveEffect {
262 StateUpdate(StateUpdate),
264 Resync(ResyncRequest),
266 Invalidate(InvalidationRequest),
268 Hook(HookSignal),
270 Speculative(SpeculativeRequest),
272}
273
274#[derive(Clone, Debug)]
276pub struct HandlerOutcome {
277 pub effects: Vec<ReactiveEffect>,
279 pub quality: StateEffectQuality,
281 pub tags: Vec<ReportTag>,
283}
284
285impl HandlerOutcome {
286 pub fn empty(quality: StateEffectQuality) -> Self {
288 Self {
289 effects: Vec::new(),
290 quality,
291 tags: Vec::new(),
292 }
293 }
294}
295
296#[derive(Clone, Debug)]
298pub struct ReactiveInputRecord<N: Network = Ethereum> {
299 pub input: ReactiveInput<N>,
301 pub context: ReactiveContext,
303}
304
305impl<N: Network> ReactiveInputRecord<N> {
306 pub fn new(input: ReactiveInput<N>, context: ReactiveContext) -> Self {
308 Self { input, context }
309 }
310
311 pub fn input_ref(&self) -> InputRef {
313 input_ref(&self.input, &self.context)
314 }
315}
316
317#[derive(Clone, Debug)]
319pub struct ReactiveInputBatch<N: Network = Ethereum> {
320 records: Vec<ReactiveInputRecord<N>>,
321}
322
323impl<N: Network> ReactiveInputBatch<N> {
324 pub fn new(records: Vec<ReactiveInputRecord<N>>) -> Self {
326 Self { records }
327 }
328
329 pub fn records(&self) -> &[ReactiveInputRecord<N>] {
331 &self.records
332 }
333
334 pub fn into_records(self) -> Vec<ReactiveInputRecord<N>> {
336 self.records
337 }
338}
339
340pub trait ReactiveHandler<N: Network = Ethereum>: Send + Sync {
342 fn id(&self) -> HandlerId;
344
345 fn interests(&self) -> Vec<ReactiveInterest<N>>;
347
348 fn log_route_index(&self) -> Option<LogRouteIndex> {
355 None
356 }
357
358 fn handle(
360 &self,
361 ctx: &ReactiveContext,
362 input: &ReactiveInput<N>,
363 state: &dyn StateView,
364 ) -> Result<HandlerOutcome, HandlerError>;
365}
366
367pub trait ReactiveHook<N: Network = Ethereum>: Send + Sync {
369 fn on_report(&self, report: Arc<ReactiveReport<N>>);
371}
372
373#[allow(clippy::large_enum_variant)]
375#[derive(Clone)]
376pub enum ReactiveInterest<N: Network = Ethereum> {
377 Logs(LogInterest),
379 Blocks(BlockInterest),
381 PendingTransactions(PendingTxInterest<N>),
383}
384
385impl<N: Network> fmt::Debug for ReactiveInterest<N> {
386 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387 match self {
388 Self::Logs(interest) => f.debug_tuple("Logs").field(interest).finish(),
389 Self::Blocks(interest) => f.debug_tuple("Blocks").field(interest).finish(),
390 Self::PendingTransactions(interest) => f
391 .debug_tuple("PendingTransactions")
392 .field(interest)
393 .finish(),
394 }
395 }
396}
397
398#[derive(Clone)]
400pub struct LogInterest {
401 pub provider_filter: Filter,
403 pub local_matcher: Option<Arc<dyn LogMatcher>>,
405 pub route_key: Option<RouteKeySpec>,
407}
408
409impl LogInterest {
410 pub fn matches(&self, log: &Log) -> bool {
412 self.provider_filter.rpc_matches(log)
413 && self
414 .local_matcher
415 .as_ref()
416 .is_none_or(|matcher| matcher.matches(log))
417 }
418
419 pub fn route_key(&self, log: &Log) -> Option<RouteKey> {
421 self.route_key.as_ref().and_then(|spec| spec.extract(log))
422 }
423}
424
425impl fmt::Debug for LogInterest {
426 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427 f.debug_struct("LogInterest")
428 .field("provider_filter", &self.provider_filter)
429 .field(
430 "local_matcher",
431 &self.local_matcher.as_ref().map(|_| "<matcher>"),
432 )
433 .field("route_key", &self.route_key)
434 .finish()
435 }
436}
437
438pub trait LogMatcher: Send + Sync {
440 fn matches(&self, log: &Log) -> bool;
442}
443
444#[derive(Clone)]
446pub enum RouteKeySpec {
447 EmitterAddress,
449 Topic {
451 index: usize,
453 },
454 DataSlice {
456 offset: usize,
458 len: usize,
460 },
461 Custom(Arc<dyn RouteKeyExtractor>),
463}
464
465impl RouteKeySpec {
466 pub fn extract(&self, log: &Log) -> Option<RouteKey> {
468 match self {
469 Self::EmitterAddress => Some(RouteKey::Address(log.address())),
470 Self::Topic { index } => log.topics().get(*index).copied().map(RouteKey::Bytes32),
471 Self::DataSlice { offset, len } => {
472 let data = log.inner.data.data.as_ref();
473 let end = offset.checked_add(*len)?;
474 data.get(*offset..end)
475 .map(|bytes| RouteKey::Bytes(bytes.to_vec()))
476 }
477 Self::Custom(extractor) => extractor.extract(log),
478 }
479 }
480}
481
482impl fmt::Debug for RouteKeySpec {
483 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484 match self {
485 Self::EmitterAddress => f.write_str("EmitterAddress"),
486 Self::Topic { index } => f.debug_struct("Topic").field("index", index).finish(),
487 Self::DataSlice { offset, len } => f
488 .debug_struct("DataSlice")
489 .field("offset", offset)
490 .field("len", len)
491 .finish(),
492 Self::Custom(_) => f.write_str("Custom(<extractor>)"),
493 }
494 }
495}
496
497pub trait RouteKeyExtractor: Send + Sync {
499 fn extract(&self, log: &Log) -> Option<RouteKey>;
501}
502
503#[derive(Clone, Debug, PartialEq, Eq, Hash)]
505pub enum RouteKey {
506 Address(Address),
508 Bytes32(B256),
510 Bytes(Vec<u8>),
512}
513
514#[non_exhaustive]
516#[derive(Clone, Debug, PartialEq, Eq, Hash)]
517pub enum LogRouteKey {
518 Emitter(Address),
520 Topic {
522 index: usize,
524 value: B256,
526 },
527 DataSlice {
529 offset: usize,
531 value: Vec<u8>,
533 },
534}
535
536#[derive(Clone, Debug, PartialEq, Eq)]
538pub struct LogRouteIndex {
539 keys: Vec<LogRouteKey>,
540}
541
542impl LogRouteIndex {
543 pub fn new(primary: LogRouteKey, additional: impl IntoIterator<Item = LogRouteKey>) -> Self {
545 let mut keys = vec![primary];
546 for key in additional {
547 if !keys.contains(&key) {
548 keys.push(key);
549 }
550 }
551 Self { keys }
552 }
553
554 pub fn single(key: LogRouteKey) -> Self {
556 Self { keys: vec![key] }
557 }
558
559 pub fn keys(&self) -> &[LogRouteKey] {
561 &self.keys
562 }
563}
564
565#[derive(Clone, Debug, PartialEq, Eq)]
567pub struct ReactiveLogRoute {
568 pub handler_id: HandlerId,
570 pub route_key: Option<RouteKey>,
572}
573
574#[derive(Clone, Debug, PartialEq, Eq, Hash)]
576pub struct BlockInterest {
577 pub mode: BlockInterestMode,
579}
580
581impl Default for BlockInterest {
582 fn default() -> Self {
583 Self {
584 mode: BlockInterestMode::Header,
585 }
586 }
587}
588
589#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
591pub enum BlockInterestMode {
592 Header,
594 FullBlock,
596}
597
598#[derive(Clone)]
600pub struct PendingTxInterest<N: Network = Ethereum> {
601 pub full_transactions: bool,
603 pub from: AddressMatcher,
605 pub to: AddressMatcher,
607 pub selectors: SelectorMatcher,
609 pub local_matcher: Option<Arc<dyn PendingTxMatcher<N>>>,
611}
612
613impl<N: Network> Default for PendingTxInterest<N> {
614 fn default() -> Self {
615 Self {
616 full_transactions: false,
617 from: AddressMatcher::Any,
618 to: AddressMatcher::Any,
619 selectors: SelectorMatcher::Any,
620 local_matcher: None,
621 }
622 }
623}
624
625impl<N: Network> PendingTxInterest<N> {
626 fn matches_hash_only(&self) -> bool {
627 !self.full_transactions
628 && self.from.is_any()
629 && self.to.is_any()
630 && self.selectors.is_any()
631 && self.local_matcher.is_none()
632 }
633
634 fn matches_tx(&self, tx: &N::TransactionResponse) -> bool {
635 self.from.matches(tx.from())
636 && self.to.matches_option(tx.to())
637 && self.selectors.matches(tx.input())
638 && self
639 .local_matcher
640 .as_ref()
641 .is_none_or(|matcher| matcher.matches(tx))
642 }
643}
644
645impl<N: Network> fmt::Debug for PendingTxInterest<N> {
646 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
647 f.debug_struct("PendingTxInterest")
648 .field("full_transactions", &self.full_transactions)
649 .field("from", &self.from)
650 .field("to", &self.to)
651 .field("selectors", &self.selectors)
652 .field(
653 "local_matcher",
654 &self.local_matcher.as_ref().map(|_| "<matcher>"),
655 )
656 .finish()
657 }
658}
659
660#[derive(Clone, Debug, PartialEq, Eq, Hash)]
662pub enum AddressMatcher {
663 Any,
665 Exact(Address),
667 AnyOf(Vec<Address>),
669}
670
671impl AddressMatcher {
672 pub fn is_any(&self) -> bool {
674 matches!(self, Self::Any)
675 }
676
677 pub fn matches(&self, address: Address) -> bool {
679 match self {
680 Self::Any => true,
681 Self::Exact(expected) => *expected == address,
682 Self::AnyOf(addresses) => addresses.contains(&address),
683 }
684 }
685
686 pub fn matches_option(&self, address: Option<Address>) -> bool {
688 match (self, address) {
689 (Self::Any, _) => true,
690 (_, Some(address)) => self.matches(address),
691 _ => false,
692 }
693 }
694}
695
696#[derive(Clone, Debug, PartialEq, Eq, Hash)]
698pub enum SelectorMatcher {
699 Any,
701 AnyOf(Vec<[u8; 4]>),
703}
704
705impl SelectorMatcher {
706 pub fn is_any(&self) -> bool {
708 matches!(self, Self::Any)
709 }
710
711 pub fn matches(&self, input: &Bytes) -> bool {
713 match self {
714 Self::Any => true,
715 Self::AnyOf(selectors) => input
716 .get(..4)
717 .and_then(|bytes| bytes.try_into().ok())
718 .is_some_and(|selector| selectors.contains(&selector)),
719 }
720 }
721}
722
723pub trait PendingTxMatcher<N: Network = Ethereum>: Send + Sync {
725 fn matches(&self, tx: &N::TransactionResponse) -> bool;
727}
728
729#[derive(Clone, Debug)]
747#[non_exhaustive]
748pub enum TrackingPolicy {
749 Slots {
754 slots: Vec<U256>,
756 },
757 WholeAccount,
762 Scalars,
767}
768
769#[derive(Clone, Copy, Debug, PartialEq, Eq)]
782pub enum RootGateCadence {
783 EveryNBlocks(NonZeroU64),
787 Disabled,
789}
790
791impl RootGateCadence {
792 pub fn every_n_blocks(n: u64) -> Self {
794 Self::EveryNBlocks(NonZeroU64::new(n.max(1)).expect("clamped to at least 1"))
795 }
796}
797
798impl Default for RootGateCadence {
799 fn default() -> Self {
803 Self::every_n_blocks(16)
804 }
805}
806
807#[derive(Clone, Debug)]
814struct TrackedRoot {
815 last_root: B256,
816 last_block: u64,
817 balance: U256,
818 nonce: u64,
819 code_hash: B256,
820}
821
822#[derive(Clone, Debug, PartialEq, Eq)]
824pub struct ResyncRequest {
825 pub id: ResyncId,
827 pub reason: ResyncReason,
829 pub block: ResyncBlock,
831 pub targets: Vec<ResyncTarget>,
833 pub priority: ResyncPriority,
835}
836
837#[derive(Clone, Debug, PartialEq, Eq, Hash)]
839pub struct ResyncId(String);
840
841impl ResyncId {
842 pub fn new(id: impl Into<String>) -> Self {
844 Self(id.into())
845 }
846}
847
848#[derive(Clone, Debug, PartialEq, Eq, Hash)]
850#[non_exhaustive]
851pub enum ResyncReason {
852 HandlerRequested,
854 SkippedStateEffect,
856 MissedBlockRange,
863 RootMoved,
873 Custom(String),
875}
876
877#[derive(Clone, Debug, PartialEq, Eq, Hash)]
879pub enum ResyncBlock {
880 Latest,
882 Safe,
884 Finalized,
886 Number(u64),
888 Hash {
890 number: u64,
892 hash: B256,
894 require_canonical: bool,
896 },
897}
898
899#[derive(Clone, Debug, PartialEq, Eq, Hash)]
901pub enum ResyncTarget {
902 StorageSlot {
904 address: Address,
906 slot: U256,
908 },
909 StorageSlots {
911 address: Address,
913 slots: Vec<U256>,
915 },
916 Account {
918 address: Address,
920 fields: AccountFieldMask,
922 },
923}
924
925#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
927pub struct AccountFieldMask {
928 pub balance: bool,
930 pub nonce: bool,
932 pub code: bool,
934}
935
936#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
938pub enum ResyncPriority {
939 Low,
941 #[default]
943 Normal,
944 High,
946}
947
948#[derive(Clone, Debug, PartialEq, Eq)]
950pub struct InvalidationRequest {
951 pub scope: PurgeScope,
953 pub address: Address,
955 pub reason: InvalidationReason,
957}
958
959#[derive(Clone, Debug, PartialEq, Eq, Hash)]
961pub enum InvalidationReason {
962 HandlerRequested,
964 Reorg,
966 Custom(String),
968}
969
970#[derive(Clone, Debug, PartialEq, Eq)]
972pub struct SpeculativeRequest {
973 pub id: SpeculativeId,
975 pub input_ref: InputRef,
977 pub labels: Vec<ReportTag>,
979}
980
981#[derive(Clone, Debug, PartialEq, Eq, Hash)]
983pub struct SpeculativeId(String);
984
985impl SpeculativeId {
986 pub fn new(id: impl Into<String>) -> Self {
988 Self(id.into())
989 }
990}
991
992#[derive(Clone, Debug, PartialEq, Eq)]
994pub struct ReactiveConfig {
995 pub hook_backpressure: HookBackpressure,
1000 pub journal_depth: usize,
1013}
1014
1015impl Default for ReactiveConfig {
1016 fn default() -> Self {
1017 Self {
1018 hook_backpressure: HookBackpressure::Block,
1019 journal_depth: 64,
1020 }
1021 }
1022}
1023
1024#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1026pub enum HookBackpressure {
1027 Block,
1029 DropNewest,
1031 DropOldest,
1033 Error,
1035}
1036
1037#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1045#[non_exhaustive]
1046pub enum CacheHealth {
1047 #[default]
1049 Healthy,
1050 Degraded {
1054 since_block: u64,
1056 },
1057 Unhealthy {
1060 since_block: u64,
1062 },
1063}
1064
1065#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1072#[non_exhaustive]
1073pub struct CacheMetricsSnapshot {
1074 pub deep_reorgs: u64,
1077 pub reorgs_recovered: u64,
1079 pub resync_requests: u64,
1081 pub resync_failures: u64,
1083 pub missed_ranges: u64,
1085 pub coverage_gaps: u64,
1087 pub pending_contamination: u64,
1089 pub stale_verdicts: u64,
1091}
1092
1093#[derive(Debug, Default)]
1099struct CacheMetrics {
1100 deep_reorgs: AtomicU64,
1101 reorgs_recovered: AtomicU64,
1102 resync_requests: AtomicU64,
1103 resync_failures: AtomicU64,
1104 missed_ranges: AtomicU64,
1105 coverage_gaps: AtomicU64,
1106 pending_contamination: AtomicU64,
1107 stale_verdicts: AtomicU64,
1108}
1109
1110impl CacheMetrics {
1111 fn snapshot(&self) -> CacheMetricsSnapshot {
1112 CacheMetricsSnapshot {
1113 deep_reorgs: self.deep_reorgs.load(Ordering::Relaxed),
1114 reorgs_recovered: self.reorgs_recovered.load(Ordering::Relaxed),
1115 resync_requests: self.resync_requests.load(Ordering::Relaxed),
1116 resync_failures: self.resync_failures.load(Ordering::Relaxed),
1117 missed_ranges: self.missed_ranges.load(Ordering::Relaxed),
1118 coverage_gaps: self.coverage_gaps.load(Ordering::Relaxed),
1119 pending_contamination: self.pending_contamination.load(Ordering::Relaxed),
1120 stale_verdicts: self.stale_verdicts.load(Ordering::Relaxed),
1121 }
1122 }
1123}
1124
1125#[derive(Clone, Debug)]
1127#[non_exhaustive]
1128pub enum ReactiveReport<N: Network = Ethereum> {
1129 Input(InputReport<N>),
1131 Decoded(DecodedReport<N>),
1133 Applied(AppliedReport<N>),
1135 Resynced(ResyncReport),
1137 BlockCommitted(BlockReport<N>),
1139 Reorg(ReorgReport<N>),
1141 MissedBlockRange(MissedRangeReport<N>),
1144 Health(HealthReport<N>),
1146 CoverageGap(CoverageGapReport<N>),
1149 Error(ReactiveErrorReport<N>),
1151}
1152
1153#[derive(Clone, Debug)]
1155pub struct InputReport<N: Network = Ethereum> {
1156 pub input_ref: InputRef,
1158 pub context: ReactiveContext,
1160 pub _network: PhantomData<N>,
1162}
1163
1164#[derive(Clone, Debug)]
1166pub struct DecodedReport<N: Network = Ethereum> {
1167 pub input_ref: InputRef,
1169 pub handler_ids: Vec<HandlerId>,
1171 pub _network: PhantomData<N>,
1173}
1174
1175#[derive(Clone, Debug)]
1177pub struct AppliedReport<N: Network = Ethereum> {
1178 pub input_ref: InputRef,
1180 pub handler_id: HandlerId,
1182 pub quality: StateEffectQuality,
1184 pub tags: Vec<ReportTag>,
1186 pub diff: StateDiff,
1188 pub state_updates: Vec<StateUpdate>,
1190 pub invalidations: Vec<InvalidationRequest>,
1192 pub resyncs: Vec<ResyncRequest>,
1194 pub speculative: Vec<SpeculativeRequest>,
1196 pub hook_signals: Vec<HookSignal>,
1198 pub _network: PhantomData<N>,
1200}
1201
1202#[derive(Clone, Debug, Default, PartialEq, Eq)]
1206pub struct ResyncReport {
1207 pub requested: Vec<ResyncRequest>,
1209 pub state_updates: Vec<StateUpdate>,
1211 pub diff: StateDiff,
1213 pub failed: Vec<ResyncFailure>,
1215}
1216
1217#[derive(Clone, Debug, PartialEq, Eq)]
1219pub struct ResyncFailure {
1220 pub request_id: ResyncId,
1222 pub block: ResyncBlock,
1224 pub target: ResyncTarget,
1226 pub kind: ResyncFailureKind,
1228 pub message: String,
1230}
1231
1232#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1234#[non_exhaustive]
1235pub enum ResyncFailureKind {
1236 MissingStorageFetcher,
1238 StorageFetchFailed,
1240 StorageFetchOmitted,
1242 MissingAccountFetcher,
1244 AccountFetchFailed,
1246 AccountFetchOmitted,
1248}
1249
1250#[derive(Clone, Debug)]
1252pub struct BlockReport<N: Network = Ethereum> {
1253 pub block: Option<BlockRef>,
1255 pub inputs: Vec<InputRef>,
1257 pub _network: PhantomData<N>,
1259}
1260
1261#[derive(Clone, Debug)]
1272pub struct ReorgReport<N: Network = Ethereum> {
1273 pub dropped: Option<BlockRef>,
1275 pub dropped_blocks: Vec<BlockRef>,
1277 pub dropped_inputs: Vec<InputRef>,
1279 pub rollback_updates: Vec<StateUpdate>,
1281 pub rollback_diff: StateDiff,
1283 pub purge_updates: Vec<StateUpdate>,
1285 pub purge_diff: StateDiff,
1287 pub canceled_resyncs: Vec<ResyncRequest>,
1289 pub reason: ReorgReason,
1291 pub _network: PhantomData<N>,
1293}
1294
1295#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1297pub enum ReorgReason {
1298 RemovedLog,
1300 ReorgedInput,
1302 ParentMismatch,
1304}
1305
1306#[derive(Clone, Debug)]
1314pub struct MissedRangeReport<N: Network = Ethereum> {
1315 pub from: u64,
1317 pub to: u64,
1319 pub block: u64,
1321 pub _network: PhantomData<N>,
1323}
1324
1325#[derive(Clone, Debug)]
1328pub struct HealthReport<N: Network = Ethereum> {
1329 pub from: CacheHealth,
1331 pub to: CacheHealth,
1333 pub block: Option<u64>,
1335 pub _network: PhantomData<N>,
1337}
1338
1339#[derive(Clone, Debug)]
1352pub struct CoverageGapReport<N: Network = Ethereum> {
1353 pub address: Address,
1355 pub block: u64,
1357 pub _network: PhantomData<N>,
1359}
1360
1361#[derive(Clone, Debug)]
1364pub struct ReactiveErrorReport<N: Network = Ethereum> {
1365 pub input_ref: Option<InputRef>,
1367 pub message: String,
1369 pub _network: PhantomData<N>,
1371}
1372
1373#[derive(Clone, Debug)]
1376pub struct ReactiveBatchReport<N: Network = Ethereum> {
1377 pub applied: Vec<AppliedReport<N>>,
1379 pub resyncs: Vec<ResyncRequest>,
1381 pub speculative: Vec<SpeculativeRequest>,
1383 pub reports: Vec<Arc<ReactiveReport<N>>>,
1385}
1386
1387impl<N: Network> Default for ReactiveBatchReport<N> {
1388 fn default() -> Self {
1389 Self {
1390 applied: Vec::new(),
1391 resyncs: Vec::new(),
1392 speculative: Vec::new(),
1393 reports: Vec::new(),
1394 }
1395 }
1396}
1397
1398#[derive(Clone, Debug, PartialEq, Eq)]
1400pub struct HandlerError {
1401 message: String,
1402}
1403
1404impl HandlerError {
1405 pub fn new(message: impl Into<String>) -> Self {
1407 Self {
1408 message: message.into(),
1409 }
1410 }
1411}
1412
1413impl fmt::Display for HandlerError {
1414 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1415 self.message.fmt(f)
1416 }
1417}
1418
1419impl std::error::Error for HandlerError {}
1420
1421impl From<String> for HandlerError {
1422 fn from(message: String) -> Self {
1423 Self::new(message)
1424 }
1425}
1426
1427impl From<&str> for HandlerError {
1428 fn from(message: &str) -> Self {
1429 Self::new(message)
1430 }
1431}
1432
1433#[derive(Debug, thiserror::Error)]
1435pub enum ReactiveError {
1436 #[error("handler `{handler_id}` failed: {source}")]
1438 HandlerFailed {
1439 handler_id: HandlerId,
1441 source: HandlerError,
1443 },
1444 #[error(
1446 "conflicting effects for input {input_ref:?} on target {target:?}: `{first}` vs `{second}`"
1447 )]
1448 ConflictingEffects {
1449 input_ref: Box<InputRef>,
1451 target: Box<EffectTarget>,
1453 first: HandlerId,
1455 second: HandlerId,
1457 },
1458 #[error(
1460 "pending input {input_ref:?} emitted invalid canonical effect `{effect_kind}` from `{handler_id}`"
1461 )]
1462 InvalidPendingEffect {
1463 input_ref: Box<InputRef>,
1465 handler_id: HandlerId,
1467 effect_kind: &'static str,
1469 },
1470 #[error(transparent)]
1472 Register(#[from] RegisterError),
1473}
1474
1475#[derive(Debug, thiserror::Error)]
1477pub enum RegisterError {
1478 #[error("handler id `{0}` is already registered")]
1480 DuplicateHandler(HandlerId),
1481}
1482
1483#[derive(Debug, thiserror::Error)]
1486pub enum ReactiveEngineRegisterError {
1487 #[error(transparent)]
1489 Register(#[from] RegisterError),
1490 #[error(transparent)]
1492 Subscriber(#[from] SubscriberError),
1493}
1494
1495#[derive(Debug, thiserror::Error)]
1498pub enum ReactiveEngineError {
1499 #[error(transparent)]
1501 Subscriber(#[from] SubscriberError),
1502 #[error(transparent)]
1504 Runtime(#[from] ReactiveError),
1505}
1506
1507#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1509pub enum EffectTarget {
1510 StorageSlot {
1512 address: Address,
1514 slot: U256,
1516 },
1517 AccountBalance {
1519 address: Address,
1521 },
1522 AccountNonce {
1524 address: Address,
1526 },
1527 AccountCode {
1529 address: Address,
1531 },
1532 MaskedStorageSlot {
1534 address: Address,
1536 slot: U256,
1538 mask: U256,
1540 },
1541}
1542
1543#[derive(Clone, Debug, PartialEq, Eq)]
1544enum AbsoluteValue {
1545 U256(U256),
1546 U64(u64),
1547 Bytes(Bytes),
1548}
1549
1550pub struct ReactiveRuntime<N: Network = Ethereum> {
1552 registry: ReactiveRegistry<N>,
1553 hooks: Vec<Arc<dyn ReactiveHook<N>>>,
1554 config: ReactiveConfig,
1555 journal: VecDeque<BlockJournal<N>>,
1556 pending_resyncs: Vec<ResyncRequest>,
1557 health: CacheHealth,
1558 metrics: CacheMetrics,
1559 freshness: Option<FreshnessRegistry>,
1568 tracking: HashMap<Address, TrackingPolicy>,
1572 tracked_roots: HashMap<Address, TrackedRoot>,
1575 root_gate_cadence: RootGateCadence,
1577 last_gate_block: Option<u64>,
1581 touched_since_gate: HashSet<Address>,
1587}
1588
1589#[derive(Clone, Debug)]
1590struct BlockJournal<N: Network = Ethereum> {
1591 block: BlockRef,
1592 inputs: Vec<InputRef>,
1593 applied: Vec<AppliedReport<N>>,
1594 resynced: Vec<ResyncReport>,
1595}
1596
1597pub struct ReactiveRegistry<N: Network = Ethereum> {
1605 handlers: BTreeMap<u128, RegisteredHandler<N>>,
1606 handler_positions: HashMap<HandlerId, u128>,
1607 next_handler_position: u128,
1608 indexed_log_handlers: HashMap<LogRouteKey, BTreeSet<u128>>,
1609 fallback_log_handlers: BTreeSet<u128>,
1610 data_slice_shapes: HashMap<(usize, usize), usize>,
1611}
1612
1613struct RegisteredHandler<N: Network = Ethereum> {
1614 id: HandlerId,
1615 handler: Arc<dyn ReactiveHandler<N>>,
1616 interests: Vec<ReactiveInterest<N>>,
1617 has_log_interests: bool,
1618 log_route_index: Option<LogRouteIndex>,
1619}
1620
1621impl<N: Network> Default for ReactiveRegistry<N> {
1622 fn default() -> Self {
1623 Self::new()
1624 }
1625}
1626
1627impl<N: Network> ReactiveRegistry<N> {
1628 pub fn new() -> Self {
1630 Self {
1631 handlers: BTreeMap::new(),
1632 handler_positions: HashMap::new(),
1633 next_handler_position: 0,
1634 indexed_log_handlers: HashMap::new(),
1635 fallback_log_handlers: BTreeSet::new(),
1636 data_slice_shapes: HashMap::new(),
1637 }
1638 }
1639
1640 pub fn register_handler(
1645 &mut self,
1646 handler: Arc<dyn ReactiveHandler<N>>,
1647 ) -> Result<(), RegisterError> {
1648 let id = handler.id();
1649 if self.handler_positions.contains_key(&id) {
1650 return Err(RegisterError::DuplicateHandler(id));
1651 }
1652 let interests = handler.interests();
1653 let has_log_interests = interests
1654 .iter()
1655 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)));
1656 let log_route_index = handler.log_route_index();
1657 if self.next_handler_position == u128::MAX {
1658 self.compact_handler_positions();
1659 }
1660 let position = self.next_handler_position;
1661 self.next_handler_position += 1;
1662 self.handler_positions.insert(id.clone(), position);
1663 if let Some(index) = &log_route_index {
1664 for key in index.keys() {
1665 if let LogRouteKey::DataSlice { offset, value } = key {
1666 *self
1667 .data_slice_shapes
1668 .entry((*offset, value.len()))
1669 .or_default() += 1;
1670 }
1671 self.indexed_log_handlers
1672 .entry(key.clone())
1673 .or_default()
1674 .insert(position);
1675 }
1676 } else if has_log_interests {
1677 self.fallback_log_handlers.insert(position);
1678 }
1679 self.handlers.insert(
1680 position,
1681 RegisteredHandler {
1682 id,
1683 handler,
1684 interests,
1685 has_log_interests,
1686 log_route_index,
1687 },
1688 );
1689 Ok(())
1690 }
1691
1692 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
1698 let position = self.handler_positions.remove(id)?;
1699 let registered = self.handlers.remove(&position)?;
1700 if let Some(index) = ®istered.log_route_index {
1701 for key in index.keys() {
1702 let remove_bucket = self
1703 .indexed_log_handlers
1704 .get_mut(key)
1705 .is_some_and(|owners| {
1706 owners.remove(&position);
1707 owners.is_empty()
1708 });
1709 if remove_bucket {
1710 self.indexed_log_handlers.remove(key);
1711 }
1712 if let LogRouteKey::DataSlice { offset, value } = key {
1713 let shape = (*offset, value.len());
1714 let remove_shape =
1715 self.data_slice_shapes.get_mut(&shape).is_some_and(|count| {
1716 *count -= 1;
1717 *count == 0
1718 });
1719 if remove_shape {
1720 self.data_slice_shapes.remove(&shape);
1721 }
1722 }
1723 }
1724 } else {
1725 self.fallback_log_handlers.remove(&position);
1726 }
1727 Some(registered.handler)
1728 }
1729
1730 pub fn contains_handler(&self, id: &HandlerId) -> bool {
1732 self.handler_positions.contains_key(id)
1733 }
1734
1735 pub fn handler_ids(&self) -> Vec<HandlerId> {
1737 self.handlers
1738 .values()
1739 .map(|handler| handler.id.clone())
1740 .collect()
1741 }
1742
1743 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
1745 self.handler_positions
1746 .get(id)
1747 .and_then(|position| self.handlers.get(position))
1748 .map(|registered| registered.interests.as_slice())
1749 }
1750
1751 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
1753 self.handlers
1754 .values()
1755 .flat_map(|handler| handler.interests.clone())
1756 .collect()
1757 }
1758
1759 pub fn log_subscription_filters(&self) -> Vec<Filter> {
1766 let mut filters = Vec::new();
1767 for interest in self.log_interests() {
1768 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
1769 }
1770 filters
1771 }
1772
1773 pub fn route_log(&self, log: &Log) -> Vec<ReactiveLogRoute> {
1779 self.log_handler_candidates(log)
1780 .into_iter()
1781 .filter_map(|handler| handler.route_log(log))
1782 .collect()
1783 }
1784
1785 fn log_handler_candidates(&self, log: &Log) -> Vec<&RegisteredHandler<N>> {
1786 let mut indexed_positions = Vec::new();
1787 if let Some(indexed) = self
1788 .indexed_log_handlers
1789 .get(&LogRouteKey::Emitter(log.address()))
1790 {
1791 indexed_positions.extend(indexed.iter().copied());
1792 }
1793 for (index, value) in log.topics().iter().copied().enumerate() {
1794 if let Some(indexed) = self
1795 .indexed_log_handlers
1796 .get(&LogRouteKey::Topic { index, value })
1797 {
1798 indexed_positions.extend(indexed.iter().copied());
1799 }
1800 }
1801 let data = log.inner.data.data.as_ref();
1802 for &(offset, len) in self.data_slice_shapes.keys() {
1803 let Some(end) = offset.checked_add(len) else {
1804 continue;
1805 };
1806 let Some(value) = data.get(offset..end) else {
1807 continue;
1808 };
1809 if let Some(indexed) = self.indexed_log_handlers.get(&LogRouteKey::DataSlice {
1810 offset,
1811 value: value.to_vec(),
1812 }) {
1813 indexed_positions.extend(indexed.iter().copied());
1814 }
1815 }
1816 if indexed_positions.is_empty() {
1817 if self.fallback_log_handlers.is_empty() {
1818 return Vec::new();
1819 }
1820 if !self.indexed_log_handlers.is_empty() {
1821 return self
1822 .fallback_log_handlers
1823 .iter()
1824 .filter_map(|position| self.handlers.get(position))
1825 .collect();
1826 }
1827 return self
1828 .handlers
1829 .values()
1830 .filter(|handler| handler.has_log_interests && handler.log_route_index.is_none())
1831 .collect();
1832 }
1833
1834 indexed_positions.extend(self.fallback_log_handlers.iter().copied());
1835 indexed_positions.sort_unstable();
1836 indexed_positions.dedup();
1837 indexed_positions
1838 .into_iter()
1839 .filter_map(|position| self.handlers.get(&position))
1840 .collect()
1841 }
1842
1843 fn handlers(&self) -> impl Iterator<Item = &RegisteredHandler<N>> {
1844 self.handlers.values()
1845 }
1846
1847 fn log_interests(&self) -> impl Iterator<Item = &LogInterest> {
1848 self.handlers.values().flat_map(|handler| {
1849 handler
1850 .interests
1851 .iter()
1852 .filter_map(|interest| match interest {
1853 ReactiveInterest::Logs(interest) => Some(interest),
1854 ReactiveInterest::Blocks(_) | ReactiveInterest::PendingTransactions(_) => None,
1855 })
1856 })
1857 }
1858
1859 fn compact_handler_positions(&mut self) {
1860 let handlers = std::mem::take(&mut self.handlers);
1861 self.handler_positions.clear();
1862 self.indexed_log_handlers.clear();
1863 self.fallback_log_handlers.clear();
1864 self.data_slice_shapes.clear();
1865
1866 for (position, (_, handler)) in handlers.into_iter().enumerate() {
1867 let position = position as u128;
1868 self.handler_positions.insert(handler.id.clone(), position);
1869 if let Some(index) = &handler.log_route_index {
1870 for key in index.keys() {
1871 if let LogRouteKey::DataSlice { offset, value } = key {
1872 *self
1873 .data_slice_shapes
1874 .entry((*offset, value.len()))
1875 .or_default() += 1;
1876 }
1877 self.indexed_log_handlers
1878 .entry(key.clone())
1879 .or_default()
1880 .insert(position);
1881 }
1882 } else if handler.has_log_interests {
1883 self.fallback_log_handlers.insert(position);
1884 }
1885 self.handlers.insert(position, handler);
1886 }
1887 self.next_handler_position = self.handlers.len() as u128;
1888 }
1889}
1890
1891impl<N: Network> ReactiveRuntime<N> {
1892 pub fn new(config: ReactiveConfig) -> Self {
1894 Self {
1895 registry: ReactiveRegistry::new(),
1896 hooks: Vec::new(),
1897 config,
1898 journal: VecDeque::new(),
1899 pending_resyncs: Vec::new(),
1900 health: CacheHealth::Healthy,
1901 metrics: CacheMetrics::default(),
1902 freshness: None,
1903 tracking: HashMap::new(),
1904 tracked_roots: HashMap::new(),
1905 root_gate_cadence: RootGateCadence::default(),
1906 last_gate_block: None,
1907 touched_since_gate: HashSet::new(),
1908 }
1909 }
1910
1911 pub fn track_account(&mut self, address: Address, policy: TrackingPolicy) {
1925 self.tracking.insert(address, policy);
1926 self.tracked_roots.remove(&address);
1927 }
1928
1929 pub fn untrack_account(&mut self, address: Address) -> bool {
1933 self.tracked_roots.remove(&address);
1934 self.tracking.remove(&address).is_some()
1935 }
1936
1937 pub fn set_root_gate_cadence(&mut self, cadence: RootGateCadence) {
1945 self.root_gate_cadence = cadence;
1946 self.last_gate_block = None;
1947 self.touched_since_gate.clear();
1948 }
1949
1950 pub fn root_gate_cadence(&self) -> RootGateCadence {
1952 self.root_gate_cadence
1953 }
1954
1955 pub fn enable_freshness_stamping(&mut self) {
1967 if self.freshness.is_none() {
1968 self.freshness = Some(FreshnessRegistry::new());
1969 }
1970 }
1971
1972 pub fn freshness(&self) -> Option<&FreshnessRegistry> {
1977 self.freshness.as_ref()
1978 }
1979
1980 pub fn freshness_mut(&mut self) -> Option<&mut FreshnessRegistry> {
1985 self.freshness.as_mut()
1986 }
1987
1988 pub fn health(&self) -> CacheHealth {
1990 self.health
1991 }
1992
1993 pub fn metrics(&self) -> CacheMetricsSnapshot {
1995 self.metrics.snapshot()
1996 }
1997
1998 pub fn reset_health(&mut self) {
2008 self.health = CacheHealth::Healthy;
2009 }
2010
2011 fn escalate_trust(&mut self, block: u64) -> Option<Arc<ReactiveReport<N>>> {
2025 let to = match self.health {
2026 CacheHealth::Healthy => CacheHealth::Degraded { since_block: block },
2027 CacheHealth::Degraded { .. } => CacheHealth::Unhealthy { since_block: block },
2028 CacheHealth::Unhealthy { .. } => return None,
2029 };
2030 self.transition_health(to, Some(block))
2031 }
2032
2033 fn transition_health(
2041 &mut self,
2042 to: CacheHealth,
2043 block: Option<u64>,
2044 ) -> Option<Arc<ReactiveReport<N>>> {
2045 if to == self.health {
2046 return None;
2047 }
2048 let from = self.health;
2049 self.health = to;
2050 Some(Arc::new(ReactiveReport::Health(HealthReport {
2051 from,
2052 to,
2053 block,
2054 _network: PhantomData,
2055 })))
2056 }
2057
2058 pub fn register_handler(
2060 &mut self,
2061 handler: Arc<dyn ReactiveHandler<N>>,
2062 ) -> Result<(), RegisterError> {
2063 self.registry.register_handler(handler)
2064 }
2065
2066 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
2074 self.registry.unregister_handler(id)
2075 }
2076
2077 pub fn contains_handler(&self, id: &HandlerId) -> bool {
2079 self.registry.contains_handler(id)
2080 }
2081
2082 pub fn handler_ids(&self) -> Vec<HandlerId> {
2084 self.registry.handler_ids()
2085 }
2086
2087 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
2089 self.registry.handler_interests(id)
2090 }
2091
2092 pub fn last_canonical_block(&self) -> Option<BlockRef> {
2102 self.journal.back().map(|entry| entry.block.clone())
2103 }
2104
2105 pub fn has_journaled_handler_effects(&self, handler_id: &HandlerId) -> bool {
2113 self.journal.iter().any(|entry| {
2114 entry
2115 .applied
2116 .iter()
2117 .any(|applied| &applied.handler_id == handler_id)
2118 })
2119 }
2120
2121 pub fn journaled_handler_ids(&self) -> HashSet<HandlerId> {
2128 self.journal
2129 .iter()
2130 .flat_map(|entry| entry.applied.iter())
2131 .map(|applied| applied.handler_id.clone())
2132 .collect()
2133 }
2134
2135 pub fn pending_resyncs(&self) -> &[ResyncRequest] {
2146 &self.pending_resyncs
2147 }
2148
2149 pub fn cancel_pending_resync(&mut self, id: &ResyncId) -> Vec<ResyncRequest> {
2158 self.cancel_pending_resyncs_by_id(std::slice::from_ref(id))
2159 }
2160
2161 pub fn cancel_pending_resyncs_by_id(&mut self, ids: &[ResyncId]) -> Vec<ResyncRequest> {
2168 if ids.is_empty() {
2169 return Vec::new();
2170 }
2171 let ids: HashSet<&ResyncId> = ids.iter().collect();
2172 let mut cancelled = Vec::new();
2173 self.pending_resyncs.retain(|request| {
2174 if ids.contains(&request.id) {
2175 cancelled.push(request.clone());
2176 false
2177 } else {
2178 true
2179 }
2180 });
2181 cancelled
2182 }
2183
2184 pub fn cancel_pending_resyncs(&mut self, address: Address) -> Vec<ResyncRequest> {
2199 let mut cancelled = Vec::new();
2200 self.pending_resyncs.retain_mut(|request| {
2201 let (matching, remaining): (Vec<_>, Vec<_>) = request
2202 .targets
2203 .drain(..)
2204 .partition(|target| resync_target_address(target) == address);
2205 request.targets = remaining;
2206 if !matching.is_empty() {
2207 cancelled.push(ResyncRequest {
2208 id: request.id.clone(),
2209 reason: request.reason.clone(),
2210 block: request.block.clone(),
2211 targets: matching,
2212 priority: request.priority,
2213 });
2214 }
2215 !request.targets.is_empty()
2216 });
2217 cancelled
2218 }
2219
2220 pub fn register_hook(&mut self, hook: Arc<dyn ReactiveHook<N>>) -> Result<(), RegisterError> {
2222 self.hooks.push(hook);
2223 Ok(())
2224 }
2225
2226 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
2228 self.registry.interests()
2229 }
2230
2231 pub fn ingest_batch(
2233 &mut self,
2234 cache: &mut EvmCache,
2235 batch: ReactiveInputBatch<N>,
2236 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
2237 let batch_report = self.ingest_batch_direct(cache, batch)?;
2238 self.dispatch_reports(&batch_report.reports);
2239 let _ = &self.config;
2240 Ok(batch_report)
2241 }
2242
2243 pub fn ingest_batch_with_resync(
2254 &mut self,
2255 cache: &mut EvmCache,
2256 batch: ReactiveInputBatch<N>,
2257 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
2258 let mut batch_report = self.ingest_batch_direct(cache, batch)?;
2259
2260 if !batch_report.resyncs.is_empty() {
2261 let resync_report = execute_resync_requests(cache, &batch_report.resyncs);
2262 let unique_requests = resync_report
2266 .requested
2267 .iter()
2268 .map(|request| &request.id)
2269 .collect::<HashSet<_>>()
2270 .len();
2271 self.metrics
2272 .resync_requests
2273 .fetch_add(unique_requests as u64, Ordering::Relaxed);
2274 self.metrics
2275 .resync_failures
2276 .fetch_add(resync_report.failed.len() as u64, Ordering::Relaxed);
2277 self.remove_pending_resyncs(batch_report.resyncs.iter().map(|request| &request.id));
2278 self.record_journal_resync(&resync_report);
2279 batch_report
2280 .reports
2281 .push(Arc::new(ReactiveReport::Resynced(resync_report)));
2282 }
2283
2284 self.dispatch_reports(&batch_report.reports);
2285 let _ = &self.config;
2286 Ok(batch_report)
2287 }
2288
2289 fn ingest_batch_direct(
2290 &mut self,
2291 cache: &mut EvmCache,
2292 batch: ReactiveInputBatch<N>,
2293 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
2294 let records = sort_records(dedupe_records(batch.into_records()));
2295
2296 let mut batch_report = ReactiveBatchReport::default();
2297 let mut reports_to_dispatch = Vec::new();
2298 let mut touched_addrs: HashSet<Address> = HashSet::new();
2303 let mut canonical_batch_block: Option<u64> = None;
2304
2305 for record in records {
2306 let input_ref = record.input_ref();
2307 reports_to_dispatch.push(Arc::new(ReactiveReport::Input(InputReport {
2308 input_ref,
2309 context: record.context.clone(),
2310 _network: PhantomData,
2311 })));
2312
2313 if let Some(reorg_report) =
2314 self.recover_for_canonical_input(cache, &record, &mut reports_to_dispatch)
2315 {
2316 self.metrics
2317 .reorgs_recovered
2318 .fetch_add(1, Ordering::Relaxed);
2319 remove_canceled_resyncs_from_batch(
2320 &mut batch_report.resyncs,
2321 &reorg_report.canceled_resyncs,
2322 );
2323 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
2324 }
2325
2326 if let Some(reorg_report) =
2327 self.recover_for_reorged_input(cache, &record, &mut reports_to_dispatch)
2328 {
2329 self.metrics
2330 .reorgs_recovered
2331 .fetch_add(1, Ordering::Relaxed);
2332 remove_canceled_resyncs_from_batch(
2333 &mut batch_report.resyncs,
2334 &reorg_report.canceled_resyncs,
2335 );
2336 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
2337 continue;
2338 }
2339
2340 if let Some(block) = canonical_record_block(&record) {
2341 canonical_batch_block = Some(block.number);
2344 self.record_journal_input(block, input_ref);
2345 }
2346
2347 if let Some(Err(err)) = advance_block_for_canonical_record(cache, &record) {
2351 reports_to_dispatch.push(Arc::new(ReactiveReport::Error(ReactiveErrorReport {
2352 input_ref: Some(input_ref),
2353 message: err.to_string(),
2354 _network: PhantomData,
2355 })));
2356 }
2357
2358 let executions = self.execute_handlers(cache, &record, input_ref)?;
2359 if executions.is_empty() {
2360 continue;
2361 }
2362
2363 reports_to_dispatch.push(Arc::new(ReactiveReport::Decoded(DecodedReport {
2364 input_ref,
2365 handler_ids: executions
2366 .iter()
2367 .map(|execution| execution.handler_id.clone())
2368 .collect(),
2369 _network: PhantomData,
2370 })));
2371
2372 detect_conflicts(input_ref, &executions)?;
2373
2374 let canonical_block_number = canonical_record_block(&record).map(|block| block.number);
2380
2381 for execution in executions {
2382 let diff = if execution.state_updates.is_empty() {
2383 StateDiff::default()
2384 } else {
2385 cache.apply_updates(&execution.state_updates)
2386 };
2387
2388 batch_report
2389 .resyncs
2390 .extend(execution.resyncs.iter().cloned());
2391 self.pending_resyncs
2392 .extend(execution.resyncs.iter().cloned());
2393 batch_report
2394 .speculative
2395 .extend(execution.speculative.iter().cloned());
2396
2397 let applied = AppliedReport {
2398 input_ref,
2399 handler_id: execution.handler_id,
2400 quality: execution.quality,
2401 tags: execution.tags,
2402 diff,
2403 state_updates: execution.state_updates,
2404 invalidations: execution.invalidations,
2405 resyncs: execution.resyncs,
2406 speculative: execution.speculative,
2407 hook_signals: execution.hook_signals,
2408 _network: PhantomData,
2409 };
2410 if let (Some(number), Some(registry)) =
2419 (canonical_block_number, self.freshness.as_mut())
2420 {
2421 for change in &applied.diff.slots {
2422 registry.valid_through_slot(change.address, change.slot, number);
2423 }
2424 }
2425
2426 collect_diff_addresses(&applied.diff, &mut touched_addrs);
2434
2435 let report = Arc::new(ReactiveReport::Applied(applied.clone()));
2436 reports_to_dispatch.push(report);
2437 if let Some(block) = canonical_record_block(&record) {
2438 self.record_journal_applied(block, applied.clone());
2439 }
2440 batch_report.applied.push(applied);
2441 }
2442 }
2443
2444 if self.root_gate_runnable(cache) {
2454 self.touched_since_gate
2455 .extend(touched_addrs.iter().copied());
2456 if self.root_gate_due(canonical_batch_block) {
2457 let accumulated = std::mem::take(&mut self.touched_since_gate);
2458 self.run_root_gate(
2459 cache,
2460 canonical_batch_block,
2461 &accumulated,
2462 &mut batch_report.resyncs,
2463 &mut reports_to_dispatch,
2464 );
2465 self.last_gate_block = canonical_batch_block;
2466 }
2467 } else {
2468 self.touched_since_gate.clear();
2475 }
2476
2477 batch_report.reports = reports_to_dispatch;
2478 Ok(batch_report)
2479 }
2480
2481 fn root_gate_runnable(&self, cache: &EvmCache) -> bool {
2486 if matches!(self.root_gate_cadence, RootGateCadence::Disabled) {
2487 return false;
2488 }
2489 let has_gated_targets = self
2490 .tracking
2491 .values()
2492 .any(|policy| !matches!(policy, TrackingPolicy::Slots { .. }));
2493 has_gated_targets && cache.account_proof_fetcher().is_some()
2494 }
2495
2496 fn root_gate_due(&self, canonical_block: Option<u64>) -> bool {
2500 let Some(block) = canonical_block else {
2501 return false;
2502 };
2503 match self.root_gate_cadence {
2504 RootGateCadence::Disabled => false,
2505 RootGateCadence::EveryNBlocks(n) => match self.last_gate_block {
2506 None => true,
2507 Some(last) => block >= last.saturating_add(n.get()),
2508 },
2509 }
2510 }
2511
2512 fn run_root_gate(
2538 &mut self,
2539 cache: &EvmCache,
2540 canonical_block: Option<u64>,
2541 touched: &HashSet<Address>,
2542 resyncs: &mut Vec<ResyncRequest>,
2543 reports: &mut Vec<Arc<ReactiveReport<N>>>,
2544 ) {
2545 if self.tracking.is_empty() {
2546 return;
2547 }
2548 let Some(block) = canonical_block else {
2549 return;
2550 };
2551 let Some(fetcher) = cache.account_proof_fetcher().cloned() else {
2552 return;
2553 };
2554
2555 let mut targets: Vec<(Address, bool)> = self
2558 .tracking
2559 .iter()
2560 .filter_map(|(address, policy)| match policy {
2561 TrackingPolicy::Slots { .. } => None,
2562 TrackingPolicy::WholeAccount => Some((*address, true)),
2563 TrackingPolicy::Scalars => Some((*address, false)),
2564 })
2565 .collect();
2566 if targets.is_empty() {
2567 return;
2568 }
2569 targets.sort_by_key(|(address, _)| *address);
2570
2571 let block_id = BlockId::number(block);
2572 let mut probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
2577 targets
2578 .iter()
2579 .map(|&(address, _)| (address, vec![]))
2580 .collect(),
2581 block_id,
2582 )
2583 .into_iter()
2584 .collect();
2585 for (address, whole_account) in targets {
2586 let Some(Ok(proof)) = probes.remove(&address) else {
2587 continue;
2590 };
2591
2592 let baseline = self.tracked_roots.get(&address).cloned();
2593 let Some(baseline) = baseline else {
2594 self.adopt_root(address, block, &proof);
2596 continue;
2597 };
2598
2599 if block <= baseline.last_block {
2604 continue;
2605 }
2606
2607 if whole_account {
2608 if proof.storage_hash == baseline.last_root {
2609 continue;
2611 }
2612 if !touched.contains(&address) {
2614 reports.push(Arc::new(ReactiveReport::CoverageGap(CoverageGapReport {
2616 address,
2617 block,
2618 _network: PhantomData,
2619 })));
2620 self.metrics.coverage_gaps.fetch_add(1, Ordering::Relaxed);
2621 resyncs.push(root_moved_account_resync(
2622 address,
2623 block,
2624 AccountFieldMask {
2625 balance: true,
2626 nonce: true,
2627 code: true,
2628 },
2629 ));
2630 }
2631 self.adopt_root(address, block, &proof);
2633 } else {
2634 let balance_moved = proof.balance != baseline.balance;
2637 let nonce_moved = proof.nonce != baseline.nonce;
2638 let code_moved = proof.code_hash != baseline.code_hash;
2639 if (balance_moved || nonce_moved || code_moved) && !touched.contains(&address) {
2640 resyncs.push(root_moved_account_resync(
2641 address,
2642 block,
2643 AccountFieldMask {
2644 balance: balance_moved,
2645 nonce: nonce_moved,
2646 code: code_moved,
2647 },
2648 ));
2649 }
2650 self.adopt_root(address, block, &proof);
2651 }
2652 }
2653 }
2654
2655 fn adopt_root(&mut self, address: Address, block: u64, proof: &AccountProof) {
2657 self.tracked_roots.insert(
2658 address,
2659 TrackedRoot {
2660 last_root: proof.storage_hash,
2661 last_block: block,
2662 balance: proof.balance,
2663 nonce: proof.nonce,
2664 code_hash: proof.code_hash,
2665 },
2666 );
2667 }
2668
2669 fn execute_handlers(
2670 &self,
2671 cache: &EvmCache,
2672 record: &ReactiveInputRecord<N>,
2673 input_ref: InputRef,
2674 ) -> Result<Vec<HandlerExecution>, ReactiveError> {
2675 let mut executions = Vec::new();
2676 let candidates: Vec<_> = match &record.input {
2677 ReactiveInput::Log(log) => self.registry.log_handler_candidates(log),
2678 ReactiveInput::BlockHeader(_)
2679 | ReactiveInput::FullBlock(_)
2680 | ReactiveInput::PendingTxHash(_)
2681 | ReactiveInput::PendingTx(_) => self.registry.handlers().collect(),
2682 };
2683 for registered in candidates {
2684 if !registered.matches(&record.input) {
2685 continue;
2686 }
2687
2688 let outcome = registered
2689 .handler
2690 .handle(&record.context, &record.input, cache)
2691 .map_err(|source| ReactiveError::HandlerFailed {
2692 handler_id: registered.id.clone(),
2693 source,
2694 })?;
2695
2696 if let Err(error) =
2697 validate_effects(input_ref, &record.context, ®istered.id, &outcome.effects)
2698 {
2699 if matches!(error, ReactiveError::InvalidPendingEffect { .. }) {
2700 self.metrics
2701 .pending_contamination
2702 .fetch_add(1, Ordering::Relaxed);
2703 }
2704 return Err(error);
2705 }
2706 executions.push(HandlerExecution::from_outcome(
2707 registered.id.clone(),
2708 input_ref,
2709 outcome,
2710 ));
2711 }
2712 Ok(executions)
2713 }
2714
2715 fn dispatch_reports(&self, reports: &[Arc<ReactiveReport<N>>]) {
2716 for report in reports {
2717 for hook in &self.hooks {
2718 hook.on_report(report.clone());
2719 }
2720 }
2721 }
2722
2723 fn recover_for_canonical_input(
2724 &mut self,
2725 cache: &mut EvmCache,
2726 record: &ReactiveInputRecord<N>,
2727 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
2728 ) -> Option<ReorgReport<N>> {
2729 let block = canonical_record_block(record)?;
2730 let latest = self.journal.back()?.block.clone();
2731
2732 if self
2733 .journal
2734 .iter()
2735 .any(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
2736 {
2737 return None;
2738 }
2739
2740 if block.number == latest.number.saturating_add(1) && block.parent_hash == Some(latest.hash)
2741 {
2742 return None;
2743 }
2744
2745 if block.number > latest.number.saturating_add(1) {
2746 self.metrics.missed_ranges.fetch_add(1, Ordering::Relaxed);
2751 health_reports.extend(self.escalate_trust(block.number));
2752 health_reports.push(Arc::new(ReactiveReport::MissedBlockRange(
2753 MissedRangeReport {
2754 from: latest.number + 1,
2755 to: block.number - 1,
2756 block: block.number,
2757 _network: PhantomData,
2758 },
2759 )));
2760 return None;
2761 }
2762
2763 let dropped = if let Some(parent_hash) = block.parent_hash {
2764 if let Some(parent_index) = self
2765 .journal
2766 .iter()
2767 .rposition(|entry| entry.block.hash == parent_hash)
2768 {
2769 self.drain_journal_after(parent_index)
2770 } else {
2771 health_reports.extend(self.warn_under_recovery(block.number));
2772 self.drain_journal_from_number(block.number)
2773 }
2774 } else {
2775 health_reports.extend(self.warn_under_recovery(block.number));
2776 self.drain_journal_from_number(block.number)
2777 };
2778
2779 self.recover_dropped_journals(cache, dropped, ReorgReason::ParentMismatch)
2780 }
2781
2782 fn recover_for_reorged_input(
2783 &mut self,
2784 cache: &mut EvmCache,
2785 record: &ReactiveInputRecord<N>,
2786 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
2787 ) -> Option<ReorgReport<N>> {
2788 let (dropped_block, reason) = reorg_signal_block(record)?;
2789 let dropped = if let Some(index) = self
2790 .journal
2791 .iter()
2792 .position(|entry| entry.block.hash == dropped_block.hash)
2793 {
2794 self.drain_journal_from(index)
2795 } else {
2796 health_reports.extend(self.warn_under_recovery(dropped_block.number));
2797 self.drain_journal_from_number(dropped_block.number)
2798 };
2799
2800 if dropped.is_empty() {
2801 let canceled_resyncs =
2802 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
2803 if canceled_resyncs.is_empty() {
2804 return None;
2805 }
2806 return Some(ReorgReport {
2807 dropped: Some(dropped_block.clone()),
2808 dropped_blocks: vec![dropped_block],
2809 dropped_inputs: Vec::new(),
2810 rollback_updates: Vec::new(),
2811 rollback_diff: StateDiff::default(),
2812 purge_updates: Vec::new(),
2813 purge_diff: StateDiff::default(),
2814 canceled_resyncs,
2815 reason,
2816 _network: PhantomData,
2817 });
2818 }
2819
2820 self.recover_dropped_journals(cache, dropped, reason)
2821 }
2822
2823 fn warn_under_recovery(&mut self, reorg_number: u64) -> Option<Arc<ReactiveReport<N>>> {
2835 let oldest_journaled = self.journal.front().map(|entry| entry.block.number);
2836 tracing::warn!(
2837 reorg_block = reorg_number,
2838 oldest_journaled = ?oldest_journaled,
2839 journal_depth = self.config.journal_depth,
2840 "reactive reorg recovery is incomplete: the reorged block is no longer \
2841 in the journal, so effects from blocks aged out of the journal are \
2842 neither rolled back nor purged (the freshness/validation loop is the \
2843 backstop). Increase ReactiveConfig::journal_depth to recover deeper \
2844 reorgs precisely."
2845 );
2846
2847 self.metrics.deep_reorgs.fetch_add(1, Ordering::Relaxed);
2848
2849 self.escalate_trust(reorg_number)
2850 }
2851
2852 fn record_journal_input(&mut self, block: &BlockRef, input_ref: InputRef) {
2853 let entry = self.journal_entry_mut(block);
2854 if !entry.inputs.contains(&input_ref) {
2855 entry.inputs.push(input_ref);
2856 }
2857 self.trim_journal();
2858 }
2859
2860 fn record_journal_applied(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
2861 self.journal_entry_mut(block).applied.push(applied);
2862 self.trim_journal();
2863 }
2864
2865 fn record_journal_resync(&mut self, report: &ResyncReport) {
2866 if report.diff.is_empty() {
2867 return;
2868 }
2869 let Some(block) = single_hash_pinned_resync_block(report) else {
2870 return;
2871 };
2872 self.journal_entry_mut(&block).resynced.push(report.clone());
2873 self.trim_journal();
2874 }
2875
2876 fn journal_entry_mut(&mut self, block: &BlockRef) -> &mut BlockJournal<N> {
2877 if let Some(index) = self
2878 .journal
2879 .iter()
2880 .position(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
2881 {
2882 return &mut self.journal[index];
2883 }
2884
2885 self.journal.push_back(BlockJournal {
2886 block: block.clone(),
2887 inputs: Vec::new(),
2888 applied: Vec::new(),
2889 resynced: Vec::new(),
2890 });
2891 let index = self.journal.len() - 1;
2892 &mut self.journal[index]
2893 }
2894
2895 fn trim_journal(&mut self) {
2896 if self.config.journal_depth == 0 {
2897 self.journal.clear();
2898 return;
2899 }
2900 while self.journal.len() > self.config.journal_depth {
2901 self.journal.pop_front();
2902 }
2903 }
2904
2905 fn drain_journal_after(&mut self, index: usize) -> Vec<BlockJournal<N>> {
2906 self.journal.drain((index + 1)..).collect()
2907 }
2908
2909 fn drain_journal_from(&mut self, index: usize) -> Vec<BlockJournal<N>> {
2910 self.journal.drain(index..).collect()
2911 }
2912
2913 fn drain_journal_from_number(&mut self, number: u64) -> Vec<BlockJournal<N>> {
2914 let Some(index) = self
2915 .journal
2916 .iter()
2917 .position(|entry| entry.block.number >= number)
2918 else {
2919 return Vec::new();
2920 };
2921 self.drain_journal_from(index)
2922 }
2923
2924 fn recover_dropped_journals(
2925 &mut self,
2926 cache: &mut EvmCache,
2927 dropped: Vec<BlockJournal<N>>,
2928 reason: ReorgReason,
2929 ) -> Option<ReorgReport<N>> {
2930 if dropped.is_empty() {
2931 return None;
2932 }
2933
2934 let dropped_blocks: Vec<_> = dropped.iter().map(|entry| entry.block.clone()).collect();
2935 let dropped_inputs: Vec<_> = dropped
2936 .iter()
2937 .flat_map(|entry| entry.inputs.iter().copied())
2938 .collect();
2939 let canceled_resyncs = self.cancel_resyncs_for_dropped_blocks(&dropped_blocks);
2940 let purge_scopes = purge_scopes_for_dropped_journals(&dropped);
2941 let rollback_updates = rollback_updates_for_dropped_journals(&dropped, &purge_scopes);
2942 let purge_updates: Vec<_> = purge_scopes
2943 .iter()
2944 .map(|(address, scope)| StateUpdate::purge(*address, scope.clone()))
2945 .collect();
2946
2947 let rollback_diff = if rollback_updates.is_empty() {
2948 StateDiff::default()
2949 } else {
2950 cache.apply_updates(&rollback_updates)
2951 };
2952 let purge_diff = if purge_updates.is_empty() {
2953 StateDiff::default()
2954 } else {
2955 cache.apply_updates(&purge_updates)
2956 };
2957
2958 Some(ReorgReport {
2959 dropped: dropped_blocks.first().cloned(),
2960 dropped_blocks,
2961 dropped_inputs,
2962 rollback_updates,
2963 rollback_diff,
2964 purge_updates,
2965 purge_diff,
2966 canceled_resyncs,
2967 reason,
2968 _network: PhantomData,
2969 })
2970 }
2971
2972 fn cancel_resyncs_for_dropped_blocks(
2973 &mut self,
2974 dropped_blocks: &[BlockRef],
2975 ) -> Vec<ResyncRequest> {
2976 let mut canceled = Vec::new();
2977 self.pending_resyncs.retain(|request| {
2978 let should_cancel = resync_request_targets_dropped_block(request, dropped_blocks);
2979 if should_cancel {
2980 canceled.push(request.clone());
2981 }
2982 !should_cancel
2983 });
2984 canceled
2985 }
2986
2987 fn remove_pending_resyncs<'a>(&mut self, ids: impl IntoIterator<Item = &'a ResyncId>) {
2988 let ids: HashSet<_> = ids.into_iter().cloned().collect();
2989 self.pending_resyncs
2990 .retain(|request| !ids.contains(&request.id));
2991 }
2992}
2993
2994fn collect_diff_addresses(diff: &StateDiff, into: &mut HashSet<Address>) {
3000 into.extend(diff.slots.iter().map(|change| change.address));
3001 into.extend(diff.accounts.iter().map(|change| change.address));
3002 into.extend(diff.purged.iter().map(|purge| purge.address));
3003 into.extend(diff.skipped.iter().map(|skipped| skipped.address));
3004 into.extend(diff.skipped_balances.iter().map(|skipped| skipped.address));
3005 into.extend(diff.skipped_masks.iter().map(|skipped| skipped.address));
3006 into.extend(diff.skipped_accounts.iter().map(|skipped| skipped.address));
3007}
3008
3009fn root_moved_account_resync(
3014 address: Address,
3015 block: u64,
3016 fields: AccountFieldMask,
3017) -> ResyncRequest {
3018 ResyncRequest {
3019 id: ResyncId::new(format!("root-moved:{address:#x}:{block}")),
3020 reason: ResyncReason::RootMoved,
3021 block: ResyncBlock::Number(block),
3022 targets: vec![ResyncTarget::Account { address, fields }],
3023 priority: ResyncPriority::Normal,
3024 }
3025}
3026
3027fn canonical_record_block<N: Network>(record: &ReactiveInputRecord<N>) -> Option<&BlockRef> {
3028 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
3029 return None;
3030 }
3031 if is_canonical_status(&record.context.chain_status) {
3032 return context_block_ref(&record.context);
3033 }
3034 None
3035}
3036
3037fn advance_block_for_canonical_record<N: Network>(
3046 cache: &mut EvmCache,
3047 record: &ReactiveInputRecord<N>,
3048) -> Option<Result<(), BlockContextError>> {
3049 if !is_canonical_status(&record.context.chain_status) {
3050 return None;
3051 }
3052 match &record.input {
3053 ReactiveInput::BlockHeader(header) => Some(cache.advance_block(header)),
3054 ReactiveInput::FullBlock(block) => Some(cache.advance_block(block.header())),
3055 _ => None,
3056 }
3057}
3058
3059fn context_block_ref(ctx: &ReactiveContext) -> Option<&BlockRef> {
3060 match &ctx.chain_status {
3061 ChainStatus::Included { block, .. }
3062 | ChainStatus::Safe { block }
3063 | ChainStatus::Finalized { block } => Some(block),
3064 ChainStatus::Reorged { dropped_from } => Some(dropped_from),
3065 ChainStatus::Pending => ctx.block.as_ref(),
3066 }
3067}
3068
3069fn reorg_signal_block<N: Network>(
3070 record: &ReactiveInputRecord<N>,
3071) -> Option<(BlockRef, ReorgReason)> {
3072 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
3073 return block_ref_from_record(record).map(|block| (block, ReorgReason::RemovedLog));
3074 }
3075
3076 if let ChainStatus::Reorged { dropped_from } = &record.context.chain_status {
3077 return Some((dropped_from.clone(), ReorgReason::ReorgedInput));
3078 }
3079
3080 None
3081}
3082
3083fn block_ref_from_record<N: Network>(record: &ReactiveInputRecord<N>) -> Option<BlockRef> {
3084 context_block_ref(&record.context)
3085 .cloned()
3086 .or_else(|| match &record.input {
3087 ReactiveInput::Log(log) => Some(BlockRef {
3088 number: log.block_number?,
3089 hash: log.block_hash?,
3090 parent_hash: None,
3091 timestamp: log.block_timestamp,
3092 }),
3093 ReactiveInput::BlockHeader(header) => Some(BlockRef {
3094 number: header.number(),
3095 hash: header.hash(),
3096 parent_hash: Some(header.parent_hash()),
3097 timestamp: Some(header.timestamp()),
3098 }),
3099 ReactiveInput::FullBlock(block) => {
3100 let header = block.header();
3101 Some(BlockRef {
3102 number: header.number(),
3103 hash: header.hash(),
3104 parent_hash: Some(header.parent_hash()),
3105 timestamp: Some(header.timestamp()),
3106 })
3107 }
3108 ReactiveInput::PendingTxHash(_) | ReactiveInput::PendingTx(_) => None,
3109 })
3110}
3111
3112fn remove_canceled_resyncs_from_batch(
3113 resyncs: &mut Vec<ResyncRequest>,
3114 canceled: &[ResyncRequest],
3115) {
3116 if canceled.is_empty() {
3117 return;
3118 }
3119 let canceled_ids: HashSet<_> = canceled.iter().map(|request| request.id.clone()).collect();
3120 resyncs.retain(|request| !canceled_ids.contains(&request.id));
3121}
3122
3123fn resync_target_address(target: &ResyncTarget) -> Address {
3124 match target {
3125 ResyncTarget::StorageSlot { address, .. }
3126 | ResyncTarget::StorageSlots { address, .. }
3127 | ResyncTarget::Account { address, .. } => *address,
3128 }
3129}
3130
3131fn resync_request_targets_dropped_block(
3132 request: &ResyncRequest,
3133 dropped_blocks: &[BlockRef],
3134) -> bool {
3135 let ResyncBlock::Hash { number, hash, .. } = &request.block else {
3136 return false;
3137 };
3138 dropped_blocks
3139 .iter()
3140 .any(|block| block.hash == *hash && block.number == *number)
3141}
3142
3143fn single_hash_pinned_resync_block(report: &ResyncReport) -> Option<BlockRef> {
3144 let first = report.requested.first()?.block.clone();
3145 if !report
3146 .requested
3147 .iter()
3148 .all(|request| request.block == first)
3149 {
3150 return None;
3151 }
3152
3153 let ResyncBlock::Hash { number, hash, .. } = first else {
3154 return None;
3155 };
3156
3157 Some(BlockRef {
3158 number,
3159 hash,
3160 parent_hash: None,
3161 timestamp: None,
3162 })
3163}
3164
3165fn purge_scopes_for_dropped_journals<N: Network>(
3166 dropped: &[BlockJournal<N>],
3167) -> Vec<(Address, PurgeScope)> {
3168 let mut scopes: Vec<(Address, PurgeScope)> = Vec::new();
3169 for entry in dropped.iter().rev() {
3170 for resynced in entry.resynced.iter().rev() {
3171 merge_purge_scopes_for_diff(&mut scopes, &resynced.diff);
3172 }
3173 for applied in entry.applied.iter().rev() {
3174 merge_purge_scopes_for_diff(&mut scopes, &applied.diff);
3175 }
3176 }
3177 scopes
3178}
3179
3180fn rollback_updates_for_dropped_journals<N: Network>(
3181 dropped: &[BlockJournal<N>],
3182 purge_scopes: &[(Address, PurgeScope)],
3183) -> Vec<StateUpdate> {
3184 let purge_addresses: HashSet<_> = purge_scopes
3185 .iter()
3186 .map(|(address, _scope)| *address)
3187 .collect();
3188 let mut updates = Vec::new();
3189 for entry in dropped.iter().rev() {
3190 for resynced in entry.resynced.iter().rev() {
3191 push_rollback_updates_for_diff(&mut updates, &resynced.diff, &purge_addresses);
3192 }
3193 for applied in entry.applied.iter().rev() {
3194 push_rollback_updates_for_diff(&mut updates, &applied.diff, &purge_addresses);
3195 }
3196 }
3197 updates
3198}
3199
3200fn merge_purge_scopes_for_diff(scopes: &mut Vec<(Address, PurgeScope)>, diff: &StateDiff) {
3201 for change in &diff.accounts {
3202 merge_purge_scope(scopes, change.address, PurgeScope::Account);
3203 }
3204 for record in &diff.purged {
3205 merge_purge_scope(scopes, record.address, record.scope.clone());
3206 }
3207}
3208
3209fn push_rollback_updates_for_diff(
3210 updates: &mut Vec<StateUpdate>,
3211 diff: &StateDiff,
3212 purge_addresses: &HashSet<Address>,
3213) {
3214 for change in diff.slots.iter().rev() {
3215 if purge_addresses.contains(&change.address) {
3216 continue;
3217 }
3218 updates.push(StateUpdate::slot(change.address, change.slot, change.old));
3219 }
3220}
3221
3222fn merge_purge_scope(scopes: &mut Vec<(Address, PurgeScope)>, address: Address, scope: PurgeScope) {
3223 if let Some((_existing_address, existing_scope)) = scopes
3224 .iter_mut()
3225 .find(|(existing_address, _scope)| *existing_address == address)
3226 {
3227 *existing_scope = merged_purge_scope(existing_scope.clone(), scope);
3228 } else {
3229 scopes.push((address, scope));
3230 }
3231}
3232
3233fn merged_purge_scope(left: PurgeScope, right: PurgeScope) -> PurgeScope {
3234 match (left, right) {
3235 (PurgeScope::Account, _) | (_, PurgeScope::Account) => PurgeScope::Account,
3236 (PurgeScope::AllStorage, _) | (_, PurgeScope::AllStorage) => PurgeScope::AllStorage,
3237 (PurgeScope::Slots(mut left), PurgeScope::Slots(right)) => {
3238 for slot in right {
3239 if !left.contains(&slot) {
3240 left.push(slot);
3241 }
3242 }
3243 PurgeScope::Slots(left)
3244 }
3245 }
3246}
3247
3248#[derive(Clone, Debug)]
3249struct StorageFetchSlot {
3250 address: Address,
3251 slot: U256,
3252 origins: Vec<StorageFetchOrigin>,
3253}
3254
3255#[derive(Clone, Debug)]
3256struct StorageFetchOrigin {
3257 request_id: ResyncId,
3258 target: ResyncTarget,
3259}
3260
3261#[derive(Clone, Debug)]
3262struct StorageFetchGroup {
3263 block: ResyncBlock,
3264 slots: Vec<StorageFetchSlot>,
3265 seen: HashSet<(Address, U256)>,
3266}
3267
3268#[derive(Clone, Debug)]
3271struct AccountResyncTarget {
3272 request_id: ResyncId,
3273 block: ResyncBlock,
3274 address: Address,
3275 fields: AccountFieldMask,
3276}
3277
3278fn resolve_trace_resyncs(
3279 cache: &EvmCache,
3280 storage_groups: &mut Vec<StorageFetchGroup>,
3281 account_targets: &mut Vec<AccountResyncTarget>,
3282 state_updates: &mut Vec<StateUpdate>,
3283) {
3284 let Some(fetcher) = cache.block_state_diff_fetcher().cloned() else {
3285 return;
3286 };
3287
3288 let mut blocks = Vec::new();
3289 let mut seen = HashSet::new();
3290 for block in storage_groups
3291 .iter()
3292 .map(|group| group.block.clone())
3293 .chain(account_targets.iter().map(|target| target.block.clone()))
3294 {
3295 if seen.insert(block.clone()) {
3296 blocks.push(block);
3297 }
3298 }
3299
3300 let mut traces = HashMap::new();
3301 for block in blocks {
3302 match (fetcher)(resync_block_to_block_id(&block)) {
3303 Ok(diff) => {
3304 traces.insert(block, diff);
3305 }
3306 Err(error) => {
3307 tracing::debug!(
3308 block = ?block,
3309 error = %error,
3310 "block trace resync source failed; falling back to point resync"
3311 );
3312 }
3313 }
3314 }
3315
3316 for group in storage_groups.iter_mut() {
3317 let Some(trace) = traces.get(&group.block) else {
3318 continue;
3319 };
3320 group.slots.retain(|slot| {
3321 if let Some(value) = trace_storage_value(trace, slot.address, slot.slot) {
3322 state_updates.push(StateUpdate::slot(slot.address, slot.slot, value));
3323 return false;
3324 }
3325 cache
3326 .cached_storage_value(slot.address, slot.slot)
3327 .is_none()
3328 });
3329 group.seen = group
3330 .slots
3331 .iter()
3332 .map(|slot| (slot.address, slot.slot))
3333 .collect();
3334 }
3335 storage_groups.retain(|group| !group.slots.is_empty());
3336
3337 let mut unresolved_accounts = Vec::new();
3338 for mut account in account_targets.drain(..) {
3339 let Some(trace) = traces.get(&account.block) else {
3340 unresolved_accounts.push(account);
3341 continue;
3342 };
3343 let Some(trace_account) = trace
3344 .accounts
3345 .iter()
3346 .find(|diff| diff.address == account.address)
3347 else {
3348 unresolved_accounts.push(account);
3349 continue;
3350 };
3351
3352 let mut patch = AccountPatch::default();
3353 let mut unresolved = AccountFieldMask::default();
3354 if account.fields.balance {
3355 if let Some(balance) = trace_account.balance {
3356 patch = patch.balance(balance);
3357 } else {
3358 unresolved.balance = true;
3359 }
3360 }
3361 if account.fields.nonce {
3362 if let Some(nonce) = trace_account.nonce {
3363 patch = patch.nonce(nonce);
3364 } else {
3365 unresolved.nonce = true;
3366 }
3367 }
3368 if account.fields.code {
3369 if let Some(code) = &trace_account.code {
3370 patch = patch.code(code.clone());
3371 } else {
3372 unresolved.code = true;
3373 }
3374 }
3375
3376 if patch.balance.is_some() || patch.nonce.is_some() || patch.code.is_some() {
3377 state_updates.push(StateUpdate::account_upsert(account.address, patch));
3378 }
3379 if !account_field_mask_empty(unresolved) {
3380 account.fields = unresolved;
3381 unresolved_accounts.push(account);
3382 }
3383 }
3384 *account_targets = unresolved_accounts;
3385}
3386
3387fn trace_storage_value(trace: &BlockStateDiff, address: Address, slot: U256) -> Option<U256> {
3388 trace
3389 .accounts
3390 .iter()
3391 .find(|account| account.address == address)
3392 .and_then(|account| {
3393 account
3394 .storage
3395 .iter()
3396 .find(|entry| entry.slot == slot)
3397 .map(|entry| entry.value)
3398 })
3399}
3400
3401fn account_field_mask_empty(mask: AccountFieldMask) -> bool {
3402 !mask.balance && !mask.nonce && !mask.code
3403}
3404
3405fn execute_resync_requests(cache: &mut EvmCache, requests: &[ResyncRequest]) -> ResyncReport {
3406 let mut failed = Vec::new();
3407 let mut storage_groups: Vec<StorageFetchGroup> = Vec::new();
3408 let mut account_targets: Vec<AccountResyncTarget> = Vec::new();
3409
3410 for request in requests {
3411 for target in &request.targets {
3412 match target {
3413 ResyncTarget::StorageSlot { address, slot } => {
3414 push_storage_resync_slot(
3415 &mut storage_groups,
3416 &request.id,
3417 &request.block,
3418 *address,
3419 *slot,
3420 );
3421 }
3422 ResyncTarget::StorageSlots { address, slots } => {
3423 for slot in slots {
3424 push_storage_resync_slot(
3425 &mut storage_groups,
3426 &request.id,
3427 &request.block,
3428 *address,
3429 *slot,
3430 );
3431 }
3432 }
3433 ResyncTarget::Account { address, fields } => {
3434 account_targets.push(AccountResyncTarget {
3435 request_id: request.id.clone(),
3436 block: request.block.clone(),
3437 address: *address,
3438 fields: *fields,
3439 });
3440 }
3441 }
3442 }
3443 }
3444
3445 let mut state_updates = Vec::new();
3446 resolve_trace_resyncs(
3447 cache,
3448 &mut storage_groups,
3449 &mut account_targets,
3450 &mut state_updates,
3451 );
3452
3453 if !storage_groups.is_empty() {
3454 if let Some(fetcher) = cache.storage_batch_fetcher().cloned() {
3455 for group in storage_groups {
3456 let block = group.block.clone();
3457 let fetches: Vec<(Address, U256)> = group
3458 .slots
3459 .iter()
3460 .map(|slot| (slot.address, slot.slot))
3461 .collect();
3462 let results = (fetcher)(fetches, resync_block_to_block_id(&block));
3463 let mut pending: HashMap<(Address, U256), StorageFetchSlot> = group
3464 .slots
3465 .iter()
3466 .cloned()
3467 .map(|slot| ((slot.address, slot.slot), slot))
3468 .collect();
3469
3470 for (address, slot, fetched) in results {
3471 let Some(requested_slot) = pending.remove(&(address, slot)) else {
3472 continue;
3473 };
3474 match fetched {
3475 Ok(value) => state_updates.push(StateUpdate::slot(address, slot, value)),
3476 Err(error) => {
3477 let message = error.to_string();
3478 push_resync_failures(
3479 &mut failed,
3480 &block,
3481 requested_slot.origins,
3482 ResyncFailureKind::StorageFetchFailed,
3483 message,
3484 );
3485 }
3486 }
3487 }
3488
3489 for requested_slot in group.slots {
3490 if pending
3491 .remove(&(requested_slot.address, requested_slot.slot))
3492 .is_some()
3493 {
3494 push_resync_failures(
3495 &mut failed,
3496 &block,
3497 requested_slot.origins,
3498 ResyncFailureKind::StorageFetchOmitted,
3499 "storage batch fetcher did not return a value for slot".to_string(),
3500 );
3501 }
3502 }
3503 }
3504 } else {
3505 for group in storage_groups {
3506 let block = group.block.clone();
3507 for slot in group.slots {
3508 push_resync_failures(
3509 &mut failed,
3510 &block,
3511 slot.origins,
3512 ResyncFailureKind::MissingStorageFetcher,
3513 "storage resync requires a storage batch fetcher".to_string(),
3514 );
3515 }
3516 }
3517 }
3518 }
3519
3520 if !account_targets.is_empty() {
3521 if let Some(fetcher) = cache.account_proof_fetcher().cloned() {
3522 let mut groups: Vec<(BlockId, Vec<_>)> = Vec::new();
3528 for account in account_targets {
3529 let block_id = resync_block_to_block_id(&account.block);
3530 match groups
3531 .iter_mut()
3532 .find(|(group_block, _)| *group_block == block_id)
3533 {
3534 Some((_, group)) => group.push(account),
3535 None => groups.push((block_id, vec![account])),
3536 }
3537 }
3538 for (block_id, group) in groups {
3539 let probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
3540 group
3541 .iter()
3542 .map(|account| (account.address, vec![]))
3543 .collect(),
3544 block_id,
3545 )
3546 .into_iter()
3547 .collect();
3548 for account in group {
3549 match probes.get(&account.address).cloned() {
3553 Some(Ok(proof)) => {
3554 let mut patch = AccountPatch::default();
3559 if account.fields.balance {
3560 patch = patch.balance(proof.balance);
3561 }
3562 if account.fields.nonce {
3563 patch = patch.nonce(proof.nonce);
3564 }
3565 state_updates.push(StateUpdate::account_upsert(account.address, patch));
3571 }
3572 Some(Err(error)) => {
3573 failed.push(ResyncFailure {
3574 request_id: account.request_id,
3575 block: account.block,
3576 target: ResyncTarget::Account {
3577 address: account.address,
3578 fields: account.fields,
3579 },
3580 kind: ResyncFailureKind::AccountFetchFailed,
3581 message: error.to_string(),
3582 });
3583 }
3584 None => {
3585 failed.push(ResyncFailure {
3586 request_id: account.request_id,
3587 block: account.block,
3588 target: ResyncTarget::Account {
3589 address: account.address,
3590 fields: account.fields,
3591 },
3592 kind: ResyncFailureKind::AccountFetchOmitted,
3593 message:
3594 "account proof fetcher did not return a result for address"
3595 .to_string(),
3596 });
3597 }
3598 }
3599 }
3600 }
3601 } else {
3602 for account in account_targets {
3603 failed.push(ResyncFailure {
3604 request_id: account.request_id,
3605 block: account.block,
3606 target: ResyncTarget::Account {
3607 address: account.address,
3608 fields: account.fields,
3609 },
3610 kind: ResyncFailureKind::MissingAccountFetcher,
3611 message: "account resync requires an account proof fetcher".to_string(),
3612 });
3613 }
3614 }
3615 }
3616
3617 let diff = if state_updates.is_empty() {
3618 StateDiff::default()
3619 } else {
3620 cache.apply_updates(&state_updates)
3621 };
3622
3623 ResyncReport {
3624 requested: requests.to_vec(),
3625 state_updates,
3626 diff,
3627 failed,
3628 }
3629}
3630
3631fn push_resync_failures(
3632 failed: &mut Vec<ResyncFailure>,
3633 block: &ResyncBlock,
3634 origins: Vec<StorageFetchOrigin>,
3635 kind: ResyncFailureKind,
3636 message: String,
3637) {
3638 for origin in origins {
3639 failed.push(ResyncFailure {
3640 request_id: origin.request_id,
3641 block: block.clone(),
3642 target: origin.target,
3643 kind,
3644 message: message.clone(),
3645 });
3646 }
3647}
3648
3649fn push_storage_resync_slot(
3650 groups: &mut Vec<StorageFetchGroup>,
3651 request_id: &ResyncId,
3652 block: &ResyncBlock,
3653 address: Address,
3654 slot: U256,
3655) {
3656 let group_index = if let Some(index) = groups.iter().position(|group| group.block == *block) {
3657 index
3658 } else {
3659 groups.push(StorageFetchGroup {
3660 block: block.clone(),
3661 slots: Vec::new(),
3662 seen: HashSet::new(),
3663 });
3664 groups.len() - 1
3665 };
3666
3667 let group = &mut groups[group_index];
3668 let origin = StorageFetchOrigin {
3669 request_id: request_id.clone(),
3670 target: ResyncTarget::StorageSlot { address, slot },
3671 };
3672 if group.seen.insert((address, slot)) {
3673 group.slots.push(StorageFetchSlot {
3674 address,
3675 slot,
3676 origins: vec![origin],
3677 });
3678 } else if let Some(existing) = group
3679 .slots
3680 .iter_mut()
3681 .find(|existing| existing.address == address && existing.slot == slot)
3682 {
3683 existing.origins.push(origin);
3684 }
3685}
3686
3687fn resync_block_to_block_id(block: &ResyncBlock) -> BlockId {
3688 match block {
3689 ResyncBlock::Latest => BlockId::latest(),
3690 ResyncBlock::Safe => BlockId::safe(),
3691 ResyncBlock::Finalized => BlockId::finalized(),
3692 ResyncBlock::Number(number) => BlockId::number(*number),
3693 ResyncBlock::Hash {
3694 number: _,
3695 hash,
3696 require_canonical,
3697 } => BlockId::from((*hash, Some(*require_canonical))),
3698 }
3699}
3700
3701impl<N: Network> RegisteredHandler<N> {
3702 fn matches(&self, input: &ReactiveInput<N>) -> bool {
3703 self.interests
3704 .iter()
3705 .any(|interest| interest_matches(interest, input))
3706 }
3707
3708 fn route_log(&self, log: &Log) -> Option<ReactiveLogRoute> {
3709 self.interests.iter().find_map(|interest| match interest {
3710 ReactiveInterest::Logs(interest) if interest.matches(log) => Some(ReactiveLogRoute {
3711 handler_id: self.id.clone(),
3712 route_key: interest.route_key(log),
3713 }),
3714 ReactiveInterest::Logs(_)
3715 | ReactiveInterest::Blocks(_)
3716 | ReactiveInterest::PendingTransactions(_) => None,
3717 })
3718 }
3719}
3720
3721fn merge_log_subscription_filter(filters: &mut Vec<Filter>, next: &Filter) {
3722 if let Some(existing) = filters
3723 .iter_mut()
3724 .find(|existing| existing.block_option == next.block_option)
3725 {
3726 merge_filter_set(&mut existing.address, &next.address);
3727 for (existing_topic, next_topic) in existing.topics.iter_mut().zip(next.topics.iter()) {
3728 merge_filter_set(existing_topic, next_topic);
3729 }
3730 } else {
3731 filters.push(next.clone());
3732 }
3733}
3734
3735fn merge_filter_set<T: Clone + Eq + Hash>(target: &mut FilterSet<T>, source: &FilterSet<T>) {
3736 if target.is_empty() {
3737 return;
3738 }
3739 if source.is_empty() {
3740 *target = FilterSet::default();
3741 return;
3742 }
3743 for value in source.iter() {
3744 target.insert(value.clone());
3745 }
3746}
3747
3748#[derive(Clone, Debug)]
3749struct HandlerExecution {
3750 handler_id: HandlerId,
3751 quality: StateEffectQuality,
3752 tags: Vec<ReportTag>,
3753 state_updates: Vec<StateUpdate>,
3754 invalidations: Vec<InvalidationRequest>,
3755 resyncs: Vec<ResyncRequest>,
3756 speculative: Vec<SpeculativeRequest>,
3757 hook_signals: Vec<HookSignal>,
3758}
3759
3760impl HandlerExecution {
3761 fn from_outcome(handler_id: HandlerId, input_ref: InputRef, outcome: HandlerOutcome) -> Self {
3762 let mut state_updates = Vec::new();
3763 let mut invalidations = Vec::new();
3764 let mut resyncs = Vec::new();
3765 let mut speculative = Vec::new();
3766 let mut hook_signals = Vec::new();
3767
3768 for effect in outcome.effects {
3769 match effect {
3770 ReactiveEffect::StateUpdate(update) => state_updates.push(update),
3771 ReactiveEffect::Invalidate(invalidation) => {
3772 state_updates.push(StateUpdate::purge(
3773 invalidation.address,
3774 invalidation.scope.clone(),
3775 ));
3776 invalidations.push(invalidation);
3777 }
3778 ReactiveEffect::Resync(request) => resyncs.push(request),
3779 ReactiveEffect::Hook(signal) => hook_signals.push(signal),
3780 ReactiveEffect::Speculative(mut request) => {
3781 request.input_ref = input_ref;
3782 speculative.push(request);
3783 }
3784 }
3785 }
3786
3787 Self {
3788 handler_id,
3789 quality: outcome.quality,
3790 tags: outcome.tags,
3791 state_updates,
3792 invalidations,
3793 resyncs,
3794 speculative,
3795 hook_signals,
3796 }
3797 }
3798}
3799
3800fn dedupe_records<N: Network>(records: Vec<ReactiveInputRecord<N>>) -> Vec<ReactiveInputRecord<N>> {
3801 let mut seen = HashSet::new();
3802 let mut deduped = Vec::with_capacity(records.len());
3803 for record in records {
3804 if seen.insert(record.input_ref()) {
3805 deduped.push(record);
3806 }
3807 }
3808 deduped
3809}
3810
3811fn sort_records<N: Network>(records: Vec<ReactiveInputRecord<N>>) -> Vec<ReactiveInputRecord<N>> {
3812 let mut indexed: Vec<(usize, ReactiveInputRecord<N>)> =
3813 records.into_iter().enumerate().collect();
3814 indexed.sort_by_key(|(index, record)| record_sort_key(*index, record));
3815 indexed.into_iter().map(|(_, record)| record).collect()
3816}
3817
3818fn record_sort_key<N: Network>(index: usize, record: &ReactiveInputRecord<N>) -> RecordSortKey {
3819 if let ReactiveInput::Log(log) = &record.input
3820 && is_canonical_status(&record.context.chain_status)
3821 && !log.removed
3822 {
3823 return RecordSortKey {
3824 class: 0,
3825 block_number: log
3826 .block_number
3827 .or(record.context.block.as_ref().map(|block| block.number))
3828 .unwrap_or(u64::MAX),
3829 transaction_index: log
3830 .transaction_index
3831 .or(record.context.transaction_index)
3832 .unwrap_or(u64::MAX),
3833 log_index: log
3834 .log_index
3835 .or(record.context.log_index)
3836 .unwrap_or(u64::MAX),
3837 original_index: index,
3838 };
3839 }
3840
3841 RecordSortKey {
3842 class: 1,
3843 block_number: 0,
3844 transaction_index: 0,
3845 log_index: 0,
3846 original_index: index,
3847 }
3848}
3849
3850#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
3851struct RecordSortKey {
3852 class: u8,
3853 block_number: u64,
3854 transaction_index: u64,
3855 log_index: u64,
3856 original_index: usize,
3857}
3858
3859fn interest_matches<N: Network>(interest: &ReactiveInterest<N>, input: &ReactiveInput<N>) -> bool {
3860 match (interest, input) {
3861 (ReactiveInterest::Logs(interest), ReactiveInput::Log(log)) => interest.matches(log),
3862 (
3863 ReactiveInterest::Blocks(BlockInterest {
3864 mode: BlockInterestMode::Header,
3865 }),
3866 ReactiveInput::BlockHeader(_),
3867 ) => true,
3868 (
3869 ReactiveInterest::Blocks(BlockInterest {
3870 mode: BlockInterestMode::FullBlock,
3871 }),
3872 ReactiveInput::FullBlock(_),
3873 ) => true,
3874 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTxHash(_)) => {
3875 interest.matches_hash_only()
3876 }
3877 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTx(tx)) => {
3878 interest.matches_tx(tx)
3879 }
3880 _ => false,
3881 }
3882}
3883
3884fn validate_effects(
3885 input_ref: InputRef,
3886 ctx: &ReactiveContext,
3887 handler_id: &HandlerId,
3888 effects: &[ReactiveEffect],
3889) -> Result<(), ReactiveError> {
3890 let pending = matches!(ctx.chain_status, ChainStatus::Pending)
3891 || matches!(input_ref, InputRef::PendingTx { .. });
3892 if !pending {
3893 return Ok(());
3894 }
3895
3896 for effect in effects {
3897 let effect_kind = match effect {
3898 ReactiveEffect::StateUpdate(_) => Some("state_update"),
3899 ReactiveEffect::Invalidate(_) => Some("invalidate"),
3900 ReactiveEffect::Resync(_) => Some("resync"),
3901 ReactiveEffect::Hook(_) | ReactiveEffect::Speculative(_) => None,
3902 };
3903 if let Some(effect_kind) = effect_kind {
3904 return Err(ReactiveError::InvalidPendingEffect {
3905 input_ref: Box::new(input_ref),
3906 handler_id: handler_id.clone(),
3907 effect_kind,
3908 });
3909 }
3910 }
3911 Ok(())
3912}
3913
3914fn detect_conflicts(
3915 input_ref: InputRef,
3916 executions: &[HandlerExecution],
3917) -> Result<(), ReactiveError> {
3918 let mut writes: HashMap<EffectTarget, (AbsoluteValue, HandlerId)> = HashMap::new();
3919 for execution in executions {
3920 for update in &execution.state_updates {
3921 for (target, value) in absolute_writes(update) {
3922 if let Some((previous_value, previous_handler)) = writes.get(&target) {
3923 if previous_value != &value {
3924 return Err(ReactiveError::ConflictingEffects {
3925 input_ref: Box::new(input_ref),
3926 target: Box::new(target),
3927 first: previous_handler.clone(),
3928 second: execution.handler_id.clone(),
3929 });
3930 }
3931 } else {
3932 writes.insert(target, (value, execution.handler_id.clone()));
3933 }
3934 }
3935 }
3936 }
3937 Ok(())
3938}
3939
3940fn absolute_writes(update: &StateUpdate) -> Vec<(EffectTarget, AbsoluteValue)> {
3941 match update {
3942 StateUpdate::Slot {
3943 address,
3944 slot,
3945 value,
3946 } => vec![(
3947 EffectTarget::StorageSlot {
3948 address: *address,
3949 slot: *slot,
3950 },
3951 AbsoluteValue::U256(*value),
3952 )],
3953 StateUpdate::SlotMasked {
3954 address,
3955 slot,
3956 mask,
3957 value,
3958 } => vec![(
3959 EffectTarget::MaskedStorageSlot {
3960 address: *address,
3961 slot: *slot,
3962 mask: *mask,
3963 },
3964 AbsoluteValue::U256(*value),
3965 )],
3966 StateUpdate::Account { address, patch } | StateUpdate::AccountUpsert { address, patch } => {
3967 account_patch_writes(*address, patch)
3968 }
3969 StateUpdate::SlotDelta { .. }
3970 | StateUpdate::BalanceDelta { .. }
3971 | StateUpdate::Purge { .. } => Vec::new(),
3972 }
3973}
3974
3975fn account_patch_writes(
3976 address: Address,
3977 patch: &AccountPatch,
3978) -> Vec<(EffectTarget, AbsoluteValue)> {
3979 let mut writes = Vec::new();
3980 if let Some(balance) = patch.balance {
3981 writes.push((
3982 EffectTarget::AccountBalance { address },
3983 AbsoluteValue::U256(balance),
3984 ));
3985 }
3986 if let Some(nonce) = patch.nonce {
3987 writes.push((
3988 EffectTarget::AccountNonce { address },
3989 AbsoluteValue::U64(nonce),
3990 ));
3991 }
3992 if let Some(code) = &patch.code {
3993 writes.push((
3994 EffectTarget::AccountCode { address },
3995 AbsoluteValue::Bytes(code.clone()),
3996 ));
3997 }
3998 writes
3999}
4000
4001fn input_ref<N: Network>(input: &ReactiveInput<N>, ctx: &ReactiveContext) -> InputRef {
4002 match input {
4003 ReactiveInput::Log(log) => InputRef::Log {
4004 chain_id: ctx.chain_id,
4005 block_hash: log
4006 .block_hash
4007 .or(ctx.block.as_ref().map(|block| block.hash))
4008 .unwrap_or_default(),
4009 transaction_hash: log.transaction_hash.unwrap_or_default(),
4010 log_index: log.log_index.or(ctx.log_index).unwrap_or_default(),
4011 },
4012 ReactiveInput::PendingTxHash(hash) => InputRef::PendingTx {
4013 chain_id: ctx.chain_id,
4014 hash: *hash,
4015 },
4016 ReactiveInput::PendingTx(tx) => InputRef::PendingTx {
4017 chain_id: ctx.chain_id,
4018 hash: tx.tx_hash(),
4019 },
4020 ReactiveInput::BlockHeader(header) => InputRef::Block {
4021 chain_id: ctx.chain_id,
4022 hash: header.hash(),
4023 number: header.number(),
4024 },
4025 ReactiveInput::FullBlock(block) => {
4026 let header = block.header();
4027 InputRef::Block {
4028 chain_id: ctx.chain_id,
4029 hash: header.hash(),
4030 number: header.number(),
4031 }
4032 }
4033 }
4034}
4035
4036fn is_canonical_status(status: &ChainStatus) -> bool {
4037 matches!(
4038 status,
4039 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. }
4040 )
4041}
4042
4043pub struct EventDecoderHandler {
4045 id: HandlerId,
4046 decoder: Arc<dyn EventDecoder>,
4047 interest: LogInterest,
4048}
4049
4050impl EventDecoderHandler {
4051 pub fn new(id: HandlerId, decoder: Arc<dyn EventDecoder>, interest: LogInterest) -> Self {
4053 Self {
4054 id,
4055 decoder,
4056 interest,
4057 }
4058 }
4059}
4060
4061impl<N: Network> ReactiveHandler<N> for EventDecoderHandler {
4062 fn id(&self) -> HandlerId {
4063 self.id.clone()
4064 }
4065
4066 fn interests(&self) -> Vec<ReactiveInterest<N>> {
4067 vec![ReactiveInterest::Logs(self.interest.clone())]
4068 }
4069
4070 fn handle(
4071 &self,
4072 _ctx: &ReactiveContext,
4073 input: &ReactiveInput<N>,
4074 state: &dyn StateView,
4075 ) -> Result<HandlerOutcome, HandlerError> {
4076 let ReactiveInput::Log(log) = input else {
4077 return Ok(HandlerOutcome::empty(StateEffectQuality::NoStateEffect));
4078 };
4079
4080 Ok(HandlerOutcome {
4081 effects: self
4082 .decoder
4083 .decode(&log.inner, state)
4084 .into_iter()
4085 .map(ReactiveEffect::StateUpdate)
4086 .collect(),
4087 quality: StateEffectQuality::ExactFromInput,
4088 tags: Vec::new(),
4089 })
4090 }
4091}
4092
4093pub trait EventSubscriber<N: Network = Ethereum>: Send {
4095 fn register_interests(
4101 &mut self,
4102 interests: &[ReactiveInterest<N>],
4103 ) -> Result<(), SubscriberError>;
4104
4105 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N>;
4107}
4108
4109pub type SubscriberNextBatch<'a, N> = Pin<
4111 Box<dyn Future<Output = Result<Option<ReactiveInputBatch<N>>, SubscriberError>> + Send + 'a>,
4112>;
4113
4114pub type SubscriberNextScopedBatch<'a, N> = Pin<
4116 Box<dyn Future<Output = Result<Option<SubscriberInputBatch<N>>, SubscriberError>> + Send + 'a>,
4117>;
4118
4119#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
4121pub enum SubscriberMode {
4122 #[default]
4128 Auto,
4129 PubSub,
4131 Polling,
4133}
4134
4135#[derive(Clone, Debug, PartialEq, Eq)]
4137pub struct SubscriberConfig {
4138 pub hydrate_pending_transactions: bool,
4140 pub max_batch_size: usize,
4142 pub max_log_addresses_per_subscription: usize,
4146 pub reconnect: SubscriberReconnectConfig,
4148}
4149
4150impl Default for SubscriberConfig {
4151 fn default() -> Self {
4152 Self {
4153 hydrate_pending_transactions: false,
4154 max_batch_size: 1024,
4155 max_log_addresses_per_subscription: 1024,
4156 reconnect: SubscriberReconnectConfig::default(),
4157 }
4158 }
4159}
4160
4161#[derive(Clone, Debug, PartialEq, Eq)]
4167pub struct SubscriberReconnectConfig {
4168 pub enabled: bool,
4170 pub initial_delay: Duration,
4172 pub retry_delay: Duration,
4175 pub max_delay: Duration,
4177 pub max_attempts: Option<usize>,
4179 pub dedupe_window: usize,
4182}
4183
4184impl Default for SubscriberReconnectConfig {
4185 fn default() -> Self {
4186 Self {
4187 enabled: true,
4188 initial_delay: Duration::ZERO,
4189 retry_delay: Duration::from_millis(250),
4190 max_delay: Duration::from_secs(30),
4191 max_attempts: Some(3),
4192 dedupe_window: 4096,
4193 }
4194 }
4195}
4196
4197#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4207pub struct SubscriberBackfill {
4208 from_block: u64,
4209 to_block: Option<u64>,
4210}
4211
4212impl SubscriberBackfill {
4213 pub fn range(from_block: u64, to_block: u64) -> Self {
4215 Self {
4216 from_block,
4217 to_block: Some(to_block),
4218 }
4219 }
4220
4221 pub fn from_block(from_block: u64) -> Self {
4223 Self {
4224 from_block,
4225 to_block: None,
4226 }
4227 }
4228
4229 pub fn start_block(&self) -> u64 {
4231 self.from_block
4232 }
4233
4234 pub fn end_block(&self) -> Option<u64> {
4236 self.to_block
4237 }
4238}
4239
4240#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
4247pub struct SubscriberOwnerEpoch {
4248 owner: HandlerId,
4249 sequence: u64,
4250}
4251
4252#[derive(Clone, Debug, PartialEq, Eq)]
4259#[non_exhaustive]
4260pub enum SubscriberInputScope {
4261 Canonical {
4263 owners: Vec<SubscriberOwnerEpoch>,
4265 },
4266 OwnerOnly {
4268 owners: Vec<SubscriberOwnerEpoch>,
4270 },
4271}
4272
4273impl SubscriberInputScope {
4274 pub fn owners(&self) -> &[SubscriberOwnerEpoch] {
4276 match self {
4277 Self::Canonical { owners } | Self::OwnerOnly { owners } => owners,
4278 }
4279 }
4280
4281 pub const fn is_canonical(&self) -> bool {
4283 matches!(self, Self::Canonical { .. })
4284 }
4285}
4286
4287#[derive(Clone, Debug)]
4289pub struct SubscriberInputRecord<N: Network = Ethereum> {
4290 record: ReactiveInputRecord<N>,
4291 scope: SubscriberInputScope,
4292}
4293
4294impl<N: Network> SubscriberInputRecord<N> {
4295 pub const fn record(&self) -> &ReactiveInputRecord<N> {
4297 &self.record
4298 }
4299
4300 pub const fn scope(&self) -> &SubscriberInputScope {
4302 &self.scope
4303 }
4304
4305 pub fn into_record(self) -> ReactiveInputRecord<N> {
4307 self.record
4308 }
4309}
4310
4311impl<N: Network> std::ops::Deref for SubscriberInputRecord<N> {
4312 type Target = ReactiveInputRecord<N>;
4313
4314 fn deref(&self) -> &Self::Target {
4315 &self.record
4316 }
4317}
4318
4319#[derive(Clone, Debug)]
4321pub struct SubscriberInputBatch<N: Network = Ethereum> {
4322 records: Vec<SubscriberInputRecord<N>>,
4323}
4324
4325#[derive(Debug)]
4328#[non_exhaustive]
4329pub enum SubscriberDriverPoll<C, N: Network = Ethereum> {
4330 Control(C),
4332 Batch(Option<SubscriberInputBatch<N>>),
4334}
4335
4336impl<N: Network> SubscriberInputBatch<N> {
4337 pub fn records(&self) -> &[SubscriberInputRecord<N>] {
4339 &self.records
4340 }
4341
4342 pub fn into_records(self) -> Vec<SubscriberInputRecord<N>> {
4344 self.records
4345 }
4346
4347 fn into_reactive_batch(self) -> ReactiveInputBatch<N> {
4348 ReactiveInputBatch::new(
4349 self.records
4350 .into_iter()
4351 .map(SubscriberInputRecord::into_record)
4352 .collect(),
4353 )
4354 }
4355}
4356
4357impl SubscriberOwnerEpoch {
4358 pub const fn owner(&self) -> &HandlerId {
4360 &self.owner
4361 }
4362
4363 pub const fn sequence(&self) -> u64 {
4365 self.sequence
4366 }
4367}
4368
4369#[derive(Clone, Debug, PartialEq, Eq)]
4371#[non_exhaustive]
4372pub enum SubscriberOwnerStart {
4373 Live,
4375 PostBlock(BlockRef),
4382}
4383
4384#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
4386#[non_exhaustive]
4387pub enum SubscriberOwnerState {
4388 Staged,
4391 Active,
4393 Removing,
4395}
4396
4397#[derive(Clone, Debug, PartialEq, Eq)]
4403pub struct SubscriberOwnerProgress {
4404 owner: SubscriberOwnerEpoch,
4405 through: BlockRef,
4406}
4407
4408impl SubscriberOwnerProgress {
4409 pub const fn owner(&self) -> &SubscriberOwnerEpoch {
4411 &self.owner
4412 }
4413
4414 pub const fn through(&self) -> &BlockRef {
4416 &self.through
4417 }
4418}
4419
4420#[derive(Debug, thiserror::Error)]
4422#[non_exhaustive]
4423pub enum SubscriberOwnerError {
4424 #[error(transparent)]
4426 Subscriber(#[from] SubscriberError),
4427 #[error("subscriber interest owner `{0}` is already registered")]
4429 AlreadyRegistered(HandlerId),
4430 #[error("post-block subscriber baseline {0} has no following block")]
4432 PostBlockOverflow(u64),
4433 #[error("subscriber owner epoch sequence exhausted")]
4435 EpochExhausted,
4436 #[error("subscriber owner epoch is not staged")]
4438 NotStaged,
4439 #[error("subscriber owner was staged live-only and has no catch-up baseline")]
4441 MissingBaseline,
4442 #[error("post-block subscriber owners support log interests only")]
4444 UnsupportedPostBlockInterest,
4445 #[error("subscriber reconcile target block {0} was not found")]
4447 BlockUnavailable(u64),
4448 #[error(
4450 "subscriber reconcile target mismatch: expected block {expected_number} {expected_hash}, got block {actual_number} {actual_hash}"
4451 )]
4452 BlockMismatch {
4453 expected_number: u64,
4455 expected_hash: B256,
4457 actual_number: u64,
4459 actual_hash: B256,
4461 },
4462 #[error("subscriber reconcile target block {target} precedes current owner position {current}")]
4464 ProgressRegression {
4465 current: u64,
4467 target: u64,
4469 },
4470 #[error(
4473 "subscriber reconcile conflicts with retained block {number} {current_hash}: target chain references {target_hash}"
4474 )]
4475 ProgressConflict {
4476 number: u64,
4478 current_hash: B256,
4480 target_hash: B256,
4482 },
4483 #[error("subscriber reconcile returned an invalid catch-up log: {0}")]
4485 InvalidBackfillLog(&'static str),
4486}
4487
4488pub trait InterestOwnerSubscriber<N: Network = Ethereum>: EventSubscriber<N> {
4500 fn add_interest_owner(
4502 &mut self,
4503 owner: HandlerId,
4504 interests: &[ReactiveInterest<N>],
4505 ) -> Result<(), SubscriberError>;
4506
4507 fn add_interest_owner_with_backfill(
4509 &mut self,
4510 owner: HandlerId,
4511 interests: &[ReactiveInterest<N>],
4512 backfill: SubscriberBackfill,
4513 ) -> Result<(), SubscriberError>;
4514
4515 fn remove_interest_owner(&mut self, owner: &HandlerId) -> Option<Vec<ReactiveInterest<N>>>;
4517
4518 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]>;
4520}
4521
4522pub struct ReactiveEngine<S, N: Network = Ethereum> {
4561 runtime: ReactiveRuntime<N>,
4562 subscriber: S,
4563}
4564
4565impl<S, N> ReactiveEngine<S, N>
4566where
4567 N: Network,
4568 S: EventSubscriber<N>,
4569{
4570 pub fn new(runtime: ReactiveRuntime<N>, subscriber: S) -> Self {
4572 Self {
4573 runtime,
4574 subscriber,
4575 }
4576 }
4577
4578 pub fn into_parts(self) -> (ReactiveRuntime<N>, S) {
4580 (self.runtime, self.subscriber)
4581 }
4582
4583 pub fn runtime(&self) -> &ReactiveRuntime<N> {
4585 &self.runtime
4586 }
4587
4588 pub fn runtime_mut(&mut self) -> &mut ReactiveRuntime<N> {
4590 &mut self.runtime
4591 }
4592
4593 pub fn subscriber(&self) -> &S {
4595 &self.subscriber
4596 }
4597
4598 pub fn subscriber_mut(&mut self) -> &mut S {
4600 &mut self.subscriber
4601 }
4602
4603 pub fn next_batch(&mut self) -> SubscriberNextBatch<'_, N> {
4605 self.subscriber.next_batch()
4606 }
4607
4608 pub fn ingest_batch(
4611 &mut self,
4612 cache: &mut EvmCache,
4613 batch: ReactiveInputBatch<N>,
4614 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4615 self.runtime.ingest_batch(cache, batch)
4616 }
4617
4618 pub fn ingest_batch_with_resync(
4622 &mut self,
4623 cache: &mut EvmCache,
4624 batch: ReactiveInputBatch<N>,
4625 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4626 self.runtime.ingest_batch_with_resync(cache, batch)
4627 }
4628
4629 pub async fn next_ingest(
4632 &mut self,
4633 cache: &mut EvmCache,
4634 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
4635 let Some(batch) = self.subscriber.next_batch().await? else {
4636 return Ok(None);
4637 };
4638 Ok(Some(self.runtime.ingest_batch(cache, batch)?))
4639 }
4640
4641 pub async fn next_ingest_with_resync(
4645 &mut self,
4646 cache: &mut EvmCache,
4647 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
4648 let Some(batch) = self.subscriber.next_batch().await? else {
4649 return Ok(None);
4650 };
4651 Ok(Some(self.runtime.ingest_batch_with_resync(cache, batch)?))
4652 }
4653}
4654
4655impl<S, N> ReactiveEngine<S, N>
4656where
4657 N: Network,
4658 S: InterestOwnerSubscriber<N>,
4659{
4660 pub fn register_handler(
4678 &mut self,
4679 handler: Arc<dyn ReactiveHandler<N>>,
4680 ) -> Result<(), ReactiveEngineRegisterError> {
4681 let backfill = self
4682 .runtime
4683 .last_canonical_block()
4684 .map(|block| SubscriberBackfill::from_block(block.number));
4685 self.register_handler_inner(handler, backfill)
4686 }
4687
4688 pub fn register_handler_with_backfill(
4694 &mut self,
4695 handler: Arc<dyn ReactiveHandler<N>>,
4696 backfill: SubscriberBackfill,
4697 ) -> Result<(), ReactiveEngineRegisterError> {
4698 self.register_handler_inner(handler, Some(backfill))
4699 }
4700
4701 pub fn register_handler_live_only(
4707 &mut self,
4708 handler: Arc<dyn ReactiveHandler<N>>,
4709 ) -> Result<(), ReactiveEngineRegisterError> {
4710 self.register_handler_inner(handler, None)
4711 }
4712
4713 fn register_handler_inner(
4714 &mut self,
4715 handler: Arc<dyn ReactiveHandler<N>>,
4716 backfill: Option<SubscriberBackfill>,
4717 ) -> Result<(), ReactiveEngineRegisterError> {
4718 let id = handler.id();
4719 self.runtime.register_handler(handler)?;
4720 let interests = self
4721 .runtime
4722 .handler_interests(&id)
4723 .expect("handler was just registered")
4724 .to_vec();
4725
4726 let subscribed = match backfill {
4727 Some(backfill) => {
4728 self.subscriber
4729 .add_interest_owner_with_backfill(id.clone(), &interests, backfill)
4730 }
4731 None => self.subscriber.add_interest_owner(id.clone(), &interests),
4732 };
4733 if let Err(error) = subscribed {
4734 self.runtime.unregister_handler(&id);
4735 return Err(error.into());
4736 }
4737
4738 Ok(())
4739 }
4740
4741 pub fn sync_handler_interests(&mut self) -> Result<(), SubscriberError> {
4757 for id in self.runtime.handler_ids() {
4758 let interests = self
4759 .runtime
4760 .handler_interests(&id)
4761 .map(<[ReactiveInterest<N>]>::to_vec)
4762 .unwrap_or_default();
4763 self.subscriber.add_interest_owner(id, &interests)?;
4764 }
4765 Ok(())
4766 }
4767
4768 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
4794 self.subscriber.remove_interest_owner(id);
4795 self.runtime.unregister_handler(id)
4796 }
4797}
4798
4799pub struct AlloySubscriber<P, N: Network = Ethereum> {
4811 provider: P,
4812 mode: SubscriberMode,
4813 config: SubscriberConfig,
4814 base_interests: Vec<ReactiveInterest<N>>,
4815 owned_interests: Vec<OwnedSubscriberInterests<N>>,
4816 next_owner_epoch: u64,
4817 interests: Vec<ReactiveInterest<N>>,
4818 log_source_ids: HashMap<Filter, usize>,
4823 next_log_source_id: usize,
4824 pending_backfills: VecDeque<QueuedSubscriberBackfill>,
4825 sources_dirty: bool,
4828 stream_revision: u64,
4831 state: AlloySubscriberState<N>,
4832 pending_records: VecDeque<SubscriberInputRecord<N>>,
4833 pending_reconcile_owner_records: VecDeque<BufferedSubscriberOwnerRecord<N>>,
4838 last_seen_log_blocks: HashMap<usize, u64>,
4839 recent_input_refs: VecDeque<InputRef>,
4840 recent_input_ref_set: HashSet<InputRef>,
4841 recent_owner_input_refs: HashMap<SubscriberOwnerEpoch, VecDeque<InputRef>>,
4842 recent_owner_input_ref_sets: HashMap<SubscriberOwnerEpoch, HashSet<InputRef>>,
4843 _network: PhantomData<N>,
4844}
4845
4846struct OwnedSubscriberInterests<N: Network = Ethereum> {
4847 owner: HandlerId,
4848 interests: Vec<ReactiveInterest<N>>,
4849 epoch: Option<SubscriberOwnerEpoch>,
4850 state: SubscriberOwnerState,
4851 baseline: Option<BlockRef>,
4852 progress: Option<SubscriberOwnerProgress>,
4853 progress_stream_revision: Option<u64>,
4854}
4855
4856#[derive(Clone)]
4857struct SubscriberOwnerReconcilePlan<N: Network = Ethereum> {
4858 epoch: SubscriberOwnerEpoch,
4859 interests: Vec<ReactiveInterest<N>>,
4860 retained: BlockRef,
4861 from_block: u64,
4862}
4863
4864struct SubscriberOwnerCatchup {
4865 logs: Vec<Log>,
4866 certified: BlockRef,
4867}
4868
4869struct SubscriberOwnerReconcileFilter {
4870 filter: Filter,
4871 from_block: u64,
4872}
4873
4874struct BufferedSubscriberOwnerRecord<N: Network = Ethereum> {
4875 record: ReactiveInputRecord<N>,
4876 owners: Vec<SubscriberOwnerEpoch>,
4877}
4878
4879const OWNER_RECONCILE_FILTERS_PER_CHUNK: usize = 256;
4880
4881struct QueuedSubscriberBackfill {
4882 owner: HandlerId,
4883 epoch: Option<SubscriberOwnerEpoch>,
4884 filter: Filter,
4885 backfill: SubscriberBackfill,
4886}
4887
4888#[cfg(feature = "reactive-ws")]
4894fn ensure_ring_crypto_provider() {
4895 use std::sync::Once;
4896 static INSTALL: Once = Once::new();
4897 INSTALL.call_once(|| {
4898 let _ = rustls::crypto::ring::default_provider().install_default();
4899 });
4900}
4901
4902impl<P, N: Network> AlloySubscriber<P, N> {
4903 pub fn new(provider: P, mode: SubscriberMode, config: SubscriberConfig) -> Self {
4905 #[cfg(feature = "reactive-ws")]
4906 ensure_ring_crypto_provider();
4907 Self {
4908 provider,
4909 mode,
4910 config,
4911 base_interests: Vec::new(),
4912 owned_interests: Vec::new(),
4913 next_owner_epoch: 0,
4914 interests: Vec::new(),
4915 log_source_ids: HashMap::new(),
4916 next_log_source_id: 0,
4917 pending_backfills: VecDeque::new(),
4918 sources_dirty: true,
4919 stream_revision: 0,
4920 state: AlloySubscriberState::Uninitialized,
4921 pending_records: VecDeque::new(),
4922 pending_reconcile_owner_records: VecDeque::new(),
4923 last_seen_log_blocks: HashMap::new(),
4924 recent_input_refs: VecDeque::new(),
4925 recent_input_ref_set: HashSet::new(),
4926 recent_owner_input_refs: HashMap::new(),
4927 recent_owner_input_ref_sets: HashMap::new(),
4928 _network: PhantomData,
4929 }
4930 }
4931
4932 pub fn provider(&self) -> &P {
4934 &self.provider
4935 }
4936
4937 pub fn mode(&self) -> SubscriberMode {
4939 self.mode
4940 }
4941
4942 pub fn config(&self) -> &SubscriberConfig {
4944 &self.config
4945 }
4946
4947 pub fn registered_interests(&self) -> &[ReactiveInterest<N>] {
4949 &self.interests
4950 }
4951
4952 pub fn stage_interest_owner(
4963 &mut self,
4964 owner: HandlerId,
4965 interests: &[ReactiveInterest<N>],
4966 start: SubscriberOwnerStart,
4967 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
4968 validate_subscriber_config(&self.config)?;
4969 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
4970 && interests
4971 .iter()
4972 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
4973 {
4974 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
4975 }
4976 if self
4977 .owned_interests
4978 .iter()
4979 .any(|entry| entry.owner == owner)
4980 {
4981 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
4982 }
4983
4984 let mut next_owned = self.clone_owned_interests();
4985 next_owned.push(OwnedSubscriberInterests {
4986 owner: owner.clone(),
4987 interests: interests.to_vec(),
4988 epoch: None,
4989 state: SubscriberOwnerState::Staged,
4990 baseline: None,
4991 progress: None,
4992 progress_stream_revision: None,
4993 });
4994 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
4995 validate_supported_interests(self.mode, &self.config, &next_registered)?;
4996
4997 let baseline = match start {
4998 SubscriberOwnerStart::Live => None,
4999 SubscriberOwnerStart::PostBlock(block) => {
5000 block
5001 .number
5002 .checked_add(1)
5003 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
5004 Some(block)
5005 }
5006 };
5007 let sequence = self
5008 .next_owner_epoch
5009 .checked_add(1)
5010 .ok_or(SubscriberOwnerError::EpochExhausted)?;
5011 let epoch = SubscriberOwnerEpoch {
5012 owner: owner.clone(),
5013 sequence,
5014 };
5015
5016 self.next_owner_epoch = sequence;
5017 let entry = next_owned
5018 .last_mut()
5019 .expect("staged owner was appended during preflight");
5020 entry.epoch = Some(epoch.clone());
5021 entry.baseline = baseline;
5022 self.owned_interests = next_owned;
5023 self.interests = next_registered;
5024 self.sources_dirty = true;
5025
5026 Ok(epoch)
5027 }
5028
5029 pub fn stage_interest_owner_replacement(
5036 &mut self,
5037 owner: HandlerId,
5038 interests: &[ReactiveInterest<N>],
5039 start: SubscriberOwnerStart,
5040 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
5041 validate_subscriber_config(&self.config)?;
5042 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
5043 && interests
5044 .iter()
5045 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
5046 {
5047 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
5048 }
5049 let active_count = self
5050 .owned_interests
5051 .iter()
5052 .filter(|entry| entry.owner == owner && entry.state == SubscriberOwnerState::Active)
5053 .count();
5054 if active_count != 1
5055 || self
5056 .owned_interests
5057 .iter()
5058 .any(|entry| entry.owner == owner && entry.state != SubscriberOwnerState::Active)
5059 {
5060 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
5061 }
5062
5063 let mut next_owned = self.clone_owned_interests();
5064 next_owned.push(OwnedSubscriberInterests {
5065 owner: owner.clone(),
5066 interests: interests.to_vec(),
5067 epoch: None,
5068 state: SubscriberOwnerState::Staged,
5069 baseline: None,
5070 progress: None,
5071 progress_stream_revision: None,
5072 });
5073 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
5074 validate_supported_interests(self.mode, &self.config, &next_registered)?;
5075
5076 let baseline = match start {
5077 SubscriberOwnerStart::Live => None,
5078 SubscriberOwnerStart::PostBlock(block) => {
5079 block
5080 .number
5081 .checked_add(1)
5082 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
5083 Some(block)
5084 }
5085 };
5086 let sequence = self
5087 .next_owner_epoch
5088 .checked_add(1)
5089 .ok_or(SubscriberOwnerError::EpochExhausted)?;
5090 let epoch = SubscriberOwnerEpoch {
5091 owner: owner.clone(),
5092 sequence,
5093 };
5094
5095 self.next_owner_epoch = sequence;
5096 let entry = next_owned
5097 .last_mut()
5098 .expect("staged replacement owner was appended during preflight");
5099 entry.epoch = Some(epoch.clone());
5100 entry.baseline = baseline;
5101 self.owned_interests = next_owned;
5102 self.interests = next_registered;
5103 self.sources_dirty = true;
5104 Ok(epoch)
5105 }
5106
5107 pub fn interest_owner_state(
5109 &self,
5110 epoch: &SubscriberOwnerEpoch,
5111 ) -> Option<SubscriberOwnerState> {
5112 self.owned_interests
5113 .iter()
5114 .find(|entry| entry.epoch.as_ref() == Some(epoch))
5115 .map(|entry| entry.state)
5116 }
5117
5118 pub fn interest_owner_progress(
5120 &self,
5121 epoch: &SubscriberOwnerEpoch,
5122 ) -> Option<&SubscriberOwnerProgress> {
5123 self.owned_interests
5124 .iter()
5125 .find(|entry| entry.epoch.as_ref() == Some(epoch))
5126 .and_then(|entry| entry.progress.as_ref())
5127 }
5128
5129 pub fn activate_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
5133 let stream_revision = self.stream_revision;
5134 let sources_dirty = self.sources_dirty;
5135 let Some(entry) = self
5136 .owned_interests
5137 .iter_mut()
5138 .find(|entry| entry.epoch.as_ref() == Some(epoch))
5139 else {
5140 return false;
5141 };
5142 if entry.state != SubscriberOwnerState::Staged
5143 || (entry.baseline.is_some()
5144 && (entry.progress.is_none()
5145 || entry.progress_stream_revision != Some(stream_revision)
5146 || sources_dirty))
5147 {
5148 return false;
5149 }
5150 entry.state = SubscriberOwnerState::Active;
5151 true
5152 }
5153
5154 pub fn commit_interest_owner_replacement(
5156 &mut self,
5157 active: &SubscriberOwnerEpoch,
5158 replacement: &SubscriberOwnerEpoch,
5159 ) -> bool {
5160 let Some(active_index) = self
5161 .owned_interests
5162 .iter()
5163 .position(|entry| entry.epoch.as_ref() == Some(active))
5164 else {
5165 return false;
5166 };
5167 let Some(replacement_index) = self
5168 .owned_interests
5169 .iter()
5170 .position(|entry| entry.epoch.as_ref() == Some(replacement))
5171 else {
5172 return false;
5173 };
5174 if active_index == replacement_index
5175 || active.owner() != replacement.owner()
5176 || self.owned_interests[active_index].state != SubscriberOwnerState::Active
5177 || self.owned_interests[replacement_index].state != SubscriberOwnerState::Staged
5178 || (self.owned_interests[replacement_index].baseline.is_some()
5179 && (self.owned_interests[replacement_index].progress.is_none()
5180 || self.owned_interests[replacement_index].progress_stream_revision
5181 != Some(self.stream_revision)
5182 || self.sources_dirty))
5183 {
5184 return false;
5185 }
5186
5187 self.owned_interests[replacement_index].state = SubscriberOwnerState::Active;
5188 self.owned_interests.remove(active_index);
5189 self.purge_owner_epoch(active);
5190 self.rebuild_registered_interests();
5191 self.retire_unreferenced_filters();
5192 self.sources_dirty = true;
5193 true
5194 }
5195
5196 pub fn prepare_interest_owner_removal(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
5205 let Some(entry) = self
5206 .owned_interests
5207 .iter_mut()
5208 .find(|entry| entry.epoch.as_ref() == Some(epoch))
5209 else {
5210 return false;
5211 };
5212 if entry.state != SubscriberOwnerState::Active {
5213 return false;
5214 }
5215 entry.state = SubscriberOwnerState::Removing;
5216 true
5217 }
5218
5219 pub fn finalize_interest_owner_removal(
5225 &mut self,
5226 epoch: &SubscriberOwnerEpoch,
5227 ) -> Option<Vec<ReactiveInterest<N>>> {
5228 let index = self.owned_interests.iter().position(|entry| {
5229 entry.epoch.as_ref() == Some(epoch) && entry.state == SubscriberOwnerState::Removing
5230 })?;
5231 let removed = self.owned_interests.remove(index).interests;
5232 self.purge_owner_epoch(epoch);
5233 self.rebuild_registered_interests();
5234 self.retire_unreferenced_filters();
5235 self.sources_dirty = true;
5236 Some(removed)
5237 }
5238
5239 pub fn abort_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
5245 let Some(index) = self
5246 .owned_interests
5247 .iter()
5248 .position(|entry| entry.epoch.as_ref() == Some(epoch))
5249 else {
5250 return false;
5251 };
5252 match self.owned_interests[index].state {
5253 SubscriberOwnerState::Staged => {
5254 self.owned_interests.remove(index);
5255 self.purge_owner_epoch(epoch);
5256 self.rebuild_registered_interests();
5257 self.retire_unreferenced_filters();
5258 self.sources_dirty = true;
5259 true
5260 }
5261 SubscriberOwnerState::Removing => {
5262 self.owned_interests[index].state = SubscriberOwnerState::Active;
5263 true
5264 }
5265 SubscriberOwnerState::Active => false,
5266 }
5267 }
5268
5269 fn purge_owner_epoch(&mut self, epoch: &SubscriberOwnerEpoch) {
5270 self.pending_backfills
5271 .retain(|backfill| backfill.epoch.as_ref() != Some(epoch));
5272 self.pending_records
5273 .retain_mut(|pending| match &mut pending.scope {
5274 SubscriberInputScope::Canonical { owners } => {
5275 owners.retain(|owner| owner != epoch);
5276 true
5277 }
5278 SubscriberInputScope::OwnerOnly { owners } => {
5279 owners.retain(|owner| owner != epoch);
5280 !owners.is_empty()
5281 }
5282 });
5283 self.pending_reconcile_owner_records.retain_mut(|pending| {
5284 pending.owners.retain(|owner| owner != epoch);
5285 !pending.owners.is_empty()
5286 });
5287 self.recent_owner_input_refs.remove(epoch);
5288 self.recent_owner_input_ref_sets.remove(epoch);
5289 }
5290
5291 pub fn add_interest_owner(
5308 &mut self,
5309 owner: HandlerId,
5310 interests: &[ReactiveInterest<N>],
5311 ) -> Result<(), SubscriberError> {
5312 self.set_interest_owner(owner, interests, None)
5313 }
5314
5315 pub fn add_interest_owner_with_backfill(
5328 &mut self,
5329 owner: HandlerId,
5330 interests: &[ReactiveInterest<N>],
5331 backfill: SubscriberBackfill,
5332 ) -> Result<(), SubscriberError> {
5333 self.set_interest_owner(owner, interests, Some(backfill))
5334 }
5335
5336 pub fn remove_interest_owner(&mut self, owner: &HandlerId) -> Option<Vec<ReactiveInterest<N>>> {
5345 let index = self
5346 .owned_interests
5347 .iter()
5348 .position(|entry| &entry.owner == owner)?;
5349 let removed = self.owned_interests.remove(index);
5350 if let Some(epoch) = &removed.epoch {
5351 self.purge_owner_epoch(epoch);
5352 } else {
5353 self.pending_backfills
5354 .retain(|backfill| &backfill.owner != owner);
5355 }
5356 self.rebuild_registered_interests();
5357 self.retire_unreferenced_filters();
5358 self.sources_dirty = true;
5359 Some(removed.interests)
5360 }
5361
5362 pub fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
5364 self.owned_interests
5365 .iter()
5366 .find(|entry| &entry.owner == owner)
5367 .map(|entry| entry.interests.as_slice())
5368 }
5369
5370 fn set_interest_owner(
5371 &mut self,
5372 owner: HandlerId,
5373 interests: &[ReactiveInterest<N>],
5374 backfill: Option<SubscriberBackfill>,
5375 ) -> Result<(), SubscriberError> {
5376 validate_subscriber_config(&self.config)?;
5377
5378 let mut next_owned = self.clone_owned_interests();
5379 let replaced_epoch = match next_owned.iter_mut().find(|entry| entry.owner == owner) {
5380 Some(entry) => {
5381 entry.interests = interests.to_vec();
5382 entry.state = SubscriberOwnerState::Active;
5383 entry.baseline = None;
5384 entry.progress = None;
5385 entry.progress_stream_revision = None;
5386 entry.epoch.take()
5387 }
5388 None => {
5389 next_owned.push(OwnedSubscriberInterests {
5390 owner: owner.clone(),
5391 interests: interests.to_vec(),
5392 epoch: None,
5393 state: SubscriberOwnerState::Active,
5394 baseline: None,
5395 progress: None,
5396 progress_stream_revision: None,
5397 });
5398 None
5399 }
5400 };
5401 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
5402 validate_supported_interests(self.mode, &self.config, &next_registered)?;
5403
5404 let previous_filters: Vec<Filter> = self
5411 .owner_interests(&owner)
5412 .map(log_filters)
5413 .unwrap_or_default();
5414 let continuity_anchor: Option<u64> = previous_filters
5415 .iter()
5416 .filter_map(|filter| self.log_anchor(filter))
5417 .min();
5418
5419 self.owned_interests = next_owned;
5420 self.interests = next_registered;
5421 if let Some(epoch) = replaced_epoch {
5422 self.purge_owner_epoch(&epoch);
5423 }
5424 self.retire_unreferenced_filters();
5425 self.sources_dirty = true;
5426
5427 self.pending_backfills
5430 .retain(|queued| queued.owner != owner);
5431 for filter in log_filters(interests) {
5432 if let Some(backfill) = backfill {
5433 self.pending_backfills.push_back(QueuedSubscriberBackfill {
5434 owner: owner.clone(),
5435 epoch: None,
5436 filter: filter.clone(),
5437 backfill,
5438 });
5439 }
5440
5441 let unchanged = previous_filters.contains(&filter);
5446 let explicit_covers = backfill.is_some_and(|explicit| {
5447 explicit.end_block().is_none()
5448 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
5449 });
5450 if let Some(anchor) = continuity_anchor
5451 && !unchanged
5452 && !explicit_covers
5453 {
5454 self.pending_backfills.push_back(QueuedSubscriberBackfill {
5455 owner: owner.clone(),
5456 epoch: None,
5457 filter,
5458 backfill: SubscriberBackfill::from_block(anchor),
5459 });
5460 }
5461 }
5462 Ok(())
5463 }
5464
5465 fn clone_owned_interests(&self) -> Vec<OwnedSubscriberInterests<N>> {
5466 self.owned_interests
5467 .iter()
5468 .map(|entry| OwnedSubscriberInterests {
5469 owner: entry.owner.clone(),
5470 interests: entry.interests.clone(),
5471 epoch: entry.epoch.clone(),
5472 state: entry.state,
5473 baseline: entry.baseline.clone(),
5474 progress: entry.progress.clone(),
5475 progress_stream_revision: entry.progress_stream_revision,
5476 })
5477 .collect()
5478 }
5479
5480 fn rebuild_registered_interests(&mut self) {
5481 self.interests = aggregate_interests(&self.base_interests, &self.owned_interests);
5482 }
5483
5484 fn log_anchor(&self, filter: &Filter) -> Option<u64> {
5487 if let Some(anchor) = self
5488 .log_source_ids
5489 .get(filter)
5490 .and_then(|id| self.last_seen_log_blocks.get(id))
5491 {
5492 return Some(*anchor);
5493 }
5494
5495 self.log_source_ids
5499 .values()
5500 .filter_map(|id| self.last_seen_log_blocks.get(id).copied())
5501 .min()
5502 }
5503
5504 #[allow(clippy::mutable_key_type)]
5511 fn logical_log_filters(&self) -> Vec<Filter> {
5512 let mut filters = log_filters(&self.base_interests);
5513 for entry in &self.owned_interests {
5514 filters.extend(log_filters(&entry.interests));
5515 }
5516 let mut seen = HashSet::new();
5517 filters.retain(|filter| seen.insert(filter.clone()));
5518 filters
5519 }
5520
5521 fn log_stream_filters(&self) -> Vec<Filter> {
5527 let mut merged = Vec::new();
5528 for filter in self.logical_log_filters() {
5529 merge_log_subscription_filter(&mut merged, &filter);
5530 }
5531
5532 let max_addresses = self.config.max_log_addresses_per_subscription.max(1);
5533 let mut planned = Vec::new();
5534 for filter in merged {
5535 let mut addresses: Vec<_> = filter.address.iter().copied().collect();
5536 if addresses.len() <= max_addresses {
5537 planned.push(filter);
5538 continue;
5539 }
5540 addresses.sort_unstable();
5541 for chunk in addresses.chunks(max_addresses) {
5542 let mut split = filter.clone();
5543 split.address = FilterSet::default();
5544 for address in chunk {
5545 split.address.insert(*address);
5546 }
5547 planned.push(split);
5548 }
5549 }
5550 planned
5551 }
5552
5553 #[allow(clippy::mutable_key_type)]
5560 fn retire_unreferenced_filters(&mut self) {
5561 let mut live: HashSet<Filter> = self.log_stream_filters().into_iter().collect();
5562 if let AlloySubscriberState::Active(streams) = &self.state {
5563 for entry in &streams.entries {
5564 match &entry.source {
5565 SubscriberStreamSource::PubSubLog { filter, .. }
5566 | SubscriberStreamSource::PollingLog { filter } => {
5567 live.insert(filter.clone());
5568 }
5569 SubscriberStreamSource::PubSubPendingHashes
5570 | SubscriberStreamSource::PubSubBlockHeaders
5571 | SubscriberStreamSource::PollingPendingHashes => {}
5572 }
5573 }
5574 }
5575 self.log_source_ids
5576 .retain(|filter, _| live.contains(filter));
5577 let live_ids: HashSet<usize> = self.log_source_ids.values().copied().collect();
5578 self.last_seen_log_blocks
5579 .retain(|id, _| live_ids.contains(id));
5580 }
5581
5582 fn drain_next_scoped_batch(&mut self) -> Option<SubscriberInputBatch<N>> {
5583 if self.pending_records.is_empty() {
5584 return None;
5585 }
5586
5587 let len = self.config.max_batch_size.min(self.pending_records.len());
5588 let records = self.pending_records.drain(..len).collect();
5589 Some(SubscriberInputBatch { records })
5590 }
5591
5592 fn reset_delivery_state(&mut self) {
5593 self.pending_records.clear();
5594 self.pending_reconcile_owner_records.clear();
5595 self.last_seen_log_blocks.clear();
5596 self.recent_input_refs.clear();
5597 self.recent_input_ref_set.clear();
5598 self.recent_owner_input_refs.clear();
5599 self.recent_owner_input_ref_sets.clear();
5600 self.pending_backfills.clear();
5601 self.log_source_ids.clear();
5602 self.next_log_source_id = 0;
5603 self.sources_dirty = true;
5604 }
5605
5606 fn bump_stream_revision(&mut self) {
5607 self.stream_revision = self.stream_revision.saturating_add(1);
5608 }
5609}
5610
5611impl<P, N> InterestOwnerSubscriber<N> for AlloySubscriber<P, N>
5612where
5613 P: Provider<N> + Send + Sync,
5614 N: Network + 'static,
5615 N::HeaderResponse: Send + 'static,
5616{
5617 fn add_interest_owner(
5618 &mut self,
5619 owner: HandlerId,
5620 interests: &[ReactiveInterest<N>],
5621 ) -> Result<(), SubscriberError> {
5622 AlloySubscriber::add_interest_owner(self, owner, interests)
5623 }
5624
5625 fn add_interest_owner_with_backfill(
5626 &mut self,
5627 owner: HandlerId,
5628 interests: &[ReactiveInterest<N>],
5629 backfill: SubscriberBackfill,
5630 ) -> Result<(), SubscriberError> {
5631 AlloySubscriber::add_interest_owner_with_backfill(self, owner, interests, backfill)
5632 }
5633
5634 fn remove_interest_owner(&mut self, owner: &HandlerId) -> Option<Vec<ReactiveInterest<N>>> {
5635 AlloySubscriber::remove_interest_owner(self, owner)
5636 }
5637
5638 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
5639 AlloySubscriber::owner_interests(self, owner)
5640 }
5641}
5642
5643enum AlloySubscriberState<N: Network> {
5644 Uninitialized,
5645 Active(SubscriberStreams<N>),
5646 Empty,
5647}
5648
5649struct SubscriberStreams<N: Network> {
5650 entries: Vec<SubscriberStreamEntry<N>>,
5651 next_index: usize,
5652}
5653
5654struct SubscriberStreamEntry<N: Network> {
5655 source: SubscriberStreamSource,
5656 stream: BoxStream<'static, SubscriberEvent<N>>,
5657}
5658
5659impl<N: Network> SubscriberStreams<N> {
5660 fn new() -> Self {
5661 Self {
5662 entries: Vec::new(),
5663 next_index: 0,
5664 }
5665 }
5666
5667 fn is_empty(&self) -> bool {
5668 self.entries.is_empty()
5669 }
5670
5671 fn push(
5672 &mut self,
5673 source: SubscriberStreamSource,
5674 stream: BoxStream<'static, SubscriberEvent<N>>,
5675 ) {
5676 self.entries.push(SubscriberStreamEntry { source, stream });
5677 }
5678
5679 #[cfg(all(test, feature = "reactive-ws"))]
5680 fn len(&self) -> usize {
5681 self.entries.len()
5682 }
5683
5684 fn contains_source(&self, source: &SubscriberStreamSource) -> bool {
5685 self.entries
5686 .iter()
5687 .any(|entry| entry.source.same_key(source))
5688 }
5689
5690 fn retain_sources(&mut self, sources: &[SubscriberStreamSource]) {
5691 self.entries
5692 .retain(|entry| sources.iter().any(|source| entry.source.same_key(source)));
5693 self.normalize_next_index();
5694 }
5695
5696 fn normalize_next_index(&mut self) {
5697 if self.entries.is_empty() {
5698 self.next_index = 0;
5699 } else if self.next_index >= self.entries.len() {
5700 self.next_index %= self.entries.len();
5701 }
5702 }
5703
5704 async fn next(&mut self) -> Option<SubscriberEvent<N>> {
5705 poll_fn(|cx| {
5706 self.normalize_next_index();
5707 if self.entries.is_empty() {
5708 return std::task::Poll::Ready(None);
5709 }
5710
5711 let mut index = self.next_index;
5712 let mut checked = 0usize;
5713 while checked < self.entries.len() {
5714 if index >= self.entries.len() {
5715 index = 0;
5716 }
5717 match self.entries[index].stream.as_mut().poll_next(cx) {
5718 std::task::Poll::Ready(Some(event)) => {
5719 if matches!(event, SubscriberEvent::StreamTerminated(_)) {
5720 self.entries.remove(index);
5721 self.next_index = if self.entries.is_empty() {
5722 0
5723 } else {
5724 index % self.entries.len()
5725 };
5726 } else {
5727 self.next_index = (index + 1) % self.entries.len();
5728 }
5729 return std::task::Poll::Ready(Some(event));
5730 }
5731 std::task::Poll::Ready(None) => {
5732 self.entries.remove(index);
5733 if self.entries.is_empty() {
5734 self.next_index = 0;
5735 return std::task::Poll::Ready(None);
5736 }
5737 }
5738 std::task::Poll::Pending => {
5739 checked += 1;
5740 index += 1;
5741 }
5742 }
5743 }
5744
5745 if self.entries.is_empty() {
5746 std::task::Poll::Ready(None)
5747 } else {
5748 self.next_index = index % self.entries.len();
5749 std::task::Poll::Pending
5750 }
5751 })
5752 .await
5753 }
5754}
5755
5756#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5757#[allow(dead_code)]
5758enum SubscriberTransport {
5759 PubSub,
5760 Polling,
5761}
5762
5763#[derive(Clone, Debug)]
5764enum SubscriberStreamSource {
5765 PubSubLog { id: usize, filter: Filter },
5766 PubSubPendingHashes,
5767 PubSubBlockHeaders,
5768 PollingLog { filter: Filter },
5769 PollingPendingHashes,
5770}
5771
5772impl SubscriberStreamSource {
5773 fn label(&self) -> &'static str {
5774 match self {
5775 Self::PubSubLog { .. } => "pubsub log",
5776 Self::PubSubPendingHashes => "pubsub pending transaction hash",
5777 Self::PubSubBlockHeaders => "pubsub block header",
5778 Self::PollingLog { .. } => "polling log",
5779 Self::PollingPendingHashes => "polling pending transaction hash",
5780 }
5781 }
5782
5783 fn is_pubsub(&self) -> bool {
5784 matches!(
5785 self,
5786 Self::PubSubLog { .. } | Self::PubSubPendingHashes | Self::PubSubBlockHeaders
5787 )
5788 }
5789
5790 fn same_key(&self, other: &Self) -> bool {
5791 match (self, other) {
5792 (Self::PubSubLog { filter: left, .. }, Self::PubSubLog { filter: right, .. })
5793 | (Self::PollingLog { filter: left }, Self::PollingLog { filter: right }) => {
5794 left == right
5795 }
5796 (Self::PubSubPendingHashes, Self::PubSubPendingHashes)
5797 | (Self::PubSubBlockHeaders, Self::PubSubBlockHeaders)
5798 | (Self::PollingPendingHashes, Self::PollingPendingHashes) => true,
5799 _ => false,
5800 }
5801 }
5802}
5803
5804#[allow(dead_code)]
5805enum SubscriberEvent<N: Network> {
5806 Log { source_id: usize, log: Log },
5807 BackfilledLogs { source_id: usize, logs: Vec<Log> },
5808 Logs(Vec<Log>),
5809 BlockHeader(N::HeaderResponse),
5810 PendingHash(B256),
5811 PendingHashes(Vec<B256>),
5812 StreamTerminated(SubscriberStreamSource),
5813}
5814
5815impl<P, N> EventSubscriber<N> for AlloySubscriber<P, N>
5816where
5817 P: Provider<N> + Send + Sync,
5818 N: Network + 'static,
5819 N::HeaderResponse: Send + 'static,
5820{
5821 fn register_interests(
5822 &mut self,
5823 interests: &[ReactiveInterest<N>],
5824 ) -> Result<(), SubscriberError> {
5825 validate_subscriber_config(&self.config)?;
5826 validate_supported_interests(self.mode, &self.config, interests)?;
5827
5828 self.base_interests = interests.to_vec();
5829 self.owned_interests.clear();
5830 self.rebuild_registered_interests();
5831 self.reset_delivery_state();
5832 self.state = AlloySubscriberState::Uninitialized;
5833 Ok(())
5834 }
5835
5836 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N> {
5837 Box::pin(async {
5838 Ok(self
5839 .next_scoped_batch()
5840 .await?
5841 .map(SubscriberInputBatch::into_reactive_batch))
5842 })
5843 }
5844}
5845
5846impl<P, N> AlloySubscriber<P, N>
5847where
5848 P: Provider<N> + Send + Sync,
5849 N: Network + 'static,
5850 N::HeaderResponse: Send + 'static,
5851{
5852 pub async fn reconcile_interest_owner(
5860 &mut self,
5861 epoch: &SubscriberOwnerEpoch,
5862 through: BlockRef,
5863 ) -> Result<SubscriberOwnerProgress, SubscriberOwnerError>
5864 where
5865 P: Clone,
5866 {
5867 self.reconcile_interest_owners(std::slice::from_ref(epoch), through)
5868 .await?
5869 .pop()
5870 .ok_or(SubscriberOwnerError::NotStaged)
5871 }
5872
5873 pub async fn reconcile_interest_owners(
5890 &mut self,
5891 epochs: &[SubscriberOwnerEpoch],
5892 through: BlockRef,
5893 ) -> Result<Vec<SubscriberOwnerProgress>, SubscriberOwnerError>
5894 where
5895 P: Clone,
5896 {
5897 if epochs.is_empty() {
5898 return Ok(Vec::new());
5899 }
5900
5901 let mut seen = HashSet::new();
5902 let mut plans = Vec::with_capacity(epochs.len());
5903 for epoch in epochs {
5904 if !seen.insert(epoch.clone()) {
5905 continue;
5906 }
5907 let entry = self
5908 .owned_interests
5909 .iter()
5910 .find(|entry| {
5911 entry.epoch.as_ref() == Some(epoch)
5912 && entry.state == SubscriberOwnerState::Staged
5913 })
5914 .ok_or(SubscriberOwnerError::NotStaged)?;
5915 let position = entry
5916 .progress
5917 .as_ref()
5918 .map(|progress| &progress.through)
5919 .or(entry.baseline.as_ref())
5920 .ok_or(SubscriberOwnerError::MissingBaseline)?;
5921 let baseline = position.number;
5922 if through.number < baseline {
5923 return Err(SubscriberOwnerError::ProgressRegression {
5924 current: baseline,
5925 target: through.number,
5926 });
5927 }
5928 let from_block = baseline
5929 .checked_add(1)
5930 .ok_or(SubscriberOwnerError::PostBlockOverflow(baseline))?;
5931 if through.number == baseline && through.hash != position.hash {
5932 return Err(SubscriberOwnerError::ProgressConflict {
5933 number: baseline,
5934 current_hash: position.hash,
5935 target_hash: through.hash,
5936 });
5937 }
5938 if through.number == from_block
5939 && through
5940 .parent_hash
5941 .is_some_and(|parent| parent != position.hash)
5942 {
5943 return Err(SubscriberOwnerError::ProgressConflict {
5944 number: baseline,
5945 current_hash: position.hash,
5946 target_hash: through.parent_hash.expect("checked as present above"),
5947 });
5948 }
5949 if entry
5950 .interests
5951 .iter()
5952 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
5953 {
5954 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
5955 }
5956 plans.push(SubscriberOwnerReconcilePlan {
5957 epoch: epoch.clone(),
5958 interests: entry.interests.clone(),
5959 retained: position.clone(),
5960 from_block,
5961 });
5962 }
5963
5964 self.ensure_streams().await?;
5967 let provider = self.provider.clone();
5968 let filters = merged_owner_reconcile_filters(&plans, through.number);
5969 let retained = plans.iter().map(|plan| plan.retained.clone()).collect();
5970 let target_epochs: HashSet<_> = plans.iter().map(|plan| plan.epoch.clone()).collect();
5971 let fetch = fetch_owner_catchup::<P, N>(provider, filters, retained, through);
5972 let SubscriberOwnerCatchup { logs, certified } =
5973 self.drive_reconcile_fetch(fetch, &target_epochs).await?;
5974
5975 self.pending_backfills.retain(|queued| {
5976 queued
5977 .epoch
5978 .as_ref()
5979 .is_none_or(|epoch| !target_epochs.contains(epoch))
5980 });
5981 let records = logs
5982 .into_iter()
5983 .map(|log| log_input_record(log, InputSource::Backfill))
5984 .collect();
5985 for record in dedupe_records(sort_records(records)) {
5986 let block_number = match &record.input {
5987 ReactiveInput::Log(log) => log
5988 .block_number
5989 .expect("bulk catch-up logs were validated before commit"),
5990 _ => unreachable!("bulk owner catch-up contains log records only"),
5991 };
5992 let owners = plans
5993 .iter()
5994 .filter(|plan| block_number >= plan.from_block)
5995 .filter(|plan| {
5996 plan.interests
5997 .iter()
5998 .any(|interest| interest_matches(interest, &record.input))
5999 })
6000 .map(|plan| plan.epoch.clone())
6001 .collect();
6002 self.enqueue_owner_record_for_owners_unmerged(record, owners);
6003 }
6004 self.promote_reconcile_owner_records(&target_epochs);
6005 self.seed_reconciled_filter_anchors(&plans, certified.number);
6006
6007 let stream_revision = self.stream_revision;
6008 let mut progress = Vec::with_capacity(plans.len());
6009 for plan in plans {
6010 let item = SubscriberOwnerProgress {
6011 owner: plan.epoch.clone(),
6012 through: certified.clone(),
6013 };
6014 let entry = self
6015 .owned_interests
6016 .iter_mut()
6017 .find(|entry| entry.epoch.as_ref() == Some(&plan.epoch))
6018 .expect("bulk reconcile holds exclusive access after epoch preflight");
6019 entry.progress = Some(item.clone());
6020 entry.progress_stream_revision = Some(stream_revision);
6021 progress.push(item);
6022 }
6023 Ok(progress)
6024 }
6025
6026 async fn drive_reconcile_fetch<T, F>(
6027 &mut self,
6028 fetch: F,
6029 target_epochs: &HashSet<SubscriberOwnerEpoch>,
6030 ) -> Result<T, SubscriberOwnerError>
6031 where
6032 F: Future<Output = Result<T, SubscriberOwnerError>>,
6033 {
6034 if !matches!(&self.state, AlloySubscriberState::Active(_)) {
6035 return fetch.await;
6036 }
6037 let mut fetch = Box::pin(fetch);
6038 loop {
6039 let event = {
6040 let live = Box::pin(self.next_event());
6041 match select(fetch, live).await {
6042 Either::Left((result, pending_live)) => {
6043 drop(pending_live);
6044 return result;
6045 }
6046 Either::Right((event, pending_fetch)) => {
6047 fetch = pending_fetch;
6048 event
6049 }
6050 }
6051 };
6052 let event = event?.ok_or_else(|| {
6053 SubscriberError::Provider(
6054 "Alloy subscriber streams ended during owner reconcile".to_owned(),
6055 )
6056 })?;
6057 self.buffer_reconcile_event_for_owners(&event, target_epochs);
6058 self.enqueue_event_excluding_owners(event, target_epochs);
6059 }
6060 }
6061
6062 pub async fn next_scoped_batch_or<C, F>(
6076 &mut self,
6077 control: Pin<&mut F>,
6078 ) -> Result<SubscriberDriverPoll<C, N>, SubscriberError>
6079 where
6080 C: Send,
6081 F: Future<Output = C> + Send,
6082 {
6083 let batch = self.next_scoped_batch();
6084 match select(control, batch).await {
6085 Either::Left((control, pending_batch)) => {
6086 drop(pending_batch);
6087 Ok(SubscriberDriverPoll::Control(control))
6088 }
6089 Either::Right((batch, _pending_control)) => batch.map(SubscriberDriverPoll::Batch),
6090 }
6091 }
6092
6093 pub fn next_scoped_batch(&mut self) -> SubscriberNextScopedBatch<'_, N> {
6104 Box::pin(async {
6105 if let Some(batch) = self.drain_next_scoped_batch() {
6106 return Ok(Some(batch));
6107 }
6108
6109 self.drain_pending_backfills().await?;
6110 if let Some(batch) = self.drain_next_scoped_batch() {
6111 return Ok(Some(batch));
6112 }
6113
6114 self.ensure_streams().await?;
6115 if let Some(batch) = self.drain_next_scoped_batch() {
6116 return Ok(Some(batch));
6117 }
6118
6119 if self.interests.is_empty() {
6120 return Ok(None);
6121 }
6122
6123 loop {
6124 let Some(event) = self.next_event().await? else {
6125 return Ok(None);
6126 };
6127
6128 self.enqueue_event(event);
6129 if let Some(batch) = self.drain_next_scoped_batch() {
6130 return Ok(Some(batch));
6131 }
6132 }
6133 })
6134 }
6135
6136 async fn ensure_streams(&mut self) -> Result<(), SubscriberError> {
6151 if !self.sources_dirty {
6152 return Ok(());
6153 }
6154 if matches!(self.state, AlloySubscriberState::Uninitialized) && self.interests.is_empty() {
6159 self.bump_stream_revision();
6160 self.sources_dirty = false;
6161 return Ok(());
6162 }
6163
6164 let desired = self.stream_sources()?;
6165 let missing: Vec<SubscriberStreamSource> = match &self.state {
6166 AlloySubscriberState::Active(streams) => desired
6167 .iter()
6168 .filter(|source| !streams.contains_source(source))
6169 .cloned()
6170 .collect(),
6171 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => desired.clone(),
6172 };
6173
6174 let mut connected = Vec::new();
6175 for source in missing {
6176 let stream = self.connect_source_stream(source.clone()).await?;
6177 if let Some(event) = self.backfill_reconnected_source(&source).await? {
6182 self.enqueue_event(event);
6183 }
6184 connected.push((source, stream));
6185 }
6186
6187 match &mut self.state {
6188 AlloySubscriberState::Active(streams) => {
6189 streams.retain_sources(&desired);
6190 for (source, stream) in connected {
6191 streams.push(source, stream);
6192 }
6193 if streams.is_empty() {
6194 self.state = AlloySubscriberState::Empty;
6195 }
6196 }
6197 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
6198 let mut streams = SubscriberStreams::new();
6199 for (source, stream) in connected {
6200 streams.push(source, stream);
6201 }
6202 self.state = if streams.is_empty() {
6203 AlloySubscriberState::Empty
6204 } else {
6205 AlloySubscriberState::Active(streams)
6206 };
6207 }
6208 }
6209
6210 self.bump_stream_revision();
6211 self.sources_dirty = false;
6212 self.retire_unreferenced_filters();
6213 Ok(())
6214 }
6215
6216 async fn drain_pending_backfills(&mut self) -> Result<(), SubscriberError> {
6228 while let Some(queued) = self.pending_backfills.front() {
6229 let epoch = queued.epoch.clone();
6231 let owner_exists = match &epoch {
6232 Some(epoch) => self.interest_owner_state(epoch).is_some(),
6233 None => self.owner_interests(&queued.owner).is_some(),
6234 };
6235 if !owner_exists {
6236 self.pending_backfills.pop_front();
6237 continue;
6238 }
6239 let filter = queued.filter.clone();
6240 let backfill = queued.backfill;
6241
6242 let to_block = match backfill.end_block() {
6243 Some(to_block) => to_block,
6244 None => self
6245 .provider
6246 .get_block_number()
6247 .await
6248 .map_err(provider_error)?,
6249 };
6250 if to_block < backfill.start_block() {
6251 self.pending_backfills.pop_front();
6254 continue;
6255 }
6256
6257 let range = filter
6258 .clone()
6259 .from_block(backfill.start_block())
6260 .to_block(to_block);
6261 let logs = self
6262 .provider
6263 .get_logs(&range)
6264 .await
6265 .map_err(provider_error)?;
6266
6267 self.pending_backfills.pop_front();
6270 let source_id = self.log_source_id(&filter);
6271 self.enqueue_backfilled_logs(logs, Some(source_id), epoch.as_ref(), Some(backfill));
6272 if epoch.is_none() {
6273 let anchor = self
6274 .last_seen_log_blocks
6275 .entry(source_id)
6276 .or_insert(to_block);
6277 *anchor = (*anchor).max(to_block);
6278 }
6279
6280 if !self.pending_records.is_empty() {
6281 break;
6282 }
6283 }
6284 Ok(())
6285 }
6286
6287 fn stream_sources(&mut self) -> Result<Vec<SubscriberStreamSource>, SubscriberError> {
6288 match resolve_subscriber_transport(self.mode)? {
6289 SubscriberTransport::PubSub => Ok(self.pubsub_stream_sources()),
6290 SubscriberTransport::Polling => Ok(self.polling_stream_sources()),
6291 }
6292 }
6293
6294 fn pubsub_stream_sources(&mut self) -> Vec<SubscriberStreamSource> {
6295 let mut sources = Vec::new();
6296 let inherited_anchor = self.last_seen_log_blocks.values().copied().min();
6297
6298 for filter in self.log_stream_filters() {
6299 let id = self.log_source_id(&filter);
6300 if let Some(anchor) = inherited_anchor {
6301 self.last_seen_log_blocks.entry(id).or_insert(anchor);
6302 }
6303 sources.push(SubscriberStreamSource::PubSubLog { id, filter });
6304 }
6305
6306 if needs_pending_hash_stream(&self.interests) {
6307 sources.push(SubscriberStreamSource::PubSubPendingHashes);
6308 }
6309
6310 if needs_header_block_stream(&self.interests) {
6311 sources.push(SubscriberStreamSource::PubSubBlockHeaders);
6312 }
6313
6314 sources
6315 }
6316
6317 fn polling_stream_sources(&self) -> Vec<SubscriberStreamSource> {
6318 let mut sources = Vec::new();
6319
6320 for filter in self.log_stream_filters() {
6321 sources.push(SubscriberStreamSource::PollingLog { filter });
6322 }
6323
6324 if needs_pending_hash_stream(&self.interests) {
6325 sources.push(SubscriberStreamSource::PollingPendingHashes);
6326 }
6327
6328 sources
6329 }
6330
6331 fn log_source_id(&mut self, filter: &Filter) -> usize {
6332 if let Some(id) = self.log_source_ids.get(filter) {
6333 return *id;
6334 }
6335
6336 let id = self.next_log_source_id;
6337 self.next_log_source_id = self.next_log_source_id.saturating_add(1);
6338 self.log_source_ids.insert(filter.clone(), id);
6339 id
6340 }
6341
6342 async fn connect_source_stream(
6343 &mut self,
6344 source: SubscriberStreamSource,
6345 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
6346 match source {
6347 SubscriberStreamSource::PubSubLog { id, filter } => {
6348 self.connect_pubsub_log_stream(id, filter).await
6349 }
6350 SubscriberStreamSource::PubSubPendingHashes => {
6351 self.connect_pubsub_pending_hash_stream().await
6352 }
6353 SubscriberStreamSource::PubSubBlockHeaders => {
6354 self.connect_pubsub_block_header_stream().await
6355 }
6356 SubscriberStreamSource::PollingLog { filter } => {
6357 self.connect_polling_log_stream(filter).await
6358 }
6359 SubscriberStreamSource::PollingPendingHashes => {
6360 self.connect_polling_pending_hash_stream().await
6361 }
6362 }
6363 }
6364
6365 async fn connect_pubsub_log_stream(
6366 &mut self,
6367 id: usize,
6368 filter: Filter,
6369 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
6370 #[cfg(feature = "reactive-ws")]
6371 {
6372 let source = SubscriberStreamSource::PubSubLog {
6373 id,
6374 filter: filter.clone(),
6375 };
6376 let stream = self
6377 .provider
6378 .subscribe_logs(&filter)
6379 .channel_size(self.config.max_batch_size.max(1))
6380 .await
6381 .map_err(provider_error)?
6382 .into_stream()
6383 .map(move |log| SubscriberEvent::Log { source_id: id, log });
6384 Ok(stream_with_termination(stream, source))
6385 }
6386
6387 #[cfg(not(feature = "reactive-ws"))]
6388 {
6389 let _ = (id, filter);
6390 Err(SubscriberError::Unsupported(
6391 "AlloySubscriber pubsub mode requires the reactive-ws feature",
6392 ))
6393 }
6394 }
6395
6396 async fn connect_pubsub_pending_hash_stream(
6397 &mut self,
6398 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
6399 #[cfg(feature = "reactive-ws")]
6400 {
6401 let stream = self
6402 .provider
6403 .subscribe_pending_transactions()
6404 .channel_size(self.config.max_batch_size.max(1))
6405 .await
6406 .map_err(provider_error)?
6407 .into_stream()
6408 .map(SubscriberEvent::PendingHash);
6409 Ok(stream_with_termination(
6410 stream,
6411 SubscriberStreamSource::PubSubPendingHashes,
6412 ))
6413 }
6414
6415 #[cfg(not(feature = "reactive-ws"))]
6416 {
6417 Err(SubscriberError::Unsupported(
6418 "AlloySubscriber pubsub mode requires the reactive-ws feature",
6419 ))
6420 }
6421 }
6422
6423 async fn connect_pubsub_block_header_stream(
6424 &mut self,
6425 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
6426 #[cfg(feature = "reactive-ws")]
6427 {
6428 let stream = self
6429 .provider
6430 .subscribe_blocks()
6431 .channel_size(self.config.max_batch_size.max(1))
6432 .await
6433 .map_err(provider_error)?
6434 .into_stream()
6435 .map(SubscriberEvent::BlockHeader);
6436 Ok(stream_with_termination(
6437 stream,
6438 SubscriberStreamSource::PubSubBlockHeaders,
6439 ))
6440 }
6441
6442 #[cfg(not(feature = "reactive-ws"))]
6443 {
6444 Err(SubscriberError::Unsupported(
6445 "AlloySubscriber pubsub mode requires the reactive-ws feature",
6446 ))
6447 }
6448 }
6449
6450 async fn connect_polling_log_stream(
6451 &mut self,
6452 filter: Filter,
6453 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
6454 #[cfg(feature = "reactive-polling")]
6455 {
6456 let source = SubscriberStreamSource::PollingLog {
6457 filter: filter.clone(),
6458 };
6459 let stream = self
6460 .provider
6461 .watch_logs(&filter)
6462 .await
6463 .map_err(provider_error)?
6464 .with_channel_size(self.config.max_batch_size.max(1))
6465 .into_stream()
6466 .map(SubscriberEvent::Logs);
6467 Ok(stream_with_termination(stream, source))
6468 }
6469
6470 #[cfg(not(feature = "reactive-polling"))]
6471 {
6472 let _ = filter;
6473 Err(SubscriberError::Unsupported(
6474 "AlloySubscriber polling mode requires the reactive-polling feature",
6475 ))
6476 }
6477 }
6478
6479 async fn connect_polling_pending_hash_stream(
6480 &mut self,
6481 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
6482 #[cfg(feature = "reactive-polling")]
6483 {
6484 let stream = self
6485 .provider
6486 .watch_pending_transactions()
6487 .await
6488 .map_err(provider_error)?
6489 .with_channel_size(self.config.max_batch_size.max(1))
6490 .into_stream()
6491 .map(SubscriberEvent::PendingHashes);
6492 Ok(stream_with_termination(
6493 stream,
6494 SubscriberStreamSource::PollingPendingHashes,
6495 ))
6496 }
6497
6498 #[cfg(not(feature = "reactive-polling"))]
6499 {
6500 Err(SubscriberError::Unsupported(
6501 "AlloySubscriber polling mode requires the reactive-polling feature",
6502 ))
6503 }
6504 }
6505
6506 async fn next_event(&mut self) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
6507 loop {
6508 let event = match &mut self.state {
6509 AlloySubscriberState::Active(streams) => streams.next().await,
6510 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
6511 return Ok(None);
6512 }
6513 };
6514
6515 let Some(event) = event else {
6516 return Err(SubscriberError::Provider(
6517 "Alloy subscriber streams terminated before the subscriber was stopped"
6518 .to_owned(),
6519 ));
6520 };
6521
6522 match event {
6523 SubscriberEvent::StreamTerminated(source) => {
6524 self.sources_dirty = true;
6528 self.bump_stream_revision();
6529 if let Some(backfill_event) = self.reconnect_source_stream(source).await? {
6530 self.sources_dirty = false;
6531 return Ok(Some(backfill_event));
6532 }
6533 self.sources_dirty = false;
6534 }
6535 event => return Ok(Some(event)),
6536 }
6537 }
6538 }
6539
6540 fn enqueue_event(&mut self, event: SubscriberEvent<N>) {
6541 self.enqueue_event_with_excluded_owners(event, None);
6542 }
6543
6544 fn buffer_reconcile_event_for_owners(
6545 &mut self,
6546 event: &SubscriberEvent<N>,
6547 target_epochs: &HashSet<SubscriberOwnerEpoch>,
6548 ) {
6549 match event {
6550 SubscriberEvent::Log { log, .. } => {
6551 self.buffer_reconcile_log_for_owners(log, InputSource::Subscription, target_epochs)
6552 }
6553 SubscriberEvent::BackfilledLogs { logs, .. } => {
6554 for log in logs {
6555 self.buffer_reconcile_log_for_owners(log, InputSource::Backfill, target_epochs);
6556 }
6557 }
6558 SubscriberEvent::Logs(logs) => {
6559 for log in logs {
6560 self.buffer_reconcile_log_for_owners(log, InputSource::Poll, target_epochs);
6561 }
6562 }
6563 SubscriberEvent::BlockHeader(_)
6564 | SubscriberEvent::PendingHash(_)
6565 | SubscriberEvent::PendingHashes(_)
6566 | SubscriberEvent::StreamTerminated(_) => {}
6567 }
6568 }
6569
6570 fn buffer_reconcile_log_for_owners(
6571 &mut self,
6572 log: &Log,
6573 source: InputSource,
6574 target_epochs: &HashSet<SubscriberOwnerEpoch>,
6575 ) {
6576 let record = log_input_record(log.clone(), source);
6577 let owners = self
6578 .staged_owners_for_record(&record)
6579 .into_iter()
6580 .filter(|owner| target_epochs.contains(owner))
6581 .collect::<Vec<_>>();
6582 if !owners.is_empty() {
6583 self.pending_reconcile_owner_records
6584 .push_back(BufferedSubscriberOwnerRecord { record, owners });
6585 }
6586 }
6587
6588 fn promote_reconcile_owner_records(&mut self, target_epochs: &HashSet<SubscriberOwnerEpoch>) {
6589 let mut retained = VecDeque::new();
6590 while let Some(mut buffered) = self.pending_reconcile_owner_records.pop_front() {
6591 let mut promoted = Vec::new();
6592 buffered.owners.retain(|owner| {
6593 if target_epochs.contains(owner) {
6594 promoted.push(owner.clone());
6595 false
6596 } else {
6597 true
6598 }
6599 });
6600 if promoted.is_empty() {
6601 retained.push_back(buffered);
6602 continue;
6603 }
6604 let promoted_record = if buffered.owners.is_empty() {
6605 buffered.record
6606 } else {
6607 let record = buffered.record.clone();
6608 retained.push_back(buffered);
6609 record
6610 };
6611 self.enqueue_owner_record_for_owners_unmerged(promoted_record, promoted);
6612 }
6613 self.pending_reconcile_owner_records = retained;
6614 }
6615
6616 fn seed_reconciled_filter_anchors(
6617 &mut self,
6618 plans: &[SubscriberOwnerReconcilePlan<N>],
6619 through: u64,
6620 ) {
6621 for filter in plans.iter().flat_map(|plan| log_filters(&plan.interests)) {
6622 let Some(source_id) = self.log_source_ids.get(&filter).copied() else {
6623 continue;
6624 };
6625 let anchor = self
6626 .last_seen_log_blocks
6627 .entry(source_id)
6628 .or_insert(through);
6629 *anchor = (*anchor).max(through);
6630 }
6631 }
6632
6633 fn enqueue_event_excluding_owners(
6634 &mut self,
6635 event: SubscriberEvent<N>,
6636 excluded: &HashSet<SubscriberOwnerEpoch>,
6637 ) {
6638 self.enqueue_event_with_excluded_owners(event, Some(excluded));
6639 }
6640
6641 fn enqueue_event_with_excluded_owners(
6642 &mut self,
6643 event: SubscriberEvent<N>,
6644 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
6645 ) {
6646 match event {
6647 SubscriberEvent::Log { source_id, log } => {
6648 if log_matches_any_interest(&log, &self.interests) {
6649 let record = log_input_record(log, InputSource::Subscription);
6650 self.note_log_block(source_id, &record);
6651 self.enqueue_record_with_excluded_owners(record, excluded);
6652 }
6653 }
6654 SubscriberEvent::BackfilledLogs { source_id, logs } => {
6655 self.enqueue_backfilled_logs_with_excluded_owners(
6656 logs,
6657 Some(source_id),
6658 None,
6659 None,
6660 excluded,
6661 );
6662 }
6663 SubscriberEvent::Logs(logs) => {
6664 for log in logs {
6665 if log_matches_any_interest(&log, &self.interests) {
6666 self.enqueue_record_with_excluded_owners(
6667 log_input_record(log, InputSource::Poll),
6668 excluded,
6669 );
6670 }
6671 }
6672 }
6673 SubscriberEvent::BlockHeader(header) => {
6674 if needs_header_block_stream(&self.interests) {
6675 let record = block_header_input_record::<N>(header);
6676 self.enqueue_record_with_excluded_owners(record, excluded);
6677 }
6678 }
6679 SubscriberEvent::PendingHash(hash) => {
6680 let record = pending_hash_input_record::<N>(hash, InputSource::Subscription);
6681 self.enqueue_record_with_excluded_owners(record, excluded);
6682 }
6683 SubscriberEvent::PendingHashes(hashes) => {
6684 for hash in hashes {
6685 self.enqueue_record_with_excluded_owners(
6686 pending_hash_input_record::<N>(hash, InputSource::Poll),
6687 excluded,
6688 );
6689 }
6690 }
6691 SubscriberEvent::StreamTerminated(_) => {}
6692 }
6693 }
6694
6695 fn enqueue_backfilled_logs(
6696 &mut self,
6697 logs: Vec<Log>,
6698 source_id: Option<usize>,
6699 owner: Option<&SubscriberOwnerEpoch>,
6700 range: Option<SubscriberBackfill>,
6701 ) {
6702 self.enqueue_backfilled_logs_with_excluded_owners(logs, source_id, owner, range, None);
6703 }
6704
6705 fn enqueue_backfilled_logs_with_excluded_owners(
6706 &mut self,
6707 logs: Vec<Log>,
6708 source_id: Option<usize>,
6709 owner: Option<&SubscriberOwnerEpoch>,
6710 range: Option<SubscriberBackfill>,
6711 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
6712 ) {
6713 for log in logs {
6714 if range.is_some_and(|range| {
6715 log.block_number.is_some_and(|block| {
6716 block < range.start_block() || range.end_block().is_some_and(|end| block > end)
6717 })
6718 }) {
6719 continue;
6720 }
6721 let matches = match owner {
6722 Some(epoch) => self
6723 .owned_interests
6724 .iter()
6725 .find(|entry| entry.epoch.as_ref() == Some(epoch))
6726 .is_some_and(|entry| log_matches_any_interest(&log, &entry.interests)),
6727 None => log_matches_any_interest(&log, &self.interests),
6728 };
6729 if matches {
6730 let record = log_input_record(log, InputSource::Backfill);
6731 if let Some(epoch) = owner {
6732 self.enqueue_owner_record(record, epoch.clone());
6733 } else {
6734 if let Some(source_id) = source_id {
6735 self.note_log_block(source_id, &record);
6736 }
6737 self.enqueue_record_with_excluded_owners(record, excluded);
6738 }
6739 }
6740 }
6741 }
6742
6743 async fn reconnect_source_stream(
6744 &mut self,
6745 source: SubscriberStreamSource,
6746 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
6747 if !source.is_pubsub() {
6748 return Err(stream_terminated_error(&source));
6749 }
6750
6751 if !self.config.reconnect.enabled {
6752 return Err(SubscriberError::Provider(format!(
6753 "Alloy subscriber {} stream terminated and reconnect is disabled",
6754 source.label()
6755 )));
6756 }
6757
6758 let mut attempts = 0usize;
6759 let mut delay = self.config.reconnect.initial_delay;
6760 let mut retry_delay = self.config.reconnect.retry_delay;
6761
6762 loop {
6763 attempts = attempts.saturating_add(1);
6764 if !delay.is_zero() {
6765 tokio::time::sleep(delay).await;
6766 }
6767
6768 match self.reconnect_source_once(source.clone()).await {
6769 Ok(backfill_event) => return Ok(backfill_event),
6770 Err(error) if reconnect_attempts_exhausted(attempts, &self.config.reconnect) => {
6771 return Err(SubscriberError::Provider(format!(
6772 "Alloy subscriber {} stream terminated and reconnect failed after {attempts} attempt(s): {error}",
6773 source.label()
6774 )));
6775 }
6776 Err(error) => {
6777 tracing::warn!(
6778 stream = source.label(),
6779 attempts,
6780 error = %error,
6781 "Alloy subscriber reconnect attempt failed"
6782 );
6783 delay = retry_delay;
6784 retry_delay =
6785 next_reconnect_delay(retry_delay, self.config.reconnect.max_delay);
6786 }
6787 }
6788 }
6789 }
6790
6791 async fn reconnect_source_once(
6792 &mut self,
6793 source: SubscriberStreamSource,
6794 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
6795 let stream = self.connect_source_stream(source.clone()).await?;
6796 let backfill_event = self.backfill_reconnected_source(&source).await?;
6797
6798 match &mut self.state {
6799 AlloySubscriberState::Active(streams) => streams.push(source, stream),
6800 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
6801 return Err(SubscriberError::Provider(
6802 "Alloy subscriber state changed before reconnect completed".to_owned(),
6803 ));
6804 }
6805 }
6806
6807 Ok(backfill_event)
6808 }
6809
6810 async fn backfill_reconnected_source(
6811 &mut self,
6812 source: &SubscriberStreamSource,
6813 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
6814 let SubscriberStreamSource::PubSubLog { id, filter } = source else {
6815 return Ok(None);
6816 };
6817 let Some(from_block) = self.last_seen_log_blocks.get(id).copied() else {
6818 return Ok(None);
6819 };
6820
6821 let latest = self
6822 .provider
6823 .get_block_number()
6824 .await
6825 .map_err(provider_error)?;
6826 if latest < from_block {
6827 return Ok(None);
6828 }
6829
6830 let logs = self
6831 .provider
6832 .get_logs(&filter.clone().from_block(from_block).to_block(latest))
6833 .await
6834 .map_err(provider_error)?;
6835 Ok(Some(SubscriberEvent::BackfilledLogs {
6836 source_id: *id,
6837 logs,
6838 }))
6839 }
6840
6841 fn note_log_block(&mut self, source_id: usize, record: &ReactiveInputRecord<N>) {
6842 if let Some(block) = record.context.block.as_ref() {
6843 self.last_seen_log_blocks.insert(source_id, block.number);
6844 }
6845 }
6846
6847 fn enqueue_record_with_excluded_owners(
6848 &mut self,
6849 record: ReactiveInputRecord<N>,
6850 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
6851 ) {
6852 let mut owners = self.staged_owners_for_record(&record);
6853 if let Some(excluded) = excluded {
6854 owners.retain(|owner| !excluded.contains(owner));
6855 }
6856 let canonical_duplicate = self.should_skip_recent_duplicate(&record);
6857 let owners = self.filter_recent_owner_duplicates(&record, owners);
6858 if canonical_duplicate {
6859 if !owners.is_empty() {
6860 self.pending_records.push_back(SubscriberInputRecord {
6861 record,
6862 scope: SubscriberInputScope::OwnerOnly { owners },
6863 });
6864 }
6865 return;
6866 }
6867 self.remember_record(&record);
6868 self.pending_records.push_back(SubscriberInputRecord {
6869 record,
6870 scope: SubscriberInputScope::Canonical { owners },
6871 });
6872 }
6873
6874 fn enqueue_owner_record(
6875 &mut self,
6876 record: ReactiveInputRecord<N>,
6877 owner: SubscriberOwnerEpoch,
6878 ) {
6879 self.enqueue_owner_record_for_owners(record, vec![owner]);
6880 }
6881
6882 fn enqueue_owner_record_for_owners(
6883 &mut self,
6884 record: ReactiveInputRecord<N>,
6885 owners: Vec<SubscriberOwnerEpoch>,
6886 ) {
6887 self.enqueue_owner_record_for_owners_inner(record, owners, true);
6888 }
6889
6890 fn enqueue_owner_record_for_owners_unmerged(
6891 &mut self,
6892 record: ReactiveInputRecord<N>,
6893 owners: Vec<SubscriberOwnerEpoch>,
6894 ) {
6895 self.enqueue_owner_record_for_owners_inner(record, owners, false);
6896 }
6897
6898 fn enqueue_owner_record_for_owners_inner(
6899 &mut self,
6900 record: ReactiveInputRecord<N>,
6901 owners: Vec<SubscriberOwnerEpoch>,
6902 merge_pending: bool,
6903 ) {
6904 let owners = self.filter_recent_owner_duplicates(&record, owners);
6905 if owners.is_empty() {
6906 return;
6907 }
6908 if merge_pending
6909 && should_dedupe_record(&record)
6910 && self.config.reconnect.dedupe_window != 0
6911 {
6912 let input_ref = record.input_ref();
6913 if let Some(pending) = self
6914 .pending_records
6915 .iter_mut()
6916 .rev()
6917 .find(|pending| pending.record.input_ref() == input_ref)
6918 {
6919 let pending_owners = match &mut pending.scope {
6920 SubscriberInputScope::Canonical { owners }
6921 | SubscriberInputScope::OwnerOnly { owners } => owners,
6922 };
6923 for owner in owners {
6924 if !pending_owners.contains(&owner) {
6925 pending_owners.push(owner);
6926 }
6927 }
6928 return;
6929 }
6930 }
6931 self.pending_records.push_back(SubscriberInputRecord {
6932 record,
6933 scope: SubscriberInputScope::OwnerOnly { owners },
6934 });
6935 }
6936
6937 fn staged_owners_for_record(
6938 &self,
6939 record: &ReactiveInputRecord<N>,
6940 ) -> Vec<SubscriberOwnerEpoch> {
6941 self.owned_interests
6942 .iter()
6943 .filter(|entry| entry.state == SubscriberOwnerState::Staged)
6944 .filter(|entry| {
6945 entry
6946 .interests
6947 .iter()
6948 .any(|interest| interest_matches(interest, &record.input))
6949 })
6950 .filter_map(|entry| entry.epoch.clone())
6951 .collect()
6952 }
6953
6954 fn filter_recent_owner_duplicates(
6955 &mut self,
6956 record: &ReactiveInputRecord<N>,
6957 owners: Vec<SubscriberOwnerEpoch>,
6958 ) -> Vec<SubscriberOwnerEpoch> {
6959 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
6960 return owners;
6961 }
6962 let input_ref = record.input_ref();
6963 let window = self.config.reconnect.dedupe_window;
6964 owners
6965 .into_iter()
6966 .filter(|owner| {
6967 let seen = self
6968 .recent_owner_input_ref_sets
6969 .entry(owner.clone())
6970 .or_default();
6971 if !seen.insert(input_ref) {
6972 return false;
6973 }
6974 let recent = self
6975 .recent_owner_input_refs
6976 .entry(owner.clone())
6977 .or_default();
6978 recent.push_back(input_ref);
6979 while recent.len() > window {
6980 if let Some(evicted) = recent.pop_front() {
6981 seen.remove(&evicted);
6982 }
6983 }
6984 true
6985 })
6986 .collect()
6987 }
6988
6989 fn should_skip_recent_duplicate(&self, record: &ReactiveInputRecord<N>) -> bool {
6990 if !should_dedupe_record(record) {
6991 return false;
6992 }
6993 self.recent_input_ref_set.contains(&record.input_ref())
6994 }
6995
6996 fn remember_record(&mut self, record: &ReactiveInputRecord<N>) {
6997 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
6998 return;
6999 }
7000
7001 let input_ref = record.input_ref();
7002 if !self.recent_input_ref_set.insert(input_ref) {
7003 return;
7004 }
7005 self.recent_input_refs.push_back(input_ref);
7006
7007 while self.recent_input_refs.len() > self.config.reconnect.dedupe_window {
7008 if let Some(evicted) = self.recent_input_refs.pop_front() {
7009 self.recent_input_ref_set.remove(&evicted);
7010 }
7011 }
7012 }
7013}
7014
7015#[cfg(any(feature = "reactive-ws", feature = "reactive-polling", test))]
7016fn stream_with_termination<N, S>(
7017 stream: S,
7018 source: SubscriberStreamSource,
7019) -> BoxStream<'static, SubscriberEvent<N>>
7020where
7021 N: Network + 'static,
7022 S: futures::Stream<Item = SubscriberEvent<N>> + Send + 'static,
7023{
7024 stream
7025 .chain(stream::once(async move {
7026 SubscriberEvent::StreamTerminated(source)
7027 }))
7028 .boxed()
7029}
7030
7031fn aggregate_interests<N: Network>(
7032 base: &[ReactiveInterest<N>],
7033 owned: &[OwnedSubscriberInterests<N>],
7034) -> Vec<ReactiveInterest<N>> {
7035 base.iter()
7036 .cloned()
7037 .chain(
7038 owned
7039 .iter()
7040 .flat_map(|entry| entry.interests.iter().cloned()),
7041 )
7042 .collect()
7043}
7044
7045fn stream_terminated_error(source: &SubscriberStreamSource) -> SubscriberError {
7046 SubscriberError::Provider(format!(
7047 "Alloy subscriber {} stream terminated before the subscriber was stopped",
7048 source.label()
7049 ))
7050}
7051
7052fn reconnect_attempts_exhausted(attempts: usize, config: &SubscriberReconnectConfig) -> bool {
7053 config
7054 .max_attempts
7055 .is_some_and(|max_attempts| attempts >= max_attempts)
7056}
7057
7058fn next_reconnect_delay(current: Duration, max: Duration) -> Duration {
7059 if current.is_zero() {
7060 return current;
7061 }
7062 current.checked_mul(2).unwrap_or(max).min(max)
7063}
7064
7065fn should_dedupe_record<N: Network>(record: &ReactiveInputRecord<N>) -> bool {
7066 match &record.input {
7067 ReactiveInput::Log(log) => {
7068 is_canonical_status(&record.context.chain_status) && !log.removed
7069 }
7070 ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_) => true,
7071 ReactiveInput::FullBlock(_) | ReactiveInput::PendingTx(_) => false,
7072 }
7073}
7074
7075#[cfg(test)]
7076mod subscriber_helper_tests {
7077 use super::*;
7078 use alloy_provider::ProviderBuilder;
7079 use alloy_transport::mock::Asserter;
7080
7081 fn rpc_log(removed: bool) -> Log {
7082 Log {
7083 inner: alloy_primitives::Log::new_unchecked(
7084 Address::repeat_byte(0x42),
7085 vec![B256::repeat_byte(0x01)],
7086 Bytes::new(),
7087 ),
7088 block_hash: Some(B256::repeat_byte(0x02)),
7089 block_number: Some(7),
7090 block_timestamp: Some(1_700_000_000),
7091 transaction_hash: Some(B256::repeat_byte(0x03)),
7092 transaction_index: Some(4),
7093 log_index: Some(5),
7094 removed,
7095 }
7096 }
7097
7098 #[tokio::test(flavor = "multi_thread")]
7099 async fn stream_with_termination_yields_terminal_source_marker() {
7100 let mut stream = stream_with_termination::<Ethereum, _>(
7101 stream::iter([SubscriberEvent::<Ethereum>::PendingHash(B256::repeat_byte(
7102 0xaa,
7103 ))]),
7104 SubscriberStreamSource::PubSubPendingHashes,
7105 );
7106
7107 assert!(matches!(
7108 stream.next().await,
7109 Some(SubscriberEvent::PendingHash(hash)) if hash == B256::repeat_byte(0xaa)
7110 ));
7111 assert!(matches!(
7112 stream.next().await,
7113 Some(SubscriberEvent::StreamTerminated(source)) if source.is_pubsub()
7114 ));
7115 assert!(stream.next().await.is_none());
7116 }
7117
7118 #[test]
7119 fn reconnect_delay_doubles_until_capped() {
7120 assert_eq!(
7121 next_reconnect_delay(Duration::from_millis(250), Duration::from_secs(1)),
7122 Duration::from_millis(500)
7123 );
7124 assert_eq!(
7125 next_reconnect_delay(Duration::from_millis(750), Duration::from_secs(1)),
7126 Duration::from_secs(1)
7127 );
7128 assert_eq!(
7129 next_reconnect_delay(Duration::ZERO, Duration::from_secs(1)),
7130 Duration::ZERO
7131 );
7132 }
7133
7134 #[test]
7135 fn canonical_logs_are_deduped_but_removed_logs_are_not() {
7136 let included = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
7137 let removed = log_input_record::<Ethereum>(rpc_log(true), InputSource::Subscription);
7138
7139 assert!(should_dedupe_record(&included));
7140 assert!(!should_dedupe_record(&removed));
7141 }
7142
7143 #[test]
7144 fn active_owner_replacement_commits_atomically_to_one_new_epoch() {
7145 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7146 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7147 provider,
7148 SubscriberMode::Polling,
7149 SubscriberConfig::default(),
7150 );
7151 let owner = HandlerId::new("replace-owner");
7152 let original = ReactiveInterest::Logs(LogInterest {
7153 provider_filter: Filter::new().address(Address::repeat_byte(0x41)),
7154 local_matcher: None,
7155 route_key: None,
7156 });
7157 let replacement_interest = ReactiveInterest::Logs(LogInterest {
7158 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
7159 local_matcher: None,
7160 route_key: None,
7161 });
7162 let active = subscriber
7163 .stage_interest_owner(owner.clone(), &[original], SubscriberOwnerStart::Live)
7164 .unwrap();
7165 assert!(subscriber.activate_interest_owner(&active));
7166 let replacement = subscriber
7167 .stage_interest_owner_replacement(
7168 owner,
7169 &[replacement_interest],
7170 SubscriberOwnerStart::Live,
7171 )
7172 .unwrap();
7173
7174 assert!(subscriber.commit_interest_owner_replacement(&active, &replacement));
7175 assert_eq!(subscriber.interest_owner_state(&active), None);
7176 assert_eq!(
7177 subscriber.interest_owner_state(&replacement),
7178 Some(SubscriberOwnerState::Active)
7179 );
7180 assert_eq!(subscriber.registered_interests().len(), 1);
7181 }
7182
7183 #[tokio::test(flavor = "multi_thread")]
7184 #[cfg(feature = "reactive-ws")]
7185 async fn aborting_staged_epoch_purges_only_its_buffered_delivery() {
7186 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7187 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7188 provider,
7189 SubscriberMode::PubSub,
7190 SubscriberConfig::default(),
7191 );
7192 let interest = ReactiveInterest::Logs(LogInterest {
7193 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
7194 local_matcher: None,
7195 route_key: None,
7196 });
7197 let owner_a = subscriber
7198 .stage_interest_owner(
7199 HandlerId::new("owner-a"),
7200 std::slice::from_ref(&interest),
7201 SubscriberOwnerStart::Live,
7202 )
7203 .unwrap();
7204 let owner_b = subscriber
7205 .stage_interest_owner(
7206 HandlerId::new("owner-b"),
7207 &[interest],
7208 SubscriberOwnerStart::Live,
7209 )
7210 .unwrap();
7211
7212 subscriber.enqueue_event(SubscriberEvent::Log {
7213 source_id: 0,
7214 log: rpc_log(false),
7215 });
7216 assert!(subscriber.abort_interest_owner(&owner_a));
7217
7218 let batch = subscriber
7219 .next_scoped_batch()
7220 .await
7221 .unwrap()
7222 .expect("shared canonical delivery remains queued");
7223 assert_eq!(batch.records.len(), 1);
7224 assert_eq!(
7225 batch.records[0].scope,
7226 SubscriberInputScope::Canonical {
7227 owners: vec![owner_b]
7228 }
7229 );
7230 }
7231
7232 #[tokio::test(flavor = "multi_thread")]
7233 #[cfg(feature = "reactive-ws")]
7234 async fn owner_backfill_dedupe_never_suppresses_canonical_delivery() {
7235 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7236 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7237 provider,
7238 SubscriberMode::PubSub,
7239 SubscriberConfig::default(),
7240 );
7241 let epoch = subscriber
7242 .stage_interest_owner(
7243 HandlerId::new("owner"),
7244 &[ReactiveInterest::Logs(LogInterest {
7245 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
7246 local_matcher: None,
7247 route_key: None,
7248 })],
7249 SubscriberOwnerStart::Live,
7250 )
7251 .unwrap();
7252 let log = rpc_log(false);
7253
7254 subscriber.enqueue_owner_record(
7255 log_input_record(log.clone(), InputSource::Backfill),
7256 epoch.clone(),
7257 );
7258 subscriber.enqueue_event(SubscriberEvent::Log { source_id: 0, log });
7259
7260 let batch = subscriber
7261 .next_scoped_batch()
7262 .await
7263 .unwrap()
7264 .expect("owner backfill and canonical live delivery");
7265 assert_eq!(batch.records.len(), 2);
7266 assert_eq!(
7267 batch.records[0].scope,
7268 SubscriberInputScope::OwnerOnly {
7269 owners: vec![epoch]
7270 }
7271 );
7272 assert_eq!(
7273 batch.records[1].scope,
7274 SubscriberInputScope::Canonical { owners: Vec::new() },
7275 "owner replay dedupe must not suppress the global live record"
7276 );
7277 }
7278
7279 #[tokio::test(flavor = "multi_thread")]
7280 #[cfg(feature = "reactive-polling")]
7281 async fn reconcile_fetch_drains_live_burst_beyond_output_batch_capacity() {
7282 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7283 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7284 provider,
7285 SubscriberMode::Polling,
7286 SubscriberConfig {
7287 max_batch_size: 2,
7288 ..SubscriberConfig::default()
7289 },
7290 );
7291 let interest = ReactiveInterest::Logs(LogInterest {
7292 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
7293 local_matcher: None,
7294 route_key: None,
7295 });
7296 let epoch = subscriber
7297 .stage_interest_owner(
7298 HandlerId::new("owner"),
7299 std::slice::from_ref(&interest),
7300 SubscriberOwnerStart::Live,
7301 )
7302 .unwrap();
7303 subscriber.sources_dirty = false;
7304
7305 let mut duplicate = rpc_log(false);
7306 duplicate.transaction_hash = Some(B256::repeat_byte(1));
7307 duplicate.log_index = Some(0);
7308 let events = (0u8..10).map(|index| {
7309 let mut log = rpc_log(false);
7310 log.transaction_hash = Some(B256::repeat_byte(index.saturating_add(1)));
7311 log.log_index = Some(index as u64);
7312 SubscriberEvent::Log { source_id: 0, log }
7313 });
7314 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
7315 let mut streams = SubscriberStreams::new();
7316 streams.push(
7317 SubscriberStreamSource::PollingLog { filter },
7318 stream::iter(events).boxed(),
7319 );
7320 subscriber.state = AlloySubscriberState::Active(streams);
7321
7322 let mut polls = 0usize;
7323 let fetched_duplicate = duplicate.clone();
7324 let fetch = poll_fn(move |cx| {
7325 polls += 1;
7326 if polls > 10 {
7327 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>(fetched_duplicate.clone()))
7328 } else {
7329 cx.waker().wake_by_ref();
7330 std::task::Poll::Pending
7331 }
7332 });
7333 let target_epochs = HashSet::from([epoch.clone()]);
7334 let fetched_duplicate = subscriber
7335 .drive_reconcile_fetch(fetch, &target_epochs)
7336 .await
7337 .unwrap();
7338 subscriber.enqueue_owner_record_for_owners_unmerged(
7339 log_input_record(fetched_duplicate, InputSource::Backfill),
7340 vec![epoch.clone()],
7341 );
7342 subscriber.promote_reconcile_owner_records(&target_epochs);
7343
7344 assert_eq!(subscriber.pending_records.len(), 20);
7345 assert!(subscriber.pending_records.iter().take(10).all(|record| {
7346 record.scope == SubscriberInputScope::Canonical { owners: Vec::new() }
7347 }));
7348 assert!(subscriber.pending_records.iter().skip(10).all(|record| {
7349 record.scope
7350 == SubscriberInputScope::OwnerOnly {
7351 owners: vec![epoch.clone()],
7352 }
7353 }));
7354 }
7355
7356 #[tokio::test(flavor = "multi_thread")]
7357 #[cfg(feature = "reactive-polling")]
7358 async fn reconcile_fetch_waits_for_provider_when_live_topology_is_empty() {
7359 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7360 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7361 provider,
7362 SubscriberMode::Polling,
7363 SubscriberConfig::default(),
7364 );
7365 subscriber.sources_dirty = false;
7366 let mut first_poll = true;
7367 let fetch = poll_fn(move |cx| {
7368 if first_poll {
7369 first_poll = false;
7370 cx.waker().wake_by_ref();
7371 std::task::Poll::Pending
7372 } else {
7373 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>("certified"))
7374 }
7375 });
7376
7377 let result = subscriber
7378 .drive_reconcile_fetch(fetch, &HashSet::new())
7379 .await
7380 .expect("an empty live topology must not be mistaken for termination");
7381 assert_eq!(result, "certified");
7382 }
7383
7384 #[tokio::test(flavor = "multi_thread")]
7385 #[cfg(all(feature = "reactive-polling", feature = "reactive-ws"))]
7386 async fn successful_owner_reconcile_seeds_its_live_filter_reconnect_anchor() {
7387 use alloy_rpc_types_eth::{Block, Header};
7388
7389 let asserter = Asserter::new();
7390 let baseline = BlockRef {
7391 number: 100,
7392 hash: B256::repeat_byte(0x64),
7393 parent_hash: Some(B256::repeat_byte(0x63)),
7394 timestamp: Some(1_700_000_100),
7395 };
7396 let through = BlockRef {
7397 number: 101,
7398 hash: B256::repeat_byte(0x65),
7399 parent_hash: Some(baseline.hash),
7400 timestamp: Some(1_700_000_101),
7401 };
7402 let rpc_block = || -> Block {
7403 Block::empty(Header {
7404 hash: through.hash,
7405 inner: alloy_consensus::Header {
7406 number: through.number,
7407 parent_hash: through.parent_hash.unwrap(),
7408 timestamp: through.timestamp.unwrap(),
7409 ..Default::default()
7410 },
7411 total_difficulty: None,
7412 size: None,
7413 })
7414 };
7415 asserter.push_success(&Some(rpc_block()));
7416 asserter.push_success(&Vec::<Log>::new());
7417 asserter.push_success(&Some(rpc_block()));
7418 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
7419 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7420 provider,
7421 SubscriberMode::PubSub,
7422 SubscriberConfig::default(),
7423 );
7424 let interest = ReactiveInterest::Logs(LogInterest {
7425 provider_filter: Filter::new().address(Address::repeat_byte(0xac)),
7426 local_matcher: None,
7427 route_key: None,
7428 });
7429 let epoch = subscriber
7430 .stage_interest_owner(
7431 HandlerId::new("reconnect-anchor"),
7432 std::slice::from_ref(&interest),
7433 SubscriberOwnerStart::PostBlock(baseline),
7434 )
7435 .unwrap();
7436 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
7437 let source = SubscriberStreamSource::PubSubLog {
7438 id: subscriber.log_source_id(&filter),
7439 filter: filter.clone(),
7440 };
7441 let mut streams = SubscriberStreams::new();
7442 streams.push(source, stream::pending().boxed());
7443 subscriber.state = AlloySubscriberState::Active(streams);
7444 subscriber.sources_dirty = false;
7445
7446 subscriber
7447 .reconcile_interest_owner(&epoch, through.clone())
7448 .await
7449 .unwrap();
7450 assert_eq!(subscriber.log_anchor(&filter), Some(through.number));
7451 assert!(asserter.read_q().is_empty());
7452 }
7453
7454 #[tokio::test(flavor = "multi_thread")]
7455 #[cfg(feature = "reactive-polling")]
7456 async fn cancelled_reconcile_retains_hidden_owner_live_delivery_for_retry() {
7457 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7458 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7459 provider,
7460 SubscriberMode::Polling,
7461 SubscriberConfig::default(),
7462 );
7463 let interest = ReactiveInterest::Logs(LogInterest {
7464 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
7465 local_matcher: None,
7466 route_key: None,
7467 });
7468 let epoch = subscriber
7469 .stage_interest_owner(
7470 HandlerId::new("owner"),
7471 std::slice::from_ref(&interest),
7472 SubscriberOwnerStart::PostBlock(BlockRef {
7473 number: 100,
7474 hash: B256::repeat_byte(0x64),
7475 parent_hash: None,
7476 timestamp: None,
7477 }),
7478 )
7479 .unwrap();
7480 subscriber.sources_dirty = false;
7481
7482 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
7483 let event = SubscriberEvent::Log {
7484 source_id: 0,
7485 log: rpc_log(false),
7486 };
7487 let mut streams = SubscriberStreams::new();
7488 streams.push(
7489 SubscriberStreamSource::PollingLog { filter },
7490 stream::once(async move { event })
7491 .chain(stream::pending())
7492 .boxed(),
7493 );
7494 subscriber.state = AlloySubscriberState::Active(streams);
7495
7496 let targets = HashSet::from([epoch.clone()]);
7497 {
7498 let fetch = futures::future::pending::<Result<(), SubscriberOwnerError>>();
7499 let drive = subscriber.drive_reconcile_fetch(fetch, &targets);
7500 futures::pin_mut!(drive);
7501 poll_fn(|cx| {
7502 assert!(drive.as_mut().poll(cx).is_pending());
7503 std::task::Poll::Ready(())
7504 })
7505 .await;
7506 }
7507
7508 assert_eq!(subscriber.pending_records.len(), 1);
7509 assert_eq!(
7510 subscriber.pending_records[0].scope,
7511 SubscriberInputScope::Canonical { owners: Vec::new() },
7512 "canonical delivery commits immediately at a cancellation-safe boundary"
7513 );
7514 assert_eq!(subscriber.pending_reconcile_owner_records.len(), 1);
7515
7516 subscriber
7517 .drive_reconcile_fetch(futures::future::ready(Ok(())), &targets)
7518 .await
7519 .unwrap();
7520 subscriber.promote_reconcile_owner_records(&targets);
7521 assert!(subscriber.pending_reconcile_owner_records.is_empty());
7522 assert_eq!(subscriber.pending_records.len(), 2);
7523 assert_eq!(
7524 subscriber.pending_records[0].scope,
7525 SubscriberInputScope::Canonical { owners: Vec::new() },
7526 "canonical delivery remains target-excluded"
7527 );
7528 assert_eq!(
7529 subscriber.pending_records[1].scope,
7530 SubscriberInputScope::OwnerOnly {
7531 owners: vec![epoch]
7532 },
7533 "retry commit appends hidden owner delivery after historical catch-up"
7534 );
7535 }
7536
7537 #[tokio::test(flavor = "multi_thread")]
7538 #[cfg(feature = "reactive-ws")]
7539 async fn control_cancellation_preserves_terminated_source_reconcile_intent() {
7540 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7541 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7542 provider,
7543 SubscriberMode::PubSub,
7544 SubscriberConfig {
7545 reconnect: SubscriberReconnectConfig {
7546 initial_delay: Duration::from_secs(60),
7547 ..SubscriberReconnectConfig::default()
7548 },
7549 ..SubscriberConfig::default()
7550 },
7551 );
7552 let interest = ReactiveInterest::Logs(LogInterest {
7553 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
7554 local_matcher: None,
7555 route_key: None,
7556 });
7557 let epoch = subscriber
7558 .stage_interest_owner(
7559 HandlerId::new("owner"),
7560 std::slice::from_ref(&interest),
7561 SubscriberOwnerStart::PostBlock(BlockRef {
7562 number: 7,
7563 hash: B256::repeat_byte(0x07),
7564 parent_hash: Some(B256::repeat_byte(0x06)),
7565 timestamp: Some(1_700_000_007),
7566 }),
7567 )
7568 .unwrap();
7569 subscriber.sources_dirty = false;
7570 subscriber.stream_revision = 1;
7571 let entry = subscriber
7572 .owned_interests
7573 .iter_mut()
7574 .find(|entry| entry.epoch.as_ref() == Some(&epoch))
7575 .unwrap();
7576 entry.progress = Some(SubscriberOwnerProgress {
7577 owner: epoch.clone(),
7578 through: entry.baseline.clone().unwrap(),
7579 });
7580 entry.progress_stream_revision = Some(1);
7581
7582 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
7583 let source = SubscriberStreamSource::PubSubLog {
7584 id: subscriber.log_source_id(&filter),
7585 filter,
7586 };
7587 let mut streams = SubscriberStreams::new();
7588 streams.push(
7589 source.clone(),
7590 stream::iter([SubscriberEvent::StreamTerminated(source)]).boxed(),
7591 );
7592 subscriber.state = AlloySubscriberState::Active(streams);
7593 let prior_revision = subscriber.stream_revision;
7594
7595 let mut first_poll = true;
7596 let control = poll_fn(move |cx| {
7597 if first_poll {
7598 first_poll = false;
7599 cx.waker().wake_by_ref();
7600 std::task::Poll::Pending
7601 } else {
7602 std::task::Poll::Ready("stop")
7603 }
7604 });
7605 futures::pin_mut!(control);
7606 let outcome = subscriber
7607 .next_scoped_batch_or(control.as_mut())
7608 .await
7609 .unwrap();
7610
7611 assert!(matches!(outcome, SubscriberDriverPoll::Control("stop")));
7612 assert!(subscriber.sources_dirty);
7613 assert!(subscriber.stream_revision > prior_revision);
7614 assert!(
7615 !subscriber.activate_interest_owner(&epoch),
7616 "progress certified against the terminated stream revision is stale"
7617 );
7618 }
7619
7620 #[test]
7621 #[cfg(feature = "reactive-ws")]
7622 fn pubsub_sources_assign_stable_log_ids_before_shared_streams() {
7623 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7624 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7625 provider,
7626 SubscriberMode::PubSub,
7627 SubscriberConfig::default(),
7628 );
7629 subscriber
7630 .register_interests(&[
7631 ReactiveInterest::Logs(LogInterest {
7632 provider_filter: Filter::new().address(Address::repeat_byte(0x01)),
7633 local_matcher: None,
7634 route_key: None,
7635 }),
7636 ReactiveInterest::Logs(LogInterest {
7637 provider_filter: Filter::new().address(Address::repeat_byte(0x02)),
7638 local_matcher: None,
7639 route_key: None,
7640 }),
7641 ReactiveInterest::PendingTransactions(PendingTxInterest::default()),
7642 ])
7643 .expect("register base interests");
7644
7645 let sources = subscriber.stream_sources().expect("stream sources");
7649 assert_eq!(sources.len(), 2);
7650 assert!(matches!(
7651 &sources[0],
7652 SubscriberStreamSource::PubSubLog { id: 0, .. }
7653 ));
7654 assert!(matches!(
7655 sources[1],
7656 SubscriberStreamSource::PubSubPendingHashes
7657 ));
7658
7659 let again = subscriber.stream_sources().expect("stream sources again");
7661 assert!(again[0].same_key(&sources[0]));
7662 }
7663
7664 #[tokio::test(flavor = "multi_thread")]
7665 #[cfg(feature = "reactive-ws")]
7666 async fn pubsub_stream_termination_attempts_reconnect_before_error() {
7667 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7668 let mut subscriber = AlloySubscriber::new(
7669 provider,
7670 SubscriberMode::PubSub,
7671 SubscriberConfig {
7672 reconnect: SubscriberReconnectConfig {
7673 initial_delay: Duration::ZERO,
7674 retry_delay: Duration::ZERO,
7675 max_delay: Duration::ZERO,
7676 max_attempts: Some(1),
7677 ..SubscriberReconnectConfig::default()
7678 },
7679 ..SubscriberConfig::default()
7680 },
7681 );
7682 subscriber.interests = vec![ReactiveInterest::PendingTransactions(
7683 PendingTxInterest::default(),
7684 )];
7685
7686 let mut streams = SubscriberStreams::new();
7687 let source = SubscriberStreamSource::PubSubPendingHashes;
7688 streams.push(
7689 source,
7690 stream::once(async {
7691 SubscriberEvent::<Ethereum>::StreamTerminated(
7692 SubscriberStreamSource::PubSubPendingHashes,
7693 )
7694 })
7695 .boxed(),
7696 );
7697 subscriber.state = AlloySubscriberState::Active(streams);
7698
7699 let result = subscriber.next_batch().await;
7700 assert!(
7701 matches!(result, Err(SubscriberError::Provider(ref message)) if message.contains("reconnect failed after 1 attempt")),
7702 "terminated pubsub streams should attempt reconnect before surfacing failure: {result:?}"
7703 );
7704 }
7705
7706 #[test]
7707 fn backfilled_logs_skip_recent_subscription_duplicates() {
7708 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7709 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7710 provider,
7711 SubscriberMode::PubSub,
7712 SubscriberConfig::default(),
7713 );
7714 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
7715 provider_filter: Filter::new()
7716 .address(Address::repeat_byte(0x42))
7717 .event_signature(B256::repeat_byte(0x01)),
7718 local_matcher: None,
7719 route_key: None,
7720 })];
7721
7722 let log = rpc_log(false);
7723 subscriber.enqueue_event(SubscriberEvent::Log {
7724 source_id: 0,
7725 log: log.clone(),
7726 });
7727 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
7728 source_id: 0,
7729 logs: vec![log],
7730 });
7731
7732 assert_eq!(subscriber.pending_records.len(), 1);
7733 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
7734 assert_eq!(
7735 subscriber.pending_records[0].context.source,
7736 InputSource::Subscription
7737 );
7738 }
7739
7740 #[test]
7741 fn backfilled_logs_surface_with_backfill_source() {
7742 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7747 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7748 provider,
7749 SubscriberMode::PubSub,
7750 SubscriberConfig::default(),
7751 );
7752 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
7753 provider_filter: Filter::new()
7754 .address(Address::repeat_byte(0x42))
7755 .event_signature(B256::repeat_byte(0x01)),
7756 local_matcher: None,
7757 route_key: None,
7758 })];
7759
7760 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
7761 source_id: 0,
7762 logs: vec![rpc_log(false)],
7763 });
7764
7765 assert_eq!(subscriber.pending_records.len(), 1);
7766 assert_eq!(
7767 subscriber.pending_records[0].context.source,
7768 InputSource::Backfill
7769 );
7770 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
7771 }
7772
7773 #[test]
7774 #[cfg(feature = "reactive-ws")]
7775 fn owner_removal_preserves_delivery_and_dedupe_state() {
7776 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7777 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7778 provider,
7779 SubscriberMode::PubSub,
7780 SubscriberConfig::default(),
7781 );
7782 subscriber
7783 .add_interest_owner(
7784 HandlerId::new("pool-a"),
7785 &[ReactiveInterest::Logs(LogInterest {
7786 provider_filter: Filter::new()
7787 .address(Address::repeat_byte(0x42))
7788 .event_signature(B256::repeat_byte(0x01)),
7789 local_matcher: None,
7790 route_key: None,
7791 })],
7792 )
7793 .expect("register pool-a owner");
7794 subscriber
7795 .add_interest_owner(
7796 HandlerId::new("pool-b"),
7797 &[ReactiveInterest::Logs(LogInterest {
7798 provider_filter: Filter::new()
7799 .address(Address::repeat_byte(0x24))
7800 .event_signature(B256::repeat_byte(0x02)),
7801 local_matcher: None,
7802 route_key: None,
7803 })],
7804 )
7805 .expect("register pool-b owner");
7806
7807 let sources = subscriber.stream_sources().expect("stream sources");
7810 subscriber.enqueue_event(SubscriberEvent::Log {
7811 source_id: 0,
7812 log: rpc_log(false),
7813 });
7814 let mut streams = SubscriberStreams::new();
7815 streams.push(
7816 sources[0].clone(),
7817 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
7818 );
7819 subscriber.state = AlloySubscriberState::Active(streams);
7820 assert_eq!(subscriber.pending_records.len(), 1);
7821 assert_eq!(subscriber.recent_input_refs.len(), 1);
7822 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
7823
7824 let removed = subscriber
7825 .remove_interest_owner(&HandlerId::new("pool-b"))
7826 .expect("pool-b should be removed");
7827
7828 assert_eq!(removed.len(), 1);
7829 assert_eq!(subscriber.pending_records.len(), 1);
7830 assert_eq!(subscriber.recent_input_refs.len(), 1);
7831 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
7832 assert!(
7833 subscriber
7834 .owner_interests(&HandlerId::new("pool-a"))
7835 .is_some()
7836 );
7837 assert!(
7838 subscriber
7839 .owner_interests(&HandlerId::new("pool-b"))
7840 .is_none()
7841 );
7842 assert_eq!(subscriber.registered_interests().len(), 1);
7843 }
7844
7845 #[test]
7846 #[cfg(feature = "reactive-ws")]
7847 fn owner_log_sources_fan_in_across_owners() {
7848 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7849 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7850 provider,
7851 SubscriberMode::PubSub,
7852 SubscriberConfig::default(),
7853 );
7854 subscriber
7855 .add_interest_owner(
7856 HandlerId::new("pool-a"),
7857 &[ReactiveInterest::Logs(LogInterest {
7858 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
7859 local_matcher: None,
7860 route_key: None,
7861 })],
7862 )
7863 .expect("register pool-a owner");
7864
7865 let initial_sources = subscriber.stream_sources().expect("initial sources");
7866 assert_eq!(initial_sources.len(), 1);
7867 let pool_a_source = initial_sources[0].clone();
7868 assert!(matches!(
7869 &pool_a_source,
7870 SubscriberStreamSource::PubSubLog { id: 0, .. }
7871 ));
7872
7873 subscriber
7874 .add_interest_owner(
7875 HandlerId::new("pool-b"),
7876 &[ReactiveInterest::Logs(LogInterest {
7877 provider_filter: Filter::new().address(Address::repeat_byte(0xb2)),
7878 local_matcher: None,
7879 route_key: None,
7880 })],
7881 )
7882 .expect("register pool-b owner");
7883
7884 let expanded_sources = subscriber.stream_sources().expect("expanded sources");
7885 assert_eq!(
7886 expanded_sources.len(),
7887 1,
7888 "compatible owner filters should share one provider subscription"
7889 );
7890 assert!(
7891 !expanded_sources[0].same_key(&pool_a_source),
7892 "the provider-facing superset changes while owner routing remains exact"
7893 );
7894
7895 subscriber
7896 .remove_interest_owner(&HandlerId::new("pool-b"))
7897 .expect("pool-b should be removed");
7898 let trimmed_sources = subscriber.stream_sources().expect("trimmed sources");
7899 assert_eq!(trimmed_sources.len(), 1);
7900 assert!(trimmed_sources[0].same_key(&pool_a_source));
7901 }
7902
7903 #[test]
7904 #[cfg(feature = "reactive-ws")]
7905 fn provider_log_fan_in_respects_address_ceiling() {
7906 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
7907 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7908 provider,
7909 SubscriberMode::PubSub,
7910 SubscriberConfig {
7911 max_log_addresses_per_subscription: 2,
7912 ..SubscriberConfig::default()
7913 },
7914 );
7915 for index in 0..5 {
7916 subscriber
7917 .add_interest_owner(
7918 HandlerId::new(format!("pool-{index}")),
7919 &[log_interest_for(index + 1)],
7920 )
7921 .expect("register pool owner");
7922 }
7923
7924 let sources = subscriber.stream_sources().expect("stream sources");
7925 assert_eq!(sources.len(), 3);
7926 let mut address_counts: Vec<_> = sources
7927 .iter()
7928 .map(|source| match source {
7929 SubscriberStreamSource::PubSubLog { filter, .. } => filter.address.iter().count(),
7930 _ => panic!("expected log source"),
7931 })
7932 .collect();
7933 address_counts.sort_unstable();
7934 assert_eq!(address_counts, vec![1, 2, 2]);
7935 }
7936
7937 #[tokio::test(flavor = "multi_thread")]
7938 #[cfg(feature = "reactive-ws")]
7939 async fn owner_backfill_seeds_reconnect_anchor_before_live_log() {
7940 let asserter = Asserter::new();
7941 asserter.push_success(&vec![rpc_log(false)]);
7942 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
7943 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
7944 provider,
7945 SubscriberMode::PubSub,
7946 SubscriberConfig::default(),
7947 );
7948 subscriber
7949 .add_interest_owner_with_backfill(
7950 HandlerId::new("pool-a"),
7951 &[ReactiveInterest::Logs(LogInterest {
7952 provider_filter: Filter::new()
7953 .address(Address::repeat_byte(0x42))
7954 .event_signature(B256::repeat_byte(0x01)),
7955 local_matcher: None,
7956 route_key: None,
7957 })],
7958 SubscriberBackfill::range(1, 7),
7959 )
7960 .expect("register pool-a with backfill");
7961
7962 subscriber
7963 .drain_pending_backfills()
7964 .await
7965 .expect("owner backfill should drain");
7966
7967 assert_eq!(subscriber.pending_records.len(), 1);
7968 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
7969 }
7970
7971 #[tokio::test(flavor = "multi_thread")]
7972 async fn subscriber_streams_poll_ready_sources_round_robin() {
7973 let first_hash = B256::repeat_byte(0x01);
7974 let second_hash = B256::repeat_byte(0x02);
7975 let mut streams = SubscriberStreams::new();
7976 streams.push(
7977 SubscriberStreamSource::PubSubPendingHashes,
7978 stream::iter([
7979 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
7980 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
7981 ])
7982 .boxed(),
7983 );
7984 streams.push(
7985 SubscriberStreamSource::PubSubBlockHeaders,
7986 stream::once(async move { SubscriberEvent::<Ethereum>::PendingHash(second_hash) })
7987 .boxed(),
7988 );
7989
7990 assert!(matches!(
7991 streams.next().await,
7992 Some(SubscriberEvent::PendingHash(hash)) if hash == first_hash
7993 ));
7994 assert!(matches!(
7995 streams.next().await,
7996 Some(SubscriberEvent::PendingHash(hash)) if hash == second_hash
7997 ));
7998 }
7999
8000 #[tokio::test(flavor = "multi_thread")]
8001 #[cfg(feature = "reactive-ws")]
8002 async fn owner_updates_ensure_streams_without_full_reset() {
8003 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
8004 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
8005 provider,
8006 SubscriberMode::PubSub,
8007 SubscriberConfig::default(),
8008 );
8009 subscriber
8010 .register_interests(&[ReactiveInterest::PendingTransactions(
8011 PendingTxInterest::default(),
8012 )])
8013 .expect("register base pending interest");
8014 subscriber
8015 .add_interest_owner(
8016 HandlerId::new("headers"),
8017 &[ReactiveInterest::Blocks(BlockInterest::default())],
8018 )
8019 .expect("register header owner");
8020
8021 let mut streams = SubscriberStreams::new();
8022 streams.push(
8023 SubscriberStreamSource::PubSubPendingHashes,
8024 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
8025 );
8026 streams.push(
8027 SubscriberStreamSource::PubSubBlockHeaders,
8028 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
8029 );
8030 subscriber.state = AlloySubscriberState::Active(streams);
8031
8032 subscriber
8033 .remove_interest_owner(&HandlerId::new("headers"))
8034 .expect("header owner should be removed");
8035 assert!(matches!(
8036 &subscriber.state,
8037 AlloySubscriberState::Active(streams) if streams.len() == 2
8038 ));
8039
8040 subscriber
8041 .ensure_streams()
8042 .await
8043 .expect("pure removal reconciliation should not touch provider");
8044
8045 assert!(matches!(
8046 &subscriber.state,
8047 AlloySubscriberState::Active(streams)
8048 if streams.len() == 1
8049 && streams.contains_source(&SubscriberStreamSource::PubSubPendingHashes)
8050 && !streams.contains_source(&SubscriberStreamSource::PubSubBlockHeaders)
8051 ));
8052
8053 subscriber
8054 .add_interest_owner(
8055 HandlerId::new("headers"),
8056 &[ReactiveInterest::Blocks(BlockInterest::default())],
8057 )
8058 .expect("re-add header owner");
8059 assert!(matches!(
8060 &subscriber.state,
8061 AlloySubscriberState::Active(streams) if streams.len() == 1
8062 ));
8063 }
8064
8065 #[cfg(feature = "reactive-ws")]
8067 fn log_interest_matching_rpc_log() -> ReactiveInterest<Ethereum> {
8068 ReactiveInterest::Logs(LogInterest {
8069 provider_filter: Filter::new()
8070 .address(Address::repeat_byte(0x42))
8071 .event_signature(B256::repeat_byte(0x01)),
8072 local_matcher: None,
8073 route_key: None,
8074 })
8075 }
8076
8077 #[cfg(feature = "reactive-ws")]
8078 fn log_interest_for(address: u8) -> ReactiveInterest<Ethereum> {
8079 ReactiveInterest::Logs(LogInterest {
8080 provider_filter: Filter::new().address(Address::repeat_byte(address)),
8081 local_matcher: None,
8082 route_key: None,
8083 })
8084 }
8085
8086 #[tokio::test(flavor = "multi_thread")]
8089 #[cfg(feature = "reactive-ws")]
8090 async fn drain_backfill_retains_queue_entry_on_provider_error() {
8091 let asserter = Asserter::new();
8092 asserter.push_failure_msg("rate limited");
8093 asserter.push_success(&vec![rpc_log(false)]);
8094 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
8095 let mut subscriber = AlloySubscriber::new(
8096 provider,
8097 SubscriberMode::PubSub,
8098 SubscriberConfig::default(),
8099 );
8100 subscriber
8101 .add_interest_owner_with_backfill(
8102 HandlerId::new("pool"),
8103 &[log_interest_matching_rpc_log()],
8104 SubscriberBackfill::range(1, 7),
8105 )
8106 .expect("register owner with backfill");
8107 assert_eq!(subscriber.pending_backfills.len(), 1);
8108
8109 let first = subscriber.drain_pending_backfills().await;
8110 assert!(first.is_err(), "provider failure should surface");
8111 assert_eq!(
8112 subscriber.pending_backfills.len(),
8113 1,
8114 "failed fetch must leave the backfill queued for retry"
8115 );
8116 assert!(subscriber.pending_records.is_empty());
8117
8118 subscriber
8119 .drain_pending_backfills()
8120 .await
8121 .expect("retry should succeed");
8122 assert!(subscriber.pending_backfills.is_empty());
8123 assert_eq!(subscriber.pending_records.len(), 1);
8124 }
8125
8126 #[tokio::test(flavor = "multi_thread")]
8129 #[cfg(feature = "reactive-ws")]
8130 async fn drain_backfill_seeds_anchor_on_empty_window() {
8131 let asserter = Asserter::new();
8132 asserter.push_success(&Vec::<Log>::new());
8133 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
8134 let mut subscriber = AlloySubscriber::new(
8135 provider,
8136 SubscriberMode::PubSub,
8137 SubscriberConfig::default(),
8138 );
8139 subscriber
8140 .add_interest_owner_with_backfill(
8141 HandlerId::new("pool"),
8142 &[log_interest_matching_rpc_log()],
8143 SubscriberBackfill::range(1, 42),
8144 )
8145 .expect("register owner with backfill");
8146
8147 subscriber
8148 .drain_pending_backfills()
8149 .await
8150 .expect("empty backfill should drain");
8151
8152 assert!(subscriber.pending_records.is_empty());
8153 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
8154 .pop()
8155 .unwrap();
8156 assert_eq!(
8157 subscriber.log_anchor(&filter),
8158 Some(42),
8159 "empty window must still seed the anchor at its upper bound"
8160 );
8161 }
8162
8163 #[tokio::test(flavor = "multi_thread")]
8166 #[cfg(feature = "reactive-ws")]
8167 async fn drain_backfill_open_ended_resolves_head_and_seeds_anchor() {
8168 let asserter = Asserter::new();
8169 asserter.push_success(&100u64); asserter.push_success(&Vec::<Log>::new()); let provider = ProviderBuilder::new().connect_mocked_client(asserter);
8172 let mut subscriber = AlloySubscriber::new(
8173 provider,
8174 SubscriberMode::PubSub,
8175 SubscriberConfig::default(),
8176 );
8177 subscriber
8178 .add_interest_owner_with_backfill(
8179 HandlerId::new("pool"),
8180 &[log_interest_matching_rpc_log()],
8181 SubscriberBackfill::from_block(10),
8182 )
8183 .expect("register owner with open-ended backfill");
8184
8185 subscriber
8186 .drain_pending_backfills()
8187 .await
8188 .expect("open-ended backfill should drain");
8189
8190 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
8191 .pop()
8192 .unwrap();
8193 assert_eq!(subscriber.log_anchor(&filter), Some(100));
8194 }
8195
8196 #[test]
8199 #[cfg(feature = "reactive-ws")]
8200 fn duplicate_filters_across_owners_map_to_single_source() {
8201 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
8202 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
8203 provider,
8204 SubscriberMode::PubSub,
8205 SubscriberConfig::default(),
8206 );
8207 subscriber
8208 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
8209 .expect("register pool-a");
8210 subscriber
8211 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xaa)])
8212 .expect("register pool-b with identical filter");
8213
8214 assert_eq!(
8215 subscriber.log_stream_filters().len(),
8216 1,
8217 "identical filters across owners must collapse to one"
8218 );
8219 let sources = subscriber.stream_sources().expect("stream sources");
8220 assert_eq!(sources.len(), 1);
8221 }
8222
8223 #[test]
8226 #[cfg(feature = "reactive-ws")]
8227 fn owner_removal_prunes_source_ids_and_anchors() {
8228 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
8229 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
8230 provider,
8231 SubscriberMode::PubSub,
8232 SubscriberConfig::default(),
8233 );
8234 subscriber
8235 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
8236 .expect("register pool-a");
8237 subscriber
8238 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xbb)])
8239 .expect("register pool-b");
8240
8241 let _ = subscriber.stream_sources().expect("stream sources");
8243 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
8244 let filter_b = log_filters(&[log_interest_for(0xbb)]).pop().unwrap();
8245 let id_a = subscriber.log_source_id(&filter_a);
8246 let id_b = subscriber.log_source_id(&filter_b);
8247 subscriber.last_seen_log_blocks.insert(id_a, 10);
8248 subscriber.last_seen_log_blocks.insert(id_b, 20);
8249 assert_eq!(
8250 subscriber.log_source_ids.len(),
8251 3,
8252 "one provider fan-in id plus two explicitly seeded logical ids"
8253 );
8254
8255 subscriber
8256 .remove_interest_owner(&HandlerId::new("pool-b"))
8257 .expect("remove pool-b");
8258
8259 assert_eq!(
8260 subscriber.log_source_ids.len(),
8261 1,
8262 "pool-b's filter id should be retired"
8263 );
8264 assert!(subscriber.log_source_ids.contains_key(&filter_a));
8265 assert_eq!(subscriber.last_seen_log_blocks.get(&id_a), Some(&10));
8266 assert_eq!(
8267 subscriber.last_seen_log_blocks.get(&id_b),
8268 None,
8269 "pool-b's anchor should be pruned"
8270 );
8271 }
8272
8273 #[test]
8278 #[cfg(feature = "reactive-ws")]
8279 fn owner_filter_growth_queues_continuity_backfill_from_prior_anchor() {
8280 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
8281 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
8282 provider,
8283 SubscriberMode::PubSub,
8284 SubscriberConfig::default(),
8285 );
8286 subscriber
8287 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
8288 .expect("register amm with pool A");
8289
8290 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
8293 let id_a = subscriber.log_source_id(&filter_a);
8294 subscriber.last_seen_log_blocks.insert(id_a, 50);
8295
8296 subscriber
8299 .add_interest_owner(
8300 HandlerId::new("amm"),
8301 &[log_interest_for(0xaa), log_interest_for(0xbb)],
8302 )
8303 .expect("grow amm to pools A+B");
8304
8305 assert_eq!(
8306 subscriber.pending_backfills.len(),
8307 1,
8308 "the changed merged filter should queue exactly one continuity backfill"
8309 );
8310 let queued = &subscriber.pending_backfills[0];
8311 assert_eq!(queued.owner, HandlerId::new("amm"));
8312 assert_eq!(queued.backfill.start_block(), 50);
8313 assert_eq!(
8314 queued.backfill.end_block(),
8315 None,
8316 "continuity backfill runs open-ended to the current head"
8317 );
8318 }
8319
8320 #[test]
8323 #[cfg(feature = "reactive-ws")]
8324 fn unchanged_owner_filter_does_not_queue_continuity_backfill() {
8325 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
8326 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
8327 provider,
8328 SubscriberMode::PubSub,
8329 SubscriberConfig::default(),
8330 );
8331 subscriber
8332 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
8333 .expect("register amm");
8334 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
8335 let id_a = subscriber.log_source_id(&filter_a);
8336 subscriber.last_seen_log_blocks.insert(id_a, 50);
8337
8338 subscriber
8339 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
8340 .expect("re-register identical interests");
8341
8342 assert!(
8343 subscriber.pending_backfills.is_empty(),
8344 "an unchanged filter shape must not queue continuity backfill"
8345 );
8346 }
8347
8348 #[test]
8352 #[cfg(feature = "reactive-ws")]
8353 fn explicit_open_ended_backfill_below_anchor_suppresses_continuity() {
8354 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
8355 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
8356 provider,
8357 SubscriberMode::PubSub,
8358 SubscriberConfig::default(),
8359 );
8360 subscriber
8361 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
8362 .expect("register amm");
8363 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
8364 let id_a = subscriber.log_source_id(&filter_a);
8365 subscriber.last_seen_log_blocks.insert(id_a, 50);
8366
8367 subscriber
8369 .add_interest_owner_with_backfill(
8370 HandlerId::new("amm"),
8371 &[log_interest_for(0xaa), log_interest_for(0xbb)],
8372 SubscriberBackfill::from_block(10),
8373 )
8374 .expect("grow amm with explicit deep backfill");
8375
8376 assert_eq!(
8377 subscriber.pending_backfills.len(),
8378 1,
8379 "only the explicit backfill should be queued; continuity is subsumed"
8380 );
8381 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 10);
8382 }
8383
8384 #[tokio::test(flavor = "multi_thread")]
8387 #[cfg(feature = "reactive-ws")]
8388 async fn ensure_streams_is_noop_when_not_dirty() {
8389 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
8390 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
8391 provider,
8392 SubscriberMode::PubSub,
8393 SubscriberConfig::default(),
8394 );
8395 subscriber
8397 .add_interest_owner(
8398 HandlerId::new("headers"),
8399 &[ReactiveInterest::Blocks(BlockInterest::default())],
8400 )
8401 .expect("register header owner");
8402 subscriber.state = AlloySubscriberState::Empty;
8404 subscriber.sources_dirty = false;
8405
8406 subscriber
8407 .ensure_streams()
8408 .await
8409 .expect("clean reconcile must be a no-op");
8410
8411 assert!(
8412 matches!(subscriber.state, AlloySubscriberState::Empty),
8413 "not-dirty ensure_streams must not connect new sources"
8414 );
8415 }
8416}
8417
8418fn resolve_subscriber_transport(
8419 mode: SubscriberMode,
8420) -> Result<SubscriberTransport, SubscriberError> {
8421 match mode {
8422 SubscriberMode::PubSub => {
8423 #[cfg(feature = "reactive-ws")]
8424 {
8425 Ok(SubscriberTransport::PubSub)
8426 }
8427 #[cfg(not(feature = "reactive-ws"))]
8428 {
8429 Err(SubscriberError::Unsupported(
8430 "AlloySubscriber pubsub mode requires the reactive-ws feature",
8431 ))
8432 }
8433 }
8434 SubscriberMode::Polling => {
8435 #[cfg(feature = "reactive-polling")]
8436 {
8437 Ok(SubscriberTransport::Polling)
8438 }
8439 #[cfg(not(feature = "reactive-polling"))]
8440 {
8441 Err(SubscriberError::Unsupported(
8442 "AlloySubscriber polling mode requires the reactive-polling feature",
8443 ))
8444 }
8445 }
8446 SubscriberMode::Auto => resolve_auto_subscriber_transport(),
8447 }
8448}
8449
8450fn resolve_auto_subscriber_transport() -> Result<SubscriberTransport, SubscriberError> {
8451 #[cfg(feature = "reactive-ws")]
8452 {
8453 Ok(SubscriberTransport::PubSub)
8454 }
8455
8456 #[cfg(all(not(feature = "reactive-ws"), feature = "reactive-polling"))]
8457 {
8458 Ok(SubscriberTransport::Polling)
8459 }
8460
8461 #[cfg(not(any(feature = "reactive-ws", feature = "reactive-polling")))]
8462 {
8463 Err(SubscriberError::Unsupported(
8464 "AlloySubscriber requires either reactive-ws or reactive-polling",
8465 ))
8466 }
8467}
8468
8469fn validate_subscriber_config(config: &SubscriberConfig) -> Result<(), SubscriberError> {
8470 if config.max_batch_size == 0 {
8471 return Err(SubscriberError::InvalidConfig(
8472 "SubscriberConfig::max_batch_size must be greater than zero",
8473 ));
8474 }
8475 if config.max_log_addresses_per_subscription == 0 {
8476 return Err(SubscriberError::InvalidConfig(
8477 "SubscriberConfig::max_log_addresses_per_subscription must be greater than zero",
8478 ));
8479 }
8480 if config.reconnect.enabled {
8481 if config.reconnect.retry_delay > config.reconnect.max_delay {
8482 return Err(SubscriberError::InvalidConfig(
8483 "SubscriberReconnectConfig::retry_delay must be less than or equal to max_delay",
8484 ));
8485 }
8486 if matches!(config.reconnect.max_attempts, Some(0)) {
8487 return Err(SubscriberError::InvalidConfig(
8488 "SubscriberReconnectConfig::max_attempts must be greater than zero when set",
8489 ));
8490 }
8491 }
8492 Ok(())
8493}
8494
8495fn validate_supported_interests<N: Network>(
8496 mode: SubscriberMode,
8497 config: &SubscriberConfig,
8498 interests: &[ReactiveInterest<N>],
8499) -> Result<(), SubscriberError> {
8500 let transport = resolve_subscriber_transport(mode)?;
8501
8502 for interest in interests {
8503 match interest {
8504 ReactiveInterest::Logs(_) => {}
8505 ReactiveInterest::PendingTransactions(interest)
8506 if !config.hydrate_pending_transactions && interest.matches_hash_only() => {}
8507 ReactiveInterest::PendingTransactions(_) => {
8508 return Err(SubscriberError::Unsupported(
8509 "AlloySubscriber currently supports pending transaction hash interests only (full pending-tx hydration is unimplemented)",
8510 ));
8511 }
8512 ReactiveInterest::Blocks(interest) => match (transport, interest.mode) {
8513 (SubscriberTransport::PubSub, BlockInterestMode::Header) => {}
8514 (_, BlockInterestMode::FullBlock) => {
8515 return Err(SubscriberError::Unsupported(
8516 "AlloySubscriber full block streams are not implemented in this transport slice",
8517 ));
8518 }
8519 (SubscriberTransport::Polling, BlockInterestMode::Header) => {
8520 return Err(SubscriberError::Unsupported(
8521 "AlloySubscriber polling block streams are not implemented in this transport slice",
8522 ));
8523 }
8524 },
8525 }
8526 }
8527
8528 Ok(())
8529}
8530
8531fn log_filters<N: Network>(interests: &[ReactiveInterest<N>]) -> Vec<Filter> {
8532 let mut filters = Vec::new();
8533 for interest in interests {
8534 if let ReactiveInterest::Logs(interest) = interest {
8535 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
8536 }
8537 }
8538 filters
8539}
8540
8541fn needs_header_block_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
8542 interests.iter().any(|interest| {
8543 matches!(
8544 interest,
8545 ReactiveInterest::Blocks(BlockInterest {
8546 mode: BlockInterestMode::Header,
8547 })
8548 )
8549 })
8550}
8551
8552fn needs_pending_hash_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
8553 interests.iter().any(|interest| {
8554 matches!(
8555 interest,
8556 ReactiveInterest::PendingTransactions(interest) if interest.matches_hash_only()
8557 )
8558 })
8559}
8560
8561fn log_matches_any_interest<N: Network>(log: &Log, interests: &[ReactiveInterest<N>]) -> bool {
8562 interests.iter().any(|interest| {
8563 matches!(
8564 interest,
8565 ReactiveInterest::Logs(interest) if interest.matches(log)
8566 )
8567 })
8568}
8569
8570fn validate_owner_backfill_logs(
8571 logs: &[Log],
8572 from_block: u64,
8573 through: &BlockRef,
8574) -> Result<(), SubscriberOwnerError> {
8575 for log in logs {
8576 if log.removed {
8577 return Err(SubscriberOwnerError::InvalidBackfillLog(
8578 "removed log in canonical catch-up",
8579 ));
8580 }
8581 let number = log
8582 .block_number
8583 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
8584 "log missing block number",
8585 ))?;
8586 let hash = log
8587 .block_hash
8588 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
8589 "log missing block hash",
8590 ))?;
8591 log.transaction_hash
8592 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
8593 "log missing transaction hash",
8594 ))?;
8595 log.transaction_index
8596 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
8597 "log missing transaction index",
8598 ))?;
8599 log.log_index
8600 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
8601 "log missing log index",
8602 ))?;
8603 if number < from_block || number > through.number {
8604 return Err(SubscriberOwnerError::InvalidBackfillLog(
8605 "log outside requested block range",
8606 ));
8607 }
8608 if number == through.number && hash != through.hash {
8609 return Err(SubscriberOwnerError::InvalidBackfillLog(
8610 "target-block log hash mismatch",
8611 ));
8612 }
8613 }
8614 Ok(())
8615}
8616
8617fn validate_owner_backfill_log_set(logs: &[Log]) -> Result<(), SubscriberOwnerError> {
8618 let mut positions = HashMap::new();
8619 let mut block_hashes = HashMap::new();
8620 let mut transaction_hashes = HashMap::new();
8621 let mut transaction_positions = HashMap::new();
8622 let mut ordering = BTreeMap::<u64, Vec<(u64, u64)>>::new();
8623 for log in logs {
8624 let number = log
8625 .block_number
8626 .expect("individual owner catch-up logs are validated before set validation");
8627 let block_hash = log
8628 .block_hash
8629 .expect("individual owner catch-up logs are validated before set validation");
8630 let transaction_hash = log
8631 .transaction_hash
8632 .expect("individual owner catch-up logs are validated before set validation");
8633 let transaction_index = log
8634 .transaction_index
8635 .expect("individual owner catch-up logs are validated before set validation");
8636 let log_index = log
8637 .log_index
8638 .expect("individual owner catch-up logs are validated before set validation");
8639 if block_hashes
8640 .insert(number, block_hash)
8641 .is_some_and(|prior| prior != block_hash)
8642 {
8643 return Err(SubscriberOwnerError::InvalidBackfillLog(
8644 "conflicting block identity in canonical catch-up",
8645 ));
8646 }
8647 if let Some(previous) = positions.insert((number, log_index), log)
8648 && previous != log
8649 {
8650 return Err(SubscriberOwnerError::InvalidBackfillLog(
8651 "conflicting logs at one canonical block position",
8652 ));
8653 }
8654 let conflicting_transaction = transaction_hashes
8655 .insert((number, transaction_index), transaction_hash)
8656 .is_some_and(|prior| prior != transaction_hash)
8657 || transaction_positions
8658 .insert((number, transaction_hash), transaction_index)
8659 .is_some_and(|prior| prior != transaction_index);
8660 if conflicting_transaction {
8661 return Err(SubscriberOwnerError::InvalidBackfillLog(
8662 "conflicting transaction identity at one canonical block position",
8663 ));
8664 }
8665 ordering
8666 .entry(number)
8667 .or_default()
8668 .push((log_index, transaction_index));
8669 }
8670 for positions in ordering.values_mut() {
8671 positions.sort_unstable();
8672 if positions.windows(2).any(|pair| pair[0].1 > pair[1].1) {
8673 return Err(SubscriberOwnerError::InvalidBackfillLog(
8674 "transaction and log positions disagree on canonical order",
8675 ));
8676 }
8677 }
8678 Ok(())
8679}
8680
8681fn merged_owner_reconcile_filters<N: Network>(
8682 plans: &[SubscriberOwnerReconcilePlan<N>],
8683 through: u64,
8684) -> Vec<SubscriberOwnerReconcileFilter> {
8685 let mut by_start = BTreeMap::<u64, Vec<Filter>>::new();
8686 for plan in plans.iter().filter(|plan| plan.from_block <= through) {
8687 let filters = by_start.entry(plan.from_block).or_default();
8688 filters.extend(
8689 log_filters(&plan.interests)
8690 .into_iter()
8691 .map(|filter| filter.from_block(plan.from_block).to_block(through)),
8692 );
8693 }
8694
8695 let mut chunks = Vec::new();
8696 for (from_block, filters) in by_start {
8697 for filters in filters.chunks(OWNER_RECONCILE_FILTERS_PER_CHUNK) {
8698 let mut merged = Vec::new();
8699 for filter in filters {
8700 merge_log_subscription_filter(&mut merged, filter);
8701 }
8702 chunks.extend(
8703 merged
8704 .into_iter()
8705 .map(|filter| SubscriberOwnerReconcileFilter { filter, from_block }),
8706 );
8707 }
8708 }
8709 chunks
8710}
8711
8712async fn fetch_owner_catchup<P, N>(
8713 provider: P,
8714 filters: Vec<SubscriberOwnerReconcileFilter>,
8715 retained: Vec<BlockRef>,
8716 through: BlockRef,
8717) -> Result<SubscriberOwnerCatchup, SubscriberOwnerError>
8718where
8719 P: Provider<N> + Send + Sync,
8720 N: Network,
8721{
8722 let _ = verify_provider_reconcile_target::<P, N>(&provider, &through).await?;
8723 let mut certified_positions = HashSet::new();
8724 for position in retained {
8725 let target_certifies_position = position == through
8726 || (position.number.checked_add(1) == Some(through.number)
8727 && through.parent_hash == Some(position.hash));
8728 if !target_certifies_position && certified_positions.insert(position.clone()) {
8729 let _ = verify_provider_reconcile_target::<P, N>(&provider, &position).await?;
8730 }
8731 }
8732 let mut logs = Vec::new();
8733 let requests = filters.into_iter().map(|filter| {
8734 let provider = &provider;
8735 async move {
8736 let logs = provider
8737 .get_logs(&filter.filter)
8738 .await
8739 .map_err(provider_error)?;
8740 Ok::<_, SubscriberOwnerError>((filter.from_block, logs))
8741 }
8742 });
8743 for (from_block, fetched) in try_join_all(requests).await? {
8744 validate_owner_backfill_logs(&fetched, from_block, &through)?;
8745 logs.extend(fetched);
8746 }
8747 validate_owner_backfill_log_set(&logs)?;
8748 let certified = verify_provider_reconcile_target::<P, N>(&provider, &through).await?;
8749 Ok(SubscriberOwnerCatchup { logs, certified })
8750}
8751
8752async fn verify_provider_reconcile_target<P, N>(
8753 provider: &P,
8754 expected: &BlockRef,
8755) -> Result<BlockRef, SubscriberOwnerError>
8756where
8757 P: Provider<N> + Send + Sync,
8758 N: Network,
8759{
8760 let block = provider
8761 .get_block_by_number(BlockNumberOrTag::Number(expected.number))
8762 .await
8763 .map_err(provider_error)?
8764 .ok_or(SubscriberOwnerError::BlockUnavailable(expected.number))?;
8765 let header = block.header();
8766 let actual = BlockRef {
8767 number: header.number(),
8768 hash: header.hash(),
8769 parent_hash: Some(header.parent_hash()),
8770 timestamp: Some(header.timestamp()),
8771 };
8772 let exact_parent = expected
8773 .parent_hash
8774 .is_none_or(|parent| Some(parent) == actual.parent_hash);
8775 let exact_timestamp = expected
8776 .timestamp
8777 .is_none_or(|timestamp| Some(timestamp) == actual.timestamp);
8778 if actual.number != expected.number
8779 || actual.hash != expected.hash
8780 || !exact_parent
8781 || !exact_timestamp
8782 {
8783 return Err(SubscriberOwnerError::BlockMismatch {
8784 expected_number: expected.number,
8785 expected_hash: expected.hash,
8786 actual_number: actual.number,
8787 actual_hash: actual.hash,
8788 });
8789 }
8790 Ok(actual)
8791}
8792
8793fn log_input_record<N: Network>(log: Log, source: InputSource) -> ReactiveInputRecord<N> {
8794 let context = log_reactive_context(&log);
8795 ReactiveInputRecord::new(
8796 ReactiveInput::Log(log),
8797 ReactiveContext { source, ..context },
8798 )
8799}
8800
8801fn log_reactive_context(log: &Log) -> ReactiveContext {
8802 let block = match (log.block_hash, log.block_number) {
8803 (Some(hash), Some(number)) => Some(BlockRef {
8804 number,
8805 hash,
8806 parent_hash: None,
8807 timestamp: log.block_timestamp,
8808 }),
8809 _ => None,
8810 };
8811
8812 let chain_status = match (&block, log.removed) {
8813 (Some(block), true) => ChainStatus::Reorged {
8814 dropped_from: block.clone(),
8815 },
8816 (Some(block), false) => ChainStatus::Included {
8817 block: block.clone(),
8818 confirmations: 0,
8819 },
8820 (None, _) => ChainStatus::Pending,
8821 };
8822
8823 ReactiveContext {
8824 chain_id: None,
8825 source: InputSource::Poll,
8826 chain_status,
8827 block,
8828 transaction_index: log.transaction_index,
8829 log_index: log.log_index,
8830 }
8831}
8832
8833fn block_header_input_record<N>(header: N::HeaderResponse) -> ReactiveInputRecord<N>
8834where
8835 N: Network,
8836{
8837 let block = BlockRef {
8838 number: header.number(),
8839 hash: HeaderResponseTrait::hash(&header),
8840 parent_hash: Some(header.parent_hash()),
8841 timestamp: Some(header.timestamp()),
8842 };
8843 ReactiveInputRecord::new(
8844 ReactiveInput::BlockHeader(header),
8845 ReactiveContext {
8846 chain_id: None,
8847 source: InputSource::Subscription,
8848 chain_status: ChainStatus::Included {
8849 block: block.clone(),
8850 confirmations: 0,
8851 },
8852 block: Some(block),
8853 transaction_index: None,
8854 log_index: None,
8855 },
8856 )
8857}
8858
8859fn pending_hash_input_record<N: Network>(
8860 hash: B256,
8861 source: InputSource,
8862) -> ReactiveInputRecord<N> {
8863 ReactiveInputRecord::new(
8864 ReactiveInput::PendingTxHash(hash),
8865 ReactiveContext {
8866 chain_id: None,
8867 source,
8868 chain_status: ChainStatus::Pending,
8869 block: None,
8870 transaction_index: None,
8871 log_index: None,
8872 },
8873 )
8874}
8875
8876fn provider_error(error: impl fmt::Display) -> SubscriberError {
8877 SubscriberError::Provider(error.to_string())
8878}
8879
8880#[derive(Debug, thiserror::Error)]
8882pub enum SubscriberError {
8883 #[error("{0}")]
8885 InvalidConfig(&'static str),
8886 #[error("{0}")]
8888 Unsupported(&'static str),
8889 #[error("provider error: {0}")]
8891 Provider(String),
8892}