1use std::{
15 any::Any,
16 borrow::Cow,
17 collections::{HashMap, HashSet, VecDeque},
18 fmt,
19 future::Future,
20 hash::Hash,
21 marker::PhantomData,
22 pin::Pin,
23 sync::Arc,
24 time::Duration,
25};
26
27use alloy_consensus::{BlockHeader as _, Transaction as _};
28use alloy_eips::BlockId;
29use alloy_network::{
30 Ethereum, Network,
31 primitives::{
32 BlockResponse as _, HeaderResponse as HeaderResponseTrait,
33 TransactionResponse as TransactionResponseTrait,
34 },
35};
36use alloy_primitives::{Address, B256, Bytes, U256};
37use alloy_provider::Provider;
38use alloy_rpc_types_eth::{Filter, FilterSet, Log};
39use futures::{
40 StreamExt,
41 stream::{self, BoxStream, SelectAll},
42};
43
44use crate::{
45 cache::EvmCache,
46 events::{EventDecoder, StateView},
47 state_update::{AccountPatch, PurgeScope, StateDiff, StateUpdate},
48};
49
50#[derive(Clone, Debug, PartialEq, Eq)]
52pub enum ReactiveInput<N: Network = Ethereum> {
53 Log(Log),
55 BlockHeader(N::HeaderResponse),
57 FullBlock(N::BlockResponse),
59 PendingTxHash(B256),
61 PendingTx(N::TransactionResponse),
63}
64
65#[derive(Clone, Debug, PartialEq, Eq)]
67pub struct ReactiveContext {
68 pub chain_id: Option<u64>,
70 pub source: InputSource,
72 pub chain_status: ChainStatus,
74 pub block: Option<BlockRef>,
76 pub transaction_index: Option<u64>,
78 pub log_index: Option<u64>,
80}
81
82#[derive(Clone, Debug, PartialEq, Eq, Hash)]
84pub struct BlockRef {
85 pub number: u64,
87 pub hash: B256,
89 pub parent_hash: Option<B256>,
91 pub timestamp: Option<u64>,
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
97pub enum ChainStatus {
98 Pending,
100 Included {
102 block: BlockRef,
104 confirmations: u64,
106 },
107 Safe {
109 block: BlockRef,
111 },
112 Finalized {
114 block: BlockRef,
116 },
117 Reorged {
119 dropped_from: BlockRef,
121 },
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
126pub enum InputSource {
127 Batch,
129 Subscription,
131 Poll,
133 Backfill,
135 Synthetic,
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
141pub enum InputRef {
142 Log {
144 chain_id: Option<u64>,
146 block_hash: B256,
148 transaction_hash: B256,
150 log_index: u64,
152 },
153 PendingTx {
155 chain_id: Option<u64>,
157 hash: B256,
159 },
160 Block {
162 chain_id: Option<u64>,
164 hash: B256,
166 number: u64,
168 },
169}
170
171#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
173pub enum StateEffectQuality {
174 ExactFromInput,
176 AppliedWithPendingResync,
178 ResyncedAuthoritatively,
180 RequiresRepair,
182 NoStateEffect,
184}
185
186#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
188pub struct HandlerId(String);
189
190impl HandlerId {
191 pub fn new(id: impl Into<String>) -> Self {
193 Self(id.into())
194 }
195
196 pub fn as_str(&self) -> &str {
198 &self.0
199 }
200}
201
202impl fmt::Display for HandlerId {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 self.0.fmt(f)
205 }
206}
207
208#[derive(Clone, Debug, PartialEq, Eq, Hash)]
210pub struct ReportTag {
211 pub key: String,
213 pub value: String,
215}
216
217impl ReportTag {
218 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
220 Self {
221 key: key.into(),
222 value: value.into(),
223 }
224 }
225}
226
227#[derive(Clone)]
229pub struct HookSignal {
230 pub namespace: Cow<'static, str>,
232 pub kind: Cow<'static, str>,
234 pub labels: Vec<ReportTag>,
236 pub payload: Option<Arc<dyn Any + Send + Sync>>,
238}
239
240impl fmt::Debug for HookSignal {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 f.debug_struct("HookSignal")
243 .field("namespace", &self.namespace)
244 .field("kind", &self.kind)
245 .field("labels", &self.labels)
246 .field("payload", &self.payload.as_ref().map(|_| "<payload>"))
247 .finish()
248 }
249}
250
251#[derive(Clone, Debug)]
253pub enum ReactiveEffect {
254 StateUpdate(StateUpdate),
256 Resync(ResyncRequest),
258 Invalidate(InvalidationRequest),
260 Hook(HookSignal),
262 Speculative(SpeculativeRequest),
264}
265
266#[derive(Clone, Debug)]
268pub struct HandlerOutcome {
269 pub effects: Vec<ReactiveEffect>,
271 pub quality: StateEffectQuality,
273 pub tags: Vec<ReportTag>,
275}
276
277impl HandlerOutcome {
278 pub fn empty(quality: StateEffectQuality) -> Self {
280 Self {
281 effects: Vec::new(),
282 quality,
283 tags: Vec::new(),
284 }
285 }
286}
287
288#[derive(Clone, Debug)]
290pub struct ReactiveInputRecord<N: Network = Ethereum> {
291 pub input: ReactiveInput<N>,
293 pub context: ReactiveContext,
295}
296
297impl<N: Network> ReactiveInputRecord<N> {
298 pub fn new(input: ReactiveInput<N>, context: ReactiveContext) -> Self {
300 Self { input, context }
301 }
302
303 pub fn input_ref(&self) -> InputRef {
305 input_ref(&self.input, &self.context)
306 }
307}
308
309#[derive(Clone, Debug)]
311pub struct ReactiveInputBatch<N: Network = Ethereum> {
312 records: Vec<ReactiveInputRecord<N>>,
313}
314
315impl<N: Network> ReactiveInputBatch<N> {
316 pub fn new(records: Vec<ReactiveInputRecord<N>>) -> Self {
318 Self { records }
319 }
320
321 pub fn records(&self) -> &[ReactiveInputRecord<N>] {
323 &self.records
324 }
325
326 pub fn into_records(self) -> Vec<ReactiveInputRecord<N>> {
328 self.records
329 }
330}
331
332pub trait ReactiveHandler<N: Network = Ethereum>: Send + Sync {
334 fn id(&self) -> HandlerId;
336
337 fn interests(&self) -> Vec<ReactiveInterest<N>>;
339
340 fn handle(
342 &self,
343 ctx: &ReactiveContext,
344 input: &ReactiveInput<N>,
345 state: &dyn StateView,
346 ) -> Result<HandlerOutcome, HandlerError>;
347}
348
349pub trait ReactiveHook<N: Network = Ethereum>: Send + Sync {
351 fn on_report(&self, report: Arc<ReactiveReport<N>>);
353}
354
355#[allow(clippy::large_enum_variant)]
357#[derive(Clone)]
358pub enum ReactiveInterest<N: Network = Ethereum> {
359 Logs(LogInterest),
361 Blocks(BlockInterest),
363 PendingTransactions(PendingTxInterest<N>),
365}
366
367impl<N: Network> fmt::Debug for ReactiveInterest<N> {
368 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
369 match self {
370 Self::Logs(interest) => f.debug_tuple("Logs").field(interest).finish(),
371 Self::Blocks(interest) => f.debug_tuple("Blocks").field(interest).finish(),
372 Self::PendingTransactions(interest) => f
373 .debug_tuple("PendingTransactions")
374 .field(interest)
375 .finish(),
376 }
377 }
378}
379
380#[derive(Clone)]
382pub struct LogInterest {
383 pub provider_filter: Filter,
385 pub local_matcher: Option<Arc<dyn LogMatcher>>,
387 pub route_key: Option<RouteKeySpec>,
389}
390
391impl LogInterest {
392 pub fn matches(&self, log: &Log) -> bool {
394 self.provider_filter.rpc_matches(log)
395 && self
396 .local_matcher
397 .as_ref()
398 .is_none_or(|matcher| matcher.matches(log))
399 }
400
401 pub fn route_key(&self, log: &Log) -> Option<RouteKey> {
403 self.route_key.as_ref().and_then(|spec| spec.extract(log))
404 }
405}
406
407impl fmt::Debug for LogInterest {
408 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409 f.debug_struct("LogInterest")
410 .field("provider_filter", &self.provider_filter)
411 .field(
412 "local_matcher",
413 &self.local_matcher.as_ref().map(|_| "<matcher>"),
414 )
415 .field("route_key", &self.route_key)
416 .finish()
417 }
418}
419
420pub trait LogMatcher: Send + Sync {
422 fn matches(&self, log: &Log) -> bool;
424}
425
426#[derive(Clone)]
428pub enum RouteKeySpec {
429 EmitterAddress,
431 Topic {
433 index: usize,
435 },
436 DataSlice {
438 offset: usize,
440 len: usize,
442 },
443 Custom(Arc<dyn RouteKeyExtractor>),
445}
446
447impl RouteKeySpec {
448 pub fn extract(&self, log: &Log) -> Option<RouteKey> {
450 match self {
451 Self::EmitterAddress => Some(RouteKey::Address(log.address())),
452 Self::Topic { index } => log.topics().get(*index).copied().map(RouteKey::Bytes32),
453 Self::DataSlice { offset, len } => {
454 let data = log.inner.data.data.as_ref();
455 let end = offset.checked_add(*len)?;
456 data.get(*offset..end)
457 .map(|bytes| RouteKey::Bytes(bytes.to_vec()))
458 }
459 Self::Custom(extractor) => extractor.extract(log),
460 }
461 }
462}
463
464impl fmt::Debug for RouteKeySpec {
465 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466 match self {
467 Self::EmitterAddress => f.write_str("EmitterAddress"),
468 Self::Topic { index } => f.debug_struct("Topic").field("index", index).finish(),
469 Self::DataSlice { offset, len } => f
470 .debug_struct("DataSlice")
471 .field("offset", offset)
472 .field("len", len)
473 .finish(),
474 Self::Custom(_) => f.write_str("Custom(<extractor>)"),
475 }
476 }
477}
478
479pub trait RouteKeyExtractor: Send + Sync {
481 fn extract(&self, log: &Log) -> Option<RouteKey>;
483}
484
485#[derive(Clone, Debug, PartialEq, Eq, Hash)]
487pub enum RouteKey {
488 Address(Address),
490 Bytes32(B256),
492 Bytes(Vec<u8>),
494}
495
496#[derive(Clone, Debug, PartialEq, Eq)]
498pub struct ReactiveLogRoute {
499 pub handler_id: HandlerId,
501 pub route_key: Option<RouteKey>,
503}
504
505#[derive(Clone, Debug, PartialEq, Eq, Hash)]
507pub struct BlockInterest {
508 pub mode: BlockInterestMode,
510}
511
512impl Default for BlockInterest {
513 fn default() -> Self {
514 Self {
515 mode: BlockInterestMode::Header,
516 }
517 }
518}
519
520#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
522pub enum BlockInterestMode {
523 Header,
525 FullBlock,
527}
528
529#[derive(Clone)]
531pub struct PendingTxInterest<N: Network = Ethereum> {
532 pub full_transactions: bool,
534 pub from: AddressMatcher,
536 pub to: AddressMatcher,
538 pub selectors: SelectorMatcher,
540 pub local_matcher: Option<Arc<dyn PendingTxMatcher<N>>>,
542}
543
544impl<N: Network> Default for PendingTxInterest<N> {
545 fn default() -> Self {
546 Self {
547 full_transactions: false,
548 from: AddressMatcher::Any,
549 to: AddressMatcher::Any,
550 selectors: SelectorMatcher::Any,
551 local_matcher: None,
552 }
553 }
554}
555
556impl<N: Network> PendingTxInterest<N> {
557 fn matches_hash_only(&self) -> bool {
558 !self.full_transactions
559 && self.from.is_any()
560 && self.to.is_any()
561 && self.selectors.is_any()
562 && self.local_matcher.is_none()
563 }
564
565 fn matches_tx(&self, tx: &N::TransactionResponse) -> bool {
566 self.from.matches(tx.from())
567 && self.to.matches_option(tx.to())
568 && self.selectors.matches(tx.input())
569 && self
570 .local_matcher
571 .as_ref()
572 .is_none_or(|matcher| matcher.matches(tx))
573 }
574}
575
576impl<N: Network> fmt::Debug for PendingTxInterest<N> {
577 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
578 f.debug_struct("PendingTxInterest")
579 .field("full_transactions", &self.full_transactions)
580 .field("from", &self.from)
581 .field("to", &self.to)
582 .field("selectors", &self.selectors)
583 .field(
584 "local_matcher",
585 &self.local_matcher.as_ref().map(|_| "<matcher>"),
586 )
587 .finish()
588 }
589}
590
591#[derive(Clone, Debug, PartialEq, Eq, Hash)]
593pub enum AddressMatcher {
594 Any,
596 Exact(Address),
598 AnyOf(Vec<Address>),
600}
601
602impl AddressMatcher {
603 pub fn is_any(&self) -> bool {
605 matches!(self, Self::Any)
606 }
607
608 pub fn matches(&self, address: Address) -> bool {
610 match self {
611 Self::Any => true,
612 Self::Exact(expected) => *expected == address,
613 Self::AnyOf(addresses) => addresses.contains(&address),
614 }
615 }
616
617 pub fn matches_option(&self, address: Option<Address>) -> bool {
619 match (self, address) {
620 (Self::Any, _) => true,
621 (_, Some(address)) => self.matches(address),
622 _ => false,
623 }
624 }
625}
626
627#[derive(Clone, Debug, PartialEq, Eq, Hash)]
629pub enum SelectorMatcher {
630 Any,
632 AnyOf(Vec<[u8; 4]>),
634}
635
636impl SelectorMatcher {
637 pub fn is_any(&self) -> bool {
639 matches!(self, Self::Any)
640 }
641
642 pub fn matches(&self, input: &Bytes) -> bool {
644 match self {
645 Self::Any => true,
646 Self::AnyOf(selectors) => input
647 .get(..4)
648 .and_then(|bytes| bytes.try_into().ok())
649 .is_some_and(|selector| selectors.contains(&selector)),
650 }
651 }
652}
653
654pub trait PendingTxMatcher<N: Network = Ethereum>: Send + Sync {
656 fn matches(&self, tx: &N::TransactionResponse) -> bool;
658}
659
660#[derive(Clone, Debug, PartialEq, Eq)]
662pub struct ResyncRequest {
663 pub id: ResyncId,
665 pub reason: ResyncReason,
667 pub block: ResyncBlock,
669 pub targets: Vec<ResyncTarget>,
671 pub priority: ResyncPriority,
673}
674
675#[derive(Clone, Debug, PartialEq, Eq, Hash)]
677pub struct ResyncId(String);
678
679impl ResyncId {
680 pub fn new(id: impl Into<String>) -> Self {
682 Self(id.into())
683 }
684}
685
686#[derive(Clone, Debug, PartialEq, Eq, Hash)]
688pub enum ResyncReason {
689 HandlerRequested,
691 SkippedStateEffect,
693 Custom(String),
695}
696
697#[derive(Clone, Debug, PartialEq, Eq, Hash)]
699pub enum ResyncBlock {
700 Latest,
702 Safe,
704 Finalized,
706 Number(u64),
708 Hash {
710 number: u64,
712 hash: B256,
714 require_canonical: bool,
716 },
717}
718
719#[derive(Clone, Debug, PartialEq, Eq, Hash)]
721pub enum ResyncTarget {
722 StorageSlot {
724 address: Address,
726 slot: U256,
728 },
729 StorageSlots {
731 address: Address,
733 slots: Vec<U256>,
735 },
736 Account {
738 address: Address,
740 fields: AccountFieldMask,
742 },
743}
744
745#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
747pub struct AccountFieldMask {
748 pub balance: bool,
750 pub nonce: bool,
752 pub code: bool,
754}
755
756#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
758pub enum ResyncPriority {
759 Low,
761 #[default]
763 Normal,
764 High,
766}
767
768#[derive(Clone, Debug, PartialEq, Eq)]
770pub struct InvalidationRequest {
771 pub scope: PurgeScope,
773 pub address: Address,
775 pub reason: InvalidationReason,
777}
778
779#[derive(Clone, Debug, PartialEq, Eq, Hash)]
781pub enum InvalidationReason {
782 HandlerRequested,
784 Reorg,
786 Custom(String),
788}
789
790#[derive(Clone, Debug, PartialEq, Eq)]
792pub struct SpeculativeRequest {
793 pub id: SpeculativeId,
795 pub input_ref: InputRef,
797 pub labels: Vec<ReportTag>,
799}
800
801#[derive(Clone, Debug, PartialEq, Eq, Hash)]
803pub struct SpeculativeId(String);
804
805impl SpeculativeId {
806 pub fn new(id: impl Into<String>) -> Self {
808 Self(id.into())
809 }
810}
811
812#[derive(Clone, Debug, PartialEq, Eq)]
814pub struct ReactiveConfig {
815 pub hook_backpressure: HookBackpressure,
820 pub journal_depth: usize,
833}
834
835impl Default for ReactiveConfig {
836 fn default() -> Self {
837 Self {
838 hook_backpressure: HookBackpressure::Block,
839 journal_depth: 64,
840 }
841 }
842}
843
844#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
846pub enum HookBackpressure {
847 Block,
849 DropNewest,
851 DropOldest,
853 Error,
855}
856
857#[derive(Clone, Debug)]
859pub enum ReactiveReport<N: Network = Ethereum> {
860 Input(InputReport<N>),
862 Decoded(DecodedReport<N>),
864 Applied(AppliedReport<N>),
866 Resynced(ResyncReport),
868 BlockCommitted(BlockReport<N>),
870 Reorg(ReorgReport<N>),
872 Error(ReactiveErrorReport<N>),
874}
875
876#[derive(Clone, Debug)]
878pub struct InputReport<N: Network = Ethereum> {
879 pub input_ref: InputRef,
881 pub context: ReactiveContext,
883 pub _network: PhantomData<N>,
885}
886
887#[derive(Clone, Debug)]
889pub struct DecodedReport<N: Network = Ethereum> {
890 pub input_ref: InputRef,
892 pub handler_ids: Vec<HandlerId>,
894 pub _network: PhantomData<N>,
896}
897
898#[derive(Clone, Debug)]
900pub struct AppliedReport<N: Network = Ethereum> {
901 pub input_ref: InputRef,
903 pub handler_id: HandlerId,
905 pub quality: StateEffectQuality,
907 pub tags: Vec<ReportTag>,
909 pub diff: StateDiff,
911 pub state_updates: Vec<StateUpdate>,
913 pub invalidations: Vec<InvalidationRequest>,
915 pub resyncs: Vec<ResyncRequest>,
917 pub speculative: Vec<SpeculativeRequest>,
919 pub hook_signals: Vec<HookSignal>,
921 pub _network: PhantomData<N>,
923}
924
925#[derive(Clone, Debug, Default, PartialEq, Eq)]
929pub struct ResyncReport {
930 pub requested: Vec<ResyncRequest>,
932 pub state_updates: Vec<StateUpdate>,
934 pub diff: StateDiff,
936 pub failed: Vec<ResyncFailure>,
938}
939
940#[derive(Clone, Debug, PartialEq, Eq)]
942pub struct ResyncFailure {
943 pub request_id: ResyncId,
945 pub block: ResyncBlock,
947 pub target: ResyncTarget,
949 pub kind: ResyncFailureKind,
951 pub message: String,
953}
954
955#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
957pub enum ResyncFailureKind {
958 MissingStorageFetcher,
960 StorageFetchFailed,
962 StorageFetchOmitted,
964 UnsupportedAccountTarget,
966}
967
968#[derive(Clone, Debug)]
970pub struct BlockReport<N: Network = Ethereum> {
971 pub block: Option<BlockRef>,
973 pub inputs: Vec<InputRef>,
975 pub _network: PhantomData<N>,
977}
978
979#[derive(Clone, Debug)]
990pub struct ReorgReport<N: Network = Ethereum> {
991 pub dropped: Option<BlockRef>,
993 pub dropped_blocks: Vec<BlockRef>,
995 pub dropped_inputs: Vec<InputRef>,
997 pub rollback_updates: Vec<StateUpdate>,
999 pub rollback_diff: StateDiff,
1001 pub purge_updates: Vec<StateUpdate>,
1003 pub purge_diff: StateDiff,
1005 pub canceled_resyncs: Vec<ResyncRequest>,
1007 pub reason: ReorgReason,
1009 pub _network: PhantomData<N>,
1011}
1012
1013#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1015pub enum ReorgReason {
1016 RemovedLog,
1018 ReorgedInput,
1020 ParentMismatch,
1022}
1023
1024#[derive(Clone, Debug)]
1027pub struct ReactiveErrorReport<N: Network = Ethereum> {
1028 pub input_ref: Option<InputRef>,
1030 pub message: String,
1032 pub _network: PhantomData<N>,
1034}
1035
1036#[derive(Clone, Debug)]
1039pub struct ReactiveBatchReport<N: Network = Ethereum> {
1040 pub applied: Vec<AppliedReport<N>>,
1042 pub resyncs: Vec<ResyncRequest>,
1044 pub speculative: Vec<SpeculativeRequest>,
1046 pub reports: Vec<Arc<ReactiveReport<N>>>,
1048}
1049
1050impl<N: Network> Default for ReactiveBatchReport<N> {
1051 fn default() -> Self {
1052 Self {
1053 applied: Vec::new(),
1054 resyncs: Vec::new(),
1055 speculative: Vec::new(),
1056 reports: Vec::new(),
1057 }
1058 }
1059}
1060
1061#[derive(Clone, Debug, PartialEq, Eq)]
1063pub struct HandlerError {
1064 message: String,
1065}
1066
1067impl HandlerError {
1068 pub fn new(message: impl Into<String>) -> Self {
1070 Self {
1071 message: message.into(),
1072 }
1073 }
1074}
1075
1076impl fmt::Display for HandlerError {
1077 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1078 self.message.fmt(f)
1079 }
1080}
1081
1082impl std::error::Error for HandlerError {}
1083
1084impl From<String> for HandlerError {
1085 fn from(message: String) -> Self {
1086 Self::new(message)
1087 }
1088}
1089
1090impl From<&str> for HandlerError {
1091 fn from(message: &str) -> Self {
1092 Self::new(message)
1093 }
1094}
1095
1096#[derive(Debug, thiserror::Error)]
1098pub enum ReactiveError {
1099 #[error("handler `{handler_id}` failed: {source}")]
1101 HandlerFailed {
1102 handler_id: HandlerId,
1104 source: HandlerError,
1106 },
1107 #[error(
1109 "conflicting effects for input {input_ref:?} on target {target:?}: `{first}` vs `{second}`"
1110 )]
1111 ConflictingEffects {
1112 input_ref: Box<InputRef>,
1114 target: Box<EffectTarget>,
1116 first: HandlerId,
1118 second: HandlerId,
1120 },
1121 #[error(
1123 "pending input {input_ref:?} emitted invalid canonical effect `{effect_kind}` from `{handler_id}`"
1124 )]
1125 InvalidPendingEffect {
1126 input_ref: Box<InputRef>,
1128 handler_id: HandlerId,
1130 effect_kind: &'static str,
1132 },
1133 #[error(transparent)]
1135 Register(#[from] RegisterError),
1136}
1137
1138#[derive(Debug, thiserror::Error)]
1140pub enum RegisterError {
1141 #[error("handler id `{0}` is already registered")]
1143 DuplicateHandler(HandlerId),
1144}
1145
1146#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1148pub enum EffectTarget {
1149 StorageSlot {
1151 address: Address,
1153 slot: U256,
1155 },
1156 AccountBalance {
1158 address: Address,
1160 },
1161 AccountNonce {
1163 address: Address,
1165 },
1166 AccountCode {
1168 address: Address,
1170 },
1171 MaskedStorageSlot {
1173 address: Address,
1175 slot: U256,
1177 mask: U256,
1179 },
1180}
1181
1182#[derive(Clone, Debug, PartialEq, Eq)]
1183enum AbsoluteValue {
1184 U256(U256),
1185 U64(u64),
1186 Bytes(Bytes),
1187}
1188
1189pub struct ReactiveRuntime<N: Network = Ethereum> {
1191 registry: ReactiveRegistry<N>,
1192 hooks: Vec<Arc<dyn ReactiveHook<N>>>,
1193 config: ReactiveConfig,
1194 journal: VecDeque<BlockJournal<N>>,
1195 pending_resyncs: Vec<ResyncRequest>,
1196}
1197
1198#[derive(Clone, Debug)]
1199struct BlockJournal<N: Network = Ethereum> {
1200 block: BlockRef,
1201 inputs: Vec<InputRef>,
1202 applied: Vec<AppliedReport<N>>,
1203 resynced: Vec<ResyncReport>,
1204}
1205
1206pub struct ReactiveRegistry<N: Network = Ethereum> {
1214 handlers: Vec<RegisteredHandler<N>>,
1215}
1216
1217struct RegisteredHandler<N: Network = Ethereum> {
1218 id: HandlerId,
1219 handler: Arc<dyn ReactiveHandler<N>>,
1220 interests: Vec<ReactiveInterest<N>>,
1221}
1222
1223impl<N: Network> Default for ReactiveRegistry<N> {
1224 fn default() -> Self {
1225 Self::new()
1226 }
1227}
1228
1229impl<N: Network> ReactiveRegistry<N> {
1230 pub fn new() -> Self {
1232 Self {
1233 handlers: Vec::new(),
1234 }
1235 }
1236
1237 pub fn register_handler(
1242 &mut self,
1243 handler: Arc<dyn ReactiveHandler<N>>,
1244 ) -> Result<(), RegisterError> {
1245 let id = handler.id();
1246 if self.handlers.iter().any(|registered| registered.id == id) {
1247 return Err(RegisterError::DuplicateHandler(id));
1248 }
1249 let interests = handler.interests();
1250 self.handlers.push(RegisteredHandler {
1251 id,
1252 handler,
1253 interests,
1254 });
1255 Ok(())
1256 }
1257
1258 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
1260 self.handlers
1261 .iter()
1262 .flat_map(|handler| handler.interests.clone())
1263 .collect()
1264 }
1265
1266 pub fn log_subscription_filters(&self) -> Vec<Filter> {
1273 let mut filters = Vec::new();
1274 for interest in self.log_interests() {
1275 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
1276 }
1277 filters
1278 }
1279
1280 pub fn route_log(&self, log: &Log) -> Vec<ReactiveLogRoute> {
1286 self.handlers
1287 .iter()
1288 .filter_map(|handler| handler.route_log(log))
1289 .collect()
1290 }
1291
1292 fn handlers(&self) -> &[RegisteredHandler<N>] {
1293 &self.handlers
1294 }
1295
1296 fn log_interests(&self) -> impl Iterator<Item = &LogInterest> {
1297 self.handlers.iter().flat_map(|handler| {
1298 handler
1299 .interests
1300 .iter()
1301 .filter_map(|interest| match interest {
1302 ReactiveInterest::Logs(interest) => Some(interest),
1303 ReactiveInterest::Blocks(_) | ReactiveInterest::PendingTransactions(_) => None,
1304 })
1305 })
1306 }
1307}
1308
1309impl<N: Network> ReactiveRuntime<N> {
1310 pub fn new(config: ReactiveConfig) -> Self {
1312 Self {
1313 registry: ReactiveRegistry::new(),
1314 hooks: Vec::new(),
1315 config,
1316 journal: VecDeque::new(),
1317 pending_resyncs: Vec::new(),
1318 }
1319 }
1320
1321 pub fn register_handler(
1323 &mut self,
1324 handler: Arc<dyn ReactiveHandler<N>>,
1325 ) -> Result<(), RegisterError> {
1326 self.registry.register_handler(handler)
1327 }
1328
1329 pub fn register_hook(&mut self, hook: Arc<dyn ReactiveHook<N>>) -> Result<(), RegisterError> {
1331 self.hooks.push(hook);
1332 Ok(())
1333 }
1334
1335 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
1337 self.registry.interests()
1338 }
1339
1340 pub fn ingest_batch(
1342 &mut self,
1343 cache: &mut EvmCache,
1344 batch: ReactiveInputBatch<N>,
1345 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
1346 let batch_report = self.ingest_batch_direct(cache, batch)?;
1347 self.dispatch_reports(&batch_report.reports);
1348 let _ = &self.config;
1349 Ok(batch_report)
1350 }
1351
1352 pub fn ingest_batch_with_resync(
1363 &mut self,
1364 cache: &mut EvmCache,
1365 batch: ReactiveInputBatch<N>,
1366 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
1367 let mut batch_report = self.ingest_batch_direct(cache, batch)?;
1368
1369 if !batch_report.resyncs.is_empty() {
1370 let resync_report = execute_resync_requests(cache, &batch_report.resyncs);
1371 self.remove_pending_resyncs(batch_report.resyncs.iter().map(|request| &request.id));
1372 self.record_journal_resync(&resync_report);
1373 batch_report
1374 .reports
1375 .push(Arc::new(ReactiveReport::Resynced(resync_report)));
1376 }
1377
1378 self.dispatch_reports(&batch_report.reports);
1379 let _ = &self.config;
1380 Ok(batch_report)
1381 }
1382
1383 fn ingest_batch_direct(
1384 &mut self,
1385 cache: &mut EvmCache,
1386 batch: ReactiveInputBatch<N>,
1387 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
1388 let records = sort_records(dedupe_records(batch.into_records()));
1389
1390 let mut batch_report = ReactiveBatchReport::default();
1391 let mut reports_to_dispatch = Vec::new();
1392
1393 for record in records {
1394 let input_ref = record.input_ref();
1395 reports_to_dispatch.push(Arc::new(ReactiveReport::Input(InputReport {
1396 input_ref,
1397 context: record.context.clone(),
1398 _network: PhantomData,
1399 })));
1400
1401 if let Some(reorg_report) = self.recover_for_canonical_input(cache, &record) {
1402 remove_canceled_resyncs_from_batch(
1403 &mut batch_report.resyncs,
1404 &reorg_report.canceled_resyncs,
1405 );
1406 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
1407 }
1408
1409 if let Some(reorg_report) = self.recover_for_reorged_input(cache, &record) {
1410 remove_canceled_resyncs_from_batch(
1411 &mut batch_report.resyncs,
1412 &reorg_report.canceled_resyncs,
1413 );
1414 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
1415 continue;
1416 }
1417
1418 if let Some(block) = canonical_record_block(&record) {
1419 self.record_journal_input(block, input_ref);
1420 }
1421
1422 let executions = self.execute_handlers(cache, &record, input_ref)?;
1423 if executions.is_empty() {
1424 continue;
1425 }
1426
1427 reports_to_dispatch.push(Arc::new(ReactiveReport::Decoded(DecodedReport {
1428 input_ref,
1429 handler_ids: executions
1430 .iter()
1431 .map(|execution| execution.handler_id.clone())
1432 .collect(),
1433 _network: PhantomData,
1434 })));
1435
1436 detect_conflicts(input_ref, &executions)?;
1437
1438 for execution in executions {
1439 let diff = if execution.state_updates.is_empty() {
1440 StateDiff::default()
1441 } else {
1442 cache.apply_updates(&execution.state_updates)
1443 };
1444
1445 batch_report
1446 .resyncs
1447 .extend(execution.resyncs.iter().cloned());
1448 self.pending_resyncs
1449 .extend(execution.resyncs.iter().cloned());
1450 batch_report
1451 .speculative
1452 .extend(execution.speculative.iter().cloned());
1453
1454 let applied = AppliedReport {
1455 input_ref,
1456 handler_id: execution.handler_id,
1457 quality: execution.quality,
1458 tags: execution.tags,
1459 diff,
1460 state_updates: execution.state_updates,
1461 invalidations: execution.invalidations,
1462 resyncs: execution.resyncs,
1463 speculative: execution.speculative,
1464 hook_signals: execution.hook_signals,
1465 _network: PhantomData,
1466 };
1467 let report = Arc::new(ReactiveReport::Applied(applied.clone()));
1468 reports_to_dispatch.push(report);
1469 if let Some(block) = canonical_record_block(&record) {
1470 self.record_journal_applied(block, applied.clone());
1471 }
1472 batch_report.applied.push(applied);
1473 }
1474 }
1475
1476 batch_report.reports = reports_to_dispatch;
1477 Ok(batch_report)
1478 }
1479
1480 fn execute_handlers(
1481 &self,
1482 cache: &EvmCache,
1483 record: &ReactiveInputRecord<N>,
1484 input_ref: InputRef,
1485 ) -> Result<Vec<HandlerExecution>, ReactiveError> {
1486 let mut executions = Vec::new();
1487 for registered in self.registry.handlers() {
1488 if !registered.matches(&record.input) {
1489 continue;
1490 }
1491
1492 let outcome = registered
1493 .handler
1494 .handle(&record.context, &record.input, cache)
1495 .map_err(|source| ReactiveError::HandlerFailed {
1496 handler_id: registered.id.clone(),
1497 source,
1498 })?;
1499
1500 validate_effects(input_ref, &record.context, ®istered.id, &outcome.effects)?;
1501 executions.push(HandlerExecution::from_outcome(
1502 registered.id.clone(),
1503 input_ref,
1504 outcome,
1505 ));
1506 }
1507 Ok(executions)
1508 }
1509
1510 fn dispatch_reports(&self, reports: &[Arc<ReactiveReport<N>>]) {
1511 for report in reports {
1512 for hook in &self.hooks {
1513 hook.on_report(report.clone());
1514 }
1515 }
1516 }
1517
1518 fn recover_for_canonical_input(
1519 &mut self,
1520 cache: &mut EvmCache,
1521 record: &ReactiveInputRecord<N>,
1522 ) -> Option<ReorgReport<N>> {
1523 let block = canonical_record_block(record)?;
1524 let latest = self.journal.back()?.block.clone();
1525
1526 if self
1527 .journal
1528 .iter()
1529 .any(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
1530 {
1531 return None;
1532 }
1533
1534 if block.number == latest.number.saturating_add(1) && block.parent_hash == Some(latest.hash)
1535 {
1536 return None;
1537 }
1538
1539 if block.number > latest.number.saturating_add(1) {
1540 return None;
1541 }
1542
1543 let dropped = if let Some(parent_hash) = block.parent_hash {
1544 if let Some(parent_index) = self
1545 .journal
1546 .iter()
1547 .rposition(|entry| entry.block.hash == parent_hash)
1548 {
1549 self.drain_journal_after(parent_index)
1550 } else {
1551 self.warn_under_recovery(block.number);
1552 self.drain_journal_from_number(block.number)
1553 }
1554 } else {
1555 self.warn_under_recovery(block.number);
1556 self.drain_journal_from_number(block.number)
1557 };
1558
1559 self.recover_dropped_journals(cache, dropped, ReorgReason::ParentMismatch)
1560 }
1561
1562 fn recover_for_reorged_input(
1563 &mut self,
1564 cache: &mut EvmCache,
1565 record: &ReactiveInputRecord<N>,
1566 ) -> Option<ReorgReport<N>> {
1567 let (dropped_block, reason) = reorg_signal_block(record)?;
1568 let dropped = if let Some(index) = self
1569 .journal
1570 .iter()
1571 .position(|entry| entry.block.hash == dropped_block.hash)
1572 {
1573 self.drain_journal_from(index)
1574 } else {
1575 self.warn_under_recovery(dropped_block.number);
1576 self.drain_journal_from_number(dropped_block.number)
1577 };
1578
1579 if dropped.is_empty() {
1580 let canceled_resyncs =
1581 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
1582 if canceled_resyncs.is_empty() {
1583 return None;
1584 }
1585 return Some(ReorgReport {
1586 dropped: Some(dropped_block.clone()),
1587 dropped_blocks: vec![dropped_block],
1588 dropped_inputs: Vec::new(),
1589 rollback_updates: Vec::new(),
1590 rollback_diff: StateDiff::default(),
1591 purge_updates: Vec::new(),
1592 purge_diff: StateDiff::default(),
1593 canceled_resyncs,
1594 reason,
1595 _network: PhantomData,
1596 });
1597 }
1598
1599 self.recover_dropped_journals(cache, dropped, reason)
1600 }
1601
1602 fn warn_under_recovery(&self, reorg_number: u64) {
1607 let oldest_journaled = self.journal.front().map(|entry| entry.block.number);
1608 tracing::warn!(
1609 reorg_block = reorg_number,
1610 oldest_journaled = ?oldest_journaled,
1611 journal_depth = self.config.journal_depth,
1612 "reactive reorg recovery is incomplete: the reorged block is no longer \
1613 in the journal, so effects from blocks aged out of the journal are \
1614 neither rolled back nor purged (the freshness/validation loop is the \
1615 backstop). Increase ReactiveConfig::journal_depth to recover deeper \
1616 reorgs precisely."
1617 );
1618 }
1619
1620 fn record_journal_input(&mut self, block: &BlockRef, input_ref: InputRef) {
1621 let entry = self.journal_entry_mut(block);
1622 if !entry.inputs.contains(&input_ref) {
1623 entry.inputs.push(input_ref);
1624 }
1625 self.trim_journal();
1626 }
1627
1628 fn record_journal_applied(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
1629 self.journal_entry_mut(block).applied.push(applied);
1630 self.trim_journal();
1631 }
1632
1633 fn record_journal_resync(&mut self, report: &ResyncReport) {
1634 if report.diff.is_empty() {
1635 return;
1636 }
1637 let Some(block) = single_hash_pinned_resync_block(report) else {
1638 return;
1639 };
1640 self.journal_entry_mut(&block).resynced.push(report.clone());
1641 self.trim_journal();
1642 }
1643
1644 fn journal_entry_mut(&mut self, block: &BlockRef) -> &mut BlockJournal<N> {
1645 if let Some(index) = self
1646 .journal
1647 .iter()
1648 .position(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
1649 {
1650 return &mut self.journal[index];
1651 }
1652
1653 self.journal.push_back(BlockJournal {
1654 block: block.clone(),
1655 inputs: Vec::new(),
1656 applied: Vec::new(),
1657 resynced: Vec::new(),
1658 });
1659 let index = self.journal.len() - 1;
1660 &mut self.journal[index]
1661 }
1662
1663 fn trim_journal(&mut self) {
1664 if self.config.journal_depth == 0 {
1665 self.journal.clear();
1666 return;
1667 }
1668 while self.journal.len() > self.config.journal_depth {
1669 self.journal.pop_front();
1670 }
1671 }
1672
1673 fn drain_journal_after(&mut self, index: usize) -> Vec<BlockJournal<N>> {
1674 self.journal.drain((index + 1)..).collect()
1675 }
1676
1677 fn drain_journal_from(&mut self, index: usize) -> Vec<BlockJournal<N>> {
1678 self.journal.drain(index..).collect()
1679 }
1680
1681 fn drain_journal_from_number(&mut self, number: u64) -> Vec<BlockJournal<N>> {
1682 let Some(index) = self
1683 .journal
1684 .iter()
1685 .position(|entry| entry.block.number >= number)
1686 else {
1687 return Vec::new();
1688 };
1689 self.drain_journal_from(index)
1690 }
1691
1692 fn recover_dropped_journals(
1693 &mut self,
1694 cache: &mut EvmCache,
1695 dropped: Vec<BlockJournal<N>>,
1696 reason: ReorgReason,
1697 ) -> Option<ReorgReport<N>> {
1698 if dropped.is_empty() {
1699 return None;
1700 }
1701
1702 let dropped_blocks: Vec<_> = dropped.iter().map(|entry| entry.block.clone()).collect();
1703 let dropped_inputs: Vec<_> = dropped
1704 .iter()
1705 .flat_map(|entry| entry.inputs.iter().copied())
1706 .collect();
1707 let canceled_resyncs = self.cancel_resyncs_for_dropped_blocks(&dropped_blocks);
1708 let purge_scopes = purge_scopes_for_dropped_journals(&dropped);
1709 let rollback_updates = rollback_updates_for_dropped_journals(&dropped, &purge_scopes);
1710 let purge_updates: Vec<_> = purge_scopes
1711 .iter()
1712 .map(|(address, scope)| StateUpdate::purge(*address, scope.clone()))
1713 .collect();
1714
1715 let rollback_diff = if rollback_updates.is_empty() {
1716 StateDiff::default()
1717 } else {
1718 cache.apply_updates(&rollback_updates)
1719 };
1720 let purge_diff = if purge_updates.is_empty() {
1721 StateDiff::default()
1722 } else {
1723 cache.apply_updates(&purge_updates)
1724 };
1725
1726 Some(ReorgReport {
1727 dropped: dropped_blocks.first().cloned(),
1728 dropped_blocks,
1729 dropped_inputs,
1730 rollback_updates,
1731 rollback_diff,
1732 purge_updates,
1733 purge_diff,
1734 canceled_resyncs,
1735 reason,
1736 _network: PhantomData,
1737 })
1738 }
1739
1740 fn cancel_resyncs_for_dropped_blocks(
1741 &mut self,
1742 dropped_blocks: &[BlockRef],
1743 ) -> Vec<ResyncRequest> {
1744 let mut canceled = Vec::new();
1745 self.pending_resyncs.retain(|request| {
1746 let should_cancel = resync_request_targets_dropped_block(request, dropped_blocks);
1747 if should_cancel {
1748 canceled.push(request.clone());
1749 }
1750 !should_cancel
1751 });
1752 canceled
1753 }
1754
1755 fn remove_pending_resyncs<'a>(&mut self, ids: impl IntoIterator<Item = &'a ResyncId>) {
1756 let ids: HashSet<_> = ids.into_iter().cloned().collect();
1757 self.pending_resyncs
1758 .retain(|request| !ids.contains(&request.id));
1759 }
1760}
1761
1762fn canonical_record_block<N: Network>(record: &ReactiveInputRecord<N>) -> Option<&BlockRef> {
1763 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
1764 return None;
1765 }
1766 if is_canonical_status(&record.context.chain_status) {
1767 return context_block_ref(&record.context);
1768 }
1769 None
1770}
1771
1772fn context_block_ref(ctx: &ReactiveContext) -> Option<&BlockRef> {
1773 match &ctx.chain_status {
1774 ChainStatus::Included { block, .. }
1775 | ChainStatus::Safe { block }
1776 | ChainStatus::Finalized { block } => Some(block),
1777 ChainStatus::Reorged { dropped_from } => Some(dropped_from),
1778 ChainStatus::Pending => ctx.block.as_ref(),
1779 }
1780}
1781
1782fn reorg_signal_block<N: Network>(
1783 record: &ReactiveInputRecord<N>,
1784) -> Option<(BlockRef, ReorgReason)> {
1785 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
1786 return block_ref_from_record(record).map(|block| (block, ReorgReason::RemovedLog));
1787 }
1788
1789 if let ChainStatus::Reorged { dropped_from } = &record.context.chain_status {
1790 return Some((dropped_from.clone(), ReorgReason::ReorgedInput));
1791 }
1792
1793 None
1794}
1795
1796fn block_ref_from_record<N: Network>(record: &ReactiveInputRecord<N>) -> Option<BlockRef> {
1797 context_block_ref(&record.context)
1798 .cloned()
1799 .or_else(|| match &record.input {
1800 ReactiveInput::Log(log) => Some(BlockRef {
1801 number: log.block_number?,
1802 hash: log.block_hash?,
1803 parent_hash: None,
1804 timestamp: log.block_timestamp,
1805 }),
1806 ReactiveInput::BlockHeader(header) => Some(BlockRef {
1807 number: header.number(),
1808 hash: header.hash(),
1809 parent_hash: Some(header.parent_hash()),
1810 timestamp: Some(header.timestamp()),
1811 }),
1812 ReactiveInput::FullBlock(block) => {
1813 let header = block.header();
1814 Some(BlockRef {
1815 number: header.number(),
1816 hash: header.hash(),
1817 parent_hash: Some(header.parent_hash()),
1818 timestamp: Some(header.timestamp()),
1819 })
1820 }
1821 ReactiveInput::PendingTxHash(_) | ReactiveInput::PendingTx(_) => None,
1822 })
1823}
1824
1825fn remove_canceled_resyncs_from_batch(
1826 resyncs: &mut Vec<ResyncRequest>,
1827 canceled: &[ResyncRequest],
1828) {
1829 if canceled.is_empty() {
1830 return;
1831 }
1832 let canceled_ids: HashSet<_> = canceled.iter().map(|request| request.id.clone()).collect();
1833 resyncs.retain(|request| !canceled_ids.contains(&request.id));
1834}
1835
1836fn resync_request_targets_dropped_block(
1837 request: &ResyncRequest,
1838 dropped_blocks: &[BlockRef],
1839) -> bool {
1840 let ResyncBlock::Hash { number, hash, .. } = &request.block else {
1841 return false;
1842 };
1843 dropped_blocks
1844 .iter()
1845 .any(|block| block.hash == *hash && block.number == *number)
1846}
1847
1848fn single_hash_pinned_resync_block(report: &ResyncReport) -> Option<BlockRef> {
1849 let first = report.requested.first()?.block.clone();
1850 if !report
1851 .requested
1852 .iter()
1853 .all(|request| request.block == first)
1854 {
1855 return None;
1856 }
1857
1858 let ResyncBlock::Hash { number, hash, .. } = first else {
1859 return None;
1860 };
1861
1862 Some(BlockRef {
1863 number,
1864 hash,
1865 parent_hash: None,
1866 timestamp: None,
1867 })
1868}
1869
1870fn purge_scopes_for_dropped_journals<N: Network>(
1871 dropped: &[BlockJournal<N>],
1872) -> Vec<(Address, PurgeScope)> {
1873 let mut scopes: Vec<(Address, PurgeScope)> = Vec::new();
1874 for entry in dropped.iter().rev() {
1875 for resynced in entry.resynced.iter().rev() {
1876 merge_purge_scopes_for_diff(&mut scopes, &resynced.diff);
1877 }
1878 for applied in entry.applied.iter().rev() {
1879 merge_purge_scopes_for_diff(&mut scopes, &applied.diff);
1880 }
1881 }
1882 scopes
1883}
1884
1885fn rollback_updates_for_dropped_journals<N: Network>(
1886 dropped: &[BlockJournal<N>],
1887 purge_scopes: &[(Address, PurgeScope)],
1888) -> Vec<StateUpdate> {
1889 let purge_addresses: HashSet<_> = purge_scopes
1890 .iter()
1891 .map(|(address, _scope)| *address)
1892 .collect();
1893 let mut updates = Vec::new();
1894 for entry in dropped.iter().rev() {
1895 for resynced in entry.resynced.iter().rev() {
1896 push_rollback_updates_for_diff(&mut updates, &resynced.diff, &purge_addresses);
1897 }
1898 for applied in entry.applied.iter().rev() {
1899 push_rollback_updates_for_diff(&mut updates, &applied.diff, &purge_addresses);
1900 }
1901 }
1902 updates
1903}
1904
1905fn merge_purge_scopes_for_diff(scopes: &mut Vec<(Address, PurgeScope)>, diff: &StateDiff) {
1906 for change in &diff.accounts {
1907 merge_purge_scope(scopes, change.address, PurgeScope::Account);
1908 }
1909 for record in &diff.purged {
1910 merge_purge_scope(scopes, record.address, record.scope.clone());
1911 }
1912}
1913
1914fn push_rollback_updates_for_diff(
1915 updates: &mut Vec<StateUpdate>,
1916 diff: &StateDiff,
1917 purge_addresses: &HashSet<Address>,
1918) {
1919 for change in diff.slots.iter().rev() {
1920 if purge_addresses.contains(&change.address) {
1921 continue;
1922 }
1923 updates.push(StateUpdate::slot(change.address, change.slot, change.old));
1924 }
1925}
1926
1927fn merge_purge_scope(scopes: &mut Vec<(Address, PurgeScope)>, address: Address, scope: PurgeScope) {
1928 if let Some((_existing_address, existing_scope)) = scopes
1929 .iter_mut()
1930 .find(|(existing_address, _scope)| *existing_address == address)
1931 {
1932 *existing_scope = merged_purge_scope(existing_scope.clone(), scope);
1933 } else {
1934 scopes.push((address, scope));
1935 }
1936}
1937
1938fn merged_purge_scope(left: PurgeScope, right: PurgeScope) -> PurgeScope {
1939 match (left, right) {
1940 (PurgeScope::Account, _) | (_, PurgeScope::Account) => PurgeScope::Account,
1941 (PurgeScope::AllStorage, _) | (_, PurgeScope::AllStorage) => PurgeScope::AllStorage,
1942 (PurgeScope::Slots(mut left), PurgeScope::Slots(right)) => {
1943 for slot in right {
1944 if !left.contains(&slot) {
1945 left.push(slot);
1946 }
1947 }
1948 PurgeScope::Slots(left)
1949 }
1950 }
1951}
1952
1953#[derive(Clone, Debug)]
1954struct StorageFetchSlot {
1955 address: Address,
1956 slot: U256,
1957 origins: Vec<StorageFetchOrigin>,
1958}
1959
1960#[derive(Clone, Debug)]
1961struct StorageFetchOrigin {
1962 request_id: ResyncId,
1963 target: ResyncTarget,
1964}
1965
1966#[derive(Clone, Debug)]
1967struct StorageFetchGroup {
1968 block: ResyncBlock,
1969 slots: Vec<StorageFetchSlot>,
1970 seen: HashSet<(Address, U256)>,
1971}
1972
1973fn execute_resync_requests(cache: &mut EvmCache, requests: &[ResyncRequest]) -> ResyncReport {
1974 let mut failed = Vec::new();
1975 let mut storage_groups: Vec<StorageFetchGroup> = Vec::new();
1976
1977 for request in requests {
1978 for target in &request.targets {
1979 match target {
1980 ResyncTarget::StorageSlot { address, slot } => {
1981 push_storage_resync_slot(
1982 &mut storage_groups,
1983 &request.id,
1984 &request.block,
1985 *address,
1986 *slot,
1987 );
1988 }
1989 ResyncTarget::StorageSlots { address, slots } => {
1990 for slot in slots {
1991 push_storage_resync_slot(
1992 &mut storage_groups,
1993 &request.id,
1994 &request.block,
1995 *address,
1996 *slot,
1997 );
1998 }
1999 }
2000 ResyncTarget::Account { .. } => failed.push(ResyncFailure {
2001 request_id: request.id.clone(),
2002 block: request.block.clone(),
2003 target: target.clone(),
2004 kind: ResyncFailureKind::UnsupportedAccountTarget,
2005 message:
2006 "account resync is unsupported until a provider-neutral account fetcher exists"
2007 .to_string(),
2008 }),
2009 }
2010 }
2011 }
2012
2013 let mut state_updates = Vec::new();
2014 if !storage_groups.is_empty() {
2015 if let Some(fetcher) = cache.storage_batch_fetcher().cloned() {
2016 for group in storage_groups {
2017 let block = group.block.clone();
2018 let fetches: Vec<(Address, U256)> = group
2019 .slots
2020 .iter()
2021 .map(|slot| (slot.address, slot.slot))
2022 .collect();
2023 let results = (fetcher)(fetches, Some(resync_block_to_block_id(&block)));
2024 let mut pending: HashMap<(Address, U256), StorageFetchSlot> = group
2025 .slots
2026 .iter()
2027 .cloned()
2028 .map(|slot| ((slot.address, slot.slot), slot))
2029 .collect();
2030
2031 for (address, slot, fetched) in results {
2032 let Some(requested_slot) = pending.remove(&(address, slot)) else {
2033 continue;
2034 };
2035 match fetched {
2036 Ok(value) => state_updates.push(StateUpdate::slot(address, slot, value)),
2037 Err(error) => {
2038 let message = error.to_string();
2039 push_resync_failures(
2040 &mut failed,
2041 &block,
2042 requested_slot.origins,
2043 ResyncFailureKind::StorageFetchFailed,
2044 message,
2045 );
2046 }
2047 }
2048 }
2049
2050 for requested_slot in group.slots {
2051 if pending
2052 .remove(&(requested_slot.address, requested_slot.slot))
2053 .is_some()
2054 {
2055 push_resync_failures(
2056 &mut failed,
2057 &block,
2058 requested_slot.origins,
2059 ResyncFailureKind::StorageFetchOmitted,
2060 "storage batch fetcher did not return a value for slot".to_string(),
2061 );
2062 }
2063 }
2064 }
2065 } else {
2066 for group in storage_groups {
2067 let block = group.block.clone();
2068 for slot in group.slots {
2069 push_resync_failures(
2070 &mut failed,
2071 &block,
2072 slot.origins,
2073 ResyncFailureKind::MissingStorageFetcher,
2074 "storage resync requires a storage batch fetcher".to_string(),
2075 );
2076 }
2077 }
2078 }
2079 }
2080
2081 let diff = if state_updates.is_empty() {
2082 StateDiff::default()
2083 } else {
2084 cache.apply_updates(&state_updates)
2085 };
2086
2087 ResyncReport {
2088 requested: requests.to_vec(),
2089 state_updates,
2090 diff,
2091 failed,
2092 }
2093}
2094
2095fn push_resync_failures(
2096 failed: &mut Vec<ResyncFailure>,
2097 block: &ResyncBlock,
2098 origins: Vec<StorageFetchOrigin>,
2099 kind: ResyncFailureKind,
2100 message: String,
2101) {
2102 for origin in origins {
2103 failed.push(ResyncFailure {
2104 request_id: origin.request_id,
2105 block: block.clone(),
2106 target: origin.target,
2107 kind,
2108 message: message.clone(),
2109 });
2110 }
2111}
2112
2113fn push_storage_resync_slot(
2114 groups: &mut Vec<StorageFetchGroup>,
2115 request_id: &ResyncId,
2116 block: &ResyncBlock,
2117 address: Address,
2118 slot: U256,
2119) {
2120 let group_index = if let Some(index) = groups.iter().position(|group| group.block == *block) {
2121 index
2122 } else {
2123 groups.push(StorageFetchGroup {
2124 block: block.clone(),
2125 slots: Vec::new(),
2126 seen: HashSet::new(),
2127 });
2128 groups.len() - 1
2129 };
2130
2131 let group = &mut groups[group_index];
2132 let origin = StorageFetchOrigin {
2133 request_id: request_id.clone(),
2134 target: ResyncTarget::StorageSlot { address, slot },
2135 };
2136 if group.seen.insert((address, slot)) {
2137 group.slots.push(StorageFetchSlot {
2138 address,
2139 slot,
2140 origins: vec![origin],
2141 });
2142 } else if let Some(existing) = group
2143 .slots
2144 .iter_mut()
2145 .find(|existing| existing.address == address && existing.slot == slot)
2146 {
2147 existing.origins.push(origin);
2148 }
2149}
2150
2151fn resync_block_to_block_id(block: &ResyncBlock) -> BlockId {
2152 match block {
2153 ResyncBlock::Latest => BlockId::latest(),
2154 ResyncBlock::Safe => BlockId::safe(),
2155 ResyncBlock::Finalized => BlockId::finalized(),
2156 ResyncBlock::Number(number) => BlockId::number(*number),
2157 ResyncBlock::Hash {
2158 number: _,
2159 hash,
2160 require_canonical,
2161 } => BlockId::from((*hash, Some(*require_canonical))),
2162 }
2163}
2164
2165impl<N: Network> RegisteredHandler<N> {
2166 fn matches(&self, input: &ReactiveInput<N>) -> bool {
2167 self.interests
2168 .iter()
2169 .any(|interest| interest_matches(interest, input))
2170 }
2171
2172 fn route_log(&self, log: &Log) -> Option<ReactiveLogRoute> {
2173 self.interests.iter().find_map(|interest| match interest {
2174 ReactiveInterest::Logs(interest) if interest.matches(log) => Some(ReactiveLogRoute {
2175 handler_id: self.id.clone(),
2176 route_key: interest.route_key(log),
2177 }),
2178 ReactiveInterest::Logs(_)
2179 | ReactiveInterest::Blocks(_)
2180 | ReactiveInterest::PendingTransactions(_) => None,
2181 })
2182 }
2183}
2184
2185fn merge_log_subscription_filter(filters: &mut Vec<Filter>, next: &Filter) {
2186 if let Some(existing) = filters
2187 .iter_mut()
2188 .find(|existing| existing.block_option == next.block_option)
2189 {
2190 merge_filter_set(&mut existing.address, &next.address);
2191 for (existing_topic, next_topic) in existing.topics.iter_mut().zip(next.topics.iter()) {
2192 merge_filter_set(existing_topic, next_topic);
2193 }
2194 } else {
2195 filters.push(next.clone());
2196 }
2197}
2198
2199fn merge_filter_set<T: Clone + Eq + Hash>(target: &mut FilterSet<T>, source: &FilterSet<T>) {
2200 if target.is_empty() {
2201 return;
2202 }
2203 if source.is_empty() {
2204 *target = FilterSet::default();
2205 return;
2206 }
2207 for value in source.iter() {
2208 target.insert(value.clone());
2209 }
2210}
2211
2212#[derive(Clone, Debug)]
2213struct HandlerExecution {
2214 handler_id: HandlerId,
2215 quality: StateEffectQuality,
2216 tags: Vec<ReportTag>,
2217 state_updates: Vec<StateUpdate>,
2218 invalidations: Vec<InvalidationRequest>,
2219 resyncs: Vec<ResyncRequest>,
2220 speculative: Vec<SpeculativeRequest>,
2221 hook_signals: Vec<HookSignal>,
2222}
2223
2224impl HandlerExecution {
2225 fn from_outcome(handler_id: HandlerId, input_ref: InputRef, outcome: HandlerOutcome) -> Self {
2226 let mut state_updates = Vec::new();
2227 let mut invalidations = Vec::new();
2228 let mut resyncs = Vec::new();
2229 let mut speculative = Vec::new();
2230 let mut hook_signals = Vec::new();
2231
2232 for effect in outcome.effects {
2233 match effect {
2234 ReactiveEffect::StateUpdate(update) => state_updates.push(update),
2235 ReactiveEffect::Invalidate(invalidation) => {
2236 state_updates.push(StateUpdate::purge(
2237 invalidation.address,
2238 invalidation.scope.clone(),
2239 ));
2240 invalidations.push(invalidation);
2241 }
2242 ReactiveEffect::Resync(request) => resyncs.push(request),
2243 ReactiveEffect::Hook(signal) => hook_signals.push(signal),
2244 ReactiveEffect::Speculative(mut request) => {
2245 request.input_ref = input_ref;
2246 speculative.push(request);
2247 }
2248 }
2249 }
2250
2251 Self {
2252 handler_id,
2253 quality: outcome.quality,
2254 tags: outcome.tags,
2255 state_updates,
2256 invalidations,
2257 resyncs,
2258 speculative,
2259 hook_signals,
2260 }
2261 }
2262}
2263
2264fn dedupe_records<N: Network>(records: Vec<ReactiveInputRecord<N>>) -> Vec<ReactiveInputRecord<N>> {
2265 let mut seen = HashSet::new();
2266 let mut deduped = Vec::with_capacity(records.len());
2267 for record in records {
2268 if seen.insert(record.input_ref()) {
2269 deduped.push(record);
2270 }
2271 }
2272 deduped
2273}
2274
2275fn sort_records<N: Network>(records: Vec<ReactiveInputRecord<N>>) -> Vec<ReactiveInputRecord<N>> {
2276 let mut indexed: Vec<(usize, ReactiveInputRecord<N>)> =
2277 records.into_iter().enumerate().collect();
2278 indexed.sort_by_key(|(index, record)| record_sort_key(*index, record));
2279 indexed.into_iter().map(|(_, record)| record).collect()
2280}
2281
2282fn record_sort_key<N: Network>(index: usize, record: &ReactiveInputRecord<N>) -> RecordSortKey {
2283 if let ReactiveInput::Log(log) = &record.input
2284 && is_canonical_status(&record.context.chain_status)
2285 && !log.removed
2286 {
2287 return RecordSortKey {
2288 class: 0,
2289 block_number: log
2290 .block_number
2291 .or(record.context.block.as_ref().map(|block| block.number))
2292 .unwrap_or(u64::MAX),
2293 transaction_index: log
2294 .transaction_index
2295 .or(record.context.transaction_index)
2296 .unwrap_or(u64::MAX),
2297 log_index: log
2298 .log_index
2299 .or(record.context.log_index)
2300 .unwrap_or(u64::MAX),
2301 original_index: index,
2302 };
2303 }
2304
2305 RecordSortKey {
2306 class: 1,
2307 block_number: 0,
2308 transaction_index: 0,
2309 log_index: 0,
2310 original_index: index,
2311 }
2312}
2313
2314#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2315struct RecordSortKey {
2316 class: u8,
2317 block_number: u64,
2318 transaction_index: u64,
2319 log_index: u64,
2320 original_index: usize,
2321}
2322
2323fn interest_matches<N: Network>(interest: &ReactiveInterest<N>, input: &ReactiveInput<N>) -> bool {
2324 match (interest, input) {
2325 (ReactiveInterest::Logs(interest), ReactiveInput::Log(log)) => interest.matches(log),
2326 (
2327 ReactiveInterest::Blocks(BlockInterest {
2328 mode: BlockInterestMode::Header,
2329 }),
2330 ReactiveInput::BlockHeader(_),
2331 ) => true,
2332 (
2333 ReactiveInterest::Blocks(BlockInterest {
2334 mode: BlockInterestMode::FullBlock,
2335 }),
2336 ReactiveInput::FullBlock(_),
2337 ) => true,
2338 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTxHash(_)) => {
2339 interest.matches_hash_only()
2340 }
2341 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTx(tx)) => {
2342 interest.matches_tx(tx)
2343 }
2344 _ => false,
2345 }
2346}
2347
2348fn validate_effects(
2349 input_ref: InputRef,
2350 ctx: &ReactiveContext,
2351 handler_id: &HandlerId,
2352 effects: &[ReactiveEffect],
2353) -> Result<(), ReactiveError> {
2354 let pending = matches!(ctx.chain_status, ChainStatus::Pending)
2355 || matches!(input_ref, InputRef::PendingTx { .. });
2356 if !pending {
2357 return Ok(());
2358 }
2359
2360 for effect in effects {
2361 let effect_kind = match effect {
2362 ReactiveEffect::StateUpdate(_) => Some("state_update"),
2363 ReactiveEffect::Invalidate(_) => Some("invalidate"),
2364 ReactiveEffect::Resync(_) => Some("resync"),
2365 ReactiveEffect::Hook(_) | ReactiveEffect::Speculative(_) => None,
2366 };
2367 if let Some(effect_kind) = effect_kind {
2368 return Err(ReactiveError::InvalidPendingEffect {
2369 input_ref: Box::new(input_ref),
2370 handler_id: handler_id.clone(),
2371 effect_kind,
2372 });
2373 }
2374 }
2375 Ok(())
2376}
2377
2378fn detect_conflicts(
2379 input_ref: InputRef,
2380 executions: &[HandlerExecution],
2381) -> Result<(), ReactiveError> {
2382 let mut writes: HashMap<EffectTarget, (AbsoluteValue, HandlerId)> = HashMap::new();
2383 for execution in executions {
2384 for update in &execution.state_updates {
2385 for (target, value) in absolute_writes(update) {
2386 if let Some((previous_value, previous_handler)) = writes.get(&target) {
2387 if previous_value != &value {
2388 return Err(ReactiveError::ConflictingEffects {
2389 input_ref: Box::new(input_ref),
2390 target: Box::new(target),
2391 first: previous_handler.clone(),
2392 second: execution.handler_id.clone(),
2393 });
2394 }
2395 } else {
2396 writes.insert(target, (value, execution.handler_id.clone()));
2397 }
2398 }
2399 }
2400 }
2401 Ok(())
2402}
2403
2404fn absolute_writes(update: &StateUpdate) -> Vec<(EffectTarget, AbsoluteValue)> {
2405 match update {
2406 StateUpdate::Slot {
2407 address,
2408 slot,
2409 value,
2410 } => vec![(
2411 EffectTarget::StorageSlot {
2412 address: *address,
2413 slot: *slot,
2414 },
2415 AbsoluteValue::U256(*value),
2416 )],
2417 StateUpdate::SlotMasked {
2418 address,
2419 slot,
2420 mask,
2421 value,
2422 } => vec![(
2423 EffectTarget::MaskedStorageSlot {
2424 address: *address,
2425 slot: *slot,
2426 mask: *mask,
2427 },
2428 AbsoluteValue::U256(*value),
2429 )],
2430 StateUpdate::Account { address, patch } | StateUpdate::AccountUpsert { address, patch } => {
2431 account_patch_writes(*address, patch)
2432 }
2433 StateUpdate::SlotDelta { .. }
2434 | StateUpdate::BalanceDelta { .. }
2435 | StateUpdate::Purge { .. } => Vec::new(),
2436 }
2437}
2438
2439fn account_patch_writes(
2440 address: Address,
2441 patch: &AccountPatch,
2442) -> Vec<(EffectTarget, AbsoluteValue)> {
2443 let mut writes = Vec::new();
2444 if let Some(balance) = patch.balance {
2445 writes.push((
2446 EffectTarget::AccountBalance { address },
2447 AbsoluteValue::U256(balance),
2448 ));
2449 }
2450 if let Some(nonce) = patch.nonce {
2451 writes.push((
2452 EffectTarget::AccountNonce { address },
2453 AbsoluteValue::U64(nonce),
2454 ));
2455 }
2456 if let Some(code) = &patch.code {
2457 writes.push((
2458 EffectTarget::AccountCode { address },
2459 AbsoluteValue::Bytes(code.clone()),
2460 ));
2461 }
2462 writes
2463}
2464
2465fn input_ref<N: Network>(input: &ReactiveInput<N>, ctx: &ReactiveContext) -> InputRef {
2466 match input {
2467 ReactiveInput::Log(log) => InputRef::Log {
2468 chain_id: ctx.chain_id,
2469 block_hash: log
2470 .block_hash
2471 .or(ctx.block.as_ref().map(|block| block.hash))
2472 .unwrap_or_default(),
2473 transaction_hash: log.transaction_hash.unwrap_or_default(),
2474 log_index: log.log_index.or(ctx.log_index).unwrap_or_default(),
2475 },
2476 ReactiveInput::PendingTxHash(hash) => InputRef::PendingTx {
2477 chain_id: ctx.chain_id,
2478 hash: *hash,
2479 },
2480 ReactiveInput::PendingTx(tx) => InputRef::PendingTx {
2481 chain_id: ctx.chain_id,
2482 hash: tx.tx_hash(),
2483 },
2484 ReactiveInput::BlockHeader(header) => InputRef::Block {
2485 chain_id: ctx.chain_id,
2486 hash: header.hash(),
2487 number: header.number(),
2488 },
2489 ReactiveInput::FullBlock(block) => {
2490 let header = block.header();
2491 InputRef::Block {
2492 chain_id: ctx.chain_id,
2493 hash: header.hash(),
2494 number: header.number(),
2495 }
2496 }
2497 }
2498}
2499
2500fn is_canonical_status(status: &ChainStatus) -> bool {
2501 matches!(
2502 status,
2503 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. }
2504 )
2505}
2506
2507pub struct EventDecoderHandler {
2509 id: HandlerId,
2510 decoder: Arc<dyn EventDecoder>,
2511 interest: LogInterest,
2512}
2513
2514impl EventDecoderHandler {
2515 pub fn new(id: HandlerId, decoder: Arc<dyn EventDecoder>, interest: LogInterest) -> Self {
2517 Self {
2518 id,
2519 decoder,
2520 interest,
2521 }
2522 }
2523}
2524
2525impl<N: Network> ReactiveHandler<N> for EventDecoderHandler {
2526 fn id(&self) -> HandlerId {
2527 self.id.clone()
2528 }
2529
2530 fn interests(&self) -> Vec<ReactiveInterest<N>> {
2531 vec![ReactiveInterest::Logs(self.interest.clone())]
2532 }
2533
2534 fn handle(
2535 &self,
2536 _ctx: &ReactiveContext,
2537 input: &ReactiveInput<N>,
2538 state: &dyn StateView,
2539 ) -> Result<HandlerOutcome, HandlerError> {
2540 let ReactiveInput::Log(log) = input else {
2541 return Ok(HandlerOutcome::empty(StateEffectQuality::NoStateEffect));
2542 };
2543
2544 Ok(HandlerOutcome {
2545 effects: self
2546 .decoder
2547 .decode(&log.inner, state)
2548 .into_iter()
2549 .map(ReactiveEffect::StateUpdate)
2550 .collect(),
2551 quality: StateEffectQuality::ExactFromInput,
2552 tags: Vec::new(),
2553 })
2554 }
2555}
2556
2557pub trait EventSubscriber<N: Network = Ethereum>: Send {
2559 fn register_interests(
2561 &mut self,
2562 interests: &[ReactiveInterest<N>],
2563 ) -> Result<(), SubscriberError>;
2564
2565 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N>;
2567}
2568
2569pub type SubscriberNextBatch<'a, N> = Pin<
2571 Box<dyn Future<Output = Result<Option<ReactiveInputBatch<N>>, SubscriberError>> + Send + 'a>,
2572>;
2573
2574#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
2576pub enum SubscriberMode {
2577 #[default]
2583 Auto,
2584 PubSub,
2586 Polling,
2588}
2589
2590#[derive(Clone, Debug, PartialEq, Eq)]
2592pub struct SubscriberConfig {
2593 pub hydrate_pending_transactions: bool,
2595 pub max_batch_size: usize,
2597 pub reconnect: SubscriberReconnectConfig,
2599}
2600
2601impl Default for SubscriberConfig {
2602 fn default() -> Self {
2603 Self {
2604 hydrate_pending_transactions: false,
2605 max_batch_size: 1024,
2606 reconnect: SubscriberReconnectConfig::default(),
2607 }
2608 }
2609}
2610
2611#[derive(Clone, Debug, PartialEq, Eq)]
2617pub struct SubscriberReconnectConfig {
2618 pub enabled: bool,
2620 pub initial_delay: Duration,
2622 pub retry_delay: Duration,
2625 pub max_delay: Duration,
2627 pub max_attempts: Option<usize>,
2629 pub dedupe_window: usize,
2632}
2633
2634impl Default for SubscriberReconnectConfig {
2635 fn default() -> Self {
2636 Self {
2637 enabled: true,
2638 initial_delay: Duration::ZERO,
2639 retry_delay: Duration::from_millis(250),
2640 max_delay: Duration::from_secs(30),
2641 max_attempts: Some(3),
2642 dedupe_window: 4096,
2643 }
2644 }
2645}
2646
2647pub struct AlloySubscriber<P, N: Network = Ethereum> {
2659 provider: P,
2660 mode: SubscriberMode,
2661 config: SubscriberConfig,
2662 interests: Vec<ReactiveInterest<N>>,
2663 state: AlloySubscriberState<N>,
2664 pending_records: VecDeque<ReactiveInputRecord<N>>,
2665 last_seen_log_blocks: HashMap<usize, u64>,
2666 recent_input_refs: VecDeque<InputRef>,
2667 recent_input_ref_set: HashSet<InputRef>,
2668 _network: PhantomData<N>,
2669}
2670
2671#[cfg(feature = "reactive-ws")]
2677fn ensure_ring_crypto_provider() {
2678 use std::sync::Once;
2679 static INSTALL: Once = Once::new();
2680 INSTALL.call_once(|| {
2681 let _ = rustls::crypto::ring::default_provider().install_default();
2682 });
2683}
2684
2685impl<P, N: Network> AlloySubscriber<P, N> {
2686 pub fn new(provider: P, mode: SubscriberMode, config: SubscriberConfig) -> Self {
2688 #[cfg(feature = "reactive-ws")]
2689 ensure_ring_crypto_provider();
2690 Self {
2691 provider,
2692 mode,
2693 config,
2694 interests: Vec::new(),
2695 state: AlloySubscriberState::Uninitialized,
2696 pending_records: VecDeque::new(),
2697 last_seen_log_blocks: HashMap::new(),
2698 recent_input_refs: VecDeque::new(),
2699 recent_input_ref_set: HashSet::new(),
2700 _network: PhantomData,
2701 }
2702 }
2703
2704 pub fn provider(&self) -> &P {
2706 &self.provider
2707 }
2708
2709 pub fn mode(&self) -> SubscriberMode {
2711 self.mode
2712 }
2713
2714 pub fn config(&self) -> &SubscriberConfig {
2716 &self.config
2717 }
2718
2719 pub fn registered_interests(&self) -> &[ReactiveInterest<N>] {
2721 &self.interests
2722 }
2723
2724 fn drain_next_batch(&mut self) -> Option<ReactiveInputBatch<N>> {
2725 if self.pending_records.is_empty() {
2726 return None;
2727 }
2728
2729 let len = self.config.max_batch_size.min(self.pending_records.len());
2730 let records = self.pending_records.drain(..len).collect();
2731 Some(ReactiveInputBatch::new(records))
2732 }
2733
2734 fn reset_delivery_state(&mut self) {
2735 self.pending_records.clear();
2736 self.last_seen_log_blocks.clear();
2737 self.recent_input_refs.clear();
2738 self.recent_input_ref_set.clear();
2739 }
2740}
2741
2742enum AlloySubscriberState<N: Network> {
2743 Uninitialized,
2744 Active(SubscriberStreams<N>),
2745 Empty,
2746}
2747
2748struct SubscriberStreams<N: Network> {
2749 streams: SelectAll<BoxStream<'static, SubscriberEvent<N>>>,
2750}
2751
2752impl<N: Network> SubscriberStreams<N> {
2753 fn new() -> Self {
2754 Self {
2755 streams: SelectAll::new(),
2756 }
2757 }
2758
2759 fn is_empty(&self) -> bool {
2760 self.streams.is_empty()
2761 }
2762
2763 fn push(&mut self, stream: BoxStream<'static, SubscriberEvent<N>>) {
2764 self.streams.push(stream);
2765 }
2766
2767 async fn next(&mut self) -> Option<SubscriberEvent<N>> {
2768 self.streams.next().await
2769 }
2770}
2771
2772#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2773#[allow(dead_code)]
2774enum SubscriberTransport {
2775 PubSub,
2776 Polling,
2777}
2778
2779#[derive(Clone, Debug)]
2780enum SubscriberStreamSource {
2781 PubSubLog { id: usize, filter: Filter },
2782 PubSubPendingHashes,
2783 PubSubBlockHeaders,
2784 PollingLog { filter: Filter },
2785 PollingPendingHashes,
2786}
2787
2788impl SubscriberStreamSource {
2789 fn label(&self) -> &'static str {
2790 match self {
2791 Self::PubSubLog { .. } => "pubsub log",
2792 Self::PubSubPendingHashes => "pubsub pending transaction hash",
2793 Self::PubSubBlockHeaders => "pubsub block header",
2794 Self::PollingLog { .. } => "polling log",
2795 Self::PollingPendingHashes => "polling pending transaction hash",
2796 }
2797 }
2798
2799 fn is_pubsub(&self) -> bool {
2800 matches!(
2801 self,
2802 Self::PubSubLog { .. } | Self::PubSubPendingHashes | Self::PubSubBlockHeaders
2803 )
2804 }
2805}
2806
2807#[allow(dead_code)]
2808enum SubscriberEvent<N: Network> {
2809 Log { source_id: usize, log: Log },
2810 BackfilledLogs { source_id: usize, logs: Vec<Log> },
2811 Logs(Vec<Log>),
2812 BlockHeader(N::HeaderResponse),
2813 PendingHash(B256),
2814 PendingHashes(Vec<B256>),
2815 StreamTerminated(SubscriberStreamSource),
2816}
2817
2818impl<P, N> EventSubscriber<N> for AlloySubscriber<P, N>
2819where
2820 P: Provider<N> + Send + Sync,
2821 N: Network + 'static,
2822 N::HeaderResponse: Send + 'static,
2823{
2824 fn register_interests(
2825 &mut self,
2826 interests: &[ReactiveInterest<N>],
2827 ) -> Result<(), SubscriberError> {
2828 validate_subscriber_config(&self.config)?;
2829 validate_supported_interests(self.mode, &self.config, interests)?;
2830
2831 self.interests = interests.to_vec();
2832 self.reset_delivery_state();
2833 self.state = AlloySubscriberState::Uninitialized;
2834 Ok(())
2835 }
2836
2837 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N> {
2838 Box::pin(async {
2839 if let Some(batch) = self.drain_next_batch() {
2840 return Ok(Some(batch));
2841 }
2842
2843 if self.interests.is_empty() {
2844 return Ok(None);
2845 }
2846
2847 if matches!(self.state, AlloySubscriberState::Uninitialized) {
2848 let streams = self.init_streams().await?;
2849 self.state = if streams.is_empty() {
2850 AlloySubscriberState::Empty
2851 } else {
2852 AlloySubscriberState::Active(streams)
2853 };
2854 }
2855
2856 loop {
2857 let Some(event) = self.next_event().await? else {
2858 return Ok(None);
2859 };
2860
2861 self.enqueue_event(event);
2862 if let Some(batch) = self.drain_next_batch() {
2863 return Ok(Some(batch));
2864 }
2865 }
2866 })
2867 }
2868}
2869
2870impl<P, N> AlloySubscriber<P, N>
2871where
2872 P: Provider<N> + Send + Sync,
2873 N: Network + 'static,
2874 N::HeaderResponse: Send + 'static,
2875{
2876 async fn init_streams(&mut self) -> Result<SubscriberStreams<N>, SubscriberError> {
2877 let mut streams = SubscriberStreams::new();
2878 for source in self.stream_sources()? {
2879 streams.push(self.connect_source_stream(source).await?);
2880 }
2881 Ok(streams)
2882 }
2883
2884 fn stream_sources(&self) -> Result<Vec<SubscriberStreamSource>, SubscriberError> {
2885 match resolve_subscriber_transport(self.mode)? {
2886 SubscriberTransport::PubSub => Ok(pubsub_stream_sources(&self.interests)),
2887 SubscriberTransport::Polling => Ok(polling_stream_sources(&self.interests)),
2888 }
2889 }
2890
2891 async fn connect_source_stream(
2892 &mut self,
2893 source: SubscriberStreamSource,
2894 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
2895 match source {
2896 SubscriberStreamSource::PubSubLog { id, filter } => {
2897 self.connect_pubsub_log_stream(id, filter).await
2898 }
2899 SubscriberStreamSource::PubSubPendingHashes => {
2900 self.connect_pubsub_pending_hash_stream().await
2901 }
2902 SubscriberStreamSource::PubSubBlockHeaders => {
2903 self.connect_pubsub_block_header_stream().await
2904 }
2905 SubscriberStreamSource::PollingLog { filter } => {
2906 self.connect_polling_log_stream(filter).await
2907 }
2908 SubscriberStreamSource::PollingPendingHashes => {
2909 self.connect_polling_pending_hash_stream().await
2910 }
2911 }
2912 }
2913
2914 async fn connect_pubsub_log_stream(
2915 &mut self,
2916 id: usize,
2917 filter: Filter,
2918 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
2919 #[cfg(feature = "reactive-ws")]
2920 {
2921 let source = SubscriberStreamSource::PubSubLog {
2922 id,
2923 filter: filter.clone(),
2924 };
2925 let stream = self
2926 .provider
2927 .subscribe_logs(&filter)
2928 .channel_size(self.config.max_batch_size.max(1))
2929 .await
2930 .map_err(provider_error)?
2931 .into_stream()
2932 .map(move |log| SubscriberEvent::Log { source_id: id, log });
2933 Ok(stream_with_termination(stream, source))
2934 }
2935
2936 #[cfg(not(feature = "reactive-ws"))]
2937 {
2938 let _ = (id, filter);
2939 Err(SubscriberError::Unsupported(
2940 "AlloySubscriber pubsub mode requires the reactive-ws feature",
2941 ))
2942 }
2943 }
2944
2945 async fn connect_pubsub_pending_hash_stream(
2946 &mut self,
2947 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
2948 #[cfg(feature = "reactive-ws")]
2949 {
2950 let stream = self
2951 .provider
2952 .subscribe_pending_transactions()
2953 .channel_size(self.config.max_batch_size.max(1))
2954 .await
2955 .map_err(provider_error)?
2956 .into_stream()
2957 .map(SubscriberEvent::PendingHash);
2958 Ok(stream_with_termination(
2959 stream,
2960 SubscriberStreamSource::PubSubPendingHashes,
2961 ))
2962 }
2963
2964 #[cfg(not(feature = "reactive-ws"))]
2965 {
2966 Err(SubscriberError::Unsupported(
2967 "AlloySubscriber pubsub mode requires the reactive-ws feature",
2968 ))
2969 }
2970 }
2971
2972 async fn connect_pubsub_block_header_stream(
2973 &mut self,
2974 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
2975 #[cfg(feature = "reactive-ws")]
2976 {
2977 let stream = self
2978 .provider
2979 .subscribe_blocks()
2980 .channel_size(self.config.max_batch_size.max(1))
2981 .await
2982 .map_err(provider_error)?
2983 .into_stream()
2984 .map(SubscriberEvent::BlockHeader);
2985 Ok(stream_with_termination(
2986 stream,
2987 SubscriberStreamSource::PubSubBlockHeaders,
2988 ))
2989 }
2990
2991 #[cfg(not(feature = "reactive-ws"))]
2992 {
2993 Err(SubscriberError::Unsupported(
2994 "AlloySubscriber pubsub mode requires the reactive-ws feature",
2995 ))
2996 }
2997 }
2998
2999 async fn connect_polling_log_stream(
3000 &mut self,
3001 filter: Filter,
3002 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
3003 #[cfg(feature = "reactive-polling")]
3004 {
3005 let source = SubscriberStreamSource::PollingLog {
3006 filter: filter.clone(),
3007 };
3008 let stream = self
3009 .provider
3010 .watch_logs(&filter)
3011 .await
3012 .map_err(provider_error)?
3013 .with_channel_size(self.config.max_batch_size.max(1))
3014 .into_stream()
3015 .map(SubscriberEvent::Logs);
3016 Ok(stream_with_termination(stream, source))
3017 }
3018
3019 #[cfg(not(feature = "reactive-polling"))]
3020 {
3021 let _ = filter;
3022 Err(SubscriberError::Unsupported(
3023 "AlloySubscriber polling mode requires the reactive-polling feature",
3024 ))
3025 }
3026 }
3027
3028 async fn connect_polling_pending_hash_stream(
3029 &mut self,
3030 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
3031 #[cfg(feature = "reactive-polling")]
3032 {
3033 let stream = self
3034 .provider
3035 .watch_pending_transactions()
3036 .await
3037 .map_err(provider_error)?
3038 .with_channel_size(self.config.max_batch_size.max(1))
3039 .into_stream()
3040 .map(SubscriberEvent::PendingHashes);
3041 Ok(stream_with_termination(
3042 stream,
3043 SubscriberStreamSource::PollingPendingHashes,
3044 ))
3045 }
3046
3047 #[cfg(not(feature = "reactive-polling"))]
3048 {
3049 Err(SubscriberError::Unsupported(
3050 "AlloySubscriber polling mode requires the reactive-polling feature",
3051 ))
3052 }
3053 }
3054
3055 async fn next_event(&mut self) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
3056 loop {
3057 let event = match &mut self.state {
3058 AlloySubscriberState::Active(streams) => streams.next().await,
3059 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
3060 return Ok(None);
3061 }
3062 };
3063
3064 let Some(event) = event else {
3065 return Err(SubscriberError::Provider(
3066 "Alloy subscriber streams terminated before the subscriber was stopped"
3067 .to_owned(),
3068 ));
3069 };
3070
3071 match event {
3072 SubscriberEvent::StreamTerminated(source) => {
3073 if let Some(backfill_event) = self.reconnect_source_stream(source).await? {
3074 return Ok(Some(backfill_event));
3075 }
3076 }
3077 event => return Ok(Some(event)),
3078 }
3079 }
3080 }
3081
3082 fn enqueue_event(&mut self, event: SubscriberEvent<N>) {
3083 match event {
3084 SubscriberEvent::Log { source_id, log } => {
3085 if log_matches_any_interest(&log, &self.interests) {
3086 let record = log_input_record(log, InputSource::Subscription);
3087 self.note_log_block(source_id, &record);
3088 self.enqueue_record(record);
3089 }
3090 }
3091 SubscriberEvent::BackfilledLogs { source_id, logs } => {
3092 for log in logs {
3093 if log_matches_any_interest(&log, &self.interests) {
3094 let record = log_input_record(log, InputSource::Backfill);
3095 self.note_log_block(source_id, &record);
3096 self.enqueue_record(record);
3097 }
3098 }
3099 }
3100 SubscriberEvent::Logs(logs) => self.pending_records.extend(
3101 logs.into_iter()
3102 .filter(|log| log_matches_any_interest(log, &self.interests))
3103 .map(|log| log_input_record(log, InputSource::Poll)),
3104 ),
3105 SubscriberEvent::BlockHeader(header) => {
3106 if needs_header_block_stream(&self.interests) {
3107 let record = block_header_input_record::<N>(header);
3108 self.enqueue_record(record);
3109 }
3110 }
3111 SubscriberEvent::PendingHash(hash) => {
3112 let record = pending_hash_input_record::<N>(hash, InputSource::Subscription);
3113 self.enqueue_record(record);
3114 }
3115 SubscriberEvent::PendingHashes(hashes) => self.pending_records.extend(
3116 hashes
3117 .into_iter()
3118 .map(|hash| pending_hash_input_record::<N>(hash, InputSource::Poll)),
3119 ),
3120 SubscriberEvent::StreamTerminated(_) => {}
3121 }
3122 }
3123
3124 async fn reconnect_source_stream(
3125 &mut self,
3126 source: SubscriberStreamSource,
3127 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
3128 if !source.is_pubsub() {
3129 return Err(stream_terminated_error(&source));
3130 }
3131
3132 if !self.config.reconnect.enabled {
3133 return Err(SubscriberError::Provider(format!(
3134 "Alloy subscriber {} stream terminated and reconnect is disabled",
3135 source.label()
3136 )));
3137 }
3138
3139 let mut attempts = 0usize;
3140 let mut delay = self.config.reconnect.initial_delay;
3141 let mut retry_delay = self.config.reconnect.retry_delay;
3142
3143 loop {
3144 attempts = attempts.saturating_add(1);
3145 if !delay.is_zero() {
3146 tokio::time::sleep(delay).await;
3147 }
3148
3149 match self.reconnect_source_once(source.clone()).await {
3150 Ok(backfill_event) => return Ok(backfill_event),
3151 Err(error) if reconnect_attempts_exhausted(attempts, &self.config.reconnect) => {
3152 return Err(SubscriberError::Provider(format!(
3153 "Alloy subscriber {} stream terminated and reconnect failed after {attempts} attempt(s): {error}",
3154 source.label()
3155 )));
3156 }
3157 Err(error) => {
3158 tracing::warn!(
3159 stream = source.label(),
3160 attempts,
3161 error = %error,
3162 "Alloy subscriber reconnect attempt failed"
3163 );
3164 delay = retry_delay;
3165 retry_delay =
3166 next_reconnect_delay(retry_delay, self.config.reconnect.max_delay);
3167 }
3168 }
3169 }
3170 }
3171
3172 async fn reconnect_source_once(
3173 &mut self,
3174 source: SubscriberStreamSource,
3175 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
3176 let stream = self.connect_source_stream(source.clone()).await?;
3177 let backfill_event = self.backfill_reconnected_source(&source).await?;
3178
3179 match &mut self.state {
3180 AlloySubscriberState::Active(streams) => streams.push(stream),
3181 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
3182 return Err(SubscriberError::Provider(
3183 "Alloy subscriber state changed before reconnect completed".to_owned(),
3184 ));
3185 }
3186 }
3187
3188 Ok(backfill_event)
3189 }
3190
3191 async fn backfill_reconnected_source(
3192 &mut self,
3193 source: &SubscriberStreamSource,
3194 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
3195 let SubscriberStreamSource::PubSubLog { id, filter } = source else {
3196 return Ok(None);
3197 };
3198 let Some(from_block) = self.last_seen_log_blocks.get(id).copied() else {
3199 return Ok(None);
3200 };
3201
3202 let latest = self
3203 .provider
3204 .get_block_number()
3205 .await
3206 .map_err(provider_error)?;
3207 if latest < from_block {
3208 return Ok(None);
3209 }
3210
3211 let logs = self
3212 .provider
3213 .get_logs(&filter.clone().from_block(from_block).to_block(latest))
3214 .await
3215 .map_err(provider_error)?;
3216 Ok(Some(SubscriberEvent::BackfilledLogs {
3217 source_id: *id,
3218 logs,
3219 }))
3220 }
3221
3222 fn note_log_block(&mut self, source_id: usize, record: &ReactiveInputRecord<N>) {
3223 if let Some(block) = record.context.block.as_ref() {
3224 self.last_seen_log_blocks.insert(source_id, block.number);
3225 }
3226 }
3227
3228 fn enqueue_record(&mut self, record: ReactiveInputRecord<N>) {
3229 if self.should_skip_recent_duplicate(&record) {
3230 return;
3231 }
3232 self.remember_record(&record);
3233 self.pending_records.push_back(record);
3234 }
3235
3236 fn should_skip_recent_duplicate(&self, record: &ReactiveInputRecord<N>) -> bool {
3237 if !should_dedupe_record(record) {
3238 return false;
3239 }
3240 self.recent_input_ref_set.contains(&record.input_ref())
3241 }
3242
3243 fn remember_record(&mut self, record: &ReactiveInputRecord<N>) {
3244 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
3245 return;
3246 }
3247
3248 let input_ref = record.input_ref();
3249 if !self.recent_input_ref_set.insert(input_ref) {
3250 return;
3251 }
3252 self.recent_input_refs.push_back(input_ref);
3253
3254 while self.recent_input_refs.len() > self.config.reconnect.dedupe_window {
3255 if let Some(evicted) = self.recent_input_refs.pop_front() {
3256 self.recent_input_ref_set.remove(&evicted);
3257 }
3258 }
3259 }
3260}
3261
3262fn stream_with_termination<N, S>(
3263 stream: S,
3264 source: SubscriberStreamSource,
3265) -> BoxStream<'static, SubscriberEvent<N>>
3266where
3267 N: Network + 'static,
3268 S: futures::Stream<Item = SubscriberEvent<N>> + Send + 'static,
3269{
3270 stream
3271 .chain(stream::once(async move {
3272 SubscriberEvent::StreamTerminated(source)
3273 }))
3274 .boxed()
3275}
3276
3277fn pubsub_stream_sources<N: Network>(
3278 interests: &[ReactiveInterest<N>],
3279) -> Vec<SubscriberStreamSource> {
3280 let mut sources = Vec::new();
3281
3282 for (id, filter) in log_filters(interests).into_iter().enumerate() {
3283 sources.push(SubscriberStreamSource::PubSubLog { id, filter });
3284 }
3285
3286 if needs_pending_hash_stream(interests) {
3287 sources.push(SubscriberStreamSource::PubSubPendingHashes);
3288 }
3289
3290 if needs_header_block_stream(interests) {
3291 sources.push(SubscriberStreamSource::PubSubBlockHeaders);
3292 }
3293
3294 sources
3295}
3296
3297fn polling_stream_sources<N: Network>(
3298 interests: &[ReactiveInterest<N>],
3299) -> Vec<SubscriberStreamSource> {
3300 let mut sources = Vec::new();
3301
3302 for filter in log_filters(interests) {
3303 sources.push(SubscriberStreamSource::PollingLog { filter });
3304 }
3305
3306 if needs_pending_hash_stream(interests) {
3307 sources.push(SubscriberStreamSource::PollingPendingHashes);
3308 }
3309
3310 sources
3311}
3312
3313fn stream_terminated_error(source: &SubscriberStreamSource) -> SubscriberError {
3314 SubscriberError::Provider(format!(
3315 "Alloy subscriber {} stream terminated before the subscriber was stopped",
3316 source.label()
3317 ))
3318}
3319
3320fn reconnect_attempts_exhausted(attempts: usize, config: &SubscriberReconnectConfig) -> bool {
3321 config
3322 .max_attempts
3323 .is_some_and(|max_attempts| attempts >= max_attempts)
3324}
3325
3326fn next_reconnect_delay(current: Duration, max: Duration) -> Duration {
3327 if current.is_zero() {
3328 return current;
3329 }
3330 current.checked_mul(2).unwrap_or(max).min(max)
3331}
3332
3333fn should_dedupe_record<N: Network>(record: &ReactiveInputRecord<N>) -> bool {
3334 match &record.input {
3335 ReactiveInput::Log(log) => {
3336 is_canonical_status(&record.context.chain_status) && !log.removed
3337 }
3338 ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_) => true,
3339 ReactiveInput::FullBlock(_) | ReactiveInput::PendingTx(_) => false,
3340 }
3341}
3342
3343#[cfg(test)]
3344mod subscriber_helper_tests {
3345 use super::*;
3346 use alloy_provider::ProviderBuilder;
3347 use alloy_transport::mock::Asserter;
3348
3349 fn rpc_log(removed: bool) -> Log {
3350 Log {
3351 inner: alloy_primitives::Log::new_unchecked(
3352 Address::repeat_byte(0x42),
3353 vec![B256::repeat_byte(0x01)],
3354 Bytes::new(),
3355 ),
3356 block_hash: Some(B256::repeat_byte(0x02)),
3357 block_number: Some(7),
3358 block_timestamp: Some(1_700_000_000),
3359 transaction_hash: Some(B256::repeat_byte(0x03)),
3360 transaction_index: Some(4),
3361 log_index: Some(5),
3362 removed,
3363 }
3364 }
3365
3366 #[tokio::test(flavor = "multi_thread")]
3367 async fn stream_with_termination_yields_terminal_source_marker() {
3368 let mut stream = stream_with_termination::<Ethereum, _>(
3369 stream::iter([SubscriberEvent::<Ethereum>::PendingHash(B256::repeat_byte(
3370 0xaa,
3371 ))]),
3372 SubscriberStreamSource::PubSubPendingHashes,
3373 );
3374
3375 assert!(matches!(
3376 stream.next().await,
3377 Some(SubscriberEvent::PendingHash(hash)) if hash == B256::repeat_byte(0xaa)
3378 ));
3379 assert!(matches!(
3380 stream.next().await,
3381 Some(SubscriberEvent::StreamTerminated(source)) if source.is_pubsub()
3382 ));
3383 assert!(stream.next().await.is_none());
3384 }
3385
3386 #[test]
3387 fn reconnect_delay_doubles_until_capped() {
3388 assert_eq!(
3389 next_reconnect_delay(Duration::from_millis(250), Duration::from_secs(1)),
3390 Duration::from_millis(500)
3391 );
3392 assert_eq!(
3393 next_reconnect_delay(Duration::from_millis(750), Duration::from_secs(1)),
3394 Duration::from_secs(1)
3395 );
3396 assert_eq!(
3397 next_reconnect_delay(Duration::ZERO, Duration::from_secs(1)),
3398 Duration::ZERO
3399 );
3400 }
3401
3402 #[test]
3403 fn canonical_logs_are_deduped_but_removed_logs_are_not() {
3404 let included = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
3405 let removed = log_input_record::<Ethereum>(rpc_log(true), InputSource::Subscription);
3406
3407 assert!(should_dedupe_record(&included));
3408 assert!(!should_dedupe_record(&removed));
3409 }
3410
3411 #[test]
3412 fn pubsub_sources_assign_stable_log_ids_before_shared_streams() {
3413 let sources = pubsub_stream_sources::<Ethereum>(&[
3414 ReactiveInterest::Logs(LogInterest {
3415 provider_filter: Filter::new().address(Address::repeat_byte(0x01)),
3416 local_matcher: None,
3417 route_key: None,
3418 }),
3419 ReactiveInterest::Logs(LogInterest {
3420 provider_filter: Filter::new().address(Address::repeat_byte(0x02)),
3421 local_matcher: None,
3422 route_key: None,
3423 }),
3424 ReactiveInterest::PendingTransactions(PendingTxInterest::default()),
3425 ]);
3426
3427 assert!(matches!(
3428 &sources[0],
3429 SubscriberStreamSource::PubSubLog { id: 0, .. }
3430 ));
3431 assert!(matches!(
3432 sources[1],
3433 SubscriberStreamSource::PubSubPendingHashes
3434 ));
3435 }
3436
3437 #[tokio::test(flavor = "multi_thread")]
3438 #[cfg(feature = "reactive-ws")]
3439 async fn pubsub_stream_termination_attempts_reconnect_before_error() {
3440 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
3441 let mut subscriber = AlloySubscriber::new(
3442 provider,
3443 SubscriberMode::PubSub,
3444 SubscriberConfig {
3445 reconnect: SubscriberReconnectConfig {
3446 initial_delay: Duration::ZERO,
3447 retry_delay: Duration::ZERO,
3448 max_delay: Duration::ZERO,
3449 max_attempts: Some(1),
3450 ..SubscriberReconnectConfig::default()
3451 },
3452 ..SubscriberConfig::default()
3453 },
3454 );
3455 subscriber.interests = vec![ReactiveInterest::PendingTransactions(
3456 PendingTxInterest::default(),
3457 )];
3458
3459 let mut streams = SubscriberStreams::new();
3460 streams.push(
3461 stream::once(async {
3462 SubscriberEvent::<Ethereum>::StreamTerminated(
3463 SubscriberStreamSource::PubSubPendingHashes,
3464 )
3465 })
3466 .boxed(),
3467 );
3468 subscriber.state = AlloySubscriberState::Active(streams);
3469
3470 let result = subscriber.next_batch().await;
3471 assert!(
3472 matches!(result, Err(SubscriberError::Provider(ref message)) if message.contains("reconnect failed after 1 attempt")),
3473 "terminated pubsub streams should attempt reconnect before surfacing failure: {result:?}"
3474 );
3475 }
3476
3477 #[test]
3478 fn backfilled_logs_skip_recent_subscription_duplicates() {
3479 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
3480 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
3481 provider,
3482 SubscriberMode::PubSub,
3483 SubscriberConfig::default(),
3484 );
3485 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
3486 provider_filter: Filter::new()
3487 .address(Address::repeat_byte(0x42))
3488 .event_signature(B256::repeat_byte(0x01)),
3489 local_matcher: None,
3490 route_key: None,
3491 })];
3492
3493 let log = rpc_log(false);
3494 subscriber.enqueue_event(SubscriberEvent::Log {
3495 source_id: 0,
3496 log: log.clone(),
3497 });
3498 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
3499 source_id: 0,
3500 logs: vec![log],
3501 });
3502
3503 assert_eq!(subscriber.pending_records.len(), 1);
3504 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
3505 assert_eq!(
3506 subscriber.pending_records[0].context.source,
3507 InputSource::Subscription
3508 );
3509 }
3510
3511 #[test]
3512 fn backfilled_logs_surface_with_backfill_source() {
3513 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
3518 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
3519 provider,
3520 SubscriberMode::PubSub,
3521 SubscriberConfig::default(),
3522 );
3523 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
3524 provider_filter: Filter::new()
3525 .address(Address::repeat_byte(0x42))
3526 .event_signature(B256::repeat_byte(0x01)),
3527 local_matcher: None,
3528 route_key: None,
3529 })];
3530
3531 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
3532 source_id: 0,
3533 logs: vec![rpc_log(false)],
3534 });
3535
3536 assert_eq!(subscriber.pending_records.len(), 1);
3537 assert_eq!(
3538 subscriber.pending_records[0].context.source,
3539 InputSource::Backfill
3540 );
3541 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
3542 }
3543}
3544
3545fn resolve_subscriber_transport(
3546 mode: SubscriberMode,
3547) -> Result<SubscriberTransport, SubscriberError> {
3548 match mode {
3549 SubscriberMode::PubSub => {
3550 #[cfg(feature = "reactive-ws")]
3551 {
3552 Ok(SubscriberTransport::PubSub)
3553 }
3554 #[cfg(not(feature = "reactive-ws"))]
3555 {
3556 Err(SubscriberError::Unsupported(
3557 "AlloySubscriber pubsub mode requires the reactive-ws feature",
3558 ))
3559 }
3560 }
3561 SubscriberMode::Polling => {
3562 #[cfg(feature = "reactive-polling")]
3563 {
3564 Ok(SubscriberTransport::Polling)
3565 }
3566 #[cfg(not(feature = "reactive-polling"))]
3567 {
3568 Err(SubscriberError::Unsupported(
3569 "AlloySubscriber polling mode requires the reactive-polling feature",
3570 ))
3571 }
3572 }
3573 SubscriberMode::Auto => resolve_auto_subscriber_transport(),
3574 }
3575}
3576
3577fn resolve_auto_subscriber_transport() -> Result<SubscriberTransport, SubscriberError> {
3578 #[cfg(feature = "reactive-ws")]
3579 {
3580 Ok(SubscriberTransport::PubSub)
3581 }
3582
3583 #[cfg(all(not(feature = "reactive-ws"), feature = "reactive-polling"))]
3584 {
3585 Ok(SubscriberTransport::Polling)
3586 }
3587
3588 #[cfg(not(any(feature = "reactive-ws", feature = "reactive-polling")))]
3589 {
3590 Err(SubscriberError::Unsupported(
3591 "AlloySubscriber requires either reactive-ws or reactive-polling",
3592 ))
3593 }
3594}
3595
3596fn validate_subscriber_config(config: &SubscriberConfig) -> Result<(), SubscriberError> {
3597 if config.max_batch_size == 0 {
3598 return Err(SubscriberError::InvalidConfig(
3599 "SubscriberConfig::max_batch_size must be greater than zero",
3600 ));
3601 }
3602 if config.reconnect.enabled {
3603 if config.reconnect.retry_delay > config.reconnect.max_delay {
3604 return Err(SubscriberError::InvalidConfig(
3605 "SubscriberReconnectConfig::retry_delay must be less than or equal to max_delay",
3606 ));
3607 }
3608 if matches!(config.reconnect.max_attempts, Some(0)) {
3609 return Err(SubscriberError::InvalidConfig(
3610 "SubscriberReconnectConfig::max_attempts must be greater than zero when set",
3611 ));
3612 }
3613 }
3614 Ok(())
3615}
3616
3617fn validate_supported_interests<N: Network>(
3618 mode: SubscriberMode,
3619 config: &SubscriberConfig,
3620 interests: &[ReactiveInterest<N>],
3621) -> Result<(), SubscriberError> {
3622 let transport = resolve_subscriber_transport(mode)?;
3623
3624 for interest in interests {
3625 match interest {
3626 ReactiveInterest::Logs(_) => {}
3627 ReactiveInterest::PendingTransactions(interest)
3628 if !config.hydrate_pending_transactions && interest.matches_hash_only() => {}
3629 ReactiveInterest::PendingTransactions(_) => {
3630 return Err(SubscriberError::Unsupported(
3631 "AlloySubscriber currently supports pending transaction hash interests only (full pending-tx hydration is unimplemented)",
3632 ));
3633 }
3634 ReactiveInterest::Blocks(interest) => match (transport, interest.mode) {
3635 (SubscriberTransport::PubSub, BlockInterestMode::Header) => {}
3636 (_, BlockInterestMode::FullBlock) => {
3637 return Err(SubscriberError::Unsupported(
3638 "AlloySubscriber full block streams are not implemented in this transport slice",
3639 ));
3640 }
3641 (SubscriberTransport::Polling, BlockInterestMode::Header) => {
3642 return Err(SubscriberError::Unsupported(
3643 "AlloySubscriber polling block streams are not implemented in this transport slice",
3644 ));
3645 }
3646 },
3647 }
3648 }
3649
3650 Ok(())
3651}
3652
3653fn log_filters<N: Network>(interests: &[ReactiveInterest<N>]) -> Vec<Filter> {
3654 let mut filters = Vec::new();
3655 for interest in interests {
3656 if let ReactiveInterest::Logs(interest) = interest {
3657 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
3658 }
3659 }
3660 filters
3661}
3662
3663fn needs_header_block_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
3664 interests.iter().any(|interest| {
3665 matches!(
3666 interest,
3667 ReactiveInterest::Blocks(BlockInterest {
3668 mode: BlockInterestMode::Header,
3669 })
3670 )
3671 })
3672}
3673
3674fn needs_pending_hash_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
3675 interests.iter().any(|interest| {
3676 matches!(
3677 interest,
3678 ReactiveInterest::PendingTransactions(interest) if interest.matches_hash_only()
3679 )
3680 })
3681}
3682
3683fn log_matches_any_interest<N: Network>(log: &Log, interests: &[ReactiveInterest<N>]) -> bool {
3684 interests.iter().any(|interest| {
3685 matches!(
3686 interest,
3687 ReactiveInterest::Logs(interest) if interest.matches(log)
3688 )
3689 })
3690}
3691
3692fn log_input_record<N: Network>(log: Log, source: InputSource) -> ReactiveInputRecord<N> {
3693 let context = log_reactive_context(&log);
3694 ReactiveInputRecord::new(
3695 ReactiveInput::Log(log),
3696 ReactiveContext { source, ..context },
3697 )
3698}
3699
3700fn log_reactive_context(log: &Log) -> ReactiveContext {
3701 let block = match (log.block_hash, log.block_number) {
3702 (Some(hash), Some(number)) => Some(BlockRef {
3703 number,
3704 hash,
3705 parent_hash: None,
3706 timestamp: log.block_timestamp,
3707 }),
3708 _ => None,
3709 };
3710
3711 let chain_status = match (&block, log.removed) {
3712 (Some(block), true) => ChainStatus::Reorged {
3713 dropped_from: block.clone(),
3714 },
3715 (Some(block), false) => ChainStatus::Included {
3716 block: block.clone(),
3717 confirmations: 0,
3718 },
3719 (None, _) => ChainStatus::Pending,
3720 };
3721
3722 ReactiveContext {
3723 chain_id: None,
3724 source: InputSource::Poll,
3725 chain_status,
3726 block,
3727 transaction_index: log.transaction_index,
3728 log_index: log.log_index,
3729 }
3730}
3731
3732fn block_header_input_record<N>(header: N::HeaderResponse) -> ReactiveInputRecord<N>
3733where
3734 N: Network,
3735{
3736 let block = BlockRef {
3737 number: header.number(),
3738 hash: HeaderResponseTrait::hash(&header),
3739 parent_hash: Some(header.parent_hash()),
3740 timestamp: Some(header.timestamp()),
3741 };
3742 ReactiveInputRecord::new(
3743 ReactiveInput::BlockHeader(header),
3744 ReactiveContext {
3745 chain_id: None,
3746 source: InputSource::Subscription,
3747 chain_status: ChainStatus::Included {
3748 block: block.clone(),
3749 confirmations: 0,
3750 },
3751 block: Some(block),
3752 transaction_index: None,
3753 log_index: None,
3754 },
3755 )
3756}
3757
3758fn pending_hash_input_record<N: Network>(
3759 hash: B256,
3760 source: InputSource,
3761) -> ReactiveInputRecord<N> {
3762 ReactiveInputRecord::new(
3763 ReactiveInput::PendingTxHash(hash),
3764 ReactiveContext {
3765 chain_id: None,
3766 source,
3767 chain_status: ChainStatus::Pending,
3768 block: None,
3769 transaction_index: None,
3770 log_index: None,
3771 },
3772 )
3773}
3774
3775fn provider_error(error: impl fmt::Display) -> SubscriberError {
3776 SubscriberError::Provider(error.to_string())
3777}
3778
3779#[derive(Debug, thiserror::Error)]
3781pub enum SubscriberError {
3782 #[error("{0}")]
3784 InvalidConfig(&'static str),
3785 #[error("{0}")]
3787 Unsupported(&'static str),
3788 #[error("provider error: {0}")]
3790 Provider(String),
3791}