1use std::{
15 any::Any,
16 borrow::Cow,
17 collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
18 fmt,
19 future::Future,
20 hash::Hash,
21 marker::PhantomData,
22 num::NonZeroU64,
23 path::PathBuf,
24 pin::Pin,
25 sync::{
26 Arc,
27 atomic::{AtomicU64, Ordering},
28 },
29 time::Duration,
30};
31
32use alloy_consensus::{BlockHeader as _, Transaction as _};
33use alloy_eips::{BlockId, BlockNumberOrTag};
34use alloy_network::{
35 Ethereum, Network,
36 primitives::{
37 BlockResponse as _, HeaderResponse as HeaderResponseTrait,
38 TransactionResponse as TransactionResponseTrait,
39 },
40};
41use alloy_primitives::{Address, B256, Bytes, FixedBytes, Keccak256, U256};
42use alloy_provider::Provider;
43use alloy_rpc_types_eth::{Filter, FilterSet, Log};
44pub use alloy_transport_balancer::EndpointId;
45use bincode::Options;
46use futures::{StreamExt, stream};
47use futures::{
48 future::{Either, poll_fn, select},
49 stream::BoxStream,
50};
51
52use crate::{
53 cache::{
54 AccountProof, BlockStateDiff, DurableCheckpointBlock, DurableCheckpointError,
55 DurableCheckpointIdentity, DurableCheckpointMetadata, DurableCheckpointStore, EvmCache,
56 EvmCacheStateSnapshot, LoadedDurableCheckpoint,
57 },
58 errors::{BlockContextError, StorageFetchResult},
59 events::{EventDecoder, StateView},
60 freshness::FreshnessRegistry,
61 state_update::{AccountPatch, PurgeScope, StateDiff, StateUpdate},
62};
63
64#[derive(Clone, Debug, PartialEq, Eq)]
66pub enum ReactiveInput<N: Network = Ethereum> {
67 Log(Log),
69 BlockHeader(N::HeaderResponse),
71 FullBlock(N::BlockResponse),
73 PendingTxHash(B256),
75 PendingTx(N::TransactionResponse),
77}
78
79#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
81pub struct ReactiveContext {
82 pub chain_id: Option<u64>,
84 pub source: InputSource,
86 pub chain_status: ChainStatus,
88 pub block: Option<BlockRef>,
90 pub transaction_index: Option<u64>,
92 pub log_index: Option<u64>,
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
98pub struct BlockRef {
99 pub number: u64,
101 pub hash: B256,
103 pub parent_hash: Option<B256>,
105 pub timestamp: Option<u64>,
107}
108
109#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
116pub struct ProviderRef {
117 pub endpoint: EndpointId,
119 pub generation: u64,
121}
122
123impl ProviderRef {
124 pub fn new(endpoint: impl Into<EndpointId>, generation: u64) -> Self {
126 Self {
127 endpoint: endpoint.into(),
128 generation,
129 }
130 }
131}
132
133#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
135pub struct FlashblockRef {
136 pub provider: ProviderRef,
138 pub payload_id: Option<FixedBytes<8>>,
143 pub index: Option<u64>,
145 pub block_number: u64,
147 pub block_hash: B256,
149 pub parent_hash: Option<B256>,
151 pub state_root: Option<B256>,
153 pub timestamp: Option<u64>,
155}
156
157impl FlashblockRef {
158 pub const fn block_ref(&self) -> BlockRef {
162 BlockRef {
163 number: self.block_number,
164 hash: self.block_hash,
165 parent_hash: self.parent_hash,
166 timestamp: self.timestamp,
167 }
168 }
169
170 fn same_payload(&self, other: &Self) -> bool {
171 self.provider == other.provider
172 && match (self.payload_id, other.payload_id) {
173 (Some(left), Some(right)) => left == right,
174 _ => {
175 self.block_number == other.block_number && self.parent_hash == other.parent_hash
176 }
177 }
178 }
179}
180
181#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
183pub enum PreconfirmationMode {
184 #[default]
186 Disabled,
187 Preferred,
190 Required,
192}
193
194#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
196pub struct BaseFlashblockPayload {
197 pub payload_id: FixedBytes<8>,
199 pub index: u64,
201 pub base: Option<BaseFlashblockBase>,
203 pub diff: BaseFlashblockDiff,
205 #[serde(default)]
207 pub metadata: Option<BaseFlashblockMetadata>,
208}
209
210#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
212pub struct BaseFlashblockBase {
213 pub parent_hash: B256,
215 #[serde(deserialize_with = "deserialize_rpc_u64")]
217 pub block_number: u64,
218 #[serde(deserialize_with = "deserialize_rpc_u64")]
220 pub timestamp: u64,
221}
222
223#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
225pub struct BaseFlashblockDiff {
226 pub state_root: B256,
228 pub block_hash: B256,
230}
231
232#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
235pub struct BaseFlashblockMetadata {
236 #[serde(deserialize_with = "deserialize_rpc_u64")]
238 pub block_number: u64,
239}
240
241#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
244#[serde(rename_all = "camelCase")]
245struct BaseFlashblockBlockPayload {
246 hash: B256,
247 #[serde(deserialize_with = "deserialize_rpc_u64")]
248 number: u64,
249 parent_hash: B256,
250 state_root: B256,
251 #[serde(deserialize_with = "deserialize_rpc_u64")]
252 timestamp: u64,
253}
254
255#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
259#[serde(untagged)]
260enum BaseFlashblockWirePayload {
261 Indexed(BaseFlashblockPayload),
262 Block(BaseFlashblockBlockPayload),
263}
264
265fn deserialize_rpc_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
266where
267 D: serde::Deserializer<'de>,
268{
269 #[derive(serde::Deserialize)]
270 #[serde(untagged)]
271 enum RpcU64 {
272 Number(u64),
273 String(String),
274 }
275
276 match <RpcU64 as serde::Deserialize>::deserialize(deserializer)? {
277 RpcU64::Number(number) => Ok(number),
278 RpcU64::String(value) => {
279 let value = value.strip_prefix("0x").unwrap_or(&value);
280 u64::from_str_radix(value, 16).map_err(serde::de::Error::custom)
281 }
282 }
283}
284
285#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
288pub struct ReactiveCanonicalBaseline {
289 pub chain_id: u64,
291 pub block: BlockRef,
293}
294
295impl ReactiveCanonicalBaseline {
296 pub const fn new(chain_id: u64, block: BlockRef) -> Self {
298 Self { chain_id, block }
299 }
300}
301
302#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
311#[non_exhaustive]
312pub enum ChainControl {
313 Reorg {
315 common_ancestor: BlockRef,
317 old_tip: BlockRef,
319 new_tip: BlockRef,
321 },
322 Safe(BlockRef),
324 Finalized(BlockRef),
326 CanonicalProgress(BlockRef),
333 Barrier {
335 id: Vec<u8>,
337 block: Option<BlockRef>,
339 },
340}
341
342#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
361pub struct CanonicalSequenceState {
362 retained_canonical_history: Vec<BlockRef>,
363 coverage_head: Option<BlockRef>,
364 safe_head: Option<BlockRef>,
365 finalized_head: Option<BlockRef>,
366}
367
368impl CanonicalSequenceState {
369 pub fn new(
375 retained_canonical_history: Vec<BlockRef>,
376 coverage_head: Option<BlockRef>,
377 safe_head: Option<BlockRef>,
378 finalized_head: Option<BlockRef>,
379 ) -> Self {
380 Self {
381 retained_canonical_history,
382 coverage_head,
383 safe_head,
384 finalized_head,
385 }
386 }
387
388 pub fn retained_canonical_history(&self) -> &[BlockRef] {
390 &self.retained_canonical_history
391 }
392
393 pub const fn coverage_head(&self) -> Option<&BlockRef> {
395 self.coverage_head.as_ref()
396 }
397
398 pub const fn safe_head(&self) -> Option<&BlockRef> {
400 self.safe_head.as_ref()
401 }
402
403 pub const fn finalized_head(&self) -> Option<&BlockRef> {
405 self.finalized_head.as_ref()
406 }
407
408 pub fn retain_recent_history(&mut self, max_entries: usize) {
417 let remove = self
418 .retained_canonical_history
419 .len()
420 .saturating_sub(max_entries);
421 self.retained_canonical_history.drain(..remove);
422 }
423
424 pub fn validate(&self) -> Result<(), ReactiveError> {
438 validate_canonical_sequence_snapshot(self)
439 }
440}
441
442#[derive(Clone, Debug, PartialEq, Eq)]
444#[non_exhaustive]
445pub enum CanonicalSequenceMutation {
446 Rewind {
448 common_ancestor: Option<BlockRef>,
454 dropped: Vec<BlockRef>,
456 },
457 Canonical(BlockRef),
459 Safe(BlockRef),
461 Finalized(BlockRef),
463}
464
465#[derive(Clone, Debug, PartialEq, Eq)]
467pub struct CanonicalSequenceValidation {
468 pre_record_state: CanonicalSequenceState,
469 next_state: CanonicalSequenceState,
470 mutations: Vec<CanonicalSequenceMutation>,
471 normalized_chain_controls: Vec<ChainControl>,
472}
473
474impl CanonicalSequenceValidation {
475 pub const fn pre_record_state(&self) -> &CanonicalSequenceState {
477 &self.pre_record_state
478 }
479
480 pub const fn next_state(&self) -> &CanonicalSequenceState {
482 &self.next_state
483 }
484
485 pub fn mutations(&self) -> &[CanonicalSequenceMutation] {
487 &self.mutations
488 }
489
490 pub fn normalized_chain_controls(&self) -> &[ChainControl] {
500 &self.normalized_chain_controls
501 }
502}
503
504#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
506#[non_exhaustive]
507pub enum ChainStatus {
508 Pending,
510 Preconfirmed {
515 flashblock: FlashblockRef,
517 },
518 Included {
520 block: BlockRef,
522 confirmations: u64,
524 },
525 Safe {
527 block: BlockRef,
529 },
530 Finalized {
532 block: BlockRef,
534 },
535 Reorged {
537 dropped_from: BlockRef,
539 },
540}
541
542#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
544#[non_exhaustive]
545pub enum InputSource {
546 Batch,
548 Subscription,
550 Poll,
552 Backfill,
554 Flashblocks,
556 Synthetic,
558}
559
560#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
562pub enum InputRef {
563 Log {
565 chain_id: Option<u64>,
567 block_hash: B256,
569 transaction_hash: B256,
571 log_index: u64,
573 },
574 PendingTx {
576 chain_id: Option<u64>,
578 hash: B256,
580 },
581 Block {
583 chain_id: Option<u64>,
585 hash: B256,
587 number: u64,
589 },
590}
591
592#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
599#[non_exhaustive]
600pub enum ReactiveInputKind {
601 CanonicalLog,
603 ReorgSignalLog,
605 BlockHeader,
607 FullBlock,
609 PendingTxHash,
611 PendingTx,
613}
614
615#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
622pub struct ReactiveInputIdentity {
623 input_ref: InputRef,
624 kind: ReactiveInputKind,
625}
626
627impl ReactiveInputIdentity {
628 pub fn try_from_parts(
642 input_ref: InputRef,
643 kind: ReactiveInputKind,
644 ) -> Result<Self, ReactiveInputIdentityError> {
645 let compatible = matches!(
646 (input_ref, kind),
647 (
648 InputRef::Log { .. },
649 ReactiveInputKind::CanonicalLog | ReactiveInputKind::ReorgSignalLog
650 ) | (
651 InputRef::Block { .. },
652 ReactiveInputKind::BlockHeader | ReactiveInputKind::FullBlock
653 ) | (
654 InputRef::PendingTx { .. },
655 ReactiveInputKind::PendingTxHash | ReactiveInputKind::PendingTx
656 )
657 );
658 if !compatible {
659 return Err(ReactiveInputIdentityError { input_ref, kind });
660 }
661 Ok(Self { input_ref, kind })
662 }
663
664 pub const fn input_ref(&self) -> InputRef {
666 self.input_ref
667 }
668
669 pub const fn kind(&self) -> ReactiveInputKind {
671 self.kind
672 }
673}
674
675#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
678#[error("reactive input kind {kind:?} is incompatible with input reference {input_ref:?}")]
679pub struct ReactiveInputIdentityError {
680 input_ref: InputRef,
681 kind: ReactiveInputKind,
682}
683
684impl ReactiveInputIdentityError {
685 pub const fn input_ref(&self) -> InputRef {
687 self.input_ref
688 }
689
690 pub const fn kind(&self) -> ReactiveInputKind {
692 self.kind
693 }
694}
695
696#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
698pub enum StateEffectQuality {
699 ExactFromInput,
701 AppliedWithPendingResync,
703 ResyncedAuthoritatively,
705 RequiresRepair,
707 NoStateEffect,
709}
710
711#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize)]
713pub struct HandlerId(String);
714
715impl HandlerId {
716 pub fn new(id: impl Into<String>) -> Self {
723 Self::try_new(id).expect("handler id must not be empty")
724 }
725
726 pub fn try_new(id: impl Into<String>) -> Result<Self, HandlerIdError> {
733 let id = id.into();
734 if id.is_empty() {
735 return Err(HandlerIdError);
736 }
737 Ok(Self(id))
738 }
739
740 pub fn as_str(&self) -> &str {
742 &self.0
743 }
744}
745
746impl<'de> serde::Deserialize<'de> for HandlerId {
747 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
748 where
749 D: serde::Deserializer<'de>,
750 {
751 let id = <String as serde::Deserialize>::deserialize(deserializer)?;
752 Self::try_new(id).map_err(serde::de::Error::custom)
753 }
754}
755
756#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
759#[error("handler id must not be empty")]
760pub struct HandlerIdError;
761
762impl fmt::Display for HandlerId {
763 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
764 self.0.fmt(f)
765 }
766}
767
768#[derive(Clone, Debug, PartialEq, Eq, Hash)]
770pub struct ReportTag {
771 pub key: String,
773 pub value: String,
775}
776
777impl ReportTag {
778 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
780 Self {
781 key: key.into(),
782 value: value.into(),
783 }
784 }
785}
786
787#[derive(Clone)]
789pub struct HookSignal {
790 pub namespace: Cow<'static, str>,
792 pub kind: Cow<'static, str>,
794 pub labels: Vec<ReportTag>,
796 pub payload: Option<Arc<dyn Any + Send + Sync>>,
798}
799
800impl fmt::Debug for HookSignal {
801 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
802 f.debug_struct("HookSignal")
803 .field("namespace", &self.namespace)
804 .field("kind", &self.kind)
805 .field("labels", &self.labels)
806 .field("payload", &self.payload.as_ref().map(|_| "<payload>"))
807 .finish()
808 }
809}
810
811#[derive(Clone, Debug)]
813pub enum ReactiveEffect {
814 StateUpdate(StateUpdate),
816 Resync(ResyncRequest),
818 Invalidate(InvalidationRequest),
820 Hook(HookSignal),
822 Speculative(SpeculativeRequest),
824}
825
826#[derive(Clone, Debug)]
828pub struct HandlerOutcome {
829 pub effects: Vec<ReactiveEffect>,
831 pub quality: StateEffectQuality,
833 pub tags: Vec<ReportTag>,
835}
836
837impl HandlerOutcome {
838 pub fn empty(quality: StateEffectQuality) -> Self {
840 Self {
841 effects: Vec::new(),
842 quality,
843 tags: Vec::new(),
844 }
845 }
846}
847
848#[derive(Clone, Debug)]
850pub struct ReactiveInputRecord<N: Network = Ethereum> {
851 pub input: ReactiveInput<N>,
853 pub context: ReactiveContext,
855 pub provider: Option<ProviderRef>,
858}
859
860impl<N: Network> ReactiveInputRecord<N> {
861 pub fn new(input: ReactiveInput<N>, context: ReactiveContext) -> Self {
863 Self {
864 input,
865 context,
866 provider: None,
867 }
868 }
869
870 #[must_use]
872 pub fn with_provider(mut self, provider: ProviderRef) -> Self {
873 self.provider = Some(provider);
874 self
875 }
876
877 pub fn input_ref(&self) -> InputRef {
879 input_ref(&self.input, &self.context)
880 }
881
882 pub fn validated_identity(&self) -> Result<ReactiveInputIdentity, ReactiveError> {
899 validate_input_record(self)?;
900 let kind = match &self.input {
901 ReactiveInput::Log(log)
902 if log.removed
903 || matches!(self.context.chain_status, ChainStatus::Reorged { .. }) =>
904 {
905 ReactiveInputKind::ReorgSignalLog
906 }
907 ReactiveInput::Log(_) => ReactiveInputKind::CanonicalLog,
908 ReactiveInput::BlockHeader(_) => ReactiveInputKind::BlockHeader,
909 ReactiveInput::FullBlock(_) => ReactiveInputKind::FullBlock,
910 ReactiveInput::PendingTxHash(_) => ReactiveInputKind::PendingTxHash,
911 ReactiveInput::PendingTx(_) => ReactiveInputKind::PendingTx,
912 };
913 ReactiveInputIdentity::try_from_parts(self.input_ref(), kind).map_err(|error| {
914 ReactiveError::InvalidInputRecord {
915 message: error.to_string(),
916 }
917 })
918 }
919
920 pub fn same_deduplicable_payload(&self, other: &Self) -> bool {
934 match (&self.input, &other.input) {
935 (ReactiveInput::Log(left), ReactiveInput::Log(right)) => {
936 left.inner == right.inner
937 && left.block_hash == right.block_hash
938 && left.block_number == right.block_number
939 && optional_metadata_compatible(
940 left.block_timestamp.as_ref(),
941 right.block_timestamp.as_ref(),
942 )
943 && left.transaction_hash == right.transaction_hash
944 && left.transaction_index == right.transaction_index
945 && left.log_index == right.log_index
946 && left.removed == right.removed
947 }
948 (ReactiveInput::BlockHeader(left), ReactiveInput::BlockHeader(right)) => {
949 left.hash() == right.hash()
950 }
951 (ReactiveInput::FullBlock(_), ReactiveInput::FullBlock(_)) => false,
952 (ReactiveInput::PendingTxHash(left), ReactiveInput::PendingTxHash(right)) => {
953 left == right
954 }
955 (ReactiveInput::PendingTx(_), ReactiveInput::PendingTx(_)) => false,
956 _ => false,
957 }
958 }
959
960 pub fn is_payload_deduplicable(&self) -> bool {
966 matches!(
967 &self.input,
968 ReactiveInput::Log(_) | ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_)
969 )
970 }
971
972 pub fn merge_compatible_duplicate(&mut self, other: &Self) -> Result<bool, ReactiveError> {
987 let identity = self.validated_identity()?;
988 let other_identity = other.validated_identity()?;
989 if identity != other_identity
990 || !self.is_payload_deduplicable()
991 || !other.is_payload_deduplicable()
992 {
993 return Ok(false);
994 }
995 if !self.same_deduplicable_payload(other) || !self.dedupe_context_is_compatible(other) {
996 return Err(ReactiveError::InvalidInputRecord {
997 message: format!(
998 "conflicting payload or semantic context for identity {:?}",
999 identity
1000 ),
1001 });
1002 }
1003 let mut merged = self.clone();
1004 merge_deduplicable_record(&mut merged, other);
1005 merged.validated_identity()?;
1006 *self = merged;
1007 Ok(true)
1008 }
1009
1010 pub fn dedupe_context_is_compatible(&self, other: &Self) -> bool {
1018 let left = &self.context;
1019 let right = &other.context;
1020 left.chain_id == right.chain_id
1021 && optional_block_refs_are_compatible(left.block.as_ref(), right.block.as_ref())
1022 && left.transaction_index == right.transaction_index
1023 && left.log_index == right.log_index
1024 && chain_statuses_are_dedupe_compatible(&left.chain_status, &right.chain_status)
1025 }
1026}
1027
1028fn chain_statuses_are_dedupe_compatible(left: &ChainStatus, right: &ChainStatus) -> bool {
1029 match (left, right) {
1030 (ChainStatus::Pending, ChainStatus::Pending)
1031 | (ChainStatus::Reorged { .. }, ChainStatus::Reorged { .. }) => true,
1032 (
1033 ChainStatus::Preconfirmed { flashblock: left },
1034 ChainStatus::Preconfirmed { flashblock: right },
1035 ) => left == right,
1036 (
1037 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. },
1038 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. },
1039 ) => true,
1040 _ => false,
1041 }
1042}
1043
1044fn optional_metadata_compatible<T: PartialEq>(left: Option<&T>, right: Option<&T>) -> bool {
1045 left.zip(right).is_none_or(|(left, right)| left == right)
1046}
1047
1048fn optional_block_refs_are_compatible(left: Option<&BlockRef>, right: Option<&BlockRef>) -> bool {
1049 match (left, right) {
1050 (None, None) => true,
1051 (Some(left), Some(right)) => {
1052 left.number == right.number
1053 && left.hash == right.hash
1054 && optional_metadata_compatible(
1055 left.parent_hash.as_ref(),
1056 right.parent_hash.as_ref(),
1057 )
1058 && optional_metadata_compatible(left.timestamp.as_ref(), right.timestamp.as_ref())
1059 }
1060 _ => false,
1061 }
1062}
1063
1064fn merge_deduplicable_record<N: Network>(
1065 retained: &mut ReactiveInputRecord<N>,
1066 incoming: &ReactiveInputRecord<N>,
1067) {
1068 if let (ReactiveInput::Log(retained), ReactiveInput::Log(incoming)) =
1069 (&mut retained.input, &incoming.input)
1070 && retained.block_timestamp.is_none()
1071 {
1072 retained.block_timestamp = incoming.block_timestamp;
1073 }
1074 if let (Some(retained), Some(incoming)) =
1075 (&mut retained.context.block, incoming.context.block.as_ref())
1076 {
1077 enrich_block_ref(retained, incoming);
1078 }
1079 retained.context.chain_status = merged_chain_status(
1080 &retained.context.chain_status,
1081 &incoming.context.chain_status,
1082 );
1083 if input_source_rank(incoming.context.source) > input_source_rank(retained.context.source) {
1084 retained.context.source = incoming.context.source;
1085 }
1086 if retained.provider.is_none() {
1087 retained.provider = incoming.provider.clone();
1088 }
1089}
1090
1091fn enrich_block_ref(retained: &mut BlockRef, incoming: &BlockRef) {
1092 if retained.parent_hash.is_none() {
1093 retained.parent_hash = incoming.parent_hash;
1094 }
1095 if retained.timestamp.is_none() {
1096 retained.timestamp = incoming.timestamp;
1097 }
1098}
1099
1100fn merged_chain_status(retained: &ChainStatus, incoming: &ChainStatus) -> ChainStatus {
1101 let merged_block = |left: &BlockRef, right: &BlockRef| {
1102 let mut block = *left;
1103 enrich_block_ref(&mut block, right);
1104 block
1105 };
1106 match (retained, incoming) {
1107 (ChainStatus::Pending, ChainStatus::Pending) => ChainStatus::Pending,
1108 (
1109 ChainStatus::Preconfirmed { flashblock: left },
1110 ChainStatus::Preconfirmed { flashblock: right },
1111 ) => {
1112 debug_assert_eq!(left, right, "compatible pre-confirmed records agree");
1113 ChainStatus::Preconfirmed {
1114 flashblock: left.clone(),
1115 }
1116 }
1117 (
1118 ChainStatus::Reorged { dropped_from: left },
1119 ChainStatus::Reorged {
1120 dropped_from: right,
1121 },
1122 ) => ChainStatus::Reorged {
1123 dropped_from: merged_block(left, right),
1124 },
1125 (left, right) => {
1126 let (left_block, left_rank, left_confirmations) = canonical_status_parts(left)
1127 .expect("compatible duplicate has a canonical lifecycle");
1128 let (right_block, right_rank, right_confirmations) = canonical_status_parts(right)
1129 .expect("compatible duplicate has a canonical lifecycle");
1130 let block = merged_block(left_block, right_block);
1131 let rank = left_rank.max(right_rank);
1132 match rank {
1133 3 => ChainStatus::Finalized { block },
1134 2 => ChainStatus::Safe { block },
1135 _ => ChainStatus::Included {
1136 block,
1137 confirmations: left_confirmations.max(right_confirmations),
1138 },
1139 }
1140 }
1141 }
1142}
1143
1144fn canonical_status_parts(status: &ChainStatus) -> Option<(&BlockRef, u8, u64)> {
1145 match status {
1146 ChainStatus::Included {
1147 block,
1148 confirmations,
1149 } => Some((block, 1, *confirmations)),
1150 ChainStatus::Safe { block } => Some((block, 2, 0)),
1151 ChainStatus::Finalized { block } => Some((block, 3, 0)),
1152 ChainStatus::Pending | ChainStatus::Preconfirmed { .. } | ChainStatus::Reorged { .. } => {
1153 None
1154 }
1155 }
1156}
1157
1158fn input_source_rank(source: InputSource) -> u8 {
1159 match source {
1160 InputSource::Backfill => 0,
1161 InputSource::Poll => 1,
1162 InputSource::Subscription => 2,
1163 InputSource::Flashblocks => 3,
1164 InputSource::Batch => 4,
1165 InputSource::Synthetic => 5,
1166 }
1167}
1168
1169#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1179pub struct SubscriberDeliveryToken(Vec<u8>);
1180
1181impl SubscriberDeliveryToken {
1182 pub fn new(bytes: Vec<u8>) -> Self {
1184 Self(bytes)
1185 }
1186
1187 pub fn as_bytes(&self) -> &[u8] {
1189 &self.0
1190 }
1191
1192 pub fn into_bytes(self) -> Vec<u8> {
1194 self.0
1195 }
1196}
1197
1198#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1204pub struct SubscriberCheckpoint(Vec<u8>);
1205
1206impl SubscriberCheckpoint {
1207 pub fn new(bytes: Vec<u8>) -> Self {
1209 Self(bytes)
1210 }
1211
1212 pub fn as_bytes(&self) -> &[u8] {
1214 &self.0
1215 }
1216
1217 pub fn into_bytes(self) -> Vec<u8> {
1219 self.0
1220 }
1221}
1222
1223#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1233pub struct SubscriberPayloadCommitment(B256);
1234
1235impl SubscriberPayloadCommitment {
1236 pub const fn new(commitment: B256) -> Self {
1238 Self(commitment)
1239 }
1240
1241 pub const fn digest(&self) -> B256 {
1243 self.0
1244 }
1245}
1246
1247#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1253#[non_exhaustive]
1254pub struct SubscriberResumePosition {
1255 pub chain_id: u64,
1257 pub coverage_head: BlockRef,
1259 pub canonical_history: Vec<BlockRef>,
1261 pub delivery_token: Option<SubscriberDeliveryToken>,
1265 pub subscriber_checkpoint: Option<SubscriberCheckpoint>,
1267}
1268
1269impl SubscriberResumePosition {
1270 pub fn new(
1272 chain_id: u64,
1273 coverage_head: BlockRef,
1274 canonical_history: Vec<BlockRef>,
1275 delivery_token: Option<SubscriberDeliveryToken>,
1276 subscriber_checkpoint: Option<SubscriberCheckpoint>,
1277 ) -> Self {
1278 Self {
1279 chain_id,
1280 coverage_head,
1281 canonical_history,
1282 delivery_token,
1283 subscriber_checkpoint,
1284 }
1285 }
1286}
1287
1288#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1296#[non_exhaustive]
1297pub enum DeliveryAudience {
1298 #[default]
1300 All,
1301 Owners(Vec<HandlerId>),
1303 AllExcept(Vec<HandlerId>),
1308}
1309
1310#[derive(
1318 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
1319)]
1320#[non_exhaustive]
1321pub enum DeliveryScope {
1322 #[default]
1324 Canonical,
1325 CanonicalProgress,
1327 OwnerCatchup,
1329 Preconfirmed,
1332}
1333
1334impl DeliveryScope {
1335 const fn advances_canonical_state(self) -> bool {
1336 matches!(self, Self::Canonical | Self::CanonicalProgress)
1337 }
1338}
1339
1340#[derive(Clone, Debug)]
1342pub struct ReactiveInputDelivery<N: Network = Ethereum> {
1343 record: ReactiveInputRecord<N>,
1344 audience: DeliveryAudience,
1345 scope: DeliveryScope,
1346}
1347
1348impl<N: Network> ReactiveInputDelivery<N> {
1349 pub fn new(
1351 record: ReactiveInputRecord<N>,
1352 audience: DeliveryAudience,
1353 scope: DeliveryScope,
1354 ) -> Self {
1355 Self {
1356 record,
1357 audience,
1358 scope,
1359 }
1360 }
1361
1362 pub const fn record(&self) -> &ReactiveInputRecord<N> {
1364 &self.record
1365 }
1366
1367 pub const fn audience(&self) -> &DeliveryAudience {
1369 &self.audience
1370 }
1371
1372 pub const fn scope(&self) -> DeliveryScope {
1374 self.scope
1375 }
1376
1377 pub fn into_parts(self) -> (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope) {
1379 (self.record, self.audience, self.scope)
1380 }
1381}
1382
1383#[derive(Clone, Debug)]
1388#[non_exhaustive]
1389pub struct ReactiveInputBatchParts<N: Network = Ethereum> {
1390 pub chain_id: Option<u64>,
1392 pub deliveries: Vec<ReactiveInputDelivery<N>>,
1394 pub delivery_token: Option<SubscriberDeliveryToken>,
1396 pub subscriber_checkpoint: Option<SubscriberCheckpoint>,
1398 pub payload_commitment: Option<SubscriberPayloadCommitment>,
1400 pub chain_controls: Vec<ChainControl>,
1402}
1403
1404#[derive(Clone, Debug)]
1406pub struct ReactiveInputBatch<N: Network = Ethereum> {
1407 records: Vec<ReactiveInputRecord<N>>,
1408 chain_id: Option<u64>,
1409 delivery_token: Option<SubscriberDeliveryToken>,
1410 subscriber_checkpoint: Option<SubscriberCheckpoint>,
1411 payload_commitment: Option<SubscriberPayloadCommitment>,
1412 audience: DeliveryAudience,
1413 record_audiences: Option<Vec<DeliveryAudience>>,
1414 delivery_scope: DeliveryScope,
1415 record_delivery_scopes: Option<Vec<DeliveryScope>>,
1416 chain_controls: Vec<ChainControl>,
1417}
1418
1419type RuntimeInputDelivery<N> = (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope);
1420
1421impl<N: Network> ReactiveInputBatch<N> {
1422 pub fn new(records: Vec<ReactiveInputRecord<N>>) -> Self {
1424 let chain_id = common_record_chain_id(&records);
1425 Self {
1426 records,
1427 chain_id,
1428 delivery_token: None,
1429 subscriber_checkpoint: None,
1430 payload_commitment: None,
1431 audience: DeliveryAudience::All,
1432 record_audiences: None,
1433 delivery_scope: DeliveryScope::Canonical,
1434 record_delivery_scopes: None,
1435 chain_controls: Vec::new(),
1436 }
1437 }
1438
1439 pub fn with_chain_id(mut self, chain_id: u64) -> Self {
1442 self.chain_id = Some(chain_id);
1443 self
1444 }
1445
1446 pub const fn chain_id(&self) -> Option<u64> {
1449 self.chain_id
1450 }
1451
1452 pub fn with_delivery_token(mut self, token: SubscriberDeliveryToken) -> Self {
1454 self.delivery_token = Some(token);
1455 self
1456 }
1457
1458 pub fn delivery_token(&self) -> Option<&SubscriberDeliveryToken> {
1460 self.delivery_token.as_ref()
1461 }
1462
1463 pub fn with_subscriber_checkpoint(mut self, checkpoint: SubscriberCheckpoint) -> Self {
1465 self.subscriber_checkpoint = Some(checkpoint);
1466 self
1467 }
1468
1469 pub fn subscriber_checkpoint(&self) -> Option<&SubscriberCheckpoint> {
1471 self.subscriber_checkpoint.as_ref()
1472 }
1473
1474 pub fn with_payload_commitment(mut self, commitment: SubscriberPayloadCommitment) -> Self {
1477 self.payload_commitment = Some(commitment);
1478 self
1479 }
1480
1481 pub const fn payload_commitment(&self) -> Option<&SubscriberPayloadCommitment> {
1483 self.payload_commitment.as_ref()
1484 }
1485
1486 pub fn with_audience(mut self, audience: DeliveryAudience) -> Self {
1488 self.audience = audience;
1489 self.record_audiences = None;
1490 self
1491 }
1492
1493 pub const fn audience(&self) -> &DeliveryAudience {
1495 &self.audience
1496 }
1497
1498 pub fn from_scoped_records(
1500 records: impl IntoIterator<Item = (ReactiveInputRecord<N>, DeliveryAudience)>,
1501 ) -> Self {
1502 let (records, record_audiences): (Vec<_>, Vec<_>) = records.into_iter().unzip();
1503 let chain_id = common_record_chain_id(&records);
1504 Self {
1505 records,
1506 chain_id,
1507 delivery_token: None,
1508 subscriber_checkpoint: None,
1509 payload_commitment: None,
1510 audience: DeliveryAudience::All,
1511 record_audiences: Some(record_audiences),
1512 delivery_scope: DeliveryScope::Canonical,
1513 record_delivery_scopes: None,
1514 chain_controls: Vec::new(),
1515 }
1516 }
1517
1518 pub fn from_deliveries(deliveries: impl IntoIterator<Item = ReactiveInputDelivery<N>>) -> Self {
1520 Self::from_scoped_records_with_delivery_scope(
1521 deliveries
1522 .into_iter()
1523 .map(ReactiveInputDelivery::into_parts),
1524 )
1525 }
1526
1527 pub fn record_audience(&self, index: usize) -> Option<&DeliveryAudience> {
1529 if index >= self.records.len() {
1530 return None;
1531 }
1532 Some(
1533 self.record_audiences
1534 .as_ref()
1535 .and_then(|audiences| audiences.get(index))
1536 .unwrap_or(&self.audience),
1537 )
1538 }
1539
1540 pub fn with_delivery_scope(mut self, scope: DeliveryScope) -> Self {
1542 self.delivery_scope = scope;
1543 self.record_delivery_scopes = None;
1544 self
1545 }
1546
1547 pub fn record_delivery_scope(&self, index: usize) -> Option<DeliveryScope> {
1549 if index >= self.records.len() {
1550 return None;
1551 }
1552 Some(
1553 self.record_delivery_scopes
1554 .as_ref()
1555 .and_then(|scopes| scopes.get(index))
1556 .copied()
1557 .unwrap_or(self.delivery_scope),
1558 )
1559 }
1560
1561 fn from_scoped_records_with_delivery_scope(
1562 records: impl IntoIterator<Item = (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
1563 ) -> Self {
1564 let mut input_records = Vec::new();
1565 let mut audiences = Vec::new();
1566 let mut scopes = Vec::new();
1567 for (record, audience, scope) in records {
1568 input_records.push(record);
1569 audiences.push(audience);
1570 scopes.push(scope);
1571 }
1572 let chain_id = common_record_chain_id(&input_records);
1573 Self {
1574 records: input_records,
1575 chain_id,
1576 delivery_token: None,
1577 subscriber_checkpoint: None,
1578 payload_commitment: None,
1579 audience: DeliveryAudience::All,
1580 record_audiences: Some(audiences),
1581 delivery_scope: DeliveryScope::Canonical,
1582 record_delivery_scopes: Some(scopes),
1583 chain_controls: Vec::new(),
1584 }
1585 }
1586
1587 pub fn with_chain_controls(mut self, controls: impl IntoIterator<Item = ChainControl>) -> Self {
1594 self.chain_controls = controls.into_iter().collect();
1595 self
1596 }
1597
1598 pub fn chain_controls(&self) -> &[ChainControl] {
1600 &self.chain_controls
1601 }
1602
1603 pub fn records(&self) -> &[ReactiveInputRecord<N>] {
1605 &self.records
1606 }
1607
1608 pub fn into_records(self) -> Vec<ReactiveInputRecord<N>> {
1615 self.records
1616 }
1617
1618 pub fn into_parts(self) -> ReactiveInputBatchParts<N> {
1620 let chain_id = self.chain_id;
1621 let delivery_token = self.delivery_token;
1622 let subscriber_checkpoint = self.subscriber_checkpoint;
1623 let payload_commitment = self.payload_commitment;
1624 let chain_controls = self.chain_controls;
1625 let audiences = self
1626 .record_audiences
1627 .unwrap_or_else(|| vec![self.audience; self.records.len()]);
1628 let scopes = self
1629 .record_delivery_scopes
1630 .unwrap_or_else(|| vec![self.delivery_scope; self.records.len()]);
1631 let deliveries = self
1632 .records
1633 .into_iter()
1634 .zip(audiences)
1635 .zip(scopes)
1636 .map(|((record, audience), scope)| ReactiveInputDelivery::new(record, audience, scope))
1637 .collect();
1638 ReactiveInputBatchParts {
1639 chain_id,
1640 deliveries,
1641 delivery_token,
1642 subscriber_checkpoint,
1643 payload_commitment,
1644 chain_controls,
1645 }
1646 }
1647
1648 fn into_runtime_parts(self) -> (Vec<RuntimeInputDelivery<N>>, Vec<ChainControl>, Option<u64>) {
1649 let audiences = self
1650 .record_audiences
1651 .unwrap_or_else(|| vec![self.audience; self.records.len()]);
1652 let scopes = self
1653 .record_delivery_scopes
1654 .unwrap_or_else(|| vec![self.delivery_scope; self.records.len()]);
1655 let records = self
1656 .records
1657 .into_iter()
1658 .zip(audiences)
1659 .zip(scopes)
1660 .map(|((record, audience), scope)| (record, audience, scope))
1661 .collect();
1662 (records, self.chain_controls, self.chain_id)
1663 }
1664
1665 fn take_delivery_token(&mut self) -> Option<SubscriberDeliveryToken> {
1666 self.delivery_token.take()
1667 }
1668
1669 fn take_subscriber_checkpoint(&mut self) -> Option<SubscriberCheckpoint> {
1670 self.subscriber_checkpoint.take()
1671 }
1672}
1673
1674fn common_record_chain_id<N: Network>(records: &[ReactiveInputRecord<N>]) -> Option<u64> {
1675 let chain_id = records.first()?.context.chain_id?;
1676 records
1677 .iter()
1678 .all(|record| record.context.chain_id == Some(chain_id))
1679 .then_some(chain_id)
1680}
1681
1682pub trait ReactiveHandler<N: Network = Ethereum>: Send + Sync {
1684 fn id(&self) -> HandlerId;
1686
1687 fn interests(&self) -> Vec<ReactiveInterest<N>>;
1689
1690 fn log_route_index(&self) -> Option<LogRouteIndex> {
1697 None
1698 }
1699
1700 fn handle(
1702 &self,
1703 ctx: &ReactiveContext,
1704 input: &ReactiveInput<N>,
1705 state: &dyn StateView,
1706 ) -> Result<HandlerOutcome, HandlerError>;
1707}
1708
1709pub trait ReactiveHook<N: Network = Ethereum>: Send + Sync {
1719 fn on_report(&self, report: Arc<ReactiveReport<N>>);
1721}
1722
1723#[allow(clippy::large_enum_variant)]
1725#[derive(Clone)]
1726pub enum ReactiveInterest<N: Network = Ethereum> {
1727 Logs(LogInterest),
1729 Blocks(BlockInterest),
1731 PendingTransactions(PendingTxInterest<N>),
1733}
1734
1735impl<N: Network> fmt::Debug for ReactiveInterest<N> {
1736 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1737 match self {
1738 Self::Logs(interest) => f.debug_tuple("Logs").field(interest).finish(),
1739 Self::Blocks(interest) => f.debug_tuple("Blocks").field(interest).finish(),
1740 Self::PendingTransactions(interest) => f
1741 .debug_tuple("PendingTransactions")
1742 .field(interest)
1743 .finish(),
1744 }
1745 }
1746}
1747
1748#[derive(Clone)]
1750pub struct LogInterest {
1751 pub provider_filter: Filter,
1753 pub local_matcher: Option<Arc<dyn LogMatcher>>,
1755 pub route_key: Option<RouteKeySpec>,
1757}
1758
1759impl LogInterest {
1760 pub fn matches(&self, log: &Log) -> bool {
1762 self.provider_filter.rpc_matches(log)
1763 && self
1764 .local_matcher
1765 .as_ref()
1766 .is_none_or(|matcher| matcher.matches(log))
1767 }
1768
1769 pub fn route_key(&self, log: &Log) -> Option<RouteKey> {
1771 self.route_key.as_ref().and_then(|spec| spec.extract(log))
1772 }
1773}
1774
1775impl fmt::Debug for LogInterest {
1776 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1777 f.debug_struct("LogInterest")
1778 .field("provider_filter", &self.provider_filter)
1779 .field(
1780 "local_matcher",
1781 &self.local_matcher.as_ref().map(|_| "<matcher>"),
1782 )
1783 .field("route_key", &self.route_key)
1784 .finish()
1785 }
1786}
1787
1788pub trait LogMatcher: Send + Sync {
1790 fn matches(&self, log: &Log) -> bool;
1792}
1793
1794#[derive(Clone)]
1796pub enum RouteKeySpec {
1797 EmitterAddress,
1799 Topic {
1801 index: usize,
1803 },
1804 DataSlice {
1806 offset: usize,
1808 len: usize,
1810 },
1811 Custom(Arc<dyn RouteKeyExtractor>),
1813}
1814
1815impl RouteKeySpec {
1816 pub fn extract(&self, log: &Log) -> Option<RouteKey> {
1818 match self {
1819 Self::EmitterAddress => Some(RouteKey::Address(log.address())),
1820 Self::Topic { index } => log.topics().get(*index).copied().map(RouteKey::Bytes32),
1821 Self::DataSlice { offset, len } => {
1822 let data = log.inner.data.data.as_ref();
1823 let end = offset.checked_add(*len)?;
1824 data.get(*offset..end)
1825 .map(|bytes| RouteKey::Bytes(bytes.to_vec()))
1826 }
1827 Self::Custom(extractor) => extractor.extract(log),
1828 }
1829 }
1830}
1831
1832impl fmt::Debug for RouteKeySpec {
1833 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1834 match self {
1835 Self::EmitterAddress => f.write_str("EmitterAddress"),
1836 Self::Topic { index } => f.debug_struct("Topic").field("index", index).finish(),
1837 Self::DataSlice { offset, len } => f
1838 .debug_struct("DataSlice")
1839 .field("offset", offset)
1840 .field("len", len)
1841 .finish(),
1842 Self::Custom(_) => f.write_str("Custom(<extractor>)"),
1843 }
1844 }
1845}
1846
1847pub trait RouteKeyExtractor: Send + Sync {
1849 fn extract(&self, log: &Log) -> Option<RouteKey>;
1851}
1852
1853#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1855pub enum RouteKey {
1856 Address(Address),
1858 Bytes32(B256),
1860 Bytes(Vec<u8>),
1862}
1863
1864#[non_exhaustive]
1866#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1867pub enum LogRouteKey {
1868 Emitter(Address),
1870 Topic {
1872 index: usize,
1874 value: B256,
1876 },
1877 DataSlice {
1879 offset: usize,
1881 value: Vec<u8>,
1883 },
1884}
1885
1886#[derive(Clone, Debug, PartialEq, Eq)]
1888pub struct LogRouteIndex {
1889 keys: Vec<LogRouteKey>,
1890}
1891
1892impl LogRouteIndex {
1893 pub fn new(primary: LogRouteKey, additional: impl IntoIterator<Item = LogRouteKey>) -> Self {
1895 let mut keys = vec![primary];
1896 for key in additional {
1897 if !keys.contains(&key) {
1898 keys.push(key);
1899 }
1900 }
1901 Self { keys }
1902 }
1903
1904 pub fn single(key: LogRouteKey) -> Self {
1906 Self { keys: vec![key] }
1907 }
1908
1909 pub fn keys(&self) -> &[LogRouteKey] {
1911 &self.keys
1912 }
1913}
1914
1915#[derive(Clone, Debug, PartialEq, Eq)]
1917pub struct ReactiveLogRoute {
1918 pub handler_id: HandlerId,
1920 pub route_key: Option<RouteKey>,
1922}
1923
1924#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1926pub struct BlockInterest {
1927 pub mode: BlockInterestMode,
1929}
1930
1931impl Default for BlockInterest {
1932 fn default() -> Self {
1933 Self {
1934 mode: BlockInterestMode::Header,
1935 }
1936 }
1937}
1938
1939#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1941pub enum BlockInterestMode {
1942 Header,
1944 FullBlock,
1946}
1947
1948#[derive(Clone)]
1950pub struct PendingTxInterest<N: Network = Ethereum> {
1951 pub full_transactions: bool,
1953 pub from: AddressMatcher,
1955 pub to: AddressMatcher,
1957 pub selectors: SelectorMatcher,
1959 pub local_matcher: Option<Arc<dyn PendingTxMatcher<N>>>,
1961}
1962
1963impl<N: Network> Default for PendingTxInterest<N> {
1964 fn default() -> Self {
1965 Self {
1966 full_transactions: false,
1967 from: AddressMatcher::Any,
1968 to: AddressMatcher::Any,
1969 selectors: SelectorMatcher::Any,
1970 local_matcher: None,
1971 }
1972 }
1973}
1974
1975impl<N: Network> PendingTxInterest<N> {
1976 fn matches_hash_only(&self) -> bool {
1977 !self.full_transactions
1978 && self.from.is_any()
1979 && self.to.is_any()
1980 && self.selectors.is_any()
1981 && self.local_matcher.is_none()
1982 }
1983
1984 fn matches_tx(&self, tx: &N::TransactionResponse) -> bool {
1985 self.from.matches(tx.from())
1986 && self.to.matches_option(tx.to())
1987 && self.selectors.matches(tx.input())
1988 && self
1989 .local_matcher
1990 .as_ref()
1991 .is_none_or(|matcher| matcher.matches(tx))
1992 }
1993}
1994
1995impl<N: Network> fmt::Debug for PendingTxInterest<N> {
1996 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1997 f.debug_struct("PendingTxInterest")
1998 .field("full_transactions", &self.full_transactions)
1999 .field("from", &self.from)
2000 .field("to", &self.to)
2001 .field("selectors", &self.selectors)
2002 .field(
2003 "local_matcher",
2004 &self.local_matcher.as_ref().map(|_| "<matcher>"),
2005 )
2006 .finish()
2007 }
2008}
2009
2010#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2012pub enum AddressMatcher {
2013 Any,
2015 Exact(Address),
2017 AnyOf(Vec<Address>),
2019}
2020
2021impl AddressMatcher {
2022 pub fn is_any(&self) -> bool {
2024 matches!(self, Self::Any)
2025 }
2026
2027 pub fn matches(&self, address: Address) -> bool {
2029 match self {
2030 Self::Any => true,
2031 Self::Exact(expected) => *expected == address,
2032 Self::AnyOf(addresses) => addresses.contains(&address),
2033 }
2034 }
2035
2036 pub fn matches_option(&self, address: Option<Address>) -> bool {
2038 match (self, address) {
2039 (Self::Any, _) => true,
2040 (_, Some(address)) => self.matches(address),
2041 _ => false,
2042 }
2043 }
2044}
2045
2046#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2048pub enum SelectorMatcher {
2049 Any,
2051 AnyOf(Vec<[u8; 4]>),
2053}
2054
2055impl SelectorMatcher {
2056 pub fn is_any(&self) -> bool {
2058 matches!(self, Self::Any)
2059 }
2060
2061 pub fn matches(&self, input: &Bytes) -> bool {
2063 match self {
2064 Self::Any => true,
2065 Self::AnyOf(selectors) => input
2066 .get(..4)
2067 .and_then(|bytes| bytes.try_into().ok())
2068 .is_some_and(|selector| selectors.contains(&selector)),
2069 }
2070 }
2071}
2072
2073pub trait PendingTxMatcher<N: Network = Ethereum>: Send + Sync {
2075 fn matches(&self, tx: &N::TransactionResponse) -> bool;
2077}
2078
2079#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
2097#[non_exhaustive]
2098pub enum TrackingPolicy {
2099 Slots {
2104 slots: Vec<U256>,
2106 },
2107 WholeAccount,
2112 Scalars,
2117}
2118
2119#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2132pub enum RootGateCadence {
2133 EveryNBlocks(NonZeroU64),
2137 Disabled,
2139}
2140
2141impl RootGateCadence {
2142 pub fn every_n_blocks(n: u64) -> Self {
2144 Self::EveryNBlocks(NonZeroU64::new(n.max(1)).expect("clamped to at least 1"))
2145 }
2146}
2147
2148impl Default for RootGateCadence {
2149 fn default() -> Self {
2153 Self::every_n_blocks(16)
2154 }
2155}
2156
2157#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
2164struct TrackedRoot {
2165 last_root: B256,
2166 last_block: u64,
2167 balance: U256,
2168 nonce: u64,
2169 code_hash: B256,
2170}
2171
2172#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2174pub struct ResyncRequest {
2175 pub id: ResyncId,
2177 pub reason: ResyncReason,
2179 pub block: ResyncBlock,
2181 pub targets: Vec<ResyncTarget>,
2183 pub priority: ResyncPriority,
2185}
2186
2187#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2189pub struct ResyncId(String);
2190
2191impl ResyncId {
2192 pub fn new(id: impl Into<String>) -> Self {
2194 Self(id.into())
2195 }
2196}
2197
2198#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2200#[non_exhaustive]
2201pub enum ResyncReason {
2202 HandlerRequested,
2204 SkippedStateEffect,
2206 MissedBlockRange,
2213 RootMoved,
2223 Custom(String),
2225}
2226
2227#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2229pub enum ResyncBlock {
2230 Latest,
2232 Pending,
2234 Safe,
2236 Finalized,
2238 Number(u64),
2240 Hash {
2242 number: u64,
2244 hash: B256,
2246 require_canonical: bool,
2248 },
2249}
2250
2251#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2253pub enum ResyncTarget {
2254 StorageSlot {
2256 address: Address,
2258 slot: U256,
2260 },
2261 StorageSlots {
2263 address: Address,
2265 slots: Vec<U256>,
2267 },
2268 Account {
2270 address: Address,
2272 fields: AccountFieldMask,
2274 },
2275}
2276
2277#[derive(
2279 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
2280)]
2281pub struct AccountFieldMask {
2282 pub balance: bool,
2284 pub nonce: bool,
2286 pub code: bool,
2288}
2289
2290#[derive(
2292 Clone,
2293 Copy,
2294 Debug,
2295 Default,
2296 PartialEq,
2297 Eq,
2298 Hash,
2299 PartialOrd,
2300 Ord,
2301 serde::Serialize,
2302 serde::Deserialize,
2303)]
2304pub enum ResyncPriority {
2305 Low,
2307 #[default]
2309 Normal,
2310 High,
2312}
2313
2314#[derive(Clone, Debug, PartialEq, Eq)]
2316pub struct InvalidationRequest {
2317 pub scope: PurgeScope,
2319 pub address: Address,
2321 pub reason: InvalidationReason,
2323}
2324
2325#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2327pub enum InvalidationReason {
2328 HandlerRequested,
2330 Reorg,
2332 Custom(String),
2334}
2335
2336#[derive(Clone, Debug, PartialEq, Eq)]
2338pub struct SpeculativeRequest {
2339 pub id: SpeculativeId,
2341 pub input_ref: InputRef,
2343 pub labels: Vec<ReportTag>,
2345}
2346
2347#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2349pub struct SpeculativeId(String);
2350
2351impl SpeculativeId {
2352 pub fn new(id: impl Into<String>) -> Self {
2354 Self(id.into())
2355 }
2356}
2357
2358#[derive(Clone, Debug, PartialEq, Eq)]
2360pub struct ReactiveConfig {
2361 pub hook_backpressure: HookBackpressure,
2366 pub journal_depth: usize,
2383}
2384
2385impl Default for ReactiveConfig {
2386 fn default() -> Self {
2387 Self {
2388 hook_backpressure: HookBackpressure::Block,
2389 journal_depth: 64,
2390 }
2391 }
2392}
2393
2394#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2396pub enum HookBackpressure {
2397 Block,
2399 DropNewest,
2401 DropOldest,
2403 Error,
2405}
2406
2407#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2415#[non_exhaustive]
2416pub enum CacheHealth {
2417 #[default]
2419 Healthy,
2420 Degraded {
2424 since_block: u64,
2426 },
2427 Unhealthy {
2430 since_block: u64,
2432 },
2433}
2434
2435#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2442#[non_exhaustive]
2443pub struct CacheMetricsSnapshot {
2444 pub deep_reorgs: u64,
2447 pub reorgs_recovered: u64,
2449 pub resync_requests: u64,
2451 pub resync_failures: u64,
2453 pub missed_ranges: u64,
2455 pub coverage_gaps: u64,
2457 pub pending_contamination: u64,
2459 pub stale_verdicts: u64,
2461}
2462
2463#[derive(Debug, Default)]
2469struct CacheMetrics {
2470 deep_reorgs: AtomicU64,
2471 reorgs_recovered: AtomicU64,
2472 resync_requests: AtomicU64,
2473 resync_failures: AtomicU64,
2474 missed_ranges: AtomicU64,
2475 coverage_gaps: AtomicU64,
2476 pending_contamination: AtomicU64,
2477 stale_verdicts: AtomicU64,
2478}
2479
2480impl CacheMetrics {
2481 fn snapshot(&self) -> CacheMetricsSnapshot {
2482 CacheMetricsSnapshot {
2483 deep_reorgs: self.deep_reorgs.load(Ordering::Relaxed),
2484 reorgs_recovered: self.reorgs_recovered.load(Ordering::Relaxed),
2485 resync_requests: self.resync_requests.load(Ordering::Relaxed),
2486 resync_failures: self.resync_failures.load(Ordering::Relaxed),
2487 missed_ranges: self.missed_ranges.load(Ordering::Relaxed),
2488 coverage_gaps: self.coverage_gaps.load(Ordering::Relaxed),
2489 pending_contamination: self.pending_contamination.load(Ordering::Relaxed),
2490 stale_verdicts: self.stale_verdicts.load(Ordering::Relaxed),
2491 }
2492 }
2493
2494 fn restore(&self, snapshot: CacheMetricsSnapshot) {
2495 self.deep_reorgs
2496 .store(snapshot.deep_reorgs, Ordering::Relaxed);
2497 self.reorgs_recovered
2498 .store(snapshot.reorgs_recovered, Ordering::Relaxed);
2499 self.resync_requests
2500 .store(snapshot.resync_requests, Ordering::Relaxed);
2501 self.resync_failures
2502 .store(snapshot.resync_failures, Ordering::Relaxed);
2503 self.missed_ranges
2504 .store(snapshot.missed_ranges, Ordering::Relaxed);
2505 self.coverage_gaps
2506 .store(snapshot.coverage_gaps, Ordering::Relaxed);
2507 self.pending_contamination
2508 .store(snapshot.pending_contamination, Ordering::Relaxed);
2509 self.stale_verdicts
2510 .store(snapshot.stale_verdicts, Ordering::Relaxed);
2511 }
2512}
2513
2514#[derive(Clone, Debug)]
2516#[non_exhaustive]
2517pub enum ReactiveReport<N: Network = Ethereum> {
2518 Input(InputReport<N>),
2520 Decoded(DecodedReport<N>),
2522 Applied(AppliedReport<N>),
2524 Resynced(ResyncReport),
2526 BlockCommitted(BlockReport<N>),
2528 Reorg(ReorgReport<N>),
2530 ChainControl(ChainControlReport),
2532 MissedBlockRange(MissedRangeReport<N>),
2535 Health(HealthReport<N>),
2537 CoverageGap(CoverageGapReport<N>),
2540 Error(ReactiveErrorReport<N>),
2542}
2543
2544#[derive(Clone, Debug, PartialEq, Eq)]
2546pub struct ChainControlReport {
2547 pub control: ChainControl,
2549}
2550
2551#[derive(Clone, Debug)]
2553pub struct InputReport<N: Network = Ethereum> {
2554 pub input_ref: InputRef,
2556 pub context: ReactiveContext,
2558 pub provider: Option<ProviderRef>,
2560 pub _network: PhantomData<N>,
2562}
2563
2564#[derive(Clone, Debug)]
2566pub struct DecodedReport<N: Network = Ethereum> {
2567 pub input_ref: InputRef,
2569 pub handler_ids: Vec<HandlerId>,
2571 pub _network: PhantomData<N>,
2573}
2574
2575#[derive(Clone, Debug)]
2577pub struct AppliedReport<N: Network = Ethereum> {
2578 pub input_ref: InputRef,
2580 pub handler_id: HandlerId,
2582 pub quality: StateEffectQuality,
2584 pub tags: Vec<ReportTag>,
2586 pub diff: StateDiff,
2588 pub state_updates: Vec<StateUpdate>,
2590 pub invalidations: Vec<InvalidationRequest>,
2592 pub resyncs: Vec<ResyncRequest>,
2594 pub speculative: Vec<SpeculativeRequest>,
2596 pub hook_signals: Vec<HookSignal>,
2598 pub _network: PhantomData<N>,
2600}
2601
2602#[derive(Clone, Debug, Default, PartialEq, Eq)]
2606pub struct ResyncReport {
2607 pub requested: Vec<ResyncRequest>,
2609 pub state_updates: Vec<StateUpdate>,
2611 pub diff: StateDiff,
2613 pub failed: Vec<ResyncFailure>,
2615}
2616
2617#[derive(Clone, Debug, PartialEq, Eq)]
2619pub struct ResyncFailure {
2620 pub request_id: ResyncId,
2622 pub block: ResyncBlock,
2624 pub target: ResyncTarget,
2626 pub kind: ResyncFailureKind,
2628 pub message: String,
2630}
2631
2632#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2634#[non_exhaustive]
2635pub enum ResyncFailureKind {
2636 MissingStorageFetcher,
2638 StorageFetchFailed,
2640 StorageFetchOmitted,
2642 MissingAccountFetcher,
2644 AccountFetchFailed,
2646 AccountFetchOmitted,
2648}
2649
2650#[derive(Clone, Debug)]
2652pub struct BlockReport<N: Network = Ethereum> {
2653 pub block: Option<BlockRef>,
2655 pub inputs: Vec<InputRef>,
2657 pub _network: PhantomData<N>,
2659}
2660
2661#[derive(Clone, Debug)]
2677pub struct ReorgReport<N: Network = Ethereum> {
2678 pub dropped: Option<BlockRef>,
2680 pub dropped_blocks: Vec<BlockRef>,
2682 pub dropped_inputs: Vec<InputRef>,
2684 pub rollback_updates: Vec<StateUpdate>,
2686 pub rollback_diff: StateDiff,
2688 pub purge_updates: Vec<StateUpdate>,
2690 pub purge_diff: StateDiff,
2692 pub canceled_resyncs: Vec<ResyncRequest>,
2694 pub reason: ReorgReason,
2696 pub _network: PhantomData<N>,
2698}
2699
2700#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2702pub enum ReorgReason {
2703 RemovedLog,
2705 ReorgedInput,
2707 ParentMismatch,
2709 Explicit,
2711}
2712
2713#[derive(Clone, Debug)]
2721pub struct MissedRangeReport<N: Network = Ethereum> {
2722 pub from: u64,
2724 pub to: u64,
2726 pub block: u64,
2728 pub _network: PhantomData<N>,
2730}
2731
2732#[derive(Clone, Debug)]
2735pub struct HealthReport<N: Network = Ethereum> {
2736 pub from: CacheHealth,
2738 pub to: CacheHealth,
2740 pub block: Option<u64>,
2742 pub _network: PhantomData<N>,
2744}
2745
2746#[derive(Clone, Debug)]
2759pub struct CoverageGapReport<N: Network = Ethereum> {
2760 pub address: Address,
2762 pub block: u64,
2764 pub _network: PhantomData<N>,
2766}
2767
2768#[derive(Clone, Debug)]
2771pub struct ReactiveErrorReport<N: Network = Ethereum> {
2772 pub input_ref: Option<InputRef>,
2774 pub message: String,
2776 pub _network: PhantomData<N>,
2778}
2779
2780#[derive(Clone, Debug)]
2783pub struct ReactiveBatchReport<N: Network = Ethereum> {
2784 pub applied: Vec<AppliedReport<N>>,
2786 pub resyncs: Vec<ResyncRequest>,
2788 pub speculative: Vec<SpeculativeRequest>,
2790 pub reports: Vec<Arc<ReactiveReport<N>>>,
2792}
2793
2794impl<N: Network> Default for ReactiveBatchReport<N> {
2795 fn default() -> Self {
2796 Self {
2797 applied: Vec::new(),
2798 resyncs: Vec::new(),
2799 speculative: Vec::new(),
2800 reports: Vec::new(),
2801 }
2802 }
2803}
2804
2805#[derive(Clone, Debug, PartialEq, Eq)]
2807pub struct HandlerError {
2808 message: String,
2809}
2810
2811impl HandlerError {
2812 pub fn new(message: impl Into<String>) -> Self {
2814 Self {
2815 message: message.into(),
2816 }
2817 }
2818}
2819
2820impl fmt::Display for HandlerError {
2821 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2822 self.message.fmt(f)
2823 }
2824}
2825
2826impl std::error::Error for HandlerError {}
2827
2828impl From<String> for HandlerError {
2829 fn from(message: String) -> Self {
2830 Self::new(message)
2831 }
2832}
2833
2834impl From<&str> for HandlerError {
2835 fn from(message: &str) -> Self {
2836 Self::new(message)
2837 }
2838}
2839
2840#[derive(Debug, thiserror::Error)]
2842#[non_exhaustive]
2843pub enum ReactiveError {
2844 #[error("handler `{handler_id}` failed: {source}")]
2846 HandlerFailed {
2847 handler_id: HandlerId,
2849 source: HandlerError,
2851 },
2852 #[error(
2854 "conflicting effects for input {input_ref:?} on target {target:?}: `{first}` vs `{second}`"
2855 )]
2856 ConflictingEffects {
2857 input_ref: Box<InputRef>,
2859 target: Box<EffectTarget>,
2861 first: HandlerId,
2863 second: HandlerId,
2865 },
2866 #[error(
2868 "pending input {input_ref:?} emitted invalid canonical effect `{effect_kind}` from `{handler_id}`"
2869 )]
2870 InvalidPendingEffect {
2871 input_ref: Box<InputRef>,
2873 handler_id: HandlerId,
2875 effect_kind: &'static str,
2877 },
2878 #[error("invalid reactive input record: {message}")]
2881 InvalidInputRecord {
2882 message: String,
2884 },
2885 #[error("invalid chain control: {message}")]
2887 InvalidChainControl {
2888 message: String,
2890 },
2891 #[error(
2894 "owner catch-up block {number} {hash} is outside the retained canonical rollback journal"
2895 )]
2896 OwnerCatchupOutsideJournal {
2897 number: u64,
2899 hash: B256,
2901 },
2902 #[error(transparent)]
2904 Register(#[from] RegisterError),
2905}
2906
2907#[derive(Debug, thiserror::Error)]
2909#[non_exhaustive]
2910pub enum RegisterError {
2911 #[error("handler id `{0}` is already registered")]
2913 DuplicateHandler(HandlerId),
2914}
2915
2916#[derive(Debug, thiserror::Error)]
2919#[non_exhaustive]
2920pub enum ReactiveEngineRegisterError {
2921 #[error(transparent)]
2923 Register(#[from] RegisterError),
2924 #[error(transparent)]
2926 Subscriber(#[from] SubscriberError),
2927 #[error(
2930 "owner backfill {start_block}..={end_block:?} must target exactly one hash-certified block in the retained rollback journal"
2931 )]
2932 BackfillOutsideJournal {
2933 start_block: u64,
2935 end_block: Option<u64>,
2937 retained_anchor: Option<BlockRef>,
2939 },
2940}
2941
2942#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
2945#[non_exhaustive]
2946pub enum ReactiveBaselineError {
2947 #[error("cannot adopt a canonical baseline after reactive processing has started")]
2949 ActiveRuntime,
2950 #[error(
2953 "canonical baseline conflicts with existing block {existing_number} {existing_hash} (requested {requested_number} {requested_hash})"
2954 )]
2955 ConflictingBaseline {
2956 existing_number: u64,
2958 existing_hash: B256,
2960 requested_number: u64,
2962 requested_hash: B256,
2964 },
2965 #[error("baseline chain id {baseline_chain_id} does not match cache chain id {cache_chain_id}")]
2967 CacheChainMismatch {
2968 baseline_chain_id: u64,
2970 cache_chain_id: u64,
2972 },
2973 #[error("cache block selector is not canonically hash-pinned to baseline {number} {hash}")]
2975 CacheBlockMismatch {
2976 number: u64,
2978 hash: B256,
2980 },
2981}
2982
2983#[derive(Debug, thiserror::Error)]
2986#[non_exhaustive]
2987pub enum ReactiveEngineError {
2988 #[error(transparent)]
2990 Subscriber(#[from] SubscriberError),
2991 #[error(transparent)]
2993 Runtime(ReactiveError),
2994 #[error(transparent)]
2996 Baseline(#[from] ReactiveBaselineError),
2997 #[error("runtime ingestion succeeded but subscriber acknowledgement failed: {0}")]
3000 Acknowledgement(#[source] SubscriberError),
3001 #[error("runtime ingestion succeeded but durable checkpoint commit failed: {0}")]
3005 Checkpoint(#[source] DurableCheckpointError),
3006 #[error("cannot durably checkpoint reactive state before observing a canonical block")]
3008 MissingCheckpointBlock,
3009 #[error("pre-confirmed Flashblock batches cannot be durably checkpointed")]
3012 PreconfirmationNotCheckpointable,
3013 #[error("failed to encode durable reactive runtime state: {0}")]
3015 RuntimeCheckpoint(String),
3016 #[error("cannot use ordinary ingestion while a durable checkpoint commit is pending")]
3019 PendingCheckpointCommit,
3020 #[error("cannot use checkpointed ingestion while an ordinary acknowledgement is pending")]
3023 PendingAcknowledgementCommit,
3024 #[error(
3028 "raw engine ingestion cannot consume delivery tokens or subscriber checkpoints; use a combined next_ingest helper"
3029 )]
3030 UncommittedDeliveryMetadata,
3031 #[error(
3033 "subscriber chain id {subscriber_chain_id} does not match cache chain id {cache_chain_id}"
3034 )]
3035 SubscriberChainMismatch {
3036 subscriber_chain_id: u64,
3038 cache_chain_id: u64,
3040 },
3041 #[error("subscriber does not advertise durable replay support")]
3043 SubscriberNotDurable,
3044 #[error(
3047 "committed delivery token has no delivery witness; replay cannot be acknowledged safely"
3048 )]
3049 MissingReplayWitness,
3050 #[error("replayed delivery token does not match its committed delivery witness")]
3053 ReplayDeliveryMismatch,
3054 #[error("failed to encode durable delivery witness: {0}")]
3056 DeliveryWitness(String),
3057 #[error(
3060 "tokened block-header, full-block, or hydrated-transaction delivery requires an exact payload commitment"
3061 )]
3062 MissingPayloadCommitment,
3063 #[error(
3066 "cache changed while durable checkpoint commit was pending (staged generation {staged_generation}, current generation {current_generation})"
3067 )]
3068 PendingCheckpointCacheChanged {
3069 staged_generation: u64,
3071 current_generation: u64,
3073 },
3074 #[error(
3077 "reorg after block {common_ancestor} exceeds the retained rollback journal (oldest retained block {oldest_journaled:?}, configured depth {journal_depth})"
3078 )]
3079 CheckpointReorgOutsideJournal {
3080 common_ancestor: u64,
3082 oldest_journaled: Option<u64>,
3084 journal_depth: usize,
3086 },
3087 #[error(
3090 "owner catch-up block {number} {hash} is outside the retained canonical rollback journal"
3091 )]
3092 OwnerCatchupOutsideJournal {
3093 number: u64,
3095 hash: B256,
3097 },
3098}
3099
3100impl From<ReactiveError> for ReactiveEngineError {
3101 fn from(error: ReactiveError) -> Self {
3102 match error {
3103 ReactiveError::OwnerCatchupOutsideJournal { number, hash } => {
3104 Self::OwnerCatchupOutsideJournal { number, hash }
3105 }
3106 error => Self::Runtime(error),
3107 }
3108 }
3109}
3110
3111#[derive(Debug, thiserror::Error)]
3113#[non_exhaustive]
3114pub enum ReactiveCheckpointRestoreError {
3115 #[error("cannot restore a durable checkpoint into a runtime with canonical journal state")]
3117 ActiveRuntime,
3118 #[error("invalid durable reactive runtime state: {0}")]
3120 InvalidRuntimeCheckpoint(String),
3121 #[error(transparent)]
3123 Checkpoint(#[from] DurableCheckpointError),
3124 #[error("subscriber rejected durable resume position: {0}")]
3126 Subscriber(#[source] SubscriberError),
3127 #[error(
3129 "subscriber chain id {subscriber_chain_id} does not match checkpoint chain id {checkpoint_chain_id}"
3130 )]
3131 SubscriberChainMismatch {
3132 subscriber_chain_id: u64,
3134 checkpoint_chain_id: u64,
3136 },
3137 #[error("subscriber does not advertise durable replay support")]
3139 SubscriberNotDurable,
3140}
3141
3142#[derive(Clone, Debug)]
3144#[non_exhaustive]
3145pub enum CheckpointedIngest<N: Network = Ethereum> {
3146 Applied(ReactiveBatchReport<N>),
3148 ReplayAcknowledged,
3151}
3152
3153#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3155pub enum EffectTarget {
3156 StorageSlot {
3158 address: Address,
3160 slot: U256,
3162 },
3163 AccountBalance {
3165 address: Address,
3167 },
3168 AccountNonce {
3170 address: Address,
3172 },
3173 AccountCode {
3175 address: Address,
3177 },
3178 MaskedStorageSlot {
3180 address: Address,
3182 slot: U256,
3184 mask: U256,
3186 },
3187}
3188
3189#[derive(Clone, Debug, PartialEq, Eq)]
3190enum AbsoluteValue {
3191 U256(U256),
3192 U64(u64),
3193 Bytes(Bytes),
3194}
3195
3196pub struct ReactiveRuntime<N: Network = Ethereum> {
3198 registry: ReactiveRegistry<N>,
3199 hooks: Vec<Arc<dyn ReactiveHook<N>>>,
3200 config: ReactiveConfig,
3201 journal: VecDeque<BlockJournal<N>>,
3202 coverage_head: Option<BlockRef>,
3203 pending_resyncs: Vec<ResyncRequest>,
3204 health: CacheHealth,
3205 safe_head: Option<BlockRef>,
3206 finalized_head: Option<BlockRef>,
3207 metrics: CacheMetrics,
3208 freshness: Option<FreshnessRegistry>,
3217 tracking: HashMap<Address, TrackingPolicy>,
3221 tracked_roots: HashMap<Address, TrackedRoot>,
3224 root_gate_cadence: RootGateCadence,
3226 last_gate_block: Option<u64>,
3230 touched_since_gate: HashSet<Address>,
3236 preconfirmed_branch: Option<PreconfirmedBranch>,
3239}
3240
3241#[derive(Clone)]
3242struct PreconfirmedBranch {
3243 flashblock: FlashblockRef,
3244 canonical_cache: EvmCacheStateSnapshot,
3245}
3246
3247#[derive(Clone, Debug)]
3248struct BlockJournal<N: Network = Ethereum> {
3249 block: BlockRef,
3250 inputs: Vec<InputRef>,
3251 applied: Vec<AppliedReport<N>>,
3252 handler_ids: Vec<HandlerId>,
3253 resynced: Vec<ResyncReport>,
3254 rollback_diffs: Vec<StateDiff>,
3255}
3256
3257const DURABLE_RUNTIME_CHECKPOINT_VERSION: u32 = 3;
3258
3259#[derive(serde::Serialize, serde::Deserialize)]
3260struct DurableRuntimeCheckpoint {
3261 version: u32,
3262 safe_head: Option<BlockRef>,
3263 finalized_head: Option<BlockRef>,
3264 health: CacheHealth,
3265 pending_resyncs: Vec<ResyncRequest>,
3266 coverage_head: Option<BlockRef>,
3267 journal: Vec<DurableBlockJournal>,
3268 freshness: Option<FreshnessRegistry>,
3269 tracking: HashMap<Address, TrackingPolicy>,
3270 tracked_roots: HashMap<Address, TrackedRoot>,
3271 root_gate_cadence: RootGateCadence,
3272 last_gate_block: Option<u64>,
3273 touched_since_gate: HashSet<Address>,
3274 metrics: CacheMetricsSnapshot,
3275}
3276
3277#[derive(serde::Serialize, serde::Deserialize)]
3278struct DurableBlockJournal {
3279 block: BlockRef,
3280 handler_ids: Vec<HandlerId>,
3281 rollback_diffs: Vec<StateDiff>,
3282}
3283
3284struct DurableRuntimeRestorePlan {
3285 checkpoint: Option<DurableRuntimeCheckpoint>,
3286 fallback_history: Vec<BlockRef>,
3287}
3288
3289impl DurableRuntimeRestorePlan {
3290 fn canonical_history(&self) -> Vec<BlockRef> {
3291 self.checkpoint.as_ref().map_or_else(
3292 || self.fallback_history.clone(),
3293 |checkpoint| checkpoint.journal.iter().map(|entry| entry.block).collect(),
3294 )
3295 }
3296}
3297
3298#[derive(Clone)]
3299struct ReactiveRuntimeState<N: Network> {
3300 journal: VecDeque<BlockJournal<N>>,
3301 coverage_head: Option<BlockRef>,
3302 pending_resyncs: Vec<ResyncRequest>,
3303 health: CacheHealth,
3304 safe_head: Option<BlockRef>,
3305 finalized_head: Option<BlockRef>,
3306 freshness: Option<FreshnessRegistry>,
3307 tracking: HashMap<Address, TrackingPolicy>,
3308 tracked_roots: HashMap<Address, TrackedRoot>,
3309 root_gate_cadence: RootGateCadence,
3310 last_gate_block: Option<u64>,
3311 touched_since_gate: HashSet<Address>,
3312 metrics: CacheMetricsSnapshot,
3313}
3314
3315#[derive(Clone)]
3316struct ChainControlState {
3317 journal_invalidated_from: Option<u64>,
3318 resolved_canonical_blocks: HashMap<(u64, B256), BlockRef>,
3319}
3320
3321#[derive(Default)]
3330struct BatchDroppedCanonical {
3331 identities: HashSet<(u64, B256)>,
3332 implicit_spans: Vec<(u64, u64)>,
3333}
3334
3335impl BatchDroppedCanonical {
3336 fn covers_implicit_number(&self, number: u64) -> bool {
3337 self.implicit_spans
3338 .iter()
3339 .any(|(from, through)| number >= *from && number <= *through)
3340 }
3341
3342 fn contains(&self, block: &BlockRef) -> bool {
3343 self.identities.contains(&(block.number, block.hash))
3344 }
3345
3346 fn record_identity(&mut self, block: &BlockRef) {
3347 self.identities.insert((block.number, block.hash));
3348 }
3349
3350 fn record_explicit(&mut self, _common_ancestor: &BlockRef, old_tip: &BlockRef) {
3351 self.identities.insert((old_tip.number, old_tip.hash));
3352 }
3353
3354 fn record_drained(&mut self, blocks: &[BlockRef]) {
3355 let Some(from) = blocks.iter().map(|block| block.number).min() else {
3356 return;
3357 };
3358 let through = blocks
3359 .iter()
3360 .map(|block| block.number)
3361 .max()
3362 .expect("a non-empty drained set has a maximum");
3363 self.implicit_spans.push((from, through));
3364 self.identities
3365 .extend(blocks.iter().map(|block| (block.number, block.hash)));
3366 }
3367}
3368
3369pub struct ReactiveRegistry<N: Network = Ethereum> {
3377 handlers: BTreeMap<u128, RegisteredHandler<N>>,
3378 handler_positions: HashMap<HandlerId, u128>,
3379 next_handler_position: u128,
3380 indexed_log_handlers: HashMap<LogRouteKey, BTreeSet<u128>>,
3381 fallback_log_handlers: BTreeSet<u128>,
3382 data_slice_shapes: HashMap<(usize, usize), usize>,
3383}
3384
3385struct RegisteredHandler<N: Network = Ethereum> {
3386 id: HandlerId,
3387 handler: Arc<dyn ReactiveHandler<N>>,
3388 interests: Vec<ReactiveInterest<N>>,
3389 has_log_interests: bool,
3390 log_route_index: Option<LogRouteIndex>,
3391}
3392
3393impl<N: Network> Default for ReactiveRegistry<N> {
3394 fn default() -> Self {
3395 Self::new()
3396 }
3397}
3398
3399impl<N: Network> ReactiveRegistry<N> {
3400 pub fn new() -> Self {
3402 Self {
3403 handlers: BTreeMap::new(),
3404 handler_positions: HashMap::new(),
3405 next_handler_position: 0,
3406 indexed_log_handlers: HashMap::new(),
3407 fallback_log_handlers: BTreeSet::new(),
3408 data_slice_shapes: HashMap::new(),
3409 }
3410 }
3411
3412 pub fn register_handler(
3422 &mut self,
3423 handler: Arc<dyn ReactiveHandler<N>>,
3424 ) -> Result<(), RegisterError> {
3425 let id = handler.id();
3426 if self.handler_positions.contains_key(&id) {
3427 return Err(RegisterError::DuplicateHandler(id));
3428 }
3429 let interests = handler.interests();
3430 self.insert_handler_prepared(id, handler, interests);
3431 Ok(())
3432 }
3433
3434 fn insert_handler_prepared(
3435 &mut self,
3436 id: HandlerId,
3437 handler: Arc<dyn ReactiveHandler<N>>,
3438 interests: Vec<ReactiveInterest<N>>,
3439 ) {
3440 debug_assert!(!self.handler_positions.contains_key(&id));
3441 let has_log_interests = interests
3442 .iter()
3443 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)));
3444 let log_route_index = handler.log_route_index();
3445 if self.next_handler_position == u128::MAX {
3446 self.compact_handler_positions();
3447 }
3448 let position = self.next_handler_position;
3449 self.next_handler_position += 1;
3450 self.handler_positions.insert(id.clone(), position);
3451 if let Some(index) = &log_route_index {
3452 for key in index.keys() {
3453 if let LogRouteKey::DataSlice { offset, value } = key {
3454 *self
3455 .data_slice_shapes
3456 .entry((*offset, value.len()))
3457 .or_default() += 1;
3458 }
3459 self.indexed_log_handlers
3460 .entry(key.clone())
3461 .or_default()
3462 .insert(position);
3463 }
3464 } else if has_log_interests {
3465 self.fallback_log_handlers.insert(position);
3466 }
3467 self.handlers.insert(
3468 position,
3469 RegisteredHandler {
3470 id,
3471 handler,
3472 interests,
3473 has_log_interests,
3474 log_route_index,
3475 },
3476 );
3477 }
3478
3479 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
3485 let position = self.handler_positions.remove(id)?;
3486 let registered = self.handlers.remove(&position)?;
3487 if let Some(index) = ®istered.log_route_index {
3488 for key in index.keys() {
3489 let remove_bucket = self
3490 .indexed_log_handlers
3491 .get_mut(key)
3492 .is_some_and(|owners| {
3493 owners.remove(&position);
3494 owners.is_empty()
3495 });
3496 if remove_bucket {
3497 self.indexed_log_handlers.remove(key);
3498 }
3499 if let LogRouteKey::DataSlice { offset, value } = key {
3500 let shape = (*offset, value.len());
3501 let remove_shape =
3502 self.data_slice_shapes.get_mut(&shape).is_some_and(|count| {
3503 *count -= 1;
3504 *count == 0
3505 });
3506 if remove_shape {
3507 self.data_slice_shapes.remove(&shape);
3508 }
3509 }
3510 }
3511 } else {
3512 self.fallback_log_handlers.remove(&position);
3513 }
3514 Some(registered.handler)
3515 }
3516
3517 pub fn contains_handler(&self, id: &HandlerId) -> bool {
3519 self.handler_positions.contains_key(id)
3520 }
3521
3522 pub fn handler_ids(&self) -> Vec<HandlerId> {
3524 self.handlers
3525 .values()
3526 .map(|handler| handler.id.clone())
3527 .collect()
3528 }
3529
3530 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
3532 self.handler_positions
3533 .get(id)
3534 .and_then(|position| self.handlers.get(position))
3535 .map(|registered| registered.interests.as_slice())
3536 }
3537
3538 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
3540 self.handlers
3541 .values()
3542 .flat_map(|handler| handler.interests.clone())
3543 .collect()
3544 }
3545
3546 pub fn log_subscription_filters(&self) -> Vec<Filter> {
3553 let mut filters = Vec::new();
3554 for interest in self.log_interests() {
3555 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
3556 }
3557 filters
3558 }
3559
3560 pub fn route_log(&self, log: &Log) -> Vec<ReactiveLogRoute> {
3566 self.log_handler_candidates(log)
3567 .into_iter()
3568 .filter_map(|handler| handler.route_log(log))
3569 .collect()
3570 }
3571
3572 fn log_handler_candidates(&self, log: &Log) -> Vec<&RegisteredHandler<N>> {
3573 let mut indexed_positions = Vec::new();
3574 if let Some(indexed) = self
3575 .indexed_log_handlers
3576 .get(&LogRouteKey::Emitter(log.address()))
3577 {
3578 indexed_positions.extend(indexed.iter().copied());
3579 }
3580 for (index, value) in log.topics().iter().copied().enumerate() {
3581 if let Some(indexed) = self
3582 .indexed_log_handlers
3583 .get(&LogRouteKey::Topic { index, value })
3584 {
3585 indexed_positions.extend(indexed.iter().copied());
3586 }
3587 }
3588 let data = log.inner.data.data.as_ref();
3589 for &(offset, len) in self.data_slice_shapes.keys() {
3590 let Some(end) = offset.checked_add(len) else {
3591 continue;
3592 };
3593 let Some(value) = data.get(offset..end) else {
3594 continue;
3595 };
3596 if let Some(indexed) = self.indexed_log_handlers.get(&LogRouteKey::DataSlice {
3597 offset,
3598 value: value.to_vec(),
3599 }) {
3600 indexed_positions.extend(indexed.iter().copied());
3601 }
3602 }
3603 if indexed_positions.is_empty() {
3604 if self.fallback_log_handlers.is_empty() {
3605 return Vec::new();
3606 }
3607 if !self.indexed_log_handlers.is_empty() {
3608 return self
3609 .fallback_log_handlers
3610 .iter()
3611 .filter_map(|position| self.handlers.get(position))
3612 .collect();
3613 }
3614 return self
3615 .handlers
3616 .values()
3617 .filter(|handler| handler.has_log_interests && handler.log_route_index.is_none())
3618 .collect();
3619 }
3620
3621 indexed_positions.extend(self.fallback_log_handlers.iter().copied());
3622 indexed_positions.sort_unstable();
3623 indexed_positions.dedup();
3624 indexed_positions
3625 .into_iter()
3626 .filter_map(|position| self.handlers.get(&position))
3627 .collect()
3628 }
3629
3630 fn handlers(&self) -> impl Iterator<Item = &RegisteredHandler<N>> {
3631 self.handlers.values()
3632 }
3633
3634 fn log_interests(&self) -> impl Iterator<Item = &LogInterest> {
3635 self.handlers.values().flat_map(|handler| {
3636 handler
3637 .interests
3638 .iter()
3639 .filter_map(|interest| match interest {
3640 ReactiveInterest::Logs(interest) => Some(interest),
3641 ReactiveInterest::Blocks(_) | ReactiveInterest::PendingTransactions(_) => None,
3642 })
3643 })
3644 }
3645
3646 fn compact_handler_positions(&mut self) {
3647 let handlers = std::mem::take(&mut self.handlers);
3648 self.handler_positions.clear();
3649 self.indexed_log_handlers.clear();
3650 self.fallback_log_handlers.clear();
3651 self.data_slice_shapes.clear();
3652
3653 for (position, (_, handler)) in handlers.into_iter().enumerate() {
3654 let position = position as u128;
3655 self.handler_positions.insert(handler.id.clone(), position);
3656 if let Some(index) = &handler.log_route_index {
3657 for key in index.keys() {
3658 if let LogRouteKey::DataSlice { offset, value } = key {
3659 *self
3660 .data_slice_shapes
3661 .entry((*offset, value.len()))
3662 .or_default() += 1;
3663 }
3664 self.indexed_log_handlers
3665 .entry(key.clone())
3666 .or_default()
3667 .insert(position);
3668 }
3669 } else if handler.has_log_interests {
3670 self.fallback_log_handlers.insert(position);
3671 }
3672 self.handlers.insert(position, handler);
3673 }
3674 self.next_handler_position = self.handlers.len() as u128;
3675 }
3676}
3677
3678impl<N: Network> ReactiveRuntime<N> {
3679 pub fn new(config: ReactiveConfig) -> Self {
3681 Self {
3682 registry: ReactiveRegistry::new(),
3683 hooks: Vec::new(),
3684 config,
3685 journal: VecDeque::new(),
3686 coverage_head: None,
3687 pending_resyncs: Vec::new(),
3688 health: CacheHealth::Healthy,
3689 safe_head: None,
3690 finalized_head: None,
3691 metrics: CacheMetrics::default(),
3692 freshness: None,
3693 tracking: HashMap::new(),
3694 tracked_roots: HashMap::new(),
3695 root_gate_cadence: RootGateCadence::default(),
3696 last_gate_block: None,
3697 touched_since_gate: HashSet::new(),
3698 preconfirmed_branch: None,
3699 }
3700 }
3701
3702 fn checkpoint_state(&self) -> ReactiveRuntimeState<N> {
3703 ReactiveRuntimeState {
3704 journal: self.journal.clone(),
3705 coverage_head: self.coverage_head,
3706 pending_resyncs: self.pending_resyncs.clone(),
3707 health: self.health,
3708 safe_head: self.safe_head,
3709 finalized_head: self.finalized_head,
3710 freshness: self.freshness.clone(),
3711 tracking: self.tracking.clone(),
3712 tracked_roots: self.tracked_roots.clone(),
3713 root_gate_cadence: self.root_gate_cadence,
3714 last_gate_block: self.last_gate_block,
3715 touched_since_gate: self.touched_since_gate.clone(),
3716 metrics: self.metrics.snapshot(),
3717 }
3718 }
3719
3720 fn is_pristine_for_checkpoint_restore(&self) -> bool {
3721 self.preconfirmed_branch.is_none()
3722 && self.journal.is_empty()
3723 && self.coverage_head.is_none()
3724 && self.pending_resyncs.is_empty()
3725 && self.health == CacheHealth::Healthy
3726 && self.safe_head.is_none()
3727 && self.finalized_head.is_none()
3728 && self.tracked_roots.is_empty()
3729 && self.last_gate_block.is_none()
3730 && self.touched_since_gate.is_empty()
3731 && self.metrics.snapshot() == CacheMetricsSnapshot::default()
3732 }
3733
3734 fn adopted_baseline_only(&self) -> Option<BlockRef> {
3735 let baseline = self.coverage_head?;
3736 let journal_is_baseline_only = if self.config.journal_depth == 0 {
3737 self.journal.is_empty()
3738 } else {
3739 self.journal.len() == 1
3740 && self.journal.front().is_some_and(|entry| {
3741 entry.block == baseline
3742 && entry.inputs.is_empty()
3743 && entry.applied.is_empty()
3744 && entry.handler_ids.is_empty()
3745 && entry.resynced.is_empty()
3746 && entry.rollback_diffs.is_empty()
3747 })
3748 };
3749 (self.preconfirmed_branch.is_none()
3750 && journal_is_baseline_only
3751 && self.pending_resyncs.is_empty()
3752 && self.health == CacheHealth::Healthy
3753 && self.safe_head.is_none()
3754 && self.finalized_head.is_none()
3755 && self.tracked_roots.is_empty()
3756 && self.last_gate_block.is_none()
3757 && self.touched_since_gate.is_empty()
3758 && self.metrics.snapshot() == CacheMetricsSnapshot::default())
3759 .then_some(baseline)
3760 }
3761
3762 fn restore_state(&mut self, state: ReactiveRuntimeState<N>) {
3763 self.journal = state.journal;
3764 self.coverage_head = state.coverage_head;
3765 self.pending_resyncs = state.pending_resyncs;
3766 self.health = state.health;
3767 self.safe_head = state.safe_head;
3768 self.finalized_head = state.finalized_head;
3769 self.freshness = state.freshness;
3770 self.tracking = state.tracking;
3771 self.tracked_roots = state.tracked_roots;
3772 self.root_gate_cadence = state.root_gate_cadence;
3773 self.last_gate_block = state.last_gate_block;
3774 self.touched_since_gate = state.touched_since_gate;
3775 self.metrics.restore(state.metrics);
3776 }
3777
3778 fn restore_transaction_state(&mut self, state: ReactiveRuntimeState<N>) {
3779 let metrics = self.metrics.snapshot();
3784 self.restore_state(state);
3785 self.metrics.restore(metrics);
3786 }
3787
3788 fn durable_checkpoint_bytes(&self) -> Result<Vec<u8>, ReactiveEngineError> {
3789 let checkpoint = DurableRuntimeCheckpoint {
3790 version: DURABLE_RUNTIME_CHECKPOINT_VERSION,
3791 safe_head: self.safe_head,
3792 finalized_head: self.finalized_head,
3793 health: self.health,
3794 pending_resyncs: self.pending_resyncs.clone(),
3795 coverage_head: self.coverage_head,
3796 journal: self
3797 .journal
3798 .iter()
3799 .map(|entry| DurableBlockJournal {
3800 block: entry.block,
3801 handler_ids: entry.handler_ids.clone(),
3802 rollback_diffs: entry.rollback_diffs.clone(),
3803 })
3804 .collect(),
3805 freshness: self.freshness.clone(),
3806 tracking: self.tracking.clone(),
3807 tracked_roots: self.tracked_roots.clone(),
3808 root_gate_cadence: self.root_gate_cadence,
3809 last_gate_block: self.last_gate_block,
3810 touched_since_gate: self.touched_since_gate.clone(),
3811 metrics: self.metrics.snapshot(),
3812 };
3813 bincode::serialize(&checkpoint)
3814 .map_err(|error| ReactiveEngineError::RuntimeCheckpoint(error.to_string()))
3815 }
3816
3817 fn plan_durable_checkpoint_restore(
3818 &self,
3819 bytes: &[u8],
3820 expected_coverage: &BlockRef,
3821 ) -> Result<DurableRuntimeRestorePlan, ReactiveCheckpointRestoreError> {
3822 let mut cursor = std::io::Cursor::new(bytes);
3823 let mut checkpoint: DurableRuntimeCheckpoint = bincode::DefaultOptions::new()
3824 .with_fixint_encoding()
3825 .with_limit(bytes.len() as u64)
3826 .deserialize_from(&mut cursor)
3827 .map_err(|error| {
3828 ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(error.to_string())
3829 })?;
3830 if cursor.position() != bytes.len() as u64 {
3831 return Err(ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(
3832 "runtime checkpoint has trailing bytes".to_owned(),
3833 ));
3834 }
3835 if checkpoint.version != DURABLE_RUNTIME_CHECKPOINT_VERSION {
3836 return Err(ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(
3837 format!(
3838 "unsupported runtime checkpoint version {}",
3839 checkpoint.version
3840 ),
3841 ));
3842 }
3843 self.validate_durable_runtime_checkpoint(&checkpoint, expected_coverage)?;
3844
3845 let retained = self.config.journal_depth.min(checkpoint.journal.len());
3846 let discard = checkpoint.journal.len() - retained;
3847 checkpoint.journal.drain(..discard);
3848 Ok(DurableRuntimeRestorePlan {
3849 checkpoint: Some(checkpoint),
3850 fallback_history: Vec::new(),
3851 })
3852 }
3853
3854 fn apply_durable_checkpoint_restore(&mut self, plan: DurableRuntimeRestorePlan) {
3855 let Some(checkpoint) = plan.checkpoint else {
3856 self.journal = plan
3857 .fallback_history
3858 .into_iter()
3859 .map(|block| BlockJournal {
3860 block,
3861 inputs: Vec::new(),
3862 applied: Vec::new(),
3863 handler_ids: Vec::new(),
3864 resynced: Vec::new(),
3865 rollback_diffs: Vec::new(),
3866 })
3867 .collect();
3868 return;
3869 };
3870 self.safe_head = checkpoint.safe_head;
3871 self.finalized_head = checkpoint.finalized_head;
3872 self.health = checkpoint.health;
3873 self.pending_resyncs = checkpoint.pending_resyncs;
3874 self.coverage_head = checkpoint.coverage_head;
3875 self.journal = checkpoint
3876 .journal
3877 .into_iter()
3878 .map(|entry| BlockJournal {
3879 block: entry.block,
3880 inputs: Vec::new(),
3881 applied: Vec::new(),
3882 handler_ids: entry.handler_ids,
3883 resynced: Vec::new(),
3884 rollback_diffs: entry.rollback_diffs,
3885 })
3886 .collect();
3887 self.freshness = checkpoint.freshness;
3888 self.tracking = checkpoint.tracking;
3889 self.tracked_roots = checkpoint.tracked_roots;
3890 self.root_gate_cadence = checkpoint.root_gate_cadence;
3891 self.last_gate_block = checkpoint.last_gate_block;
3892 self.touched_since_gate = checkpoint.touched_since_gate;
3893 self.metrics.restore(checkpoint.metrics);
3894 }
3895
3896 fn validate_durable_runtime_checkpoint(
3897 &self,
3898 checkpoint: &DurableRuntimeCheckpoint,
3899 expected_coverage: &BlockRef,
3900 ) -> Result<(), ReactiveCheckpointRestoreError> {
3901 let invalid =
3902 |message: String| ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(message);
3903 let Some(coverage) = checkpoint.coverage_head.as_ref() else {
3904 return Err(invalid(
3905 "runtime checkpoint is missing its canonical coverage head".into(),
3906 ));
3907 };
3908 if !optional_block_refs_are_compatible(Some(coverage), Some(expected_coverage)) {
3909 return Err(invalid(format!(
3910 "runtime coverage {}:{:?} conflicts with checkpoint metadata {}:{:?}",
3911 coverage.number, coverage.hash, expected_coverage.number, expected_coverage.hash
3912 )));
3913 }
3914 for (label, head) in [
3915 ("safe", checkpoint.safe_head.as_ref()),
3916 ("finalized", checkpoint.finalized_head.as_ref()),
3917 ] {
3918 let Some(head) = head else { continue };
3919 if head.number > coverage.number
3920 || (head.number == coverage.number && head.hash != coverage.hash)
3921 {
3922 return Err(invalid(format!(
3923 "{label} head {}:{:?} lies beyond or conflicts with canonical coverage {}:{:?}",
3924 head.number, head.hash, coverage.number, coverage.hash
3925 )));
3926 }
3927 if head.number.checked_add(1) == Some(coverage.number)
3928 && coverage
3929 .parent_hash
3930 .is_some_and(|parent| parent != head.hash)
3931 {
3932 return Err(invalid(format!(
3933 "canonical coverage does not descend from adjacent {label} head"
3934 )));
3935 }
3936 }
3937 if let (Some(finalized), Some(safe)) = (
3938 checkpoint.finalized_head.as_ref(),
3939 checkpoint.safe_head.as_ref(),
3940 ) {
3941 if finalized.number > safe.number
3942 || (finalized.number == safe.number && finalized.hash != safe.hash)
3943 {
3944 return Err(invalid(
3945 "finalized head is above or conflicts with the safe head".into(),
3946 ));
3947 }
3948 if finalized.number.checked_add(1) == Some(safe.number)
3949 && safe.parent_hash != Some(finalized.hash)
3950 {
3951 return Err(invalid(
3952 "adjacent safe head does not descend from finalized head".into(),
3953 ));
3954 }
3955 }
3956
3957 let mut previous: Option<&DurableBlockJournal> = None;
3958 for entry in &checkpoint.journal {
3959 if entry.block.number > coverage.number
3960 || (entry.block.number == coverage.number && entry.block.hash != coverage.hash)
3961 {
3962 return Err(invalid(format!(
3963 "journal block {}:{:?} lies beyond or conflicts with canonical coverage",
3964 entry.block.number, entry.block.hash
3965 )));
3966 }
3967 if let Some(previous) = previous {
3968 if entry.block.number <= previous.block.number {
3969 return Err(invalid(
3970 "runtime journal block numbers are not strictly increasing".into(),
3971 ));
3972 }
3973 if previous.block.number.checked_add(1) == Some(entry.block.number)
3974 && entry.block.parent_hash.is_some()
3975 && entry.block.parent_hash != Some(previous.block.hash)
3976 {
3977 return Err(invalid(
3978 "adjacent runtime journal blocks are not parent-linked".into(),
3979 ));
3980 }
3981 }
3982 for (label, head) in [
3983 ("safe", checkpoint.safe_head.as_ref()),
3984 ("finalized", checkpoint.finalized_head.as_ref()),
3985 ] {
3986 if let Some(head) = head
3987 && head.number == entry.block.number
3988 && !optional_block_refs_are_compatible(Some(head), Some(&entry.block))
3989 {
3990 return Err(invalid(format!(
3991 "{label} head conflicts with the retained journal at block {}",
3992 head.number
3993 )));
3994 }
3995 }
3996 let mut handler_ids = HashSet::new();
3997 if entry
3998 .handler_ids
3999 .iter()
4000 .any(|handler_id| !handler_ids.insert(handler_id))
4001 {
4002 return Err(invalid(
4003 "runtime journal contains duplicate handler generation ids".into(),
4004 ));
4005 }
4006 previous = Some(entry);
4007 }
4008 if let Some(tail) = checkpoint.journal.last()
4009 && tail.block.number == coverage.number
4010 && !optional_block_refs_are_compatible(Some(&tail.block), Some(coverage))
4011 {
4012 return Err(invalid(format!(
4013 "runtime journal tail conflicts with canonical coverage at block {}",
4014 coverage.number
4015 )));
4016 }
4017 if let Some(tail) = checkpoint.journal.last()
4018 && tail.block.number.checked_add(1) == Some(coverage.number)
4019 && coverage
4020 .parent_hash
4021 .is_some_and(|parent_hash| parent_hash != tail.block.hash)
4022 {
4023 return Err(invalid(format!(
4024 "canonical coverage does not descend from adjacent runtime journal tail at block {}",
4025 tail.block.number
4026 )));
4027 }
4028
4029 if let Some(last_gate_block) = checkpoint.last_gate_block {
4030 if last_gate_block > coverage.number {
4031 return Err(invalid(
4032 "root-gate cursor lies beyond canonical coverage".into(),
4033 ));
4034 }
4035 } else if !checkpoint.tracked_roots.is_empty() {
4036 return Err(invalid(
4037 "root-gate baselines exist without a completed gate cursor".into(),
4038 ));
4039 }
4040 for (address, baseline) in &checkpoint.tracked_roots {
4041 let Some(policy) = checkpoint.tracking.get(address) else {
4042 return Err(invalid(
4043 "root-gate baseline has no corresponding tracking policy".into(),
4044 ));
4045 };
4046 if matches!(policy, TrackingPolicy::Slots { .. }) {
4047 return Err(invalid(
4048 "slot-only tracking cannot carry an account root baseline".into(),
4049 ));
4050 }
4051 if baseline.last_block > coverage.number
4052 || checkpoint
4053 .last_gate_block
4054 .is_some_and(|last_gate| baseline.last_block > last_gate)
4055 {
4056 return Err(invalid(
4057 "root-gate baseline lies beyond the committed gate window".into(),
4058 ));
4059 }
4060 }
4061 Ok(())
4062 }
4063
4064 pub fn track_account(&mut self, address: Address, policy: TrackingPolicy) {
4078 self.tracking.insert(address, policy);
4079 self.tracked_roots.remove(&address);
4080 }
4081
4082 pub fn untrack_account(&mut self, address: Address) -> bool {
4086 self.tracked_roots.remove(&address);
4087 self.tracking.remove(&address).is_some()
4088 }
4089
4090 pub fn set_root_gate_cadence(&mut self, cadence: RootGateCadence) {
4098 self.root_gate_cadence = cadence;
4099 self.last_gate_block = None;
4100 self.touched_since_gate.clear();
4101 }
4102
4103 pub fn root_gate_cadence(&self) -> RootGateCadence {
4105 self.root_gate_cadence
4106 }
4107
4108 pub fn enable_freshness_stamping(&mut self) {
4120 if self.freshness.is_none() {
4121 self.freshness = Some(FreshnessRegistry::new());
4122 }
4123 }
4124
4125 pub fn freshness(&self) -> Option<&FreshnessRegistry> {
4130 self.freshness.as_ref()
4131 }
4132
4133 pub fn freshness_mut(&mut self) -> Option<&mut FreshnessRegistry> {
4138 self.freshness.as_mut()
4139 }
4140
4141 pub fn health(&self) -> CacheHealth {
4143 self.health
4144 }
4145
4146 pub fn metrics(&self) -> CacheMetricsSnapshot {
4148 self.metrics.snapshot()
4149 }
4150
4151 pub fn reset_health(&mut self) {
4161 self.health = CacheHealth::Healthy;
4162 }
4163
4164 fn escalate_trust(&mut self, block: u64) -> Option<Arc<ReactiveReport<N>>> {
4178 let to = match self.health {
4179 CacheHealth::Healthy => CacheHealth::Degraded { since_block: block },
4180 CacheHealth::Degraded { .. } => CacheHealth::Unhealthy { since_block: block },
4181 CacheHealth::Unhealthy { .. } => return None,
4182 };
4183 self.transition_health(to, Some(block))
4184 }
4185
4186 fn transition_health(
4194 &mut self,
4195 to: CacheHealth,
4196 block: Option<u64>,
4197 ) -> Option<Arc<ReactiveReport<N>>> {
4198 if to == self.health {
4199 return None;
4200 }
4201 let from = self.health;
4202 self.health = to;
4203 Some(Arc::new(ReactiveReport::Health(HealthReport {
4204 from,
4205 to,
4206 block,
4207 _network: PhantomData,
4208 })))
4209 }
4210
4211 pub fn register_handler(
4218 &mut self,
4219 handler: Arc<dyn ReactiveHandler<N>>,
4220 ) -> Result<(), RegisterError> {
4221 self.registry.register_handler(handler)
4222 }
4223
4224 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
4232 self.registry.unregister_handler(id)
4233 }
4234
4235 pub fn contains_handler(&self, id: &HandlerId) -> bool {
4237 self.registry.contains_handler(id)
4238 }
4239
4240 pub fn handler_ids(&self) -> Vec<HandlerId> {
4242 self.registry.handler_ids()
4243 }
4244
4245 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
4247 self.registry.handler_interests(id)
4248 }
4249
4250 pub fn last_canonical_block(&self) -> Option<BlockRef> {
4260 self.coverage_head
4261 }
4262
4263 pub fn adopt_canonical_baseline(
4280 &mut self,
4281 baseline: BlockRef,
4282 ) -> Result<(), ReactiveBaselineError> {
4283 self.validate_canonical_baseline_adoption(baseline)?;
4284 if self.adopted_baseline_only().is_some() {
4285 return Ok(());
4286 }
4287
4288 self.coverage_head = Some(baseline);
4289 if self.config.journal_depth > 0 {
4290 self.journal.push_back(BlockJournal {
4291 block: baseline,
4292 inputs: Vec::new(),
4293 applied: Vec::new(),
4294 handler_ids: Vec::new(),
4295 resynced: Vec::new(),
4296 rollback_diffs: Vec::new(),
4297 });
4298 }
4299 Ok(())
4300 }
4301
4302 fn validate_canonical_baseline_adoption(
4303 &self,
4304 baseline: BlockRef,
4305 ) -> Result<(), ReactiveBaselineError> {
4306 if let Some(existing) = self.adopted_baseline_only() {
4307 return if existing == baseline {
4308 Ok(())
4309 } else {
4310 Err(ReactiveBaselineError::ConflictingBaseline {
4311 existing_number: existing.number,
4312 existing_hash: existing.hash,
4313 requested_number: baseline.number,
4314 requested_hash: baseline.hash,
4315 })
4316 };
4317 }
4318 if !self.is_pristine_for_checkpoint_restore() {
4319 return Err(ReactiveBaselineError::ActiveRuntime);
4320 }
4321 Ok(())
4322 }
4323
4324 pub const fn safe_head(&self) -> Option<&BlockRef> {
4326 self.safe_head.as_ref()
4327 }
4328
4329 pub const fn finalized_head(&self) -> Option<&BlockRef> {
4331 self.finalized_head.as_ref()
4332 }
4333
4334 pub fn has_journaled_handler_effects(&self, handler_id: &HandlerId) -> bool {
4342 self.journal
4343 .iter()
4344 .any(|entry| entry.handler_ids.contains(handler_id))
4345 }
4346
4347 pub fn journaled_handler_ids(&self) -> HashSet<HandlerId> {
4354 self.journal
4355 .iter()
4356 .flat_map(|entry| entry.handler_ids.iter().cloned())
4357 .collect()
4358 }
4359
4360 pub fn pending_resyncs(&self) -> &[ResyncRequest] {
4371 &self.pending_resyncs
4372 }
4373
4374 pub fn cancel_pending_resync(&mut self, id: &ResyncId) -> Vec<ResyncRequest> {
4383 self.cancel_pending_resyncs_by_id(std::slice::from_ref(id))
4384 }
4385
4386 pub fn cancel_pending_resyncs_by_id(&mut self, ids: &[ResyncId]) -> Vec<ResyncRequest> {
4393 if ids.is_empty() {
4394 return Vec::new();
4395 }
4396 let ids: HashSet<&ResyncId> = ids.iter().collect();
4397 let mut cancelled = Vec::new();
4398 self.pending_resyncs.retain(|request| {
4399 if ids.contains(&request.id) {
4400 cancelled.push(request.clone());
4401 false
4402 } else {
4403 true
4404 }
4405 });
4406 cancelled
4407 }
4408
4409 pub fn cancel_pending_resyncs(&mut self, address: Address) -> Vec<ResyncRequest> {
4424 let mut cancelled = Vec::new();
4425 self.pending_resyncs.retain_mut(|request| {
4426 let (matching, remaining): (Vec<_>, Vec<_>) = request
4427 .targets
4428 .drain(..)
4429 .partition(|target| resync_target_address(target) == address);
4430 request.targets = remaining;
4431 if !matching.is_empty() {
4432 cancelled.push(ResyncRequest {
4433 id: request.id.clone(),
4434 reason: request.reason.clone(),
4435 block: request.block.clone(),
4436 targets: matching,
4437 priority: request.priority,
4438 });
4439 }
4440 !request.targets.is_empty()
4441 });
4442 cancelled
4443 }
4444
4445 pub fn register_hook(&mut self, hook: Arc<dyn ReactiveHook<N>>) -> Result<(), RegisterError> {
4452 self.hooks.push(hook);
4453 Ok(())
4454 }
4455
4456 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
4458 self.registry.interests()
4459 }
4460
4461 pub fn ingest_batch(
4476 &mut self,
4477 cache: &mut EvmCache,
4478 batch: ReactiveInputBatch<N>,
4479 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4480 let preconfirmation = batch_preconfirmation(&batch)?;
4481 if let Some(flashblock) = preconfirmation.as_ref() {
4482 self.prepare_preconfirmed_branch(cache, flashblock)?;
4483 } else {
4484 self.discard_preconfirmed_branch(cache);
4485 }
4486 let cache_state = EvmCacheStateSnapshot::capture(cache);
4487 let runtime_state = self.checkpoint_state();
4488 let batch_report = match self.ingest_batch_direct(cache, batch) {
4489 Ok(report) => report,
4490 Err(error) => {
4491 cache_state.restore(cache);
4492 self.restore_transaction_state(runtime_state);
4493 return Err(error);
4494 }
4495 };
4496 if let Some(flashblock) = preconfirmation {
4497 self.restore_transaction_state(runtime_state);
4498 if let Some(branch) = self.preconfirmed_branch.as_mut() {
4499 branch.flashblock = flashblock;
4500 }
4501 }
4502 self.dispatch_reports(&batch_report.reports);
4503 let _ = &self.config;
4504 Ok(batch_report)
4505 }
4506
4507 pub fn ingest_batch_with_resync(
4524 &mut self,
4525 cache: &mut EvmCache,
4526 batch: ReactiveInputBatch<N>,
4527 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4528 let preconfirmation = batch_preconfirmation(&batch)?;
4529 if let Some(flashblock) = preconfirmation.as_ref() {
4530 self.prepare_preconfirmed_branch(cache, flashblock)?;
4531 } else {
4532 self.discard_preconfirmed_branch(cache);
4533 }
4534 let cache_state = EvmCacheStateSnapshot::capture(cache);
4535 let runtime_state = self.checkpoint_state();
4536 let batch_report = match self.ingest_batch_with_resync_direct(cache, batch) {
4537 Ok(report) => report,
4538 Err(error) => {
4539 cache_state.restore(cache);
4540 self.restore_transaction_state(runtime_state);
4541 return Err(error);
4542 }
4543 };
4544
4545 if let Some(flashblock) = preconfirmation {
4546 self.restore_transaction_state(runtime_state);
4547 if let Some(branch) = self.preconfirmed_branch.as_mut() {
4548 branch.flashblock = flashblock;
4549 }
4550 }
4551
4552 self.dispatch_reports(&batch_report.reports);
4553 let _ = &self.config;
4554 Ok(batch_report)
4555 }
4556
4557 pub fn active_preconfirmation(&self) -> Option<&FlashblockRef> {
4560 self.preconfirmed_branch
4561 .as_ref()
4562 .map(|branch| &branch.flashblock)
4563 }
4564
4565 pub fn discard_preconfirmation(&mut self, cache: &mut EvmCache) {
4568 self.discard_preconfirmed_branch(cache);
4569 }
4570
4571 fn discard_preconfirmed_branch(&mut self, cache: &mut EvmCache) {
4572 if let Some(branch) = self.preconfirmed_branch.take() {
4573 branch.canonical_cache.restore(cache);
4574 }
4575 }
4576
4577 fn prepare_preconfirmed_branch(
4578 &mut self,
4579 cache: &mut EvmCache,
4580 incoming: &FlashblockRef,
4581 ) -> Result<(), ReactiveError> {
4582 if let Some(active) = self.preconfirmed_branch.as_ref()
4583 && active.flashblock.same_payload(incoming)
4584 {
4585 if let (Some(active_index), Some(incoming_index)) =
4586 (active.flashblock.index, incoming.index)
4587 && incoming_index < active_index
4588 {
4589 return Err(ReactiveError::InvalidInputRecord {
4590 message: format!(
4591 "Flashblock index regressed from {active_index} to {incoming_index}"
4592 ),
4593 });
4594 }
4595 if active.flashblock.index == incoming.index
4596 && active.flashblock.block_hash != incoming.block_hash
4597 {
4598 return Err(ReactiveError::InvalidInputRecord {
4599 message:
4600 "same Flashblock payload/index carried conflicting partial block hashes"
4601 .into(),
4602 });
4603 }
4604 return Ok(());
4605 }
4606
4607 self.discard_preconfirmed_branch(cache);
4608 self.preconfirmed_branch = Some(PreconfirmedBranch {
4609 flashblock: incoming.clone(),
4610 canonical_cache: EvmCacheStateSnapshot::capture(cache),
4611 });
4612 Ok(())
4613 }
4614
4615 fn ingest_batch_with_resync_direct(
4616 &mut self,
4617 cache: &mut EvmCache,
4618 batch: ReactiveInputBatch<N>,
4619 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4620 let mut batch_report = self.ingest_batch_direct(cache, batch)?;
4621 if !batch_report.resyncs.is_empty() {
4622 let resync_report = execute_resync_requests(cache, &batch_report.resyncs);
4623 let unique_requests = resync_report
4627 .requested
4628 .iter()
4629 .map(|request| &request.id)
4630 .collect::<HashSet<_>>()
4631 .len();
4632 self.metrics
4633 .resync_requests
4634 .fetch_add(unique_requests as u64, Ordering::Relaxed);
4635 self.metrics
4636 .resync_failures
4637 .fetch_add(resync_report.failed.len() as u64, Ordering::Relaxed);
4638 self.remove_pending_resyncs(batch_report.resyncs.iter().map(|request| &request.id));
4639 self.record_journal_resync(&resync_report);
4640 batch_report
4641 .reports
4642 .push(Arc::new(ReactiveReport::Resynced(resync_report)));
4643 }
4644 Ok(batch_report)
4645 }
4646
4647 fn ingest_batch_direct(
4648 &mut self,
4649 cache: &mut EvmCache,
4650 batch: ReactiveInputBatch<N>,
4651 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4652 let (records, chain_controls, batch_chain_id) = batch.into_runtime_parts();
4653 if let Some(chain_id) = batch_chain_id
4654 && chain_id != cache.chain_id()
4655 {
4656 return Err(ReactiveError::InvalidInputRecord {
4657 message: format!(
4658 "batch chain id {chain_id} does not match cache chain id {}",
4659 cache.chain_id()
4660 ),
4661 });
4662 }
4663 if !chain_controls.is_empty() && batch_chain_id.is_none() {
4664 return Err(ReactiveError::InvalidChainControl {
4665 message: "chain-control batches require an authoritative batch chain id".into(),
4666 });
4667 }
4668 for (record, _, _) in &records {
4669 record.validated_identity()?;
4670 if let Some(chain_id) = record.context.chain_id
4671 && chain_id != cache.chain_id()
4672 {
4673 return Err(ReactiveError::InvalidInputRecord {
4674 message: format!(
4675 "input chain id {chain_id} does not match cache chain id {}",
4676 cache.chain_id()
4677 ),
4678 });
4679 }
4680 }
4681 let records = sort_scoped_records(dedupe_scoped_records(records)?);
4682
4683 let mut batch_report = ReactiveBatchReport::default();
4684 let mut reports_to_dispatch = Vec::new();
4685 let control_split = validate_control_phase_order(&chain_controls)?;
4686 let (pre_record_controls, post_record_controls) = chain_controls.split_at(control_split);
4687 let pre_record_state =
4688 self.validate_ingest_sequence(pre_record_controls, post_record_controls, &records)?;
4689 self.validate_owner_catchup_against_journal(&pre_record_state, &records)?;
4690 let mut batch_dropped = BatchDroppedCanonical::default();
4691 for control in pre_record_controls {
4692 if let ChainControl::Reorg {
4693 common_ancestor,
4694 old_tip,
4695 ..
4696 } = control
4697 {
4698 batch_dropped.record_explicit(common_ancestor, old_tip);
4699 let drained = self
4700 .journal
4701 .iter()
4702 .filter(|entry| entry.block.number > common_ancestor.number)
4703 .map(|entry| entry.block)
4704 .collect::<Vec<_>>();
4705 batch_dropped.record_drained(&drained);
4706 }
4707 }
4708 let certified_progress_through = post_record_controls
4709 .iter()
4710 .filter_map(canonical_coverage_control_block)
4711 .map(|block| block.number)
4712 .max();
4713 for control in pre_record_controls.iter().cloned() {
4714 self.apply_chain_control(cache, control, &mut batch_report, &mut reports_to_dispatch);
4715 }
4716 let mut touched_addrs: HashSet<Address> = HashSet::new();
4721 let mut canonical_batch_block: Option<u64> = None;
4722
4723 for (record, audience, delivery_scope) in records {
4724 let raw_canonical_block = canonical_record_block(&record).copied();
4725 let canonical_block = raw_canonical_block.map(|block| {
4726 pre_record_state
4727 .resolved_canonical_blocks
4728 .get(&(block.number, block.hash))
4729 .copied()
4730 .unwrap_or(block)
4731 });
4732 let input_ref = record.input_ref();
4733 reports_to_dispatch.push(Arc::new(ReactiveReport::Input(InputReport {
4734 input_ref,
4735 context: record.context.clone(),
4736 provider: record.provider.clone(),
4737 _network: PhantomData,
4738 })));
4739
4740 let recovered_reorg = if delivery_scope.advances_canonical_state() {
4741 if let Some(block) = canonical_block.as_ref() {
4742 let gap_is_certified = delivery_scope == DeliveryScope::CanonicalProgress
4743 && certified_progress_through
4744 .is_some_and(|through| block.number <= through);
4745 let parentless_replacement_is_proven = raw_canonical_block.is_some_and(|raw| {
4746 raw.parent_hash.is_none()
4747 && batch_dropped.covers_implicit_number(raw.number)
4748 });
4749 self.recover_for_canonical_input(
4750 cache,
4751 block,
4752 gap_is_certified,
4753 parentless_replacement_is_proven,
4754 &mut reports_to_dispatch,
4755 )
4756 } else {
4757 None
4758 }
4759 } else {
4760 None
4761 };
4762 let recovered_reorg_for_input = recovered_reorg.is_some();
4763 if let Some(reorg_report) = recovered_reorg {
4764 self.metrics
4765 .reorgs_recovered
4766 .fetch_add(1, Ordering::Relaxed);
4767 remove_canceled_resyncs_from_batch(
4768 &mut batch_report.resyncs,
4769 &reorg_report.canceled_resyncs,
4770 );
4771 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
4772 }
4773
4774 if reorg_signal_block(&record).is_some() {
4780 if delivery_scope.advances_canonical_state()
4781 && let Some(reorg_report) = self.recover_for_reorged_input(
4782 cache,
4783 &record,
4784 &mut batch_dropped,
4785 &mut reports_to_dispatch,
4786 )
4787 {
4788 self.metrics
4789 .reorgs_recovered
4790 .fetch_add(1, Ordering::Relaxed);
4791 remove_canceled_resyncs_from_batch(
4792 &mut batch_report.resyncs,
4793 &reorg_report.canceled_resyncs,
4794 );
4795 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
4796 }
4797 continue;
4798 }
4799
4800 if delivery_scope == DeliveryScope::OwnerCatchup {
4807 self.validate_owner_catchup_record_against_current_journal(&record)?;
4808 }
4809
4810 if delivery_scope.advances_canonical_state()
4811 && let Some(block) = canonical_block.as_ref()
4812 {
4813 canonical_batch_block = Some(block.number);
4816 self.record_journal_input(block, input_ref);
4817 }
4818
4819 if delivery_scope.advances_canonical_state()
4825 && let Some(block) = canonical_block.as_ref()
4826 {
4827 match advance_block_for_canonical_record(cache, &record) {
4828 Some(Ok(())) => {
4829 cache.advance_compact_block(block.number, block.hash, block.timestamp, true)
4830 }
4831 Some(Err(err)) => {
4832 cache.advance_compact_block(
4833 block.number,
4834 block.hash,
4835 block.timestamp,
4836 false,
4837 );
4838 reports_to_dispatch.push(Arc::new(ReactiveReport::Error(
4839 ReactiveErrorReport {
4840 input_ref: Some(input_ref),
4841 message: err.to_string(),
4842 _network: PhantomData,
4843 },
4844 )));
4845 }
4846 None => cache.advance_compact_block(
4847 block.number,
4848 block.hash,
4849 block.timestamp,
4850 !recovered_reorg_for_input,
4851 ),
4852 }
4853 }
4854
4855 let executions = self.execute_handlers(cache, &record, input_ref, &audience)?;
4856 if executions.is_empty() {
4857 continue;
4858 }
4859
4860 reports_to_dispatch.push(Arc::new(ReactiveReport::Decoded(DecodedReport {
4861 input_ref,
4862 handler_ids: executions
4863 .iter()
4864 .map(|execution| execution.handler_id.clone())
4865 .collect(),
4866 _network: PhantomData,
4867 })));
4868
4869 detect_conflicts(input_ref, &executions)?;
4870
4871 let canonical_block_number = delivery_scope
4877 .advances_canonical_state()
4878 .then_some(canonical_block)
4879 .flatten()
4880 .map(|block| block.number);
4881
4882 for execution in executions {
4883 let diff = if execution.state_updates.is_empty() {
4884 StateDiff::default()
4885 } else {
4886 cache.apply_updates(&execution.state_updates)
4887 };
4888
4889 batch_report
4890 .resyncs
4891 .extend(execution.resyncs.iter().cloned());
4892 self.pending_resyncs
4893 .extend(execution.resyncs.iter().cloned());
4894 batch_report
4895 .speculative
4896 .extend(execution.speculative.iter().cloned());
4897
4898 let applied = AppliedReport {
4899 input_ref,
4900 handler_id: execution.handler_id,
4901 quality: execution.quality,
4902 tags: execution.tags,
4903 diff,
4904 state_updates: execution.state_updates,
4905 invalidations: execution.invalidations,
4906 resyncs: execution.resyncs,
4907 speculative: execution.speculative,
4908 hook_signals: execution.hook_signals,
4909 _network: PhantomData,
4910 };
4911 if let (Some(number), Some(registry)) =
4920 (canonical_block_number, self.freshness.as_mut())
4921 {
4922 for change in &applied.diff.slots {
4923 registry.valid_through_slot(change.address, change.slot, number);
4924 }
4925 }
4926
4927 if delivery_scope.advances_canonical_state() {
4935 collect_diff_addresses(&applied.diff, &mut touched_addrs);
4936 }
4937
4938 let report = Arc::new(ReactiveReport::Applied(applied.clone()));
4939 reports_to_dispatch.push(report);
4940 if let Some(block) = canonical_block.as_ref() {
4941 if delivery_scope.advances_canonical_state() {
4942 self.record_journal_applied(block, applied.clone());
4943 } else {
4944 self.record_journal_applied_if_present(block, applied.clone());
4945 }
4946 }
4947 batch_report.applied.push(applied);
4948 }
4949 }
4950
4951 for control in post_record_controls.iter().cloned() {
4956 if let Some(block) = canonical_coverage_control_block(&control) {
4957 canonical_batch_block = Some(
4958 canonical_batch_block.map_or(block.number, |current| current.max(block.number)),
4959 );
4960 }
4961 self.apply_chain_control(cache, control, &mut batch_report, &mut reports_to_dispatch);
4962 }
4963
4964 if self.root_gate_runnable(cache) {
4974 self.touched_since_gate
4975 .extend(touched_addrs.iter().copied());
4976 if self.root_gate_due(canonical_batch_block) {
4977 let accumulated = std::mem::take(&mut self.touched_since_gate);
4978 self.run_root_gate(
4979 cache,
4980 canonical_batch_block,
4981 &accumulated,
4982 &mut batch_report.resyncs,
4983 &mut reports_to_dispatch,
4984 );
4985 self.last_gate_block = canonical_batch_block;
4986 }
4987 } else {
4988 self.touched_since_gate.clear();
4995 }
4996
4997 batch_report.reports = reports_to_dispatch;
4998 Ok(batch_report)
4999 }
5000
5001 fn validate_owner_catchup_against_journal(
5015 &self,
5016 control_state: &ChainControlState,
5017 records: &[(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)],
5018 ) -> Result<(), ReactiveError> {
5019 for (record, _, delivery_scope) in records {
5020 if *delivery_scope != DeliveryScope::OwnerCatchup {
5021 continue;
5022 }
5023 if reorg_signal_block(record).is_some() {
5028 continue;
5029 }
5030 let context_block = canonical_record_block(record).ok_or_else(|| {
5031 ReactiveError::InvalidChainControl {
5032 message: "owner catch-up input has no canonical block identity".into(),
5033 }
5034 })?;
5035 let block = resolve_record_block_payload_metadata(record, *context_block)?;
5036 let invalidated_by_control = control_state
5037 .journal_invalidated_from
5038 .is_some_and(|from| block.number >= from);
5039 let rollbackable = !invalidated_by_control
5040 && self.journal.iter().any(|entry| {
5041 optional_block_refs_are_compatible(Some(&entry.block), Some(&block))
5042 });
5043 if !rollbackable {
5044 return Err(ReactiveError::OwnerCatchupOutsideJournal {
5045 number: block.number,
5046 hash: block.hash,
5047 });
5048 }
5049 }
5050 Ok(())
5051 }
5052
5053 fn validate_owner_catchup_record_against_current_journal(
5054 &self,
5055 record: &ReactiveInputRecord<N>,
5056 ) -> Result<(), ReactiveError> {
5057 let context_block =
5058 canonical_record_block(record).ok_or_else(|| ReactiveError::InvalidChainControl {
5059 message: "owner catch-up input has no canonical block identity".into(),
5060 })?;
5061 let block = resolve_record_block_payload_metadata(record, *context_block)?;
5062 if self
5063 .journal
5064 .iter()
5065 .any(|entry| optional_block_refs_are_compatible(Some(&entry.block), Some(&block)))
5066 {
5067 return Ok(());
5068 }
5069 Err(ReactiveError::OwnerCatchupOutsideJournal {
5070 number: block.number,
5071 hash: block.hash,
5072 })
5073 }
5074
5075 fn root_gate_runnable(&self, cache: &EvmCache) -> bool {
5080 if matches!(self.root_gate_cadence, RootGateCadence::Disabled) {
5081 return false;
5082 }
5083 let has_gated_targets = self
5084 .tracking
5085 .values()
5086 .any(|policy| !matches!(policy, TrackingPolicy::Slots { .. }));
5087 has_gated_targets && cache.account_proof_fetcher().is_some()
5088 }
5089
5090 fn root_gate_due(&self, canonical_block: Option<u64>) -> bool {
5094 let Some(block) = canonical_block else {
5095 return false;
5096 };
5097 match self.root_gate_cadence {
5098 RootGateCadence::Disabled => false,
5099 RootGateCadence::EveryNBlocks(n) => match self.last_gate_block {
5100 None => true,
5101 Some(last) => block >= last.saturating_add(n.get()),
5102 },
5103 }
5104 }
5105
5106 fn run_root_gate(
5132 &mut self,
5133 cache: &EvmCache,
5134 canonical_block: Option<u64>,
5135 touched: &HashSet<Address>,
5136 resyncs: &mut Vec<ResyncRequest>,
5137 reports: &mut Vec<Arc<ReactiveReport<N>>>,
5138 ) {
5139 if self.tracking.is_empty() {
5140 return;
5141 }
5142 let Some(block) = canonical_block else {
5143 return;
5144 };
5145 let Some(fetcher) = cache.account_proof_fetcher().cloned() else {
5146 return;
5147 };
5148
5149 let mut targets: Vec<(Address, bool)> = self
5152 .tracking
5153 .iter()
5154 .filter_map(|(address, policy)| match policy {
5155 TrackingPolicy::Slots { .. } => None,
5156 TrackingPolicy::WholeAccount => Some((*address, true)),
5157 TrackingPolicy::Scalars => Some((*address, false)),
5158 })
5159 .collect();
5160 if targets.is_empty() {
5161 return;
5162 }
5163 targets.sort_by_key(|(address, _)| *address);
5164
5165 let block_id = BlockId::number(block);
5166 let mut probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
5171 targets
5172 .iter()
5173 .map(|&(address, _)| (address, vec![]))
5174 .collect(),
5175 block_id,
5176 )
5177 .into_iter()
5178 .collect();
5179 for (address, whole_account) in targets {
5180 let Some(Ok(proof)) = probes.remove(&address) else {
5181 continue;
5184 };
5185
5186 let baseline = self.tracked_roots.get(&address).cloned();
5187 let Some(baseline) = baseline else {
5188 self.adopt_root(address, block, &proof);
5190 continue;
5191 };
5192
5193 if block <= baseline.last_block {
5198 continue;
5199 }
5200
5201 if whole_account {
5202 if proof.storage_hash == baseline.last_root {
5203 continue;
5205 }
5206 if !touched.contains(&address) {
5208 reports.push(Arc::new(ReactiveReport::CoverageGap(CoverageGapReport {
5210 address,
5211 block,
5212 _network: PhantomData,
5213 })));
5214 self.metrics.coverage_gaps.fetch_add(1, Ordering::Relaxed);
5215 resyncs.push(root_moved_account_resync(
5216 address,
5217 block,
5218 AccountFieldMask {
5219 balance: true,
5220 nonce: true,
5221 code: true,
5222 },
5223 ));
5224 }
5225 self.adopt_root(address, block, &proof);
5227 } else {
5228 let balance_moved = proof.balance != baseline.balance;
5231 let nonce_moved = proof.nonce != baseline.nonce;
5232 let code_moved = proof.code_hash != baseline.code_hash;
5233 if (balance_moved || nonce_moved || code_moved) && !touched.contains(&address) {
5234 resyncs.push(root_moved_account_resync(
5235 address,
5236 block,
5237 AccountFieldMask {
5238 balance: balance_moved,
5239 nonce: nonce_moved,
5240 code: code_moved,
5241 },
5242 ));
5243 }
5244 self.adopt_root(address, block, &proof);
5245 }
5246 }
5247 }
5248
5249 fn adopt_root(&mut self, address: Address, block: u64, proof: &AccountProof) {
5251 self.tracked_roots.insert(
5252 address,
5253 TrackedRoot {
5254 last_root: proof.storage_hash,
5255 last_block: block,
5256 balance: proof.balance,
5257 nonce: proof.nonce,
5258 code_hash: proof.code_hash,
5259 },
5260 );
5261 }
5262
5263 fn execute_handlers(
5264 &self,
5265 cache: &EvmCache,
5266 record: &ReactiveInputRecord<N>,
5267 input_ref: InputRef,
5268 audience: &DeliveryAudience,
5269 ) -> Result<Vec<HandlerExecution>, ReactiveError> {
5270 let mut executions = Vec::new();
5271 let candidates: Vec<_> = match &record.input {
5272 ReactiveInput::Log(log) => self.registry.log_handler_candidates(log),
5273 ReactiveInput::BlockHeader(_)
5274 | ReactiveInput::FullBlock(_)
5275 | ReactiveInput::PendingTxHash(_)
5276 | ReactiveInput::PendingTx(_) => self.registry.handlers().collect(),
5277 };
5278 for registered in candidates {
5279 match audience {
5280 DeliveryAudience::Owners(owners) if !owners.contains(®istered.id) => continue,
5281 DeliveryAudience::AllExcept(excluded) if excluded.contains(®istered.id) => {
5282 continue;
5283 }
5284 DeliveryAudience::All
5285 | DeliveryAudience::Owners(_)
5286 | DeliveryAudience::AllExcept(_) => {}
5287 }
5288 if !registered.matches(&record.input) {
5289 continue;
5290 }
5291
5292 let outcome = registered
5293 .handler
5294 .handle(&record.context, &record.input, cache)
5295 .map_err(|source| ReactiveError::HandlerFailed {
5296 handler_id: registered.id.clone(),
5297 source,
5298 })?;
5299
5300 if let Err(error) =
5301 validate_effects(input_ref, &record.context, ®istered.id, &outcome.effects)
5302 {
5303 if matches!(error, ReactiveError::InvalidPendingEffect { .. }) {
5304 self.metrics
5305 .pending_contamination
5306 .fetch_add(1, Ordering::Relaxed);
5307 }
5308 return Err(error);
5309 }
5310 executions.push(HandlerExecution::from_outcome(
5311 registered.id.clone(),
5312 input_ref,
5313 outcome,
5314 matches!(
5315 record.context.chain_status,
5316 ChainStatus::Preconfirmed { .. }
5317 ),
5318 ));
5319 }
5320 Ok(executions)
5321 }
5322
5323 fn dispatch_reports(&self, reports: &[Arc<ReactiveReport<N>>]) {
5324 for report in reports {
5325 for hook in &self.hooks {
5326 hook.on_report(report.clone());
5327 }
5328 }
5329 }
5330
5331 fn apply_chain_control(
5332 &mut self,
5333 cache: &mut EvmCache,
5334 control: ChainControl,
5335 batch_report: &mut ReactiveBatchReport<N>,
5336 reports: &mut Vec<Arc<ReactiveReport<N>>>,
5337 ) {
5338 match &control {
5339 ChainControl::Safe(block) => set_or_enrich_block_ref(&mut self.safe_head, block),
5340 ChainControl::Finalized(block) => {
5341 set_or_enrich_block_ref(&mut self.finalized_head, block);
5342 }
5343 ChainControl::CanonicalProgress(block)
5344 | ChainControl::Barrier {
5345 block: Some(block), ..
5346 } => {
5347 let preserve_env = self.coverage_head.as_ref().is_some_and(|current| {
5348 optional_block_refs_are_compatible(Some(current), Some(block))
5349 });
5350 cache.advance_compact_block(
5351 block.number,
5352 block.hash,
5353 block.timestamp,
5354 preserve_env,
5355 );
5356 advance_or_enrich_coverage(&mut self.coverage_head, block);
5357 let enriched = self.journal_entry_mut(block).block;
5358 advance_or_enrich_coverage(&mut self.coverage_head, &enriched);
5359 self.trim_journal();
5360 }
5361 ChainControl::Barrier { block: None, .. } => {}
5362 ChainControl::Reorg {
5363 common_ancestor,
5364 old_tip,
5365 ..
5366 } => {
5367 cache.invalidate_cached_block_hashes_from(common_ancestor.number.saturating_add(1));
5368 self.rebase_validation_state_from(common_ancestor.number.saturating_add(1));
5369 let dropped = if let Some(ancestor_index) = self.journal.iter().rposition(|entry| {
5370 entry.block.number == common_ancestor.number
5371 && entry.block.hash == common_ancestor.hash
5372 }) {
5373 self.drain_journal_after(ancestor_index)
5374 } else {
5375 if self
5380 .journal
5381 .front()
5382 .is_none_or(|entry| entry.block.number > common_ancestor.number)
5383 {
5384 reports.extend(
5385 self.warn_under_recovery(common_ancestor.number.saturating_add(1)),
5386 );
5387 }
5388 self.drain_journal_from_number(common_ancestor.number.saturating_add(1))
5389 };
5390
5391 let reorg_report = self
5392 .recover_dropped_journals(cache, dropped, ReorgReason::Explicit)
5393 .unwrap_or_else(|| ReorgReport {
5394 dropped: Some(*old_tip),
5395 dropped_blocks: Vec::new(),
5396 dropped_inputs: Vec::new(),
5397 rollback_updates: Vec::new(),
5398 rollback_diff: StateDiff::default(),
5399 purge_updates: Vec::new(),
5400 purge_diff: StateDiff::default(),
5401 canceled_resyncs: self
5402 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(old_tip)),
5403 reason: ReorgReason::Explicit,
5404 _network: PhantomData,
5405 });
5406 remove_canceled_resyncs_from_batch(
5407 &mut batch_report.resyncs,
5408 &reorg_report.canceled_resyncs,
5409 );
5410 self.metrics
5411 .reorgs_recovered
5412 .fetch_add(1, Ordering::Relaxed);
5413 reports.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5414
5415 if self.safe_head.as_ref().is_some_and(|head| {
5416 head.number > common_ancestor.number
5417 || (head.number == common_ancestor.number
5418 && head.hash != common_ancestor.hash)
5419 }) {
5420 self.safe_head = None;
5421 }
5422 if self.finalized_head.as_ref().is_some_and(|head| {
5423 head.number > common_ancestor.number
5424 || (head.number == common_ancestor.number
5425 && head.hash != common_ancestor.hash)
5426 }) {
5427 self.finalized_head = None;
5428 }
5429 let mut enriched_ancestor = *common_ancestor;
5430 if let Some(entry) = self.journal.iter().find(|entry| {
5431 entry.block.number == common_ancestor.number
5432 && entry.block.hash == common_ancestor.hash
5433 }) {
5434 enrich_block_ref(&mut enriched_ancestor, &entry.block);
5435 }
5436 if let Some(current) = self.coverage_head.as_ref()
5437 && current.number == common_ancestor.number
5438 && current.hash == common_ancestor.hash
5439 {
5440 enrich_block_ref(&mut enriched_ancestor, current);
5441 }
5442 self.coverage_head = Some(enriched_ancestor);
5443 cache.advance_compact_block(
5444 enriched_ancestor.number,
5445 enriched_ancestor.hash,
5446 enriched_ancestor.timestamp,
5447 false,
5448 );
5449 let enriched_ancestor = self.journal_entry_mut(&enriched_ancestor).block;
5450 self.coverage_head = Some(enriched_ancestor);
5451 self.trim_journal();
5452 }
5453 }
5454 reports.push(Arc::new(ReactiveReport::ChainControl(ChainControlReport {
5455 control,
5456 })));
5457 }
5458
5459 fn validate_ingest_sequence(
5460 &self,
5461 pre_record_controls: &[ChainControl],
5462 post_record_controls: &[ChainControl],
5463 records: &[(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)],
5464 ) -> Result<ChainControlState, ReactiveError> {
5465 let mut controls =
5466 Vec::with_capacity(pre_record_controls.len() + post_record_controls.len());
5467 controls.extend_from_slice(pre_record_controls);
5468 controls.extend_from_slice(post_record_controls);
5469 let state = CanonicalSequenceState::new(
5470 self.journal.iter().map(|entry| entry.block).collect(),
5471 self.coverage_head,
5472 self.safe_head,
5473 self.finalized_head,
5474 );
5475 let record_metadata = records
5476 .iter()
5477 .map(|(record, _, scope)| (record, *scope))
5478 .collect::<Vec<_>>();
5479 let validation = validate_canonical_sequence_parts(
5480 &state,
5481 &controls,
5482 &record_metadata,
5483 CanonicalSequenceValidationPolicy::ObserveIncompleteRollback,
5484 )
5485 .map_err(CanonicalSequenceError::into_reactive_error)?;
5486 let mut resolved_canonical_blocks = HashMap::new();
5487 for mutation in validation.mutations() {
5488 if let CanonicalSequenceMutation::Canonical(block) = mutation {
5489 resolved_canonical_blocks
5490 .entry((block.number, block.hash))
5491 .and_modify(|known| enrich_block_ref(known, block))
5492 .or_insert(*block);
5493 }
5494 }
5495 Ok(ChainControlState {
5496 journal_invalidated_from: pre_record_controls
5497 .iter()
5498 .filter_map(|control| match control {
5499 ChainControl::Reorg {
5500 common_ancestor, ..
5501 } => Some(common_ancestor.number.saturating_add(1)),
5502 _ => None,
5503 })
5504 .min(),
5505 resolved_canonical_blocks,
5506 })
5507 }
5508
5509 fn recover_for_canonical_input(
5510 &mut self,
5511 cache: &mut EvmCache,
5512 block: &BlockRef,
5513 gap_is_certified: bool,
5514 parentless_replacement_is_proven: bool,
5515 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
5516 ) -> Option<ReorgReport<N>> {
5517 let latest = self
5518 .coverage_head
5519 .or_else(|| self.journal.back().map(|entry| entry.block))?;
5520
5521 if latest.number == block.number && latest.hash == block.hash {
5522 return None;
5523 }
5524
5525 if self
5526 .journal
5527 .iter()
5528 .any(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
5529 {
5530 return None;
5531 }
5532
5533 if latest.number.checked_add(1) == Some(block.number)
5534 && (block.parent_hash == Some(latest.hash)
5535 || (parentless_replacement_is_proven && block.parent_hash.is_none()))
5536 {
5537 return None;
5538 }
5539
5540 if latest
5541 .number
5542 .checked_add(1)
5543 .is_some_and(|next| block.number > next)
5544 {
5545 if !gap_is_certified {
5552 self.metrics.missed_ranges.fetch_add(1, Ordering::Relaxed);
5553 health_reports.extend(self.escalate_trust(block.number));
5554 health_reports.push(Arc::new(ReactiveReport::MissedBlockRange(
5555 MissedRangeReport {
5556 from: latest.number + 1,
5557 to: block.number - 1,
5558 block: block.number,
5559 _network: PhantomData,
5560 },
5561 )));
5562 }
5563 return None;
5564 }
5565
5566 let (dropped, authenticated_anchor) = if let Some(parent_hash) = block.parent_hash {
5567 if let Some(parent_index) = self.journal.iter().rposition(|entry| {
5568 entry.block.number.checked_add(1) == Some(block.number)
5569 && entry.block.hash == parent_hash
5570 }) {
5571 let parent = self.journal[parent_index].block;
5572 cache.invalidate_cached_block_hashes_from(parent.number.saturating_add(1));
5573 (self.drain_journal_after(parent_index), Some(parent))
5574 } else {
5575 let proven_finalized_anchor = self.finalized_head.filter(|finalized| {
5581 finalized.number.checked_add(1) == Some(block.number)
5582 && parent_hash == finalized.hash
5583 });
5584 let invalidated_from = proven_finalized_anchor
5585 .map_or(0, |finalized| finalized.number.saturating_add(1));
5586 cache.invalidate_cached_block_hashes_from(invalidated_from);
5587 if block.number > 0 {
5588 cache.set_cached_block_hash(block.number.saturating_sub(1), parent_hash);
5592 }
5593 health_reports.extend(self.warn_under_recovery(block.number));
5594 let dropped = if let Some(finalized) = proven_finalized_anchor {
5595 self.drain_journal_from_number(finalized.number.saturating_add(1))
5596 } else {
5597 self.drain_journal_from_number(0)
5598 };
5599 (dropped, proven_finalized_anchor)
5600 }
5601 } else {
5602 cache.invalidate_cached_block_hashes_from(0);
5604 health_reports.extend(self.warn_under_recovery(block.number));
5605 (self.drain_journal_from_number(0), None)
5606 };
5607
5608 self.rebase_validation_state_from(
5609 authenticated_anchor.map_or(0, |anchor| anchor.number.saturating_add(1)),
5610 );
5611 let report = self
5612 .recover_dropped_journals(cache, dropped, ReorgReason::ParentMismatch)
5613 .or_else(|| {
5614 Some(ReorgReport {
5615 dropped: Some(latest),
5616 dropped_blocks: Vec::new(),
5617 dropped_inputs: Vec::new(),
5618 rollback_updates: Vec::new(),
5619 rollback_diff: StateDiff::default(),
5620 purge_updates: Vec::new(),
5621 purge_diff: StateDiff::default(),
5622 canceled_resyncs: self
5623 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&latest)),
5624 reason: ReorgReason::ParentMismatch,
5625 _network: PhantomData,
5626 })
5627 });
5628 self.coverage_head = authenticated_anchor;
5629 for head in [&mut self.safe_head, &mut self.finalized_head] {
5630 if head.is_some_and(|head| {
5631 authenticated_anchor.is_none_or(|anchor| {
5632 head.number > anchor.number
5633 || (head.number == anchor.number && head.hash != anchor.hash)
5634 })
5635 }) {
5636 *head = None;
5637 }
5638 }
5639 if let Some(anchor) = authenticated_anchor {
5640 cache.advance_compact_block(anchor.number, anchor.hash, anchor.timestamp, false);
5641 }
5642 report
5643 }
5644
5645 fn recover_for_reorged_input(
5646 &mut self,
5647 cache: &mut EvmCache,
5648 record: &ReactiveInputRecord<N>,
5649 batch_dropped: &mut BatchDroppedCanonical,
5650 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
5651 ) -> Option<ReorgReport<N>> {
5652 let (incoming_dropped_block, reason) = reorg_signal_block(record)?;
5653 if batch_dropped.contains(&incoming_dropped_block) {
5654 let canceled_resyncs = self
5659 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&incoming_dropped_block));
5660 return (!canceled_resyncs.is_empty()).then(|| ReorgReport {
5661 dropped: Some(incoming_dropped_block),
5662 dropped_blocks: vec![incoming_dropped_block],
5663 dropped_inputs: Vec::new(),
5664 rollback_updates: Vec::new(),
5665 rollback_diff: StateDiff::default(),
5666 purge_updates: Vec::new(),
5667 purge_diff: StateDiff::default(),
5668 canceled_resyncs,
5669 reason,
5670 _network: PhantomData,
5671 });
5672 }
5673 let exact_index = self.journal.iter().position(|entry| {
5674 entry.block.number == incoming_dropped_block.number
5675 && entry.block.hash == incoming_dropped_block.hash
5676 });
5677 let mut dropped_block = exact_index
5678 .map(|index| self.journal[index].block)
5679 .or_else(|| {
5680 self.coverage_head.filter(|known| {
5681 known.number == incoming_dropped_block.number
5682 && known.hash == incoming_dropped_block.hash
5683 })
5684 })
5685 .unwrap_or(incoming_dropped_block);
5686 enrich_block_ref(&mut dropped_block, &incoming_dropped_block);
5687 let replacement_is_known = exact_index.is_none()
5688 && (self.journal.iter().any(|entry| {
5689 entry.block.number == dropped_block.number && entry.block.hash != dropped_block.hash
5690 }) || self.coverage_head.is_some_and(|head| {
5691 head.number == dropped_block.number && head.hash != dropped_block.hash
5692 }));
5693
5694 if replacement_is_known {
5695 let canceled_resyncs =
5699 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
5700 return (!canceled_resyncs.is_empty()).then(|| ReorgReport {
5701 dropped: Some(dropped_block),
5702 dropped_blocks: vec![dropped_block],
5703 dropped_inputs: Vec::new(),
5704 rollback_updates: Vec::new(),
5705 rollback_diff: StateDiff::default(),
5706 purge_updates: Vec::new(),
5707 purge_diff: StateDiff::default(),
5708 canceled_resyncs,
5709 reason,
5710 _network: PhantomData,
5711 });
5712 }
5713
5714 let authenticated_anchor = exact_index.and_then(|index| {
5715 let ancestor_number = dropped_block.number.checked_sub(1)?;
5716 let retained = self
5717 .journal
5718 .iter()
5719 .take(index)
5720 .rev()
5721 .find(|entry| entry.block.number == ancestor_number)
5722 .map(|entry| entry.block);
5723 let synthetic_parent = dropped_block.parent_hash.map(|hash| BlockRef {
5724 number: ancestor_number,
5725 hash,
5726 parent_hash: None,
5727 timestamp: None,
5728 });
5729 let finalized_fallback = self
5730 .finalized_head
5731 .filter(|head| head.number == ancestor_number);
5732 let mut anchor = retained.or(synthetic_parent).or(finalized_fallback)?;
5733 for head in [self.safe_head.as_ref(), self.finalized_head.as_ref()]
5734 .into_iter()
5735 .flatten()
5736 {
5737 if head.number == anchor.number && head.hash == anchor.hash {
5738 enrich_block_ref(&mut anchor, head);
5739 }
5740 }
5741 Some(anchor)
5742 });
5743
5744 cache.invalidate_cached_block_hashes_from(dropped_block.number);
5745 let dropped = if let Some(index) = exact_index {
5746 self.drain_journal_from(index)
5747 } else {
5748 health_reports.extend(self.warn_under_recovery(dropped_block.number));
5749 self.drain_journal_from_number(dropped_block.number)
5750 };
5751 let drained_blocks = dropped.iter().map(|entry| entry.block).collect::<Vec<_>>();
5752 batch_dropped.record_drained(&drained_blocks);
5753 batch_dropped.record_identity(&dropped_block);
5754 self.rebase_validation_state_from(dropped_block.number);
5755
5756 let recovered_journal = !dropped.is_empty();
5757 let report = if !recovered_journal {
5758 let canceled_resyncs =
5759 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
5760 Some(ReorgReport {
5761 dropped: Some(dropped_block),
5762 dropped_blocks: Vec::new(),
5763 dropped_inputs: Vec::new(),
5764 rollback_updates: Vec::new(),
5765 rollback_diff: StateDiff::default(),
5766 purge_updates: Vec::new(),
5767 purge_diff: StateDiff::default(),
5768 canceled_resyncs,
5769 reason,
5770 _network: PhantomData,
5771 })
5772 } else {
5773 self.recover_dropped_journals(cache, dropped, reason)
5774 };
5775
5776 if recovered_journal {
5777 if let Some(anchor) = authenticated_anchor {
5778 self.coverage_head = Some(anchor);
5779 }
5780 let coverage = self.coverage_head;
5781 for head in [&mut self.safe_head, &mut self.finalized_head] {
5782 if head.is_some_and(|head| {
5783 coverage.is_none_or(|coverage| {
5784 head.number > coverage.number
5785 || (head.number == coverage.number && head.hash != coverage.hash)
5786 })
5787 }) {
5788 *head = None;
5789 }
5790 }
5791 }
5792
5793 if recovered_journal
5794 && report.is_some()
5795 && let Some(head) = self.coverage_head
5796 {
5797 cache.advance_compact_block(head.number, head.hash, head.timestamp, false);
5798 }
5799 report
5800 }
5801
5802 fn warn_under_recovery(&mut self, reorg_number: u64) -> Option<Arc<ReactiveReport<N>>> {
5814 let oldest_journaled = self.journal.front().map(|entry| entry.block.number);
5815 tracing::warn!(
5816 reorg_block = reorg_number,
5817 oldest_journaled = ?oldest_journaled,
5818 journal_depth = self.config.journal_depth,
5819 "reactive reorg recovery is incomplete: the reorged block is no longer \
5820 in the journal, so effects from blocks aged out of the journal are \
5821 neither rolled back nor purged (the freshness/validation loop is the \
5822 backstop). Increase ReactiveConfig::journal_depth to recover deeper \
5823 reorgs precisely."
5824 );
5825
5826 self.metrics.deep_reorgs.fetch_add(1, Ordering::Relaxed);
5827
5828 self.escalate_trust(reorg_number)
5829 }
5830
5831 fn record_journal_input(&mut self, block: &BlockRef, input_ref: InputRef) {
5832 advance_or_enrich_coverage(&mut self.coverage_head, block);
5833 let entry = self.journal_entry_mut(block);
5834 let enriched = entry.block;
5835 if !entry.inputs.contains(&input_ref) {
5836 entry.inputs.push(input_ref);
5837 }
5838 advance_or_enrich_coverage(&mut self.coverage_head, &enriched);
5839 self.trim_journal();
5840 }
5841
5842 fn record_journal_applied(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
5843 let entry = self.journal_entry_mut(block);
5844 if !entry.handler_ids.contains(&applied.handler_id) {
5845 entry.handler_ids.push(applied.handler_id.clone());
5846 }
5847 entry.rollback_diffs.push(applied.diff.clone());
5848 entry.applied.push(applied);
5849 self.trim_journal();
5850 }
5851
5852 fn record_journal_applied_if_present(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
5853 let Some(entry) = self
5854 .journal
5855 .iter_mut()
5856 .find(|entry| entry.block.number == block.number && entry.block.hash == block.hash)
5857 else {
5858 return;
5859 };
5860 if !entry.handler_ids.contains(&applied.handler_id) {
5861 entry.handler_ids.push(applied.handler_id.clone());
5862 }
5863 entry.rollback_diffs.push(applied.diff.clone());
5864 entry.applied.push(applied);
5865 }
5866
5867 fn record_journal_resync(&mut self, report: &ResyncReport) {
5868 if report.diff.is_empty() {
5869 return;
5870 }
5871 let Some(block) = single_hash_pinned_resync_block(report) else {
5872 return;
5873 };
5874 let entry = self.journal_entry_mut(&block);
5875 entry.rollback_diffs.push(report.diff.clone());
5876 entry.resynced.push(report.clone());
5877 self.trim_journal();
5878 }
5879
5880 fn journal_entry_mut(&mut self, block: &BlockRef) -> &mut BlockJournal<N> {
5881 if let Some(index) = self
5882 .journal
5883 .iter()
5884 .position(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
5885 {
5886 enrich_block_ref(&mut self.journal[index].block, block);
5887 return &mut self.journal[index];
5888 }
5889
5890 self.journal.push_back(BlockJournal {
5891 block: *block,
5892 inputs: Vec::new(),
5893 applied: Vec::new(),
5894 handler_ids: Vec::new(),
5895 resynced: Vec::new(),
5896 rollback_diffs: Vec::new(),
5897 });
5898 let index = self.journal.len() - 1;
5899 &mut self.journal[index]
5900 }
5901
5902 fn trim_journal(&mut self) {
5903 if self.config.journal_depth == 0 {
5904 self.journal.clear();
5905 return;
5906 }
5907 while self.journal.len() > self.config.journal_depth {
5908 self.journal.pop_front();
5909 }
5910 }
5911
5912 fn drain_journal_after(&mut self, index: usize) -> Vec<BlockJournal<N>> {
5913 self.journal.drain((index + 1)..).collect()
5914 }
5915
5916 fn drain_journal_from(&mut self, index: usize) -> Vec<BlockJournal<N>> {
5917 self.journal.drain(index..).collect()
5918 }
5919
5920 fn drain_journal_from_number(&mut self, number: u64) -> Vec<BlockJournal<N>> {
5921 let Some(index) = self
5922 .journal
5923 .iter()
5924 .position(|entry| entry.block.number >= number)
5925 else {
5926 return Vec::new();
5927 };
5928 self.drain_journal_from(index)
5929 }
5930
5931 fn recover_dropped_journals(
5932 &mut self,
5933 cache: &mut EvmCache,
5934 dropped: Vec<BlockJournal<N>>,
5935 reason: ReorgReason,
5936 ) -> Option<ReorgReport<N>> {
5937 if dropped.is_empty() {
5938 return None;
5939 }
5940
5941 let first_dropped_block = dropped
5942 .iter()
5943 .map(|entry| entry.block.number)
5944 .min()
5945 .expect("non-empty dropped journal set");
5946 self.rebase_validation_state_from(first_dropped_block);
5947 if self
5948 .safe_head
5949 .is_some_and(|head| head.number >= first_dropped_block)
5950 {
5951 self.safe_head = None;
5952 }
5953
5954 let dropped_blocks: Vec<_> = dropped.iter().map(|entry| entry.block).collect();
5955 let dropped_inputs: Vec<_> = dropped
5956 .iter()
5957 .flat_map(|entry| entry.inputs.iter().copied())
5958 .collect();
5959 let canceled_resyncs = self.cancel_resyncs_for_dropped_blocks(&dropped_blocks);
5960 let purge_scopes = purge_scopes_for_dropped_journals(&dropped);
5961 let rollback_updates = rollback_updates_for_dropped_journals(&dropped, &purge_scopes);
5962 let purge_updates: Vec<_> = purge_scopes
5963 .iter()
5964 .map(|(address, scope)| StateUpdate::purge(*address, scope.clone()))
5965 .collect();
5966
5967 let rollback_diff = if rollback_updates.is_empty() {
5968 StateDiff::default()
5969 } else {
5970 cache.apply_updates(&rollback_updates)
5971 };
5972 let purge_diff = if purge_updates.is_empty() {
5973 StateDiff::default()
5974 } else {
5975 cache.apply_updates(&purge_updates)
5976 };
5977 self.coverage_head = self.journal.back().map(|entry| entry.block);
5978
5979 Some(ReorgReport {
5980 dropped: dropped_blocks.first().cloned(),
5981 dropped_blocks,
5982 dropped_inputs,
5983 rollback_updates,
5984 rollback_diff,
5985 purge_updates,
5986 purge_diff,
5987 canceled_resyncs,
5988 reason,
5989 _network: PhantomData,
5990 })
5991 }
5992
5993 fn rebase_validation_state_from(&mut self, first_dropped_block: u64) {
5994 if let Some(freshness) = self.freshness.as_mut() {
5995 freshness.invalidate_valid_through_from(first_dropped_block);
5996 }
5997 self.tracked_roots
5998 .retain(|_, baseline| baseline.last_block < first_dropped_block);
5999 if self
6000 .last_gate_block
6001 .is_some_and(|block| block >= first_dropped_block)
6002 {
6003 self.last_gate_block = self
6004 .tracked_roots
6005 .values()
6006 .map(|baseline| baseline.last_block)
6007 .max();
6008 }
6009 self.touched_since_gate.clear();
6013 }
6014
6015 fn cancel_resyncs_for_dropped_blocks(
6016 &mut self,
6017 dropped_blocks: &[BlockRef],
6018 ) -> Vec<ResyncRequest> {
6019 let mut canceled = Vec::new();
6020 self.pending_resyncs.retain(|request| {
6021 let should_cancel = resync_request_targets_dropped_block(request, dropped_blocks);
6022 if should_cancel {
6023 canceled.push(request.clone());
6024 }
6025 !should_cancel
6026 });
6027 canceled
6028 }
6029
6030 fn remove_pending_resyncs<'a>(&mut self, ids: impl IntoIterator<Item = &'a ResyncId>) {
6031 let ids: HashSet<_> = ids.into_iter().cloned().collect();
6032 self.pending_resyncs
6033 .retain(|request| !ids.contains(&request.id));
6034 }
6035}
6036
6037pub fn validate_canonical_sequence<N: Network>(
6069 state: &CanonicalSequenceState,
6070 batch: &ReactiveInputBatch<N>,
6071) -> Result<CanonicalSequenceValidation, ReactiveError> {
6072 validate_canonical_sequence_diagnostic(state, batch)
6073 .map_err(CanonicalSequenceError::into_reactive_error)
6074}
6075
6076pub fn validate_canonical_sequence_diagnostic<N: Network>(
6091 state: &CanonicalSequenceState,
6092 batch: &ReactiveInputBatch<N>,
6093) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6094 validate_canonical_sequence_internal(
6095 state,
6096 batch,
6097 CanonicalSequenceValidationPolicy::RequireCompleteRollback,
6098 )
6099}
6100
6101pub fn normalize_and_validate_canonical_sequence<N: Network>(
6124 state: &CanonicalSequenceState,
6125 batch: &ReactiveInputBatch<N>,
6126) -> Result<CanonicalSequenceValidation, ReactiveError> {
6127 normalize_and_validate_canonical_sequence_diagnostic(state, batch)
6128 .map_err(CanonicalSequenceError::into_reactive_error)
6129}
6130
6131pub fn normalize_and_validate_canonical_sequence_diagnostic<N: Network>(
6146 state: &CanonicalSequenceState,
6147 batch: &ReactiveInputBatch<N>,
6148) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6149 validate_canonical_sequence_internal(
6150 state,
6151 batch,
6152 CanonicalSequenceValidationPolicy::RequireCompleteRollbackNormalizeCoverage,
6153 )
6154}
6155
6156fn validate_canonical_sequence_internal<N: Network>(
6157 state: &CanonicalSequenceState,
6158 batch: &ReactiveInputBatch<N>,
6159 policy: CanonicalSequenceValidationPolicy,
6160) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6161 let records = batch
6162 .records()
6163 .iter()
6164 .enumerate()
6165 .map(|(index, record)| {
6166 (
6167 record.clone(),
6168 DeliveryAudience::All,
6169 batch
6170 .record_delivery_scope(index)
6171 .expect("enumerated record always has a delivery scope"),
6172 )
6173 })
6174 .collect::<Vec<_>>();
6175 let records = sort_scoped_records(dedupe_scoped_records(records)?);
6176 let records = records
6177 .iter()
6178 .map(|(record, _, scope)| (record, *scope))
6179 .collect::<Vec<_>>();
6180 validate_canonical_sequence_parts(state, batch.chain_controls(), &records, policy)
6181}
6182
6183#[derive(Clone, Copy)]
6184enum CanonicalSequenceValidationPolicy {
6185 RequireCompleteRollback,
6186 RequireCompleteRollbackNormalizeCoverage,
6187 ObserveIncompleteRollback,
6188}
6189
6190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6193#[non_exhaustive]
6194pub enum CanonicalRollbackKind {
6195 Explicit,
6197 Removed,
6199 ImplicitParent,
6201 MissingReplacement,
6203}
6204
6205#[derive(Debug, thiserror::Error)]
6210#[non_exhaustive]
6211pub enum CanonicalSequenceError {
6212 #[error(transparent)]
6214 Invalid(#[from] ReactiveError),
6215 #[error(
6218 "{kind:?} rollback after block {common_ancestor} exceeds retained canonical history starting at {oldest_retained:?}"
6219 )]
6220 IncompleteRollback {
6221 common_ancestor: u64,
6223 oldest_retained: Option<u64>,
6225 kind: CanonicalRollbackKind,
6227 },
6228}
6229
6230#[derive(Clone, Copy, Debug)]
6231struct RequiredReorgAnchor {
6232 number: u64,
6233 block: Option<BlockRef>,
6234 permits_missing_child_parent: bool,
6235 must_be_consumed: bool,
6236}
6237
6238#[derive(Debug)]
6239struct SequenceRewind {
6240 common_ancestor: Option<BlockRef>,
6241 dropped: Vec<BlockRef>,
6242}
6243
6244impl RequiredReorgAnchor {
6245 const fn hash(self) -> Option<B256> {
6246 match self.block {
6247 Some(block) => Some(block.hash),
6248 None => None,
6249 }
6250 }
6251}
6252
6253impl CanonicalSequenceError {
6254 pub const fn requires_history(&self) -> bool {
6257 matches!(self, Self::IncompleteRollback { .. })
6258 }
6259
6260 pub fn into_reactive_error(self) -> ReactiveError {
6262 match self {
6263 Self::Invalid(error) => error,
6264 Self::IncompleteRollback {
6265 common_ancestor,
6266 oldest_retained,
6267 kind,
6268 } => ReactiveError::InvalidChainControl {
6269 message: format!(
6270 "{kind:?} rollback after block {common_ancestor} exceeds retained canonical history starting at {oldest_retained:?}"
6271 ),
6272 },
6273 }
6274 }
6275}
6276
6277impl CanonicalSequenceValidationPolicy {
6278 const fn requires_complete_rollback(self) -> bool {
6279 matches!(
6280 self,
6281 Self::RequireCompleteRollback | Self::RequireCompleteRollbackNormalizeCoverage
6282 )
6283 }
6284
6285 const fn normalizes_coverage(self) -> bool {
6286 matches!(self, Self::RequireCompleteRollbackNormalizeCoverage)
6287 }
6288}
6289
6290fn validate_canonical_sequence_parts<N: Network>(
6291 initial: &CanonicalSequenceState,
6292 controls: &[ChainControl],
6293 records: &[(&ReactiveInputRecord<N>, DeliveryScope)],
6294 policy: CanonicalSequenceValidationPolicy,
6295) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6296 validate_canonical_sequence_snapshot(initial)?;
6297 let control_split = validate_control_phase_order(controls)?;
6298 let (pre_record_controls, post_record_controls) = controls.split_at(control_split);
6299 let mut state = initial.clone();
6300 let mut asserted_blocks = HashMap::<u64, BlockRef>::new();
6301 let mut mutations = Vec::new();
6302 let mut normalized_chain_controls = Vec::with_capacity(controls.len());
6303 let mut batch_dropped = BatchDroppedCanonical::default();
6304 let mut removed_assertions = HashMap::<(u64, B256), BlockRef>::new();
6305 let mut removed_heights_by_hash = HashMap::<B256, u64>::new();
6306 let mut record_proof_control_identities = HashSet::<(u64, B256)>::new();
6307 let rollback_oldest = initial
6308 .retained_canonical_history
6309 .first()
6310 .map(|block| block.number);
6311
6312 for control in pre_record_controls {
6313 normalized_chain_controls.push(control.clone());
6314 validate_sequence_control(&state, control)?;
6315 assert_chain_control_identities(&mut asserted_blocks, control)?;
6316 let ChainControl::Reorg {
6317 common_ancestor,
6318 old_tip,
6319 ..
6320 } = control
6321 else {
6322 unreachable!("phase validation leaves only reorg controls before records")
6323 };
6324 let exact_ancestor = state.retained_canonical_history.iter().any(|block| {
6325 block.number == common_ancestor.number && block.hash == common_ancestor.hash
6326 });
6327 let rollback_horizon_covers_ancestor = state
6328 .retained_canonical_history
6329 .first()
6330 .is_some_and(|oldest| oldest.number <= common_ancestor.number);
6331 if policy.requires_complete_rollback()
6332 && !exact_ancestor
6333 && !rollback_horizon_covers_ancestor
6334 {
6335 return Err(CanonicalSequenceError::IncompleteRollback {
6336 common_ancestor: common_ancestor.number,
6337 oldest_retained: rollback_oldest,
6338 kind: CanonicalRollbackKind::Explicit,
6339 });
6340 }
6341 let dropped = state
6342 .retained_canonical_history
6343 .iter()
6344 .copied()
6345 .filter(|block| block.number > common_ancestor.number)
6346 .collect::<Vec<_>>();
6347 state
6348 .retained_canonical_history
6349 .retain(|block| block.number <= common_ancestor.number);
6350 upsert_sequence_history(&mut state.retained_canonical_history, common_ancestor)?;
6351 let mut enriched_ancestor = *common_ancestor;
6352 if let Some(retained) = state.retained_canonical_history.iter().find(|block| {
6353 block.number == common_ancestor.number && block.hash == common_ancestor.hash
6354 }) {
6355 enrich_block_ref(&mut enriched_ancestor, retained);
6356 }
6357 if let Some(coverage) = state.coverage_head.as_ref()
6358 && coverage.number == common_ancestor.number
6359 && coverage.hash == common_ancestor.hash
6360 {
6361 enrich_block_ref(&mut enriched_ancestor, coverage);
6362 }
6363 upsert_sequence_history(&mut state.retained_canonical_history, &enriched_ancestor)?;
6364 state.coverage_head = Some(enriched_ancestor);
6365 clear_sequence_heads_above(&mut state, &enriched_ancestor);
6366 batch_dropped.record_explicit(common_ancestor, old_tip);
6367 batch_dropped.record_drained(&dropped);
6368 mutations.push(CanonicalSequenceMutation::Rewind {
6369 common_ancestor: Some(enriched_ancestor),
6370 dropped,
6371 });
6372 }
6373 let pre_record_state = state.clone();
6374 let mut required_reorg_anchor = None::<RequiredReorgAnchor>;
6375
6376 for (record, scope) in records {
6377 if !scope.advances_canonical_state() {
6378 continue;
6379 }
6380 if let Some((incoming_dropped_block, _)) = reorg_signal_block(record) {
6381 let incoming_dropped_block =
6382 resolve_record_block_payload_metadata(record, incoming_dropped_block)?;
6383 validate_sequence_matching_metadata(&state, &incoming_dropped_block, "removed record")?;
6384 validate_sequence_adjacent_parent_identity(
6385 &state,
6386 &incoming_dropped_block,
6387 "removed record",
6388 )?;
6389 let mut dropped_block = state
6390 .retained_canonical_history
6391 .iter()
6392 .find(|known| {
6393 known.number == incoming_dropped_block.number
6394 && known.hash == incoming_dropped_block.hash
6395 })
6396 .copied()
6397 .or_else(|| {
6398 state.coverage_head.filter(|known| {
6399 known.number == incoming_dropped_block.number
6400 && known.hash == incoming_dropped_block.hash
6401 })
6402 })
6403 .unwrap_or(incoming_dropped_block);
6404 enrich_block_ref(&mut dropped_block, &incoming_dropped_block);
6405 validate_sequence_implicit_finality(&state, record, None)?;
6406 if dropped_block.number == 0 {
6407 return Err(ReactiveError::InvalidChainControl {
6408 message: "a removed/reorged genesis block has no canonical parent anchor"
6409 .into(),
6410 }
6411 .into());
6412 }
6413 let removed_identity = (dropped_block.number, dropped_block.hash);
6414 if let Some(previous_number) =
6415 removed_heights_by_hash.insert(dropped_block.hash, dropped_block.number)
6416 && previous_number != dropped_block.number
6417 {
6418 return Err(ReactiveError::InvalidChainControl {
6419 message: format!(
6420 "removed hash {:?} is reused at heights {} and {}",
6421 dropped_block.hash, previous_number, dropped_block.number
6422 ),
6423 }
6424 .into());
6425 }
6426 if let Some(previous) = removed_assertions.get_mut(&removed_identity) {
6427 if !optional_block_refs_are_compatible(Some(previous), Some(&dropped_block)) {
6428 return Err(ReactiveError::InvalidChainControl {
6429 message: format!(
6430 "duplicate removed block {}:{:?} carries conflicting metadata",
6431 dropped_block.number, dropped_block.hash
6432 ),
6433 }
6434 .into());
6435 }
6436 enrich_block_ref(previous, &dropped_block);
6437 } else {
6438 removed_assertions.insert(removed_identity, dropped_block);
6439 }
6440 if asserted_blocks
6441 .get(&dropped_block.number)
6442 .is_some_and(|asserted| asserted.hash == dropped_block.hash)
6443 {
6444 return Err(ReactiveError::InvalidChainControl {
6445 message: format!(
6446 "removed block {}:{:?} is asserted canonical by the same envelope",
6447 dropped_block.number, dropped_block.hash
6448 ),
6449 }
6450 .into());
6451 }
6452 if batch_dropped.contains(&dropped_block) {
6453 continue;
6454 }
6455 if let Some(index) = state.retained_canonical_history.iter().position(|block| {
6456 block.number == dropped_block.number && block.hash == dropped_block.hash
6457 }) {
6458 let dropped = state.retained_canonical_history.split_off(index);
6459 batch_dropped.record_drained(&dropped);
6460 let ancestor_number = dropped_block
6461 .number
6462 .checked_sub(1)
6463 .expect("genesis removal was rejected above");
6464 let retained_anchor = state
6465 .retained_canonical_history
6466 .iter()
6467 .rev()
6468 .find(|head| head.number == ancestor_number)
6469 .copied();
6470 let authenticated_anchor = retained_anchor
6471 .or_else(|| {
6472 dropped_block.parent_hash.map(|hash| BlockRef {
6473 number: ancestor_number,
6474 hash,
6475 parent_hash: None,
6476 timestamp: None,
6477 })
6478 })
6479 .or_else(|| {
6480 state
6481 .finalized_head
6482 .filter(|head| head.number == ancestor_number)
6483 });
6484 let authenticated_anchor = authenticated_anchor.map(|mut anchor| {
6485 for head in [state.safe_head.as_ref(), state.finalized_head.as_ref()]
6486 .into_iter()
6487 .flatten()
6488 {
6489 if head.number == anchor.number && head.hash == anchor.hash {
6490 enrich_block_ref(&mut anchor, head);
6491 }
6492 }
6493 anchor
6494 });
6495 required_reorg_anchor = Some(RequiredReorgAnchor {
6496 number: ancestor_number,
6497 block: authenticated_anchor,
6498 permits_missing_child_parent: retained_anchor.is_some(),
6499 must_be_consumed: authenticated_anchor.is_none()
6500 && state.retained_canonical_history.is_empty(),
6501 });
6502 state.coverage_head = authenticated_anchor
6503 .or_else(|| state.retained_canonical_history.last().copied());
6504 if let Some(head) = state.coverage_head {
6505 clear_sequence_heads_above(&mut state, &head);
6506 } else {
6507 state.safe_head = None;
6508 state.finalized_head = None;
6509 }
6510 mutations.push(CanonicalSequenceMutation::Rewind {
6511 common_ancestor: state.coverage_head,
6512 dropped,
6513 });
6514 } else {
6515 let replacement_is_known = state.retained_canonical_history.iter().any(|block| {
6516 block.number == dropped_block.number && block.hash != dropped_block.hash
6517 }) || state.coverage_head.is_some_and(|head| {
6518 head.number == dropped_block.number && head.hash != dropped_block.hash
6519 });
6520 if !replacement_is_known {
6521 if policy.requires_complete_rollback() {
6526 return Err(CanonicalSequenceError::IncompleteRollback {
6527 common_ancestor: dropped_block
6528 .number
6529 .checked_sub(1)
6530 .expect("genesis removal was rejected above"),
6531 oldest_retained: rollback_oldest,
6532 kind: CanonicalRollbackKind::Removed,
6533 });
6534 }
6535 continue;
6536 }
6537 }
6538 continue;
6539 }
6540
6541 let Some(context_block) = canonical_record_block(record) else {
6542 continue;
6543 };
6544 let incoming_block = resolve_record_block_payload_metadata(record, *context_block)?;
6545 if post_record_controls
6546 .iter()
6547 .filter_map(canonical_coverage_control_block)
6548 .any(|asserted| {
6549 asserted.number == incoming_block.number
6550 && asserted.hash == incoming_block.hash
6551 && optional_block_refs_are_compatible(Some(asserted), Some(&incoming_block))
6552 && ((incoming_block.parent_hash.is_none() && asserted.parent_hash.is_some())
6553 || (incoming_block.timestamp.is_none() && asserted.timestamp.is_some()))
6554 })
6555 {
6556 record_proof_control_identities.insert((incoming_block.number, incoming_block.hash));
6557 }
6558 let mut resolved_block = incoming_block;
6559 if let Some(asserted) = asserted_blocks
6560 .get(&incoming_block.number)
6561 .filter(|asserted| asserted.hash == incoming_block.hash)
6562 {
6563 if !optional_block_refs_are_compatible(Some(asserted), Some(&incoming_block)) {
6564 return Err(ReactiveError::InvalidChainControl {
6565 message: format!(
6566 "canonical record {}:{:?} conflicts with the same envelope's asserted metadata",
6567 incoming_block.number, incoming_block.hash
6568 ),
6569 }
6570 .into());
6571 }
6572 enrich_block_ref(&mut resolved_block, asserted);
6573 }
6574 for asserted in post_record_controls
6575 .iter()
6576 .filter_map(chain_control_canonical_assertion)
6577 .filter(|asserted| {
6578 asserted.number == incoming_block.number && asserted.hash == incoming_block.hash
6579 })
6580 {
6581 if !optional_block_refs_are_compatible(Some(&resolved_block), Some(asserted)) {
6582 return Err(ReactiveError::InvalidChainControl {
6583 message: format!(
6584 "canonical record {}:{:?} conflicts with the same envelope's asserted metadata",
6585 incoming_block.number, incoming_block.hash
6586 ),
6587 }
6588 .into());
6589 }
6590 enrich_block_ref(&mut resolved_block, asserted);
6591 }
6592 let replacement_anchor =
6593 required_reorg_anchor.filter(|required| resolved_block.number > required.number);
6594 if resolved_block.parent_hash.is_none()
6595 && replacement_anchor.is_some_and(|anchor| {
6596 anchor.permits_missing_child_parent
6597 && anchor.number.checked_add(1) == Some(resolved_block.number)
6598 })
6599 {
6600 resolved_block.parent_hash = replacement_anchor.and_then(RequiredReorgAnchor::hash);
6601 }
6602 let block = &resolved_block;
6603 if removed_assertions.contains_key(&(block.number, block.hash)) {
6604 return Err(ReactiveError::InvalidChainControl {
6605 message: format!(
6606 "canonical block {}:{:?} is also removed by the same envelope",
6607 block.number, block.hash
6608 ),
6609 }
6610 .into());
6611 }
6612 if let Some(removed_number) = removed_heights_by_hash.get(&block.hash)
6613 && *removed_number != block.number
6614 {
6615 return Err(ReactiveError::InvalidChainControl {
6616 message: format!(
6617 "canonical hash {:?} at height {} is removed at height {} by the same envelope",
6618 block.hash, block.number, removed_number
6619 ),
6620 }
6621 .into());
6622 }
6623 let replacement_proven_by_removal =
6624 validate_replacement_reorg_anchor(replacement_anchor, block, policy, rollback_oldest)?;
6625 if replacement_anchor.is_some() {
6626 required_reorg_anchor = None;
6627 }
6628 validate_sequence_matching_metadata(&state, block, "canonical record")?;
6629 validate_sequence_implicit_finality(&state, record, Some(block))?;
6630 let implicit_replacement_requires_history = if replacement_proven_by_removal {
6631 false
6632 } else {
6633 sequence_implicit_replacement_requires_history(&state, block, policy)?
6634 };
6635 if implicit_replacement_requires_history && policy.requires_complete_rollback() {
6636 return Err(CanonicalSequenceError::IncompleteRollback {
6637 common_ancestor: block.number.saturating_sub(1),
6638 oldest_retained: rollback_oldest,
6639 kind: CanonicalRollbackKind::ImplicitParent,
6640 });
6641 }
6642 assert_canonical_block_identity(&mut asserted_blocks, block, "canonical record")?;
6643 let allow_parentless_extension = replacement_anchor.is_some_and(|anchor| {
6644 anchor.permits_missing_child_parent
6645 && anchor.number.checked_add(1) == Some(block.number)
6646 });
6647 if let Some(rewind) =
6648 apply_sequence_canonical_block(&mut state, block, allow_parentless_extension)?
6649 {
6650 mutations.push(CanonicalSequenceMutation::Rewind {
6651 common_ancestor: rewind.common_ancestor,
6652 dropped: rewind.dropped,
6653 });
6654 }
6655 mutations.push(CanonicalSequenceMutation::Canonical(*block));
6656 }
6657
6658 for control in post_record_controls {
6659 if let Some(block) = chain_control_canonical_assertion(control)
6660 && removed_assertions.contains_key(&(block.number, block.hash))
6661 {
6662 return Err(ReactiveError::InvalidChainControl {
6663 message: format!(
6664 "canonical block {}:{:?} is also removed by the same envelope",
6665 block.number, block.hash
6666 ),
6667 }
6668 .into());
6669 }
6670 if let Some(block) = chain_control_canonical_assertion(control)
6671 && let Some(removed_number) = removed_heights_by_hash.get(&block.hash)
6672 && *removed_number != block.number
6673 {
6674 return Err(ReactiveError::InvalidChainControl {
6675 message: format!(
6676 "canonical hash {:?} at height {} is removed at height {} by the same envelope",
6677 block.hash, block.number, removed_number
6678 ),
6679 }
6680 .into());
6681 }
6682 let replacement_anchor = canonical_coverage_control_block(control).and_then(|block| {
6683 required_reorg_anchor.filter(|required| block.number > required.number)
6684 });
6685 if let Some(block) = canonical_coverage_control_block(control) {
6686 validate_replacement_reorg_anchor(replacement_anchor, block, policy, rollback_oldest)?;
6687 if replacement_anchor.is_some() {
6688 required_reorg_anchor = None;
6689 }
6690 }
6691 assert_chain_control_identities(&mut asserted_blocks, control)?;
6692 let preserves_record_proof =
6693 canonical_coverage_control_block(control).is_some_and(|block| {
6694 record_proof_control_identities.contains(&(block.number, block.hash))
6695 });
6696 if policy.normalizes_coverage()
6697 && !preserves_record_proof
6698 && let Some(block) = canonical_coverage_control_block(control)
6699 && state
6700 .coverage_head
6701 .is_some_and(|head| block.number <= head.number)
6702 {
6703 let is_equal_coverage = state
6704 .coverage_head
6705 .is_some_and(|head| block.number == head.number);
6706 let known = state
6707 .coverage_head
6708 .as_ref()
6709 .filter(|head| head.number == block.number && head.hash == block.hash)
6710 .or_else(|| {
6711 state
6712 .retained_canonical_history
6713 .iter()
6714 .find(|entry| entry.number == block.number && entry.hash == block.hash)
6715 });
6716 if let Some(known) = known
6717 && optional_block_refs_are_compatible(Some(known), Some(block))
6718 && (!is_equal_coverage || !sequence_block_adds_metadata(&state, block))
6719 {
6720 if let ChainControl::Barrier { id, .. } = control {
6721 normalized_chain_controls.push(ChainControl::Barrier {
6722 id: id.clone(),
6723 block: None,
6724 });
6725 }
6726 continue;
6727 }
6728 }
6729 validate_sequence_control(&state, control)?;
6730 normalized_chain_controls.push(control.clone());
6731 match control {
6732 ChainControl::Safe(block) => {
6733 set_or_enrich_block_ref(&mut state.safe_head, block);
6734 mutations.push(CanonicalSequenceMutation::Safe(
6735 state.safe_head.expect("safe head was just installed"),
6736 ));
6737 }
6738 ChainControl::Finalized(block) => {
6739 set_or_enrich_block_ref(&mut state.finalized_head, block);
6740 mutations.push(CanonicalSequenceMutation::Finalized(
6741 state
6742 .finalized_head
6743 .expect("finalized head was just installed"),
6744 ));
6745 }
6746 ChainControl::CanonicalProgress(block)
6747 | ChainControl::Barrier {
6748 block: Some(block), ..
6749 } => {
6750 let allow_parentless_extension = replacement_anchor.is_some_and(|anchor| {
6751 anchor.permits_missing_child_parent
6752 && anchor.number.checked_add(1) == Some(block.number)
6753 }) || (replacement_anchor.is_none()
6754 && block.parent_hash.is_none()
6755 && state
6756 .coverage_head
6757 .is_some_and(|head| head.number.checked_add(1) == Some(block.number)));
6758 if let Some(rewind) =
6759 apply_sequence_canonical_block(&mut state, block, allow_parentless_extension)?
6760 {
6761 mutations.push(CanonicalSequenceMutation::Rewind {
6762 common_ancestor: rewind.common_ancestor,
6763 dropped: rewind.dropped,
6764 });
6765 }
6766 mutations.push(CanonicalSequenceMutation::Canonical(*block));
6767 }
6768 ChainControl::Barrier { block: None, .. } => {}
6769 ChainControl::Reorg { .. } => {
6770 unreachable!("phase validation excludes post-record reorg controls")
6771 }
6772 }
6773 }
6774
6775 if let Some(required) = required_reorg_anchor
6776 && required.must_be_consumed
6777 && policy.requires_complete_rollback()
6778 {
6779 return Err(CanonicalSequenceError::IncompleteRollback {
6780 common_ancestor: required.number,
6781 oldest_retained: rollback_oldest,
6782 kind: CanonicalRollbackKind::MissingReplacement,
6783 });
6784 }
6785
6786 validate_canonical_sequence_snapshot(&state)?;
6787 Ok(CanonicalSequenceValidation {
6788 pre_record_state,
6789 next_state: state,
6790 mutations,
6791 normalized_chain_controls,
6792 })
6793}
6794
6795fn validate_canonical_sequence_snapshot(
6796 state: &CanonicalSequenceState,
6797) -> Result<(), ReactiveError> {
6798 let invalid = |message: String| ReactiveError::InvalidChainControl { message };
6799 let supplied_blocks = state
6800 .retained_canonical_history
6801 .iter()
6802 .chain(state.coverage_head.iter())
6803 .chain(state.safe_head.iter())
6804 .chain(state.finalized_head.iter())
6805 .collect::<Vec<_>>();
6806 validate_known_parent_hash_heights(&supplied_blocks)?;
6807 let mut prior = None::<BlockRef>;
6808 for block in &state.retained_canonical_history {
6809 if let Some(previous) = prior {
6810 if block.number < previous.number {
6811 return Err(invalid(
6812 "retained canonical history is not ordered by block number".into(),
6813 ));
6814 }
6815 if block.number == previous.number {
6816 let qualifier = if optional_block_refs_are_compatible(Some(&previous), Some(block))
6817 {
6818 "duplicate"
6819 } else {
6820 "conflicting"
6821 };
6822 return Err(invalid(format!(
6823 "retained canonical history contains {qualifier} identities at block {}",
6824 block.number
6825 )));
6826 }
6827 if previous.number.checked_add(1) == Some(block.number)
6828 && block.parent_hash.is_some()
6829 && block.parent_hash != Some(previous.hash)
6830 {
6831 return Err(invalid(format!(
6832 "adjacent retained block {}:{:?} does not descend from {}:{:?}",
6833 block.number, block.hash, previous.number, previous.hash
6834 )));
6835 }
6836 }
6837 prior = Some(*block);
6838 }
6839 if state.coverage_head.is_none() && !state.retained_canonical_history.is_empty() {
6840 return Err(invalid(
6841 "retained canonical history requires an authoritative coverage head".into(),
6842 ));
6843 }
6844 if let Some(head) = state.coverage_head.as_ref() {
6845 if let Some(retained) = state
6846 .retained_canonical_history
6847 .iter()
6848 .find(|entry| entry.number == head.number)
6849 && !optional_block_refs_are_compatible(Some(retained), Some(head))
6850 {
6851 return Err(invalid(format!(
6852 "coverage head {}:{:?} conflicts with retained identity {:?}",
6853 head.number, head.hash, retained
6854 )));
6855 }
6856 if state
6857 .retained_canonical_history
6858 .last()
6859 .is_some_and(|retained| retained.number > head.number)
6860 {
6861 return Err(invalid(
6862 "retained canonical history advances beyond the coverage head".into(),
6863 ));
6864 }
6865 if let Some(retained) = state.retained_canonical_history.last()
6866 && retained.number.checked_add(1) == Some(head.number)
6867 && head.parent_hash.is_some()
6868 && head.parent_hash != Some(retained.hash)
6869 {
6870 return Err(invalid(format!(
6871 "coverage head {}:{:?} does not descend from adjacent retained block {}:{:?}",
6872 head.number, head.hash, retained.number, retained.hash
6873 )));
6874 }
6875 }
6876 if let Some(safe) = state.safe_head.as_ref() {
6877 validate_sequence_known_identity(state, safe, "safe")?;
6878 validate_sequence_head_within_coverage(state, safe, "safe")?;
6879 validate_coverage_descends_from_adjacent_head(state.coverage_head.as_ref(), safe, "safe")?;
6880 }
6881 if let Some(finalized) = state.finalized_head.as_ref() {
6882 validate_sequence_known_identity(state, finalized, "finalized")?;
6883 validate_sequence_head_within_coverage(state, finalized, "finalized")?;
6884 validate_coverage_descends_from_adjacent_head(
6885 state.coverage_head.as_ref(),
6886 finalized,
6887 "finalized",
6888 )?;
6889 }
6890 validate_adjacent_finality(state.finalized_head.as_ref(), state.safe_head.as_ref())?;
6891 if let (Some(finalized), Some(safe)) = (state.finalized_head, state.safe_head)
6892 && (finalized.number > safe.number
6893 || (finalized.number == safe.number && finalized.hash != safe.hash))
6894 {
6895 return Err(invalid(
6896 "finalized head cannot advance beyond or conflict with safe head".into(),
6897 ));
6898 }
6899 Ok(())
6900}
6901
6902fn validate_known_parent_hash_heights(blocks: &[&BlockRef]) -> Result<(), ReactiveError> {
6903 let mut heights_by_hash = HashMap::<B256, u64>::with_capacity(blocks.len());
6904 let mut resolved_by_height = HashMap::<u64, BlockRef>::with_capacity(blocks.len());
6905 for block in blocks.iter().copied() {
6906 if let Some(previous_height) = heights_by_hash.insert(block.hash, block.number)
6907 && previous_height != block.number
6908 {
6909 return Err(ReactiveError::InvalidChainControl {
6910 message: format!(
6911 "canonical hash {:?} is reused at heights {} and {}",
6912 block.hash, previous_height, block.number
6913 ),
6914 });
6915 }
6916 if let Some(resolved) = resolved_by_height.get_mut(&block.number) {
6917 if !optional_block_refs_are_compatible(Some(resolved), Some(block)) {
6918 return Err(ReactiveError::InvalidChainControl {
6919 message: format!(
6920 "canonical aliases at height {} carry conflicting identities or metadata",
6921 block.number
6922 ),
6923 });
6924 }
6925 enrich_block_ref(resolved, block);
6926 } else {
6927 resolved_by_height.insert(block.number, *block);
6928 }
6929 }
6930 for child in resolved_by_height.values() {
6931 let Some(parent_hash) = child.parent_hash else {
6932 continue;
6933 };
6934 if let Some(parent_number) = heights_by_hash.get(&parent_hash)
6935 && parent_number.checked_add(1) != Some(child.number)
6936 {
6937 return Err(ReactiveError::InvalidChainControl {
6938 message: format!(
6939 "block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
6940 child.number, child.hash, parent_hash, parent_number
6941 ),
6942 });
6943 }
6944 if let Some(parent_number) = child.number.checked_sub(1)
6945 && let Some(parent) = resolved_by_height.get(&parent_number)
6946 && parent.hash != parent_hash
6947 {
6948 return Err(ReactiveError::InvalidChainControl {
6949 message: format!(
6950 "block {}:{:?} does not descend from supplied adjacent identity {}:{:?}",
6951 child.number, child.hash, parent.number, parent.hash
6952 ),
6953 });
6954 }
6955 }
6956 Ok(())
6957}
6958
6959fn validate_coverage_descends_from_adjacent_head(
6960 coverage: Option<&BlockRef>,
6961 head: &BlockRef,
6962 label: &str,
6963) -> Result<(), ReactiveError> {
6964 let Some(coverage) = coverage else {
6965 return Ok(());
6966 };
6967 if head.number.checked_add(1) == Some(coverage.number)
6968 && coverage
6969 .parent_hash
6970 .is_some_and(|parent| parent != head.hash)
6971 {
6972 return Err(ReactiveError::InvalidChainControl {
6973 message: format!(
6974 "canonical coverage {}:{:?} does not descend from adjacent {label} head {}:{:?}",
6975 coverage.number, coverage.hash, head.number, head.hash
6976 ),
6977 });
6978 }
6979 Ok(())
6980}
6981
6982fn validate_sequence_control(
6983 state: &CanonicalSequenceState,
6984 control: &ChainControl,
6985) -> Result<(), ReactiveError> {
6986 let invalid = |message: String| ReactiveError::InvalidChainControl { message };
6987 match control {
6988 ChainControl::Safe(block) => {
6989 validate_sequence_known_identity(state, block, "safe")?;
6990 validate_sequence_head_within_coverage(state, block, "safe")?;
6991 if let Some(current) = state.safe_head.as_ref()
6992 && (block.number < current.number
6993 || (block.number == current.number
6994 && (block.hash != current.hash
6995 || !optional_block_refs_are_compatible(Some(block), Some(current)))))
6996 {
6997 return Err(invalid(format!(
6998 "safe head {}:{:?} conflicts with current {}:{:?}",
6999 block.number, block.hash, current.number, current.hash
7000 )));
7001 }
7002 if let Some(finalized) = state.finalized_head.as_ref()
7003 && (block.number < finalized.number
7004 || (block.number == finalized.number && block.hash != finalized.hash))
7005 {
7006 return Err(invalid(
7007 "safe head cannot precede or conflict with finalized head".into(),
7008 ));
7009 }
7010 validate_adjacent_finality(state.finalized_head.as_ref(), Some(block))?;
7011 }
7012 ChainControl::Finalized(block) => {
7013 validate_sequence_known_identity(state, block, "finalized")?;
7014 validate_sequence_head_within_coverage(state, block, "finalized")?;
7015 if let Some(current) = state.finalized_head.as_ref()
7016 && (block.number < current.number
7017 || (block.number == current.number
7018 && (block.hash != current.hash
7019 || !optional_block_refs_are_compatible(Some(block), Some(current)))))
7020 {
7021 return Err(invalid(format!(
7022 "finalized head {}:{:?} conflicts with current {}:{:?}",
7023 block.number, block.hash, current.number, current.hash
7024 )));
7025 }
7026 if let Some(safe) = state.safe_head.as_ref()
7027 && (block.number > safe.number
7028 || (block.number == safe.number && block.hash != safe.hash))
7029 {
7030 return Err(invalid(
7031 "finalized head cannot advance beyond or conflict with safe head".into(),
7032 ));
7033 }
7034 validate_adjacent_finality(Some(block), state.safe_head.as_ref())?;
7035 }
7036 ChainControl::CanonicalProgress(block)
7037 | ChainControl::Barrier {
7038 block: Some(block), ..
7039 } => {
7040 validate_sequence_known_identity(state, block, "canonical coverage")?;
7041 if let Some(current) = state.coverage_head.as_ref()
7042 && (block.number < current.number
7043 || (block.number == current.number && block.hash != current.hash))
7044 {
7045 return Err(invalid(format!(
7046 "canonical coverage {}:{:?} conflicts with current {}:{:?}",
7047 block.number, block.hash, current.number, current.hash
7048 )));
7049 }
7050 if let Some(current) = state.coverage_head.as_ref()
7051 && current.number.checked_add(1) == Some(block.number)
7052 && block.parent_hash.is_some()
7053 && block.parent_hash != Some(current.hash)
7054 {
7055 return Err(invalid(format!(
7056 "canonical coverage {}:{:?} does not descend from current {}:{:?}",
7057 block.number, block.hash, current.number, current.hash
7058 )));
7059 }
7060 }
7061 ChainControl::Barrier { block: None, .. } => {}
7062 ChainControl::Reorg {
7063 common_ancestor,
7064 old_tip,
7065 new_tip,
7066 } => {
7067 validate_sequence_known_identity(state, common_ancestor, "reorg common ancestor")?;
7068 validate_reorg_ancestor_against_retained_branch(state, common_ancestor)?;
7069 validate_sequence_known_hash_height(state, old_tip, "reorg old tip")?;
7070 validate_sequence_known_hash_height(state, new_tip, "reorg new tip")?;
7071 validate_sequence_known_parent_height(state, old_tip, "reorg old tip")?;
7072 validate_sequence_known_parent_height(state, new_tip, "reorg new tip")?;
7073 validate_sequence_adjacent_parent_identity(state, old_tip, "reorg old tip")?;
7074 if let Some(current) = state.coverage_head.as_ref()
7075 && (old_tip.number != current.number
7076 || old_tip.hash != current.hash
7077 || !optional_block_refs_are_compatible(Some(old_tip), Some(current)))
7078 {
7079 return Err(invalid(format!(
7080 "reorg old tip {}:{:?} does not exactly match current metadata {}:{:?}",
7081 old_tip.number, old_tip.hash, current.number, current.hash
7082 )));
7083 }
7084 if common_ancestor.number > old_tip.number || common_ancestor.number > new_tip.number {
7085 return Err(invalid(
7086 "reorg common ancestor cannot be above either branch tip".into(),
7087 ));
7088 }
7089 if common_ancestor.number == old_tip.number || common_ancestor.number == new_tip.number
7090 {
7091 return Err(invalid(
7092 "reorg must replace non-empty old and new branches above the common ancestor"
7093 .into(),
7094 ));
7095 }
7096 if old_tip.number == new_tip.number && old_tip.hash == new_tip.hash {
7097 return Err(invalid(
7098 "reorg old and new tips cannot have the same canonical identity".into(),
7099 ));
7100 }
7101 for (label, tip) in [("old", old_tip), ("new", new_tip)] {
7102 if common_ancestor.number.checked_add(1) == Some(tip.number)
7103 && tip.parent_hash != Some(common_ancestor.hash)
7104 {
7105 return Err(invalid(format!(
7106 "reorg {label} tip does not descend from the common ancestor"
7107 )));
7108 }
7109 }
7110 if let Some(finalized) = state.finalized_head.as_ref()
7111 && (common_ancestor.number < finalized.number
7112 || (common_ancestor.number == finalized.number
7113 && common_ancestor.hash != finalized.hash))
7114 {
7115 return Err(invalid(
7116 "reorg would cross or conflict with the finalized head".into(),
7117 ));
7118 }
7119 }
7120 }
7121 Ok(())
7122}
7123
7124fn validate_sequence_known_identity(
7125 state: &CanonicalSequenceState,
7126 block: &BlockRef,
7127 label: &str,
7128) -> Result<(), ReactiveError> {
7129 validate_sequence_known_hash_height(state, block, label)?;
7130 validate_sequence_known_parent_height(state, block, label)?;
7131 let known = state
7132 .coverage_head
7133 .as_ref()
7134 .filter(|head| head.number == block.number)
7135 .or_else(|| {
7136 state
7137 .retained_canonical_history
7138 .iter()
7139 .find(|entry| entry.number == block.number)
7140 });
7141 if let Some(known) = known
7142 && !optional_block_refs_are_compatible(Some(known), Some(block))
7143 {
7144 return Err(ReactiveError::InvalidChainControl {
7145 message: format!(
7146 "{label} block {}:{:?} conflicts with known canonical block {:?}",
7147 block.number, block.hash, known
7148 ),
7149 });
7150 }
7151 Ok(())
7152}
7153
7154fn validate_sequence_known_parent_height(
7155 state: &CanonicalSequenceState,
7156 block: &BlockRef,
7157 label: &str,
7158) -> Result<(), ReactiveError> {
7159 let Some(parent_hash) = block.parent_hash else {
7160 return Ok(());
7161 };
7162 let known_parent = state
7163 .retained_canonical_history
7164 .iter()
7165 .chain(state.coverage_head.iter())
7166 .chain(state.safe_head.iter())
7167 .chain(state.finalized_head.iter())
7168 .find(|known| known.hash == parent_hash);
7169 if let Some(parent) = known_parent
7170 && parent.number.checked_add(1) != Some(block.number)
7171 {
7172 return Err(ReactiveError::InvalidChainControl {
7173 message: format!(
7174 "{label} block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
7175 block.number, block.hash, parent.hash, parent.number
7176 ),
7177 });
7178 }
7179 Ok(())
7180}
7181
7182fn validate_sequence_head_within_coverage(
7183 state: &CanonicalSequenceState,
7184 block: &BlockRef,
7185 label: &str,
7186) -> Result<(), ReactiveError> {
7187 let Some(coverage) = state.coverage_head.as_ref() else {
7188 return Err(ReactiveError::InvalidChainControl {
7189 message: format!("{label} head requires an authoritative coverage head"),
7190 });
7191 };
7192 if block.number > coverage.number
7193 || (block.number == coverage.number
7194 && !optional_block_refs_are_compatible(Some(block), Some(coverage)))
7195 {
7196 return Err(ReactiveError::InvalidChainControl {
7197 message: format!(
7198 "{label} head {}:{:?} advances beyond or conflicts with coverage {}:{:?}",
7199 block.number, block.hash, coverage.number, coverage.hash
7200 ),
7201 });
7202 }
7203 Ok(())
7204}
7205
7206fn validate_sequence_matching_metadata(
7207 state: &CanonicalSequenceState,
7208 block: &BlockRef,
7209 label: &str,
7210) -> Result<(), ReactiveError> {
7211 validate_sequence_known_hash_height(state, block, label)?;
7212 validate_sequence_known_parent_height(state, block, label)?;
7213 let known = state
7214 .coverage_head
7215 .as_ref()
7216 .filter(|head| head.number == block.number && head.hash == block.hash)
7217 .or_else(|| {
7218 state
7219 .retained_canonical_history
7220 .iter()
7221 .find(|entry| entry.number == block.number && entry.hash == block.hash)
7222 });
7223 if let Some(known) = known
7224 && !optional_block_refs_are_compatible(Some(known), Some(block))
7225 {
7226 return Err(ReactiveError::InvalidChainControl {
7227 message: format!(
7228 "{label} block {}:{:?} carries metadata conflicting with known canonical block {:?}",
7229 block.number, block.hash, known
7230 ),
7231 });
7232 }
7233 Ok(())
7234}
7235
7236fn validate_sequence_known_hash_height(
7237 state: &CanonicalSequenceState,
7238 block: &BlockRef,
7239 label: &str,
7240) -> Result<(), ReactiveError> {
7241 let known = state
7242 .retained_canonical_history
7243 .iter()
7244 .chain(state.coverage_head.iter())
7245 .chain(state.safe_head.iter())
7246 .chain(state.finalized_head.iter())
7247 .find(|known| known.hash == block.hash);
7248 if let Some(known) = known
7249 && known.number != block.number
7250 {
7251 return Err(ReactiveError::InvalidChainControl {
7252 message: format!(
7253 "{label} block {}:{:?} reuses a canonical hash already known at height {}",
7254 block.number, block.hash, known.number
7255 ),
7256 });
7257 }
7258 Ok(())
7259}
7260
7261fn validate_reorg_ancestor_against_retained_branch(
7262 state: &CanonicalSequenceState,
7263 ancestor: &BlockRef,
7264) -> Result<(), ReactiveError> {
7265 let adjacent_number = ancestor.number.checked_add(1);
7266 for retained in state
7267 .retained_canonical_history
7268 .iter()
7269 .chain(state.coverage_head.iter())
7270 .chain(state.safe_head.iter())
7271 .chain(state.finalized_head.iter())
7272 {
7273 if Some(retained.number) == adjacent_number
7274 && retained
7275 .parent_hash
7276 .is_some_and(|parent| parent != ancestor.hash)
7277 {
7278 return Err(ReactiveError::InvalidChainControl {
7279 message: format!(
7280 "reorg common ancestor {}:{:?} conflicts with retained child {}:{:?} parent {:?}",
7281 ancestor.number,
7282 ancestor.hash,
7283 retained.number,
7284 retained.hash,
7285 retained.parent_hash
7286 ),
7287 });
7288 }
7289 if retained.parent_hash == Some(ancestor.hash) && Some(retained.number) != adjacent_number {
7290 return Err(ReactiveError::InvalidChainControl {
7291 message: format!(
7292 "reorg common ancestor {}:{:?} is named as the non-adjacent parent of retained block {}:{:?}",
7293 ancestor.number, ancestor.hash, retained.number, retained.hash
7294 ),
7295 });
7296 }
7297 }
7298 Ok(())
7299}
7300
7301fn validate_sequence_adjacent_parent_identity(
7302 state: &CanonicalSequenceState,
7303 block: &BlockRef,
7304 label: &str,
7305) -> Result<(), ReactiveError> {
7306 let Some(parent_hash) = block.parent_hash else {
7307 return Ok(());
7308 };
7309 let Some(parent_number) = block.number.checked_sub(1) else {
7310 return Ok(());
7311 };
7312 let known_parent = state
7313 .retained_canonical_history
7314 .iter()
7315 .chain(state.coverage_head.iter())
7316 .chain(state.safe_head.iter())
7317 .chain(state.finalized_head.iter())
7318 .find(|known| known.number == parent_number);
7319 if let Some(known_parent) = known_parent
7320 && known_parent.hash != parent_hash
7321 {
7322 return Err(ReactiveError::InvalidChainControl {
7323 message: format!(
7324 "{label} block {}:{:?} names parent {:?}, which conflicts with known adjacent block {}:{:?}",
7325 block.number, block.hash, parent_hash, known_parent.number, known_parent.hash
7326 ),
7327 });
7328 }
7329 Ok(())
7330}
7331
7332fn sequence_block_adds_metadata(state: &CanonicalSequenceState, incoming: &BlockRef) -> bool {
7333 state
7334 .coverage_head
7335 .iter()
7336 .chain(state.retained_canonical_history.iter())
7337 .filter(|known| known.number == incoming.number && known.hash == incoming.hash)
7338 .any(|known| {
7339 (known.parent_hash.is_none() && incoming.parent_hash.is_some())
7340 || (known.timestamp.is_none() && incoming.timestamp.is_some())
7341 })
7342}
7343
7344fn validate_sequence_implicit_finality<N: Network>(
7345 state: &CanonicalSequenceState,
7346 record: &ReactiveInputRecord<N>,
7347 resolved_canonical_block: Option<&BlockRef>,
7348) -> Result<(), ReactiveError> {
7349 let Some(finalized) = state.finalized_head.as_ref() else {
7350 return Ok(());
7351 };
7352 if let Some((dropped, _)) = reorg_signal_block(record) {
7353 if dropped.number <= finalized.number {
7354 return Err(ReactiveError::InvalidChainControl {
7355 message: format!(
7356 "implicit reorg at {}:{:?} would cross finalized head {}:{:?}",
7357 dropped.number, dropped.hash, finalized.number, finalized.hash
7358 ),
7359 });
7360 }
7361 return Ok(());
7362 }
7363 let Some(block) = resolved_canonical_block.or_else(|| canonical_record_block(record)) else {
7364 return Ok(());
7365 };
7366 let Some(latest) = state.coverage_head.as_ref() else {
7367 return Ok(());
7368 };
7369 if (block.number == latest.number && block.hash == latest.hash)
7370 || state
7371 .retained_canonical_history
7372 .iter()
7373 .any(|entry| entry.number == block.number && entry.hash == block.hash)
7374 || (latest.number.checked_add(1) == Some(block.number)
7375 && block.parent_hash == Some(latest.hash))
7376 || latest
7377 .number
7378 .checked_add(1)
7379 .is_some_and(|next| block.number > next)
7380 {
7381 return Ok(());
7382 }
7383 let crosses_finalized = if block.number <= finalized.number {
7384 true
7385 } else if let Some(parent_hash) = block.parent_hash {
7386 if finalized.number.checked_add(1) == Some(block.number) && parent_hash == finalized.hash {
7387 false
7388 } else if let Some(parent_index) =
7389 state.retained_canonical_history.iter().rposition(|entry| {
7390 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
7391 })
7392 {
7393 state
7394 .retained_canonical_history
7395 .iter()
7396 .skip(parent_index + 1)
7397 .any(|entry| entry.number <= finalized.number)
7398 } else {
7399 true
7400 }
7401 } else {
7402 true
7403 };
7404 if crosses_finalized {
7405 return Err(ReactiveError::InvalidChainControl {
7406 message: format!(
7407 "canonical input {}:{:?} would replace finalized head {}:{:?}",
7408 block.number, block.hash, finalized.number, finalized.hash
7409 ),
7410 });
7411 }
7412 Ok(())
7413}
7414
7415fn validate_required_reorg_anchor(
7416 required: Option<RequiredReorgAnchor>,
7417 block: &BlockRef,
7418) -> Result<(), ReactiveError> {
7419 let Some(required) = required else {
7420 return Ok(());
7421 };
7422 let ancestor_hash = required.hash();
7423 let restores_ancestor =
7424 block.number == required.number && ancestor_hash.is_some_and(|hash| block.hash == hash);
7425 let replaces_removed_child = required.number.checked_add(1) == Some(block.number)
7426 && ancestor_hash.is_some()
7427 && (block.parent_hash == ancestor_hash
7428 || (block.parent_hash.is_none() && required.permits_missing_child_parent));
7429 if restores_ancestor || replaces_removed_child {
7430 return Ok(());
7431 }
7432 Err(ReactiveError::InvalidChainControl {
7433 message: format!(
7434 "canonical replacement {}:{:?} does not prove the removed tip's parent at block {}",
7435 block.number, block.hash, required.number
7436 ),
7437 })
7438}
7439
7440fn validate_replacement_reorg_anchor(
7441 required: Option<RequiredReorgAnchor>,
7442 block: &BlockRef,
7443 policy: CanonicalSequenceValidationPolicy,
7444 oldest_retained: Option<u64>,
7445) -> Result<bool, CanonicalSequenceError> {
7446 let Some(required) = required else {
7447 return Ok(false);
7448 };
7449 match validate_required_reorg_anchor(Some(required), block) {
7450 Ok(()) => Ok(true),
7451 Err(error) if required.block.is_some() => Err(error.into()),
7452 Err(_) if policy.requires_complete_rollback() => {
7453 Err(CanonicalSequenceError::IncompleteRollback {
7454 common_ancestor: required.number,
7455 oldest_retained,
7456 kind: CanonicalRollbackKind::MissingReplacement,
7457 })
7458 }
7459 Err(_) => Ok(false),
7460 }
7461}
7462
7463fn apply_sequence_canonical_block(
7464 state: &mut CanonicalSequenceState,
7465 block: &BlockRef,
7466 allow_parentless_adjacent_extension: bool,
7467) -> Result<Option<SequenceRewind>, ReactiveError> {
7468 let latest = state.coverage_head;
7469 let already_known = state
7470 .retained_canonical_history
7471 .iter()
7472 .any(|entry| entry.number == block.number && entry.hash == block.hash);
7473 let repeats_tip =
7474 latest.is_some_and(|head| head.number == block.number && head.hash == block.hash);
7475 let extends_tip = latest.is_some_and(|head| {
7476 head.number.checked_add(1) == Some(block.number)
7477 && (block.parent_hash == Some(head.hash)
7478 || (allow_parentless_adjacent_extension && block.parent_hash.is_none()))
7479 });
7480 let forward_gap = latest.is_some_and(|head| {
7481 head.number
7482 .checked_add(1)
7483 .is_some_and(|next| block.number > next)
7484 });
7485 let mut rewind = None;
7486
7487 if latest.is_some() && !already_known && !repeats_tip && !extends_tip && !forward_gap {
7488 let retained_parent = block.parent_hash.and_then(|parent_hash| {
7489 state
7490 .retained_canonical_history
7491 .iter()
7492 .rposition(|entry| {
7493 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
7494 })
7495 .map(|index| (index, state.retained_canonical_history[index]))
7496 });
7497 let finalized_parent = block.parent_hash.and_then(|parent_hash| {
7498 state.finalized_head.filter(|finalized| {
7499 finalized.number.checked_add(1) == Some(block.number)
7500 && finalized.hash == parent_hash
7501 })
7502 });
7503 let (common_ancestor, dropped) = if let Some((parent_index, parent)) = retained_parent {
7504 let dropped = state.retained_canonical_history.split_off(parent_index + 1);
7505 (Some(parent), dropped)
7506 } else if let Some(finalized) = finalized_parent {
7507 let dropped = state
7508 .retained_canonical_history
7509 .iter()
7510 .position(|entry| entry.number > finalized.number)
7511 .map_or_else(Vec::new, |index| {
7512 state.retained_canonical_history.split_off(index)
7513 });
7514 (Some(finalized), dropped)
7515 } else {
7516 (None, std::mem::take(&mut state.retained_canonical_history))
7522 };
7523 state.coverage_head = common_ancestor;
7524 if let Some(common_ancestor) = common_ancestor {
7525 clear_sequence_heads_above(state, &common_ancestor);
7526 } else {
7527 state.safe_head = None;
7528 state.finalized_head = None;
7529 }
7530 rewind = Some(SequenceRewind {
7531 common_ancestor,
7532 dropped,
7533 });
7534 }
7535 upsert_sequence_history(&mut state.retained_canonical_history, block)?;
7536 advance_or_enrich_coverage(&mut state.coverage_head, block);
7537 Ok(rewind)
7538}
7539
7540fn sequence_implicit_replacement_requires_history(
7541 state: &CanonicalSequenceState,
7542 block: &BlockRef,
7543 policy: CanonicalSequenceValidationPolicy,
7544) -> Result<bool, ReactiveError> {
7545 let Some(latest) = state.coverage_head else {
7546 return Ok(false);
7547 };
7548 let already_known = state
7549 .retained_canonical_history
7550 .iter()
7551 .any(|entry| entry.number == block.number && entry.hash == block.hash);
7552 let repeats_tip = block.number == latest.number && block.hash == latest.hash;
7553 let extends_tip = latest.number.checked_add(1) == Some(block.number)
7554 && block.parent_hash == Some(latest.hash);
7555 let forward_gap = latest
7556 .number
7557 .checked_add(1)
7558 .is_some_and(|next| block.number > next);
7559 if already_known || repeats_tip || extends_tip || forward_gap {
7560 return Ok(false);
7561 }
7562 let Some(parent_hash) = block.parent_hash else {
7563 if policy.requires_complete_rollback() {
7564 return Err(ReactiveError::InvalidChainControl {
7565 message: format!(
7566 "implicit canonical replacement {}:{:?} must identify its parent",
7567 block.number, block.hash
7568 ),
7569 });
7570 }
7571 return Ok(true);
7572 };
7573 let known_adjacent_parent = block.number.checked_sub(1).and_then(|parent_number| {
7574 state
7575 .retained_canonical_history
7576 .iter()
7577 .chain(state.coverage_head.iter())
7578 .chain(state.safe_head.iter())
7579 .chain(state.finalized_head.iter())
7580 .find(|known| known.number == parent_number)
7581 });
7582 if let Some(known_parent) = known_adjacent_parent
7583 && known_parent.hash != parent_hash
7584 && policy.requires_complete_rollback()
7585 {
7586 return Err(ReactiveError::InvalidChainControl {
7587 message: format!(
7588 "implicit canonical replacement {}:{:?} names parent {:?}, which conflicts with known adjacent block {}:{:?}",
7589 block.number, block.hash, parent_hash, known_parent.number, known_parent.hash
7590 ),
7591 });
7592 }
7593 let retained_parent = state.retained_canonical_history.iter().any(|entry| {
7594 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
7595 });
7596 let finalized_parent = state.finalized_head.is_some_and(|finalized| {
7597 finalized.number.checked_add(1) == Some(block.number) && parent_hash == finalized.hash
7598 });
7599 Ok(!retained_parent && !finalized_parent)
7600}
7601
7602fn upsert_sequence_history(
7603 history: &mut Vec<BlockRef>,
7604 block: &BlockRef,
7605) -> Result<(), ReactiveError> {
7606 if let Some(existing) = history
7607 .iter_mut()
7608 .find(|entry| entry.number == block.number)
7609 {
7610 if existing.hash != block.hash {
7611 return Err(ReactiveError::InvalidChainControl {
7612 message: format!(
7613 "canonical block {}:{:?} conflicts with retained identity {:?}",
7614 block.number, block.hash, existing
7615 ),
7616 });
7617 }
7618 if !optional_block_refs_are_compatible(Some(existing), Some(block)) {
7619 return Err(ReactiveError::InvalidChainControl {
7620 message: format!(
7621 "canonical block {}:{:?} carries conflicting retained metadata",
7622 block.number, block.hash
7623 ),
7624 });
7625 }
7626 enrich_block_ref(existing, block);
7627 } else {
7628 history.push(*block);
7629 history.sort_by_key(|entry| entry.number);
7630 }
7631 Ok(())
7632}
7633
7634fn clear_sequence_heads_above(state: &mut CanonicalSequenceState, ancestor: &BlockRef) {
7635 if state.safe_head.as_ref().is_some_and(|head| {
7636 head.number > ancestor.number
7637 || (head.number == ancestor.number && head.hash != ancestor.hash)
7638 }) {
7639 state.safe_head = None;
7640 }
7641 if state.finalized_head.as_ref().is_some_and(|head| {
7642 head.number > ancestor.number
7643 || (head.number == ancestor.number && head.hash != ancestor.hash)
7644 }) {
7645 state.finalized_head = None;
7646 }
7647}
7648
7649fn validate_control_phase_order(controls: &[ChainControl]) -> Result<usize, ReactiveError> {
7650 let split = controls
7651 .iter()
7652 .position(|control| !matches!(control, ChainControl::Reorg { .. }))
7653 .unwrap_or(controls.len());
7654 if controls[split..]
7655 .iter()
7656 .any(|control| matches!(control, ChainControl::Reorg { .. }))
7657 {
7658 return Err(ReactiveError::InvalidChainControl {
7659 message: "reorg controls must precede records and all post-record controls in a batch"
7660 .into(),
7661 });
7662 }
7663 Ok(split)
7664}
7665
7666fn canonical_coverage_control_block(control: &ChainControl) -> Option<&BlockRef> {
7667 match control {
7668 ChainControl::CanonicalProgress(block)
7669 | ChainControl::Barrier {
7670 block: Some(block), ..
7671 } => Some(block),
7672 ChainControl::Reorg { .. }
7673 | ChainControl::Safe(_)
7674 | ChainControl::Finalized(_)
7675 | ChainControl::Barrier { block: None, .. } => None,
7676 }
7677}
7678
7679fn chain_control_canonical_assertion(control: &ChainControl) -> Option<&BlockRef> {
7680 match control {
7681 ChainControl::Safe(block)
7682 | ChainControl::Finalized(block)
7683 | ChainControl::CanonicalProgress(block)
7684 | ChainControl::Barrier {
7685 block: Some(block), ..
7686 } => Some(block),
7687 ChainControl::Reorg { .. } | ChainControl::Barrier { block: None, .. } => None,
7688 }
7689}
7690
7691fn assert_chain_control_identities(
7692 asserted_blocks: &mut HashMap<u64, BlockRef>,
7693 control: &ChainControl,
7694) -> Result<(), ReactiveError> {
7695 match control {
7696 ChainControl::Safe(block)
7697 | ChainControl::Finalized(block)
7698 | ChainControl::CanonicalProgress(block)
7699 | ChainControl::Barrier {
7700 block: Some(block), ..
7701 } => assert_canonical_block_identity(asserted_blocks, block, "chain control"),
7702 ChainControl::Barrier { block: None, .. } => Ok(()),
7703 ChainControl::Reorg {
7704 common_ancestor,
7705 new_tip,
7706 ..
7707 } => {
7708 asserted_blocks.retain(|number, _| *number <= common_ancestor.number);
7709 assert_canonical_block_identity(
7710 asserted_blocks,
7711 common_ancestor,
7712 "reorg common ancestor",
7713 )?;
7714 assert_canonical_block_identity(asserted_blocks, new_tip, "reorg new tip")
7715 }
7716 }
7717}
7718
7719fn assert_canonical_block_identity(
7720 asserted_blocks: &mut HashMap<u64, BlockRef>,
7721 block: &BlockRef,
7722 label: &str,
7723) -> Result<(), ReactiveError> {
7724 for asserted in asserted_blocks.values() {
7725 if asserted.hash == block.hash && asserted.number != block.number {
7726 return Err(ReactiveError::InvalidChainControl {
7727 message: format!(
7728 "{label} hash {:?} is already asserted at height {}, not {}",
7729 block.hash, asserted.number, block.number
7730 ),
7731 });
7732 }
7733 if block
7734 .parent_hash
7735 .is_some_and(|parent| parent == asserted.hash)
7736 && asserted.number.checked_add(1) != Some(block.number)
7737 {
7738 return Err(ReactiveError::InvalidChainControl {
7739 message: format!(
7740 "{label} block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
7741 block.number, block.hash, asserted.hash, asserted.number
7742 ),
7743 });
7744 }
7745 if asserted
7746 .parent_hash
7747 .is_some_and(|parent| parent == block.hash)
7748 && block.number.checked_add(1) != Some(asserted.number)
7749 {
7750 return Err(ReactiveError::InvalidChainControl {
7751 message: format!(
7752 "block {}:{:?} asserted earlier names {label} hash {:?} from non-adjacent height {} as its parent",
7753 asserted.number, asserted.hash, block.hash, block.number
7754 ),
7755 });
7756 }
7757 }
7758 if let Some(known) = asserted_blocks.get_mut(&block.number) {
7759 if !optional_block_refs_are_compatible(Some(known), Some(block)) {
7760 return Err(ReactiveError::InvalidChainControl {
7761 message: format!(
7762 "{label} block {}:{:?} conflicts with block identity {:?} asserted earlier in the batch",
7763 block.number, block.hash, known
7764 ),
7765 });
7766 }
7767 enrich_block_ref(known, block);
7768 } else {
7769 asserted_blocks.insert(block.number, *block);
7770 }
7771 Ok(())
7772}
7773
7774fn set_or_enrich_block_ref(current: &mut Option<BlockRef>, incoming: &BlockRef) {
7775 match current {
7776 Some(current) if current.number == incoming.number && current.hash == incoming.hash => {
7777 enrich_block_ref(current, incoming);
7778 }
7779 _ => *current = Some(*incoming),
7780 }
7781}
7782
7783fn advance_or_enrich_coverage(current: &mut Option<BlockRef>, incoming: &BlockRef) {
7784 match current {
7785 Some(current) if current.number == incoming.number && current.hash == incoming.hash => {
7786 enrich_block_ref(current, incoming);
7787 }
7788 Some(current) if current.number >= incoming.number => {}
7789 _ => *current = Some(*incoming),
7790 }
7791}
7792
7793fn validate_adjacent_finality(
7794 finalized: Option<&BlockRef>,
7795 safe: Option<&BlockRef>,
7796) -> Result<(), ReactiveError> {
7797 let Some((finalized, safe)) = finalized.zip(safe) else {
7798 return Ok(());
7799 };
7800 if finalized.number.checked_add(1) == Some(safe.number)
7801 && safe.parent_hash != Some(finalized.hash)
7802 {
7803 return Err(ReactiveError::InvalidChainControl {
7804 message: "adjacent safe head does not descend from finalized head".into(),
7805 });
7806 }
7807 Ok(())
7808}
7809
7810fn collect_diff_addresses(diff: &StateDiff, into: &mut HashSet<Address>) {
7816 into.extend(diff.slots.iter().map(|change| change.address));
7817 into.extend(diff.accounts.iter().map(|change| change.address));
7818 into.extend(diff.purged.iter().map(|purge| purge.address));
7819 into.extend(diff.skipped.iter().map(|skipped| skipped.address));
7820 into.extend(diff.skipped_balances.iter().map(|skipped| skipped.address));
7821 into.extend(diff.skipped_masks.iter().map(|skipped| skipped.address));
7822 into.extend(diff.skipped_accounts.iter().map(|skipped| skipped.address));
7823}
7824
7825fn root_moved_account_resync(
7830 address: Address,
7831 block: u64,
7832 fields: AccountFieldMask,
7833) -> ResyncRequest {
7834 ResyncRequest {
7835 id: ResyncId::new(format!("root-moved:{address:#x}:{block}")),
7836 reason: ResyncReason::RootMoved,
7837 block: ResyncBlock::Number(block),
7838 targets: vec![ResyncTarget::Account { address, fields }],
7839 priority: ResyncPriority::Normal,
7840 }
7841}
7842
7843fn batch_preconfirmation<N: Network>(
7844 batch: &ReactiveInputBatch<N>,
7845) -> Result<Option<FlashblockRef>, ReactiveError> {
7846 let mut flashblock: Option<FlashblockRef> = None;
7847 let mut has_non_preconfirmed = false;
7848 for (index, record) in batch.records().iter().enumerate() {
7849 match &record.context.chain_status {
7850 ChainStatus::Preconfirmed {
7851 flashblock: current,
7852 } => {
7853 if batch.record_delivery_scope(index) != Some(DeliveryScope::Preconfirmed) {
7854 return Err(ReactiveError::InvalidInputRecord {
7855 message: "pre-confirmed input requires pre-confirmed delivery scope".into(),
7856 });
7857 }
7858 if flashblock.as_ref().is_some_and(|known| known != current) {
7859 return Err(ReactiveError::InvalidInputRecord {
7860 message: "one batch cannot mix distinct Flashblock snapshots".into(),
7861 });
7862 }
7863 flashblock.get_or_insert_with(|| current.clone());
7864 }
7865 _ => has_non_preconfirmed = true,
7866 }
7867 }
7868 if flashblock.is_some() && (has_non_preconfirmed || !batch.chain_controls().is_empty()) {
7869 return Err(ReactiveError::InvalidInputRecord {
7870 message: "pre-confirmed delivery cannot mix canonical inputs or chain controls".into(),
7871 });
7872 }
7873 Ok(flashblock)
7874}
7875
7876fn canonical_record_block<N: Network>(record: &ReactiveInputRecord<N>) -> Option<&BlockRef> {
7877 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
7878 return None;
7879 }
7880 if is_canonical_status(&record.context.chain_status) {
7881 return context_block_ref(&record.context);
7882 }
7883 None
7884}
7885
7886fn resolve_record_block_payload_metadata<N: Network>(
7887 record: &ReactiveInputRecord<N>,
7888 mut block: BlockRef,
7889) -> Result<BlockRef, ReactiveError> {
7890 let ReactiveInput::Log(log) = &record.input else {
7891 return Ok(block);
7892 };
7893 if log.block_number != Some(block.number) || log.block_hash != Some(block.hash) {
7894 return Err(ReactiveError::InvalidInputRecord {
7895 message: "log payload and canonical context carry different block identities".into(),
7896 });
7897 }
7898 if let Some(timestamp) = log.block_timestamp {
7899 if block.timestamp.is_some_and(|known| known != timestamp) {
7900 return Err(ReactiveError::InvalidInputRecord {
7901 message: "log payload and canonical context carry different block timestamps"
7902 .into(),
7903 });
7904 }
7905 block.timestamp = Some(timestamp);
7906 }
7907 Ok(block)
7908}
7909
7910fn validate_input_record<N: Network>(record: &ReactiveInputRecord<N>) -> Result<(), ReactiveError> {
7911 let invalid = |message: String| ReactiveError::InvalidInputRecord { message };
7912 if let ChainStatus::Preconfirmed { flashblock } = &record.context.chain_status
7913 && record.context.block != Some(flashblock.block_ref())
7914 {
7915 return Err(invalid(
7916 "pre-confirmed status and context carry different partial block identities".into(),
7917 ));
7918 }
7919 let status_block = match &record.context.chain_status {
7920 ChainStatus::Included { block, .. }
7921 | ChainStatus::Safe { block }
7922 | ChainStatus::Finalized { block }
7923 | ChainStatus::Reorged {
7924 dropped_from: block,
7925 } => Some(block),
7926 ChainStatus::Preconfirmed { .. } => record.context.block.as_ref(),
7927 ChainStatus::Pending => None,
7928 };
7929 match (status_block, record.context.block.as_ref()) {
7930 (Some(status), Some(context)) if status == context => {}
7931 (Some(_), Some(_)) => {
7932 return Err(invalid(
7933 "chain status and context carry different block identities".into(),
7934 ));
7935 }
7936 (Some(_), None) => {
7937 return Err(invalid(
7938 "included or reorged input is missing its context block".into(),
7939 ));
7940 }
7941 (None, Some(_)) => {
7942 return Err(invalid(
7943 "pending input cannot carry a canonical context block".into(),
7944 ));
7945 }
7946 (None, None) => {}
7947 }
7948
7949 match &record.input {
7950 ReactiveInput::Log(log) => {
7951 let Some(block) = status_block else {
7952 return Err(invalid(
7953 "log input must carry an included or reorged block identity".into(),
7954 ));
7955 };
7956 if log.removed && !matches!(record.context.chain_status, ChainStatus::Reorged { .. }) {
7957 return Err(invalid(
7958 "removed log must carry reorged chain status".into(),
7959 ));
7960 }
7961 let block_number = log
7962 .block_number
7963 .ok_or_else(|| invalid("log is missing its block number".into()))?;
7964 let block_hash = log
7965 .block_hash
7966 .ok_or_else(|| invalid("log is missing its block hash".into()))?;
7967 log.transaction_hash
7968 .ok_or_else(|| invalid("log is missing its transaction hash".into()))?;
7969 let transaction_index = log
7970 .transaction_index
7971 .ok_or_else(|| invalid("log is missing its transaction index".into()))?;
7972 let log_index = log
7973 .log_index
7974 .ok_or_else(|| invalid("log is missing its log index".into()))?;
7975 if block_number != block.number
7976 || block_hash != block.hash
7977 || !optional_metadata_compatible(
7978 log.block_timestamp.as_ref(),
7979 block.timestamp.as_ref(),
7980 )
7981 {
7982 return Err(invalid(
7983 "log payload and context carry different block identities".into(),
7984 ));
7985 }
7986 if record.context.transaction_index != Some(transaction_index)
7987 || record.context.log_index != Some(log_index)
7988 {
7989 return Err(invalid(
7990 "log payload and context carry different transaction/log positions".into(),
7991 ));
7992 }
7993 }
7994 ReactiveInput::BlockHeader(header) => {
7995 if let Some(block) = status_block {
7996 if header.number() != block.number
7997 || header.hash() != block.hash
7998 || Some(header.parent_hash()) != block.parent_hash
7999 || Some(header.timestamp()) != block.timestamp
8000 {
8001 return Err(invalid(
8002 "block header payload and context carry different block identities".into(),
8003 ));
8004 }
8005 } else if !matches!(record.context.chain_status, ChainStatus::Pending) {
8006 return Err(invalid("block header has an unsupported lifecycle".into()));
8007 }
8008 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8009 return Err(invalid(
8010 "block header context cannot carry transaction/log positions".into(),
8011 ));
8012 }
8013 }
8014 ReactiveInput::FullBlock(block_response) => {
8015 let header = block_response.header();
8016 if let Some(block) = status_block {
8017 if header.number() != block.number
8018 || header.hash() != block.hash
8019 || Some(header.parent_hash()) != block.parent_hash
8020 || Some(header.timestamp()) != block.timestamp
8021 {
8022 return Err(invalid(
8023 "full-block payload and context carry different block identities".into(),
8024 ));
8025 }
8026 } else if !matches!(record.context.chain_status, ChainStatus::Pending) {
8027 return Err(invalid("full block has an unsupported lifecycle".into()));
8028 }
8029 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8030 return Err(invalid(
8031 "full-block context cannot carry transaction/log positions".into(),
8032 ));
8033 }
8034 if let Some(transactions) = block_response.transactions().as_transactions() {
8035 for (index, transaction) in transactions.iter().enumerate() {
8036 if transaction
8037 .block_hash()
8038 .is_some_and(|hash| hash != header.hash())
8039 || transaction
8040 .block_number()
8041 .is_some_and(|number| number != header.number())
8042 || transaction
8043 .transaction_index()
8044 .is_some_and(|position| position != index as u64)
8045 {
8046 return Err(invalid(format!(
8047 "full-block transaction {index} carries contradictory inclusion metadata"
8048 )));
8049 }
8050 if transaction
8051 .chain_id()
8052 .zip(record.context.chain_id)
8053 .is_some_and(|(transaction, context)| transaction != context)
8054 {
8055 return Err(invalid(format!(
8056 "full-block transaction {index} carries a chain id conflicting with its context"
8057 )));
8058 }
8059 }
8060 }
8061 }
8062 ReactiveInput::PendingTxHash(_) => {
8063 if !matches!(record.context.chain_status, ChainStatus::Pending) {
8064 return Err(invalid(
8065 "pending transaction input must carry pending chain status".into(),
8066 ));
8067 }
8068 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8069 return Err(invalid(
8070 "pending transaction context cannot carry canonical positions".into(),
8071 ));
8072 }
8073 }
8074 ReactiveInput::PendingTx(transaction) => {
8075 if !matches!(record.context.chain_status, ChainStatus::Pending) {
8076 return Err(invalid(
8077 "pending transaction input must carry pending chain status".into(),
8078 ));
8079 }
8080 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8081 return Err(invalid(
8082 "pending transaction context cannot carry canonical positions".into(),
8083 ));
8084 }
8085 if transaction.block_hash().is_some()
8086 || transaction.block_number().is_some()
8087 || transaction.transaction_index().is_some()
8088 {
8089 return Err(invalid(
8090 "hydrated pending transaction cannot carry inclusion metadata".into(),
8091 ));
8092 }
8093 if transaction
8094 .chain_id()
8095 .zip(record.context.chain_id)
8096 .is_some_and(|(transaction, context)| transaction != context)
8097 {
8098 return Err(invalid(
8099 "pending transaction carries a chain id conflicting with its context".into(),
8100 ));
8101 }
8102 }
8103 }
8104 Ok(())
8105}
8106
8107fn advance_block_for_canonical_record<N: Network>(
8116 cache: &mut EvmCache,
8117 record: &ReactiveInputRecord<N>,
8118) -> Option<Result<(), BlockContextError>> {
8119 if !is_canonical_status(&record.context.chain_status) {
8120 return None;
8121 }
8122 match &record.input {
8123 ReactiveInput::BlockHeader(header) => Some(cache.advance_block(header)),
8124 ReactiveInput::FullBlock(block) => Some(cache.advance_block(block.header())),
8125 _ => None,
8126 }
8127}
8128
8129fn context_block_ref(ctx: &ReactiveContext) -> Option<&BlockRef> {
8130 match &ctx.chain_status {
8131 ChainStatus::Included { block, .. }
8132 | ChainStatus::Safe { block }
8133 | ChainStatus::Finalized { block } => Some(block),
8134 ChainStatus::Reorged { dropped_from } => Some(dropped_from),
8135 ChainStatus::Preconfirmed { .. } => ctx.block.as_ref(),
8136 ChainStatus::Pending => ctx.block.as_ref(),
8137 }
8138}
8139
8140fn reorg_signal_block<N: Network>(
8141 record: &ReactiveInputRecord<N>,
8142) -> Option<(BlockRef, ReorgReason)> {
8143 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
8144 return block_ref_from_record(record).map(|block| (block, ReorgReason::RemovedLog));
8145 }
8146
8147 if let ChainStatus::Reorged { dropped_from } = &record.context.chain_status {
8148 return Some((*dropped_from, ReorgReason::ReorgedInput));
8149 }
8150
8151 None
8152}
8153
8154fn block_ref_from_record<N: Network>(record: &ReactiveInputRecord<N>) -> Option<BlockRef> {
8155 context_block_ref(&record.context)
8156 .cloned()
8157 .or_else(|| match &record.input {
8158 ReactiveInput::Log(log) => Some(BlockRef {
8159 number: log.block_number?,
8160 hash: log.block_hash?,
8161 parent_hash: None,
8162 timestamp: log.block_timestamp,
8163 }),
8164 ReactiveInput::BlockHeader(header) => Some(BlockRef {
8165 number: header.number(),
8166 hash: header.hash(),
8167 parent_hash: Some(header.parent_hash()),
8168 timestamp: Some(header.timestamp()),
8169 }),
8170 ReactiveInput::FullBlock(block) => {
8171 let header = block.header();
8172 Some(BlockRef {
8173 number: header.number(),
8174 hash: header.hash(),
8175 parent_hash: Some(header.parent_hash()),
8176 timestamp: Some(header.timestamp()),
8177 })
8178 }
8179 ReactiveInput::PendingTxHash(_) | ReactiveInput::PendingTx(_) => None,
8180 })
8181}
8182
8183fn remove_canceled_resyncs_from_batch(
8184 resyncs: &mut Vec<ResyncRequest>,
8185 canceled: &[ResyncRequest],
8186) {
8187 if canceled.is_empty() {
8188 return;
8189 }
8190 let canceled_ids: HashSet<_> = canceled.iter().map(|request| request.id.clone()).collect();
8191 resyncs.retain(|request| !canceled_ids.contains(&request.id));
8192}
8193
8194fn resync_target_address(target: &ResyncTarget) -> Address {
8195 match target {
8196 ResyncTarget::StorageSlot { address, .. }
8197 | ResyncTarget::StorageSlots { address, .. }
8198 | ResyncTarget::Account { address, .. } => *address,
8199 }
8200}
8201
8202fn resync_request_targets_dropped_block(
8203 request: &ResyncRequest,
8204 dropped_blocks: &[BlockRef],
8205) -> bool {
8206 let ResyncBlock::Hash { number, hash, .. } = &request.block else {
8207 return false;
8208 };
8209 dropped_blocks
8210 .iter()
8211 .any(|block| block.hash == *hash && block.number == *number)
8212}
8213
8214fn single_hash_pinned_resync_block(report: &ResyncReport) -> Option<BlockRef> {
8215 let first = report.requested.first()?.block.clone();
8216 if !report
8217 .requested
8218 .iter()
8219 .all(|request| request.block == first)
8220 {
8221 return None;
8222 }
8223
8224 let ResyncBlock::Hash { number, hash, .. } = first else {
8225 return None;
8226 };
8227
8228 Some(BlockRef {
8229 number,
8230 hash,
8231 parent_hash: None,
8232 timestamp: None,
8233 })
8234}
8235
8236fn purge_scopes_for_dropped_journals<N: Network>(
8237 dropped: &[BlockJournal<N>],
8238) -> Vec<(Address, PurgeScope)> {
8239 let mut scopes: Vec<(Address, PurgeScope)> = Vec::new();
8240 for entry in dropped.iter().rev() {
8241 for diff in entry.rollback_diffs.iter().rev() {
8242 merge_purge_scopes_for_diff(&mut scopes, diff);
8243 }
8244 }
8245 scopes
8246}
8247
8248fn rollback_updates_for_dropped_journals<N: Network>(
8249 dropped: &[BlockJournal<N>],
8250 purge_scopes: &[(Address, PurgeScope)],
8251) -> Vec<StateUpdate> {
8252 let purge_addresses: HashSet<_> = purge_scopes
8253 .iter()
8254 .map(|(address, _scope)| *address)
8255 .collect();
8256 let mut updates = Vec::new();
8257 for entry in dropped.iter().rev() {
8258 for diff in entry.rollback_diffs.iter().rev() {
8259 push_rollback_updates_for_diff(&mut updates, diff, &purge_addresses);
8260 }
8261 }
8262 updates
8263}
8264
8265fn merge_purge_scopes_for_diff(scopes: &mut Vec<(Address, PurgeScope)>, diff: &StateDiff) {
8266 for change in &diff.accounts {
8267 merge_purge_scope(scopes, change.address, PurgeScope::Account);
8268 }
8269 for record in &diff.purged {
8270 merge_purge_scope(scopes, record.address, record.scope.clone());
8271 }
8272}
8273
8274fn push_rollback_updates_for_diff(
8275 updates: &mut Vec<StateUpdate>,
8276 diff: &StateDiff,
8277 purge_addresses: &HashSet<Address>,
8278) {
8279 for change in diff.slots.iter().rev() {
8280 if purge_addresses.contains(&change.address) {
8281 continue;
8282 }
8283 updates.push(StateUpdate::slot(change.address, change.slot, change.old));
8284 }
8285}
8286
8287fn merge_purge_scope(scopes: &mut Vec<(Address, PurgeScope)>, address: Address, scope: PurgeScope) {
8288 if let Some((_existing_address, existing_scope)) = scopes
8289 .iter_mut()
8290 .find(|(existing_address, _scope)| *existing_address == address)
8291 {
8292 *existing_scope = merged_purge_scope(existing_scope.clone(), scope);
8293 } else {
8294 scopes.push((address, scope));
8295 }
8296}
8297
8298fn merged_purge_scope(left: PurgeScope, right: PurgeScope) -> PurgeScope {
8299 match (left, right) {
8300 (PurgeScope::Account, _) | (_, PurgeScope::Account) => PurgeScope::Account,
8301 (PurgeScope::AllStorage, _) | (_, PurgeScope::AllStorage) => PurgeScope::AllStorage,
8302 (PurgeScope::Slots(mut left), PurgeScope::Slots(right)) => {
8303 for slot in right {
8304 if !left.contains(&slot) {
8305 left.push(slot);
8306 }
8307 }
8308 PurgeScope::Slots(left)
8309 }
8310 }
8311}
8312
8313#[derive(Clone, Debug)]
8314struct StorageFetchSlot {
8315 address: Address,
8316 slot: U256,
8317 origins: Vec<StorageFetchOrigin>,
8318}
8319
8320#[derive(Clone, Debug)]
8321struct StorageFetchOrigin {
8322 request_id: ResyncId,
8323 target: ResyncTarget,
8324}
8325
8326#[derive(Clone, Debug)]
8327struct StorageFetchGroup {
8328 block: ResyncBlock,
8329 slots: Vec<StorageFetchSlot>,
8330 seen: HashSet<(Address, U256)>,
8331}
8332
8333#[derive(Clone, Debug)]
8336struct AccountResyncTarget {
8337 request_id: ResyncId,
8338 block: ResyncBlock,
8339 address: Address,
8340 fields: AccountFieldMask,
8341}
8342
8343fn resolve_trace_resyncs(
8344 cache: &EvmCache,
8345 storage_groups: &mut Vec<StorageFetchGroup>,
8346 account_targets: &mut Vec<AccountResyncTarget>,
8347 state_updates: &mut Vec<StateUpdate>,
8348) {
8349 let Some(fetcher) = cache.block_state_diff_fetcher().cloned() else {
8350 return;
8351 };
8352
8353 let mut blocks = Vec::new();
8354 let mut seen = HashSet::new();
8355 for block in storage_groups
8356 .iter()
8357 .map(|group| group.block.clone())
8358 .chain(account_targets.iter().map(|target| target.block.clone()))
8359 {
8360 if seen.insert(block.clone()) {
8361 blocks.push(block);
8362 }
8363 }
8364
8365 let mut traces = HashMap::new();
8366 for block in blocks {
8367 match (fetcher)(resync_block_to_block_id(&block)) {
8368 Ok(diff) => {
8369 traces.insert(block, diff);
8370 }
8371 Err(error) => {
8372 tracing::debug!(
8373 block = ?block,
8374 error = %error,
8375 "block trace resync source failed; falling back to point resync"
8376 );
8377 }
8378 }
8379 }
8380
8381 for group in storage_groups.iter_mut() {
8382 let Some(trace) = traces.get(&group.block) else {
8383 continue;
8384 };
8385 group.slots.retain(|slot| {
8386 if let Some(value) = trace_storage_value(trace, slot.address, slot.slot) {
8387 state_updates.push(StateUpdate::slot(slot.address, slot.slot, value));
8388 return false;
8389 }
8390 cache
8391 .cached_storage_value(slot.address, slot.slot)
8392 .is_none()
8393 });
8394 group.seen = group
8395 .slots
8396 .iter()
8397 .map(|slot| (slot.address, slot.slot))
8398 .collect();
8399 }
8400 storage_groups.retain(|group| !group.slots.is_empty());
8401
8402 let mut unresolved_accounts = Vec::new();
8403 for mut account in account_targets.drain(..) {
8404 let Some(trace) = traces.get(&account.block) else {
8405 unresolved_accounts.push(account);
8406 continue;
8407 };
8408 let Some(trace_account) = trace
8409 .accounts
8410 .iter()
8411 .find(|diff| diff.address == account.address)
8412 else {
8413 unresolved_accounts.push(account);
8414 continue;
8415 };
8416
8417 let mut patch = AccountPatch::default();
8418 let mut unresolved = AccountFieldMask::default();
8419 if account.fields.balance {
8420 if let Some(balance) = trace_account.balance {
8421 patch = patch.balance(balance);
8422 } else {
8423 unresolved.balance = true;
8424 }
8425 }
8426 if account.fields.nonce {
8427 if let Some(nonce) = trace_account.nonce {
8428 patch = patch.nonce(nonce);
8429 } else {
8430 unresolved.nonce = true;
8431 }
8432 }
8433 if account.fields.code {
8434 if let Some(code) = &trace_account.code {
8435 patch = patch.code(code.clone());
8436 } else {
8437 unresolved.code = true;
8438 }
8439 }
8440
8441 if patch.balance.is_some() || patch.nonce.is_some() || patch.code.is_some() {
8442 state_updates.push(StateUpdate::account_upsert(account.address, patch));
8443 }
8444 if !account_field_mask_empty(unresolved) {
8445 account.fields = unresolved;
8446 unresolved_accounts.push(account);
8447 }
8448 }
8449 *account_targets = unresolved_accounts;
8450}
8451
8452fn trace_storage_value(trace: &BlockStateDiff, address: Address, slot: U256) -> Option<U256> {
8453 trace
8454 .accounts
8455 .iter()
8456 .find(|account| account.address == address)
8457 .and_then(|account| {
8458 account
8459 .storage
8460 .iter()
8461 .find(|entry| entry.slot == slot)
8462 .map(|entry| entry.value)
8463 })
8464}
8465
8466fn account_field_mask_empty(mask: AccountFieldMask) -> bool {
8467 !mask.balance && !mask.nonce && !mask.code
8468}
8469
8470fn execute_resync_requests(cache: &mut EvmCache, requests: &[ResyncRequest]) -> ResyncReport {
8471 let mut failed = Vec::new();
8472 let mut storage_groups: Vec<StorageFetchGroup> = Vec::new();
8473 let mut account_targets: Vec<AccountResyncTarget> = Vec::new();
8474
8475 for request in requests {
8476 for target in &request.targets {
8477 match target {
8478 ResyncTarget::StorageSlot { address, slot } => {
8479 push_storage_resync_slot(
8480 &mut storage_groups,
8481 &request.id,
8482 &request.block,
8483 *address,
8484 *slot,
8485 );
8486 }
8487 ResyncTarget::StorageSlots { address, slots } => {
8488 for slot in slots {
8489 push_storage_resync_slot(
8490 &mut storage_groups,
8491 &request.id,
8492 &request.block,
8493 *address,
8494 *slot,
8495 );
8496 }
8497 }
8498 ResyncTarget::Account { address, fields } => {
8499 account_targets.push(AccountResyncTarget {
8500 request_id: request.id.clone(),
8501 block: request.block.clone(),
8502 address: *address,
8503 fields: *fields,
8504 });
8505 }
8506 }
8507 }
8508 }
8509
8510 let mut state_updates = Vec::new();
8511 resolve_trace_resyncs(
8512 cache,
8513 &mut storage_groups,
8514 &mut account_targets,
8515 &mut state_updates,
8516 );
8517
8518 if !storage_groups.is_empty() {
8519 if let Some(fetcher) = cache.storage_batch_fetcher().cloned() {
8520 for group in storage_groups {
8521 let block = group.block.clone();
8522 let fetches: Vec<(Address, U256)> = group
8523 .slots
8524 .iter()
8525 .map(|slot| (slot.address, slot.slot))
8526 .collect();
8527 let results = (fetcher)(fetches, resync_block_to_block_id(&block));
8528 let mut pending: HashMap<(Address, U256), StorageFetchSlot> = group
8529 .slots
8530 .iter()
8531 .cloned()
8532 .map(|slot| ((slot.address, slot.slot), slot))
8533 .collect();
8534
8535 for (address, slot, fetched) in results {
8536 let Some(requested_slot) = pending.remove(&(address, slot)) else {
8537 continue;
8538 };
8539 match fetched {
8540 Ok(value) => state_updates.push(StateUpdate::slot(address, slot, value)),
8541 Err(error) => {
8542 let message = error.to_string();
8543 push_resync_failures(
8544 &mut failed,
8545 &block,
8546 requested_slot.origins,
8547 ResyncFailureKind::StorageFetchFailed,
8548 message,
8549 );
8550 }
8551 }
8552 }
8553
8554 for requested_slot in group.slots {
8555 if pending
8556 .remove(&(requested_slot.address, requested_slot.slot))
8557 .is_some()
8558 {
8559 push_resync_failures(
8560 &mut failed,
8561 &block,
8562 requested_slot.origins,
8563 ResyncFailureKind::StorageFetchOmitted,
8564 "storage batch fetcher did not return a value for slot".to_string(),
8565 );
8566 }
8567 }
8568 }
8569 } else {
8570 for group in storage_groups {
8571 let block = group.block.clone();
8572 for slot in group.slots {
8573 push_resync_failures(
8574 &mut failed,
8575 &block,
8576 slot.origins,
8577 ResyncFailureKind::MissingStorageFetcher,
8578 "storage resync requires a storage batch fetcher".to_string(),
8579 );
8580 }
8581 }
8582 }
8583 }
8584
8585 if !account_targets.is_empty() {
8586 if let Some(fetcher) = cache.account_proof_fetcher().cloned() {
8587 let mut groups: Vec<(BlockId, Vec<_>)> = Vec::new();
8593 for account in account_targets {
8594 let block_id = resync_block_to_block_id(&account.block);
8595 match groups
8596 .iter_mut()
8597 .find(|(group_block, _)| *group_block == block_id)
8598 {
8599 Some((_, group)) => group.push(account),
8600 None => groups.push((block_id, vec![account])),
8601 }
8602 }
8603 for (block_id, group) in groups {
8604 let probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
8605 group
8606 .iter()
8607 .map(|account| (account.address, vec![]))
8608 .collect(),
8609 block_id,
8610 )
8611 .into_iter()
8612 .collect();
8613 for account in group {
8614 match probes.get(&account.address).cloned() {
8618 Some(Ok(proof)) => {
8619 let mut patch = AccountPatch::default();
8624 if account.fields.balance {
8625 patch = patch.balance(proof.balance);
8626 }
8627 if account.fields.nonce {
8628 patch = patch.nonce(proof.nonce);
8629 }
8630 state_updates.push(StateUpdate::account_upsert(account.address, patch));
8636 }
8637 Some(Err(error)) => {
8638 failed.push(ResyncFailure {
8639 request_id: account.request_id,
8640 block: account.block,
8641 target: ResyncTarget::Account {
8642 address: account.address,
8643 fields: account.fields,
8644 },
8645 kind: ResyncFailureKind::AccountFetchFailed,
8646 message: error.to_string(),
8647 });
8648 }
8649 None => {
8650 failed.push(ResyncFailure {
8651 request_id: account.request_id,
8652 block: account.block,
8653 target: ResyncTarget::Account {
8654 address: account.address,
8655 fields: account.fields,
8656 },
8657 kind: ResyncFailureKind::AccountFetchOmitted,
8658 message:
8659 "account proof fetcher did not return a result for address"
8660 .to_string(),
8661 });
8662 }
8663 }
8664 }
8665 }
8666 } else {
8667 for account in account_targets {
8668 failed.push(ResyncFailure {
8669 request_id: account.request_id,
8670 block: account.block,
8671 target: ResyncTarget::Account {
8672 address: account.address,
8673 fields: account.fields,
8674 },
8675 kind: ResyncFailureKind::MissingAccountFetcher,
8676 message: "account resync requires an account proof fetcher".to_string(),
8677 });
8678 }
8679 }
8680 }
8681
8682 let diff = if state_updates.is_empty() {
8683 StateDiff::default()
8684 } else {
8685 cache.apply_updates(&state_updates)
8686 };
8687
8688 ResyncReport {
8689 requested: requests.to_vec(),
8690 state_updates,
8691 diff,
8692 failed,
8693 }
8694}
8695
8696fn push_resync_failures(
8697 failed: &mut Vec<ResyncFailure>,
8698 block: &ResyncBlock,
8699 origins: Vec<StorageFetchOrigin>,
8700 kind: ResyncFailureKind,
8701 message: String,
8702) {
8703 for origin in origins {
8704 failed.push(ResyncFailure {
8705 request_id: origin.request_id,
8706 block: block.clone(),
8707 target: origin.target,
8708 kind,
8709 message: message.clone(),
8710 });
8711 }
8712}
8713
8714fn push_storage_resync_slot(
8715 groups: &mut Vec<StorageFetchGroup>,
8716 request_id: &ResyncId,
8717 block: &ResyncBlock,
8718 address: Address,
8719 slot: U256,
8720) {
8721 let group_index = if let Some(index) = groups.iter().position(|group| group.block == *block) {
8722 index
8723 } else {
8724 groups.push(StorageFetchGroup {
8725 block: block.clone(),
8726 slots: Vec::new(),
8727 seen: HashSet::new(),
8728 });
8729 groups.len() - 1
8730 };
8731
8732 let group = &mut groups[group_index];
8733 let origin = StorageFetchOrigin {
8734 request_id: request_id.clone(),
8735 target: ResyncTarget::StorageSlot { address, slot },
8736 };
8737 if group.seen.insert((address, slot)) {
8738 group.slots.push(StorageFetchSlot {
8739 address,
8740 slot,
8741 origins: vec![origin],
8742 });
8743 } else if let Some(existing) = group
8744 .slots
8745 .iter_mut()
8746 .find(|existing| existing.address == address && existing.slot == slot)
8747 {
8748 existing.origins.push(origin);
8749 }
8750}
8751
8752fn resync_block_to_block_id(block: &ResyncBlock) -> BlockId {
8753 match block {
8754 ResyncBlock::Latest => BlockId::latest(),
8755 ResyncBlock::Pending => BlockId::pending(),
8756 ResyncBlock::Safe => BlockId::safe(),
8757 ResyncBlock::Finalized => BlockId::finalized(),
8758 ResyncBlock::Number(number) => BlockId::number(*number),
8759 ResyncBlock::Hash {
8760 number: _,
8761 hash,
8762 require_canonical,
8763 } => BlockId::from((*hash, Some(*require_canonical))),
8764 }
8765}
8766
8767impl<N: Network> RegisteredHandler<N> {
8768 fn matches(&self, input: &ReactiveInput<N>) -> bool {
8769 self.interests
8770 .iter()
8771 .any(|interest| interest_matches(interest, input))
8772 }
8773
8774 fn route_log(&self, log: &Log) -> Option<ReactiveLogRoute> {
8775 self.interests.iter().find_map(|interest| match interest {
8776 ReactiveInterest::Logs(interest) if interest.matches(log) => Some(ReactiveLogRoute {
8777 handler_id: self.id.clone(),
8778 route_key: interest.route_key(log),
8779 }),
8780 ReactiveInterest::Logs(_)
8781 | ReactiveInterest::Blocks(_)
8782 | ReactiveInterest::PendingTransactions(_) => None,
8783 })
8784 }
8785}
8786
8787fn merge_log_subscription_filter(filters: &mut Vec<Filter>, next: &Filter) {
8788 let mut candidate = next.clone();
8789 let mut insertion_index = filters.len();
8790 let mut index = 0;
8791 while index < filters.len() {
8792 if filters[index].block_option != candidate.block_option {
8793 index += 1;
8794 continue;
8795 }
8796 if let Some(merged) = exact_filter_union(&candidate, &filters[index]) {
8797 candidate = merged;
8798 insertion_index = insertion_index.min(index);
8799 filters.remove(index);
8800 index = 0;
8801 } else {
8802 index += 1;
8803 }
8804 }
8805 filters.insert(insertion_index.min(filters.len()), candidate);
8806}
8807
8808fn exact_filter_union(left: &Filter, right: &Filter) -> Option<Filter> {
8809 if filter_subsumes(left, right) {
8810 return Some(left.clone());
8811 }
8812 if filter_subsumes(right, left) {
8813 return Some(right.clone());
8814 }
8815 let differing_dimensions = usize::from(left.address != right.address)
8816 + left
8817 .topics
8818 .iter()
8819 .zip(right.topics.iter())
8820 .filter(|(left, right)| left != right)
8821 .count();
8822 if differing_dimensions != 1 {
8823 return None;
8824 }
8825
8826 let mut merged = left.clone();
8827 if merged.address != right.address {
8828 merge_filter_set(&mut merged.address, &right.address);
8829 } else {
8830 for (merged_topic, right_topic) in merged.topics.iter_mut().zip(right.topics.iter()) {
8831 if merged_topic != right_topic {
8832 merge_filter_set(merged_topic, right_topic);
8833 break;
8834 }
8835 }
8836 }
8837 Some(merged)
8838}
8839
8840fn filter_subsumes(left: &Filter, right: &Filter) -> bool {
8841 filter_set_subsumes(&left.address, &right.address)
8842 && left
8843 .topics
8844 .iter()
8845 .zip(right.topics.iter())
8846 .all(|(left, right)| filter_set_subsumes(left, right))
8847}
8848
8849fn filter_set_subsumes<T: Eq + Hash>(left: &FilterSet<T>, right: &FilterSet<T>) -> bool {
8850 left.is_empty()
8851 || (!right.is_empty()
8852 && right
8853 .iter()
8854 .all(|value| left.iter().any(|known| known == value)))
8855}
8856
8857fn merge_filter_set<T: Clone + Eq + Hash>(target: &mut FilterSet<T>, source: &FilterSet<T>) {
8858 if target.is_empty() {
8859 return;
8860 }
8861 if source.is_empty() {
8862 *target = FilterSet::default();
8863 return;
8864 }
8865 for value in source.iter() {
8866 target.insert(value.clone());
8867 }
8868}
8869
8870#[derive(Clone, Debug)]
8871struct HandlerExecution {
8872 handler_id: HandlerId,
8873 quality: StateEffectQuality,
8874 tags: Vec<ReportTag>,
8875 state_updates: Vec<StateUpdate>,
8876 invalidations: Vec<InvalidationRequest>,
8877 resyncs: Vec<ResyncRequest>,
8878 speculative: Vec<SpeculativeRequest>,
8879 hook_signals: Vec<HookSignal>,
8880}
8881
8882impl HandlerExecution {
8883 fn from_outcome(
8884 handler_id: HandlerId,
8885 input_ref: InputRef,
8886 outcome: HandlerOutcome,
8887 preconfirmed: bool,
8888 ) -> Self {
8889 let mut state_updates = Vec::new();
8890 let mut invalidations = Vec::new();
8891 let mut resyncs = Vec::new();
8892 let mut speculative = Vec::new();
8893 let mut hook_signals = Vec::new();
8894
8895 for effect in outcome.effects {
8896 match effect {
8897 ReactiveEffect::StateUpdate(update) => state_updates.push(update),
8898 ReactiveEffect::Invalidate(invalidation) => {
8899 state_updates.push(StateUpdate::purge(
8900 invalidation.address,
8901 invalidation.scope.clone(),
8902 ));
8903 invalidations.push(invalidation);
8904 }
8905 ReactiveEffect::Resync(mut request) => {
8906 if preconfirmed {
8907 request.block = ResyncBlock::Pending;
8908 }
8909 resyncs.push(request);
8910 }
8911 ReactiveEffect::Hook(signal) => hook_signals.push(signal),
8912 ReactiveEffect::Speculative(mut request) => {
8913 request.input_ref = input_ref;
8914 speculative.push(request);
8915 }
8916 }
8917 }
8918
8919 Self {
8920 handler_id,
8921 quality: outcome.quality,
8922 tags: outcome.tags,
8923 state_updates,
8924 invalidations,
8925 resyncs,
8926 speculative,
8927 hook_signals,
8928 }
8929 }
8930}
8931
8932fn dedupe_records<N: Network>(
8933 records: Vec<ReactiveInputRecord<N>>,
8934) -> Result<Vec<ReactiveInputRecord<N>>, ReactiveError> {
8935 let mut positions = HashMap::<ReactiveInputIdentity, usize>::new();
8936 let mut deduped = Vec::with_capacity(records.len());
8937 for record in records {
8938 let identity = record.validated_identity()?;
8939 if !record.is_payload_deduplicable() {
8940 deduped.push(record);
8941 continue;
8942 }
8943 if let Some(index) = positions.get(&identity).copied() {
8944 let merged = deduped[index].merge_compatible_duplicate(&record)?;
8945 debug_assert!(merged, "same indexed identity is deduplicable");
8946 } else {
8947 positions.insert(identity, deduped.len());
8948 deduped.push(record);
8949 }
8950 }
8951 Ok(deduped)
8952}
8953
8954fn dedupe_scoped_records<N: Network>(
8955 records: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
8956) -> Result<Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>, ReactiveError> {
8957 let mut positions: HashMap<ReactiveInputIdentity, usize> = HashMap::new();
8958 let mut deduped: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)> =
8959 Vec::with_capacity(records.len());
8960 for (record, audience, delivery_scope) in records {
8961 let identity = record.validated_identity()?;
8962 if !record.is_payload_deduplicable() {
8963 deduped.push((record, audience, delivery_scope));
8964 continue;
8965 }
8966 if let Some(index) = positions.get(&identity).copied() {
8967 let merged = deduped[index].0.merge_compatible_duplicate(&record)?;
8968 debug_assert!(merged, "same indexed identity is deduplicable");
8969 merge_delivery_audience(&mut deduped[index].1, audience);
8970 merge_delivery_scope(&mut deduped[index].2, delivery_scope);
8971 } else {
8972 positions.insert(identity, deduped.len());
8973 deduped.push((record, audience, delivery_scope));
8974 }
8975 }
8976 Ok(deduped)
8977}
8978
8979fn merge_delivery_scope(into: &mut DeliveryScope, incoming: DeliveryScope) {
8980 *into = match (*into, incoming) {
8981 (DeliveryScope::Canonical, _) | (_, DeliveryScope::Canonical) => DeliveryScope::Canonical,
8982 (DeliveryScope::CanonicalProgress, _) | (_, DeliveryScope::CanonicalProgress) => {
8983 DeliveryScope::CanonicalProgress
8984 }
8985 (DeliveryScope::Preconfirmed, DeliveryScope::Preconfirmed)
8986 | (DeliveryScope::Preconfirmed, DeliveryScope::OwnerCatchup)
8987 | (DeliveryScope::OwnerCatchup, DeliveryScope::Preconfirmed) => DeliveryScope::Preconfirmed,
8988 (DeliveryScope::OwnerCatchup, DeliveryScope::OwnerCatchup) => DeliveryScope::OwnerCatchup,
8989 };
8990}
8991
8992fn merge_delivery_audience(into: &mut DeliveryAudience, incoming: DeliveryAudience) {
8993 match (&mut *into, incoming) {
8994 (DeliveryAudience::All, _) => {}
8995 (current, DeliveryAudience::All) => *current = DeliveryAudience::All,
8996 (DeliveryAudience::Owners(current), DeliveryAudience::Owners(incoming)) => {
8997 for owner in incoming {
8998 if !current.contains(&owner) {
8999 current.push(owner);
9000 }
9001 }
9002 }
9003 (DeliveryAudience::AllExcept(current), DeliveryAudience::AllExcept(incoming)) => {
9004 current.retain(|owner| incoming.contains(owner));
9005 }
9006 (DeliveryAudience::AllExcept(excluded), DeliveryAudience::Owners(included)) => {
9007 excluded.retain(|owner| !included.contains(owner));
9008 }
9009 (current @ DeliveryAudience::Owners(_), DeliveryAudience::AllExcept(mut excluded)) => {
9010 let DeliveryAudience::Owners(included) = current else {
9011 unreachable!("match arm restricts the audience variant")
9012 };
9013 excluded.retain(|owner| !included.contains(owner));
9014 *current = DeliveryAudience::AllExcept(excluded);
9015 }
9016 }
9017}
9018
9019fn sort_records<N: Network>(records: Vec<ReactiveInputRecord<N>>) -> Vec<ReactiveInputRecord<N>> {
9020 let mut indexed: Vec<(usize, ReactiveInputRecord<N>)> =
9021 records.into_iter().enumerate().collect();
9022 indexed.sort_by_key(|(index, record)| record_sort_key(*index, record));
9023 indexed.into_iter().map(|(_, record)| record).collect()
9024}
9025
9026fn sort_scoped_records<N: Network>(
9027 records: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
9028) -> Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)> {
9029 let mut indexed: Vec<_> = records.into_iter().enumerate().collect();
9030 indexed.sort_by_key(|(index, (record, _, _))| record_sort_key(*index, record));
9031 indexed
9032 .into_iter()
9033 .map(|(_, scoped_record)| scoped_record)
9034 .collect()
9035}
9036
9037fn record_sort_key<N: Network>(index: usize, record: &ReactiveInputRecord<N>) -> RecordSortKey {
9038 if let Some((block, _)) = reorg_signal_block(record) {
9039 return RecordSortKey {
9040 class: 0,
9041 block_number: block.number,
9042 record_class: 0,
9043 transaction_index: record.context.transaction_index.unwrap_or(u64::MAX),
9044 log_index: record.context.log_index.unwrap_or(u64::MAX),
9045 original_index: index,
9046 };
9047 }
9048 if is_canonical_status(&record.context.chain_status)
9049 && let Some(block) = record.context.block.as_ref()
9050 {
9051 let (record_class, transaction_index, log_index) = match &record.input {
9052 ReactiveInput::BlockHeader(_) | ReactiveInput::FullBlock(_) => (0, 0, 0),
9053 ReactiveInput::Log(log) if !log.removed => (
9054 1,
9055 log.transaction_index
9056 .or(record.context.transaction_index)
9057 .unwrap_or(u64::MAX),
9058 log.log_index
9059 .or(record.context.log_index)
9060 .unwrap_or(u64::MAX),
9061 ),
9062 ReactiveInput::Log(_)
9063 | ReactiveInput::PendingTxHash(_)
9064 | ReactiveInput::PendingTx(_) => (2, u64::MAX, u64::MAX),
9065 };
9066 return RecordSortKey {
9067 class: 1,
9068 block_number: block.number,
9069 record_class,
9070 transaction_index,
9071 log_index,
9072 original_index: index,
9073 };
9074 }
9075
9076 RecordSortKey {
9077 class: 2,
9078 block_number: 0,
9079 record_class: 0,
9080 transaction_index: 0,
9081 log_index: 0,
9082 original_index: index,
9083 }
9084}
9085
9086#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
9087struct RecordSortKey {
9088 class: u8,
9089 block_number: u64,
9090 record_class: u8,
9091 transaction_index: u64,
9092 log_index: u64,
9093 original_index: usize,
9094}
9095
9096fn interest_matches<N: Network>(interest: &ReactiveInterest<N>, input: &ReactiveInput<N>) -> bool {
9097 match (interest, input) {
9098 (ReactiveInterest::Logs(interest), ReactiveInput::Log(log)) => interest.matches(log),
9099 (
9100 ReactiveInterest::Blocks(BlockInterest {
9101 mode: BlockInterestMode::Header,
9102 }),
9103 ReactiveInput::BlockHeader(_),
9104 ) => true,
9105 (
9106 ReactiveInterest::Blocks(BlockInterest {
9107 mode: BlockInterestMode::FullBlock,
9108 }),
9109 ReactiveInput::FullBlock(_),
9110 ) => true,
9111 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTxHash(_)) => {
9112 interest.matches_hash_only()
9113 }
9114 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTx(tx)) => {
9115 interest.matches_tx(tx)
9116 }
9117 _ => false,
9118 }
9119}
9120
9121fn validate_effects(
9122 input_ref: InputRef,
9123 ctx: &ReactiveContext,
9124 handler_id: &HandlerId,
9125 effects: &[ReactiveEffect],
9126) -> Result<(), ReactiveError> {
9127 let pending = matches!(ctx.chain_status, ChainStatus::Pending)
9128 || matches!(input_ref, InputRef::PendingTx { .. });
9129 if !pending {
9130 return Ok(());
9131 }
9132
9133 for effect in effects {
9134 let effect_kind = match effect {
9135 ReactiveEffect::StateUpdate(_) => Some("state_update"),
9136 ReactiveEffect::Invalidate(_) => Some("invalidate"),
9137 ReactiveEffect::Resync(_) => Some("resync"),
9138 ReactiveEffect::Hook(_) | ReactiveEffect::Speculative(_) => None,
9139 };
9140 if let Some(effect_kind) = effect_kind {
9141 return Err(ReactiveError::InvalidPendingEffect {
9142 input_ref: Box::new(input_ref),
9143 handler_id: handler_id.clone(),
9144 effect_kind,
9145 });
9146 }
9147 }
9148 Ok(())
9149}
9150
9151fn detect_conflicts(
9152 input_ref: InputRef,
9153 executions: &[HandlerExecution],
9154) -> Result<(), ReactiveError> {
9155 let mut writes: HashMap<EffectTarget, (AbsoluteValue, HandlerId)> = HashMap::new();
9156 for execution in executions {
9157 for update in &execution.state_updates {
9158 for (target, value) in absolute_writes(update) {
9159 if let Some((previous_value, previous_handler)) = writes.get(&target) {
9160 if previous_value != &value {
9161 return Err(ReactiveError::ConflictingEffects {
9162 input_ref: Box::new(input_ref),
9163 target: Box::new(target),
9164 first: previous_handler.clone(),
9165 second: execution.handler_id.clone(),
9166 });
9167 }
9168 } else {
9169 writes.insert(target, (value, execution.handler_id.clone()));
9170 }
9171 }
9172 }
9173 }
9174 Ok(())
9175}
9176
9177fn absolute_writes(update: &StateUpdate) -> Vec<(EffectTarget, AbsoluteValue)> {
9178 match update {
9179 StateUpdate::Slot {
9180 address,
9181 slot,
9182 value,
9183 } => vec![(
9184 EffectTarget::StorageSlot {
9185 address: *address,
9186 slot: *slot,
9187 },
9188 AbsoluteValue::U256(*value),
9189 )],
9190 StateUpdate::SlotMasked {
9191 address,
9192 slot,
9193 mask,
9194 value,
9195 } => vec![(
9196 EffectTarget::MaskedStorageSlot {
9197 address: *address,
9198 slot: *slot,
9199 mask: *mask,
9200 },
9201 AbsoluteValue::U256(*value),
9202 )],
9203 StateUpdate::Account { address, patch } | StateUpdate::AccountUpsert { address, patch } => {
9204 account_patch_writes(*address, patch)
9205 }
9206 StateUpdate::SlotDelta { .. }
9207 | StateUpdate::BalanceDelta { .. }
9208 | StateUpdate::Purge { .. } => Vec::new(),
9209 }
9210}
9211
9212fn account_patch_writes(
9213 address: Address,
9214 patch: &AccountPatch,
9215) -> Vec<(EffectTarget, AbsoluteValue)> {
9216 let mut writes = Vec::new();
9217 if let Some(balance) = patch.balance {
9218 writes.push((
9219 EffectTarget::AccountBalance { address },
9220 AbsoluteValue::U256(balance),
9221 ));
9222 }
9223 if let Some(nonce) = patch.nonce {
9224 writes.push((
9225 EffectTarget::AccountNonce { address },
9226 AbsoluteValue::U64(nonce),
9227 ));
9228 }
9229 if let Some(code) = &patch.code {
9230 writes.push((
9231 EffectTarget::AccountCode { address },
9232 AbsoluteValue::Bytes(code.clone()),
9233 ));
9234 }
9235 writes
9236}
9237
9238fn input_ref<N: Network>(input: &ReactiveInput<N>, ctx: &ReactiveContext) -> InputRef {
9239 match input {
9240 ReactiveInput::Log(log) => InputRef::Log {
9241 chain_id: ctx.chain_id,
9242 block_hash: log
9243 .block_hash
9244 .or(ctx.block.as_ref().map(|block| block.hash))
9245 .unwrap_or_default(),
9246 transaction_hash: log.transaction_hash.unwrap_or_default(),
9247 log_index: log.log_index.or(ctx.log_index).unwrap_or_default(),
9248 },
9249 ReactiveInput::PendingTxHash(hash) => InputRef::PendingTx {
9250 chain_id: ctx.chain_id,
9251 hash: *hash,
9252 },
9253 ReactiveInput::PendingTx(tx) => InputRef::PendingTx {
9254 chain_id: ctx.chain_id,
9255 hash: tx.tx_hash(),
9256 },
9257 ReactiveInput::BlockHeader(header) => InputRef::Block {
9258 chain_id: ctx.chain_id,
9259 hash: header.hash(),
9260 number: header.number(),
9261 },
9262 ReactiveInput::FullBlock(block) => {
9263 let header = block.header();
9264 InputRef::Block {
9265 chain_id: ctx.chain_id,
9266 hash: header.hash(),
9267 number: header.number(),
9268 }
9269 }
9270 }
9271}
9272
9273fn is_canonical_status(status: &ChainStatus) -> bool {
9274 matches!(
9275 status,
9276 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. }
9277 )
9278}
9279
9280pub struct EventDecoderHandler {
9282 id: HandlerId,
9283 decoder: Arc<dyn EventDecoder>,
9284 interest: LogInterest,
9285}
9286
9287impl EventDecoderHandler {
9288 pub fn new(id: HandlerId, decoder: Arc<dyn EventDecoder>, interest: LogInterest) -> Self {
9290 Self {
9291 id,
9292 decoder,
9293 interest,
9294 }
9295 }
9296}
9297
9298impl<N: Network> ReactiveHandler<N> for EventDecoderHandler {
9299 fn id(&self) -> HandlerId {
9300 self.id.clone()
9301 }
9302
9303 fn interests(&self) -> Vec<ReactiveInterest<N>> {
9304 vec![ReactiveInterest::Logs(self.interest.clone())]
9305 }
9306
9307 fn handle(
9308 &self,
9309 _ctx: &ReactiveContext,
9310 input: &ReactiveInput<N>,
9311 state: &dyn StateView,
9312 ) -> Result<HandlerOutcome, HandlerError> {
9313 let ReactiveInput::Log(log) = input else {
9314 return Ok(HandlerOutcome::empty(StateEffectQuality::NoStateEffect));
9315 };
9316
9317 Ok(HandlerOutcome {
9318 effects: self
9319 .decoder
9320 .decode(&log.inner, state)
9321 .into_iter()
9322 .map(ReactiveEffect::StateUpdate)
9323 .collect(),
9324 quality: StateEffectQuality::ExactFromInput,
9325 tags: Vec::new(),
9326 })
9327 }
9328}
9329
9330#[derive(
9332 Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
9333)]
9334#[non_exhaustive]
9335pub enum SubscriberCapability {
9336 Logs,
9338 BlockHeaders,
9340 FullBlocks,
9342 PendingTransactionHashes,
9344 PendingTransactions,
9346 HistoricalBackfill,
9348 Live,
9350 DurableReplay,
9360 OwnerScopedDelivery,
9362 DynamicInterests,
9364 ExplicitReorgs,
9366 FinalityUpdates,
9368 Barriers,
9370 Preconfirmations,
9372}
9373
9374#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9380pub struct SubscriberCapabilities {
9381 supported: BTreeSet<SubscriberCapability>,
9382}
9383
9384impl SubscriberCapabilities {
9385 pub fn new(capabilities: impl IntoIterator<Item = SubscriberCapability>) -> Self {
9387 Self {
9388 supported: capabilities.into_iter().collect(),
9389 }
9390 }
9391
9392 pub fn supports(&self, capability: SubscriberCapability) -> bool {
9394 self.supported.contains(&capability)
9395 }
9396
9397 pub fn iter(&self) -> impl Iterator<Item = SubscriberCapability> + '_ {
9399 self.supported.iter().copied()
9400 }
9401
9402 pub fn supports_live(&self) -> bool {
9404 self.supports(SubscriberCapability::Live)
9405 }
9406
9407 pub fn supports_durable_replay(&self) -> bool {
9410 self.supports(SubscriberCapability::DurableReplay)
9411 }
9412
9413 pub fn supports_explicit_reorgs(&self) -> bool {
9415 self.supports(SubscriberCapability::ExplicitReorgs)
9416 }
9417}
9418
9419impl FromIterator<SubscriberCapability> for SubscriberCapabilities {
9420 fn from_iter<T: IntoIterator<Item = SubscriberCapability>>(iter: T) -> Self {
9421 Self::new(iter)
9422 }
9423}
9424
9425pub trait EventSubscriber<N: Network = Ethereum>: Send {
9427 fn chain_id(&self) -> Option<u64> {
9434 None
9435 }
9436
9437 fn capabilities(&self) -> SubscriberCapabilities {
9439 SubscriberCapabilities::default()
9440 }
9441
9442 fn register_interests(
9461 &mut self,
9462 interests: &[ReactiveInterest<N>],
9463 ) -> SubscriberOperation<'_, ()>;
9464
9465 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N>;
9477
9478 fn restore_position(
9501 &mut self,
9502 _position: &SubscriberResumePosition,
9503 ) -> Result<(), SubscriberError> {
9504 Ok(())
9505 }
9506
9507 fn acknowledge_delivery(
9521 &mut self,
9522 _token: SubscriberDeliveryToken,
9523 ) -> SubscriberOperation<'_, ()> {
9524 Box::pin(async { Ok(()) })
9525 }
9526}
9527
9528pub type SubscriberOperation<'a, T> =
9533 Pin<Box<dyn Future<Output = Result<T, SubscriberError>> + Send + 'a>>;
9534
9535pub type SubscriberNextBatch<'a, N> = Pin<
9537 Box<dyn Future<Output = Result<Option<ReactiveInputBatch<N>>, SubscriberError>> + Send + 'a>,
9538>;
9539
9540pub type SubscriberNextScopedBatch<'a, N> = Pin<
9542 Box<dyn Future<Output = Result<Option<SubscriberInputBatch<N>>, SubscriberError>> + Send + 'a>,
9543>;
9544
9545#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
9547pub enum SubscriberMode {
9548 #[default]
9554 Auto,
9555 PubSub,
9557 Polling,
9559}
9560
9561#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9562enum FlashblocksAdapter {
9563 BaseNative,
9564 OpPending,
9565}
9566
9567fn flashblocks_adapter(chain_id: u64) -> Option<FlashblocksAdapter> {
9568 match chain_id {
9569 8_453 | 84_532 => Some(FlashblocksAdapter::BaseNative),
9570 10 | 11_155_420 => Some(FlashblocksAdapter::OpPending),
9571 _ => None,
9572 }
9573}
9574
9575#[derive(Clone, Debug, PartialEq, Eq)]
9577pub struct SubscriberConfig {
9578 pub preconfirmations: PreconfirmationMode,
9581 pub flashblock_poll_interval: Duration,
9584 pub hydrate_pending_transactions: bool,
9586 pub verify_log_block_context: bool,
9597 pub max_batch_size: usize,
9599 pub max_log_addresses_per_subscription: usize,
9603 pub max_pending_records: usize,
9608 pub max_pending_backfills: usize,
9610 pub max_backfill_log_bytes: usize,
9613 pub max_reconcile_requests_in_flight: usize,
9616 pub reconnect: SubscriberReconnectConfig,
9618}
9619
9620impl Default for SubscriberConfig {
9621 fn default() -> Self {
9622 Self {
9623 preconfirmations: PreconfirmationMode::Disabled,
9624 flashblock_poll_interval: Duration::from_millis(100),
9625 hydrate_pending_transactions: false,
9626 verify_log_block_context: false,
9627 max_batch_size: 1024,
9628 max_log_addresses_per_subscription: 1024,
9629 max_pending_records: 16_384,
9630 max_pending_backfills: 4_096,
9631 max_backfill_log_bytes: 64 * 1024 * 1024,
9632 max_reconcile_requests_in_flight: 8,
9633 reconnect: SubscriberReconnectConfig::default(),
9634 }
9635 }
9636}
9637
9638#[derive(Clone, Debug, PartialEq, Eq)]
9644pub struct SubscriberReconnectConfig {
9645 pub enabled: bool,
9647 pub initial_delay: Duration,
9649 pub retry_delay: Duration,
9652 pub max_delay: Duration,
9654 pub max_attempts: Option<usize>,
9656 pub dedupe_window: usize,
9659}
9660
9661impl Default for SubscriberReconnectConfig {
9662 fn default() -> Self {
9663 Self {
9664 enabled: true,
9665 initial_delay: Duration::ZERO,
9666 retry_delay: Duration::from_millis(250),
9667 max_delay: Duration::from_secs(30),
9668 max_attempts: Some(3),
9669 dedupe_window: 4096,
9670 }
9671 }
9672}
9673
9674#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9686pub struct SubscriberBackfill {
9687 from_block: u64,
9688 to_block: Option<u64>,
9689 retained_anchor: Option<BlockRef>,
9690}
9691
9692impl SubscriberBackfill {
9693 pub fn range(from_block: u64, to_block: u64) -> Self {
9695 Self {
9696 from_block,
9697 to_block: Some(to_block),
9698 retained_anchor: None,
9699 }
9700 }
9701
9702 pub fn from_block(from_block: u64) -> Self {
9704 Self {
9705 from_block,
9706 to_block: None,
9707 retained_anchor: None,
9708 }
9709 }
9710
9711 pub fn from_canonical_block(block: BlockRef) -> Self {
9718 Self {
9719 from_block: block.number,
9720 to_block: None,
9721 retained_anchor: Some(block),
9722 }
9723 }
9724
9725 pub fn from_canonical_block_through(
9733 block: BlockRef,
9734 to_block: u64,
9735 ) -> Result<Self, SubscriberError> {
9736 if to_block < block.number {
9737 return Err(SubscriberError::InvalidConfig(
9738 "inclusive backfill upper bound precedes its retained anchor",
9739 ));
9740 }
9741 Ok(Self {
9742 from_block: block.number,
9743 to_block: Some(to_block),
9744 retained_anchor: Some(block),
9745 })
9746 }
9747
9748 pub fn after_canonical_block(block: BlockRef) -> Result<Self, SubscriberError> {
9764 Self::after_canonical_block_inner(block, None)
9765 }
9766
9767 pub fn after_canonical_block_through(
9779 block: BlockRef,
9780 to_block: u64,
9781 ) -> Result<Self, SubscriberError> {
9782 if to_block < block.number {
9783 return Err(SubscriberError::InvalidConfig(
9784 "exclusive backfill upper bound precedes its retained baseline",
9785 ));
9786 }
9787 Self::after_canonical_block_inner(block, Some(to_block))
9788 }
9789
9790 fn after_canonical_block_inner(
9791 block: BlockRef,
9792 to_block: Option<u64>,
9793 ) -> Result<Self, SubscriberError> {
9794 let from_block = block
9795 .number
9796 .checked_add(1)
9797 .ok_or(SubscriberError::InvalidConfig(
9798 "cannot construct an exclusive backfill after block u64::MAX",
9799 ))?;
9800 Ok(Self {
9801 from_block,
9802 to_block,
9803 retained_anchor: Some(block),
9804 })
9805 }
9806
9807 pub fn start_block(&self) -> u64 {
9809 self.from_block
9810 }
9811
9812 pub fn end_block(&self) -> Option<u64> {
9814 self.to_block
9815 }
9816
9817 pub fn retained_anchor(&self) -> Option<&BlockRef> {
9819 self.retained_anchor.as_ref()
9820 }
9821}
9822
9823#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
9830pub struct SubscriberOwnerEpoch {
9831 owner: HandlerId,
9832 sequence: u64,
9833}
9834
9835#[derive(Clone, Debug, PartialEq, Eq)]
9842#[non_exhaustive]
9843pub enum SubscriberInputScope {
9844 Canonical {
9846 owners: Vec<SubscriberOwnerEpoch>,
9848 },
9849 CanonicalResidual {
9853 owners: Vec<SubscriberOwnerEpoch>,
9855 excluded: Vec<HandlerId>,
9857 },
9858 OwnerOnly {
9860 owners: Vec<SubscriberOwnerEpoch>,
9862 },
9863 OwnerOnlyHandlers {
9865 owners: Vec<HandlerId>,
9867 },
9868 Preconfirmed,
9871}
9872
9873impl SubscriberInputScope {
9874 pub fn owners(&self) -> &[SubscriberOwnerEpoch] {
9876 match self {
9877 Self::Canonical { owners }
9878 | Self::CanonicalResidual { owners, .. }
9879 | Self::OwnerOnly { owners } => owners,
9880 Self::OwnerOnlyHandlers { .. } | Self::Preconfirmed => &[],
9881 }
9882 }
9883
9884 pub const fn is_canonical(&self) -> bool {
9886 matches!(
9887 self,
9888 Self::Canonical { .. } | Self::CanonicalResidual { .. }
9889 )
9890 }
9891
9892 pub const fn is_preconfirmed(&self) -> bool {
9894 matches!(self, Self::Preconfirmed)
9895 }
9896}
9897
9898#[derive(Clone, Debug)]
9900pub struct SubscriberInputRecord<N: Network = Ethereum> {
9901 record: ReactiveInputRecord<N>,
9902 scope: SubscriberInputScope,
9903}
9904
9905impl<N: Network> SubscriberInputRecord<N> {
9906 pub const fn record(&self) -> &ReactiveInputRecord<N> {
9908 &self.record
9909 }
9910
9911 pub const fn scope(&self) -> &SubscriberInputScope {
9913 &self.scope
9914 }
9915
9916 pub fn into_record(self) -> ReactiveInputRecord<N> {
9918 self.record
9919 }
9920}
9921
9922impl<N: Network> std::ops::Deref for SubscriberInputRecord<N> {
9923 type Target = ReactiveInputRecord<N>;
9924
9925 fn deref(&self) -> &Self::Target {
9926 &self.record
9927 }
9928}
9929
9930#[derive(Clone, Debug)]
9932pub struct SubscriberInputBatch<N: Network = Ethereum> {
9933 records: Vec<SubscriberInputRecord<N>>,
9934 chain_id: Option<u64>,
9935 chain_controls: Vec<ChainControl>,
9936}
9937
9938#[derive(Debug)]
9941#[non_exhaustive]
9942pub enum SubscriberDriverPoll<C, N: Network = Ethereum> {
9943 Control(C),
9945 Batch(Option<SubscriberInputBatch<N>>),
9947}
9948
9949impl<N: Network> SubscriberInputBatch<N> {
9950 pub fn records(&self) -> &[SubscriberInputRecord<N>] {
9952 &self.records
9953 }
9954
9955 pub fn into_records(self) -> Vec<SubscriberInputRecord<N>> {
9957 self.records
9958 }
9959
9960 pub fn chain_controls(&self) -> &[ChainControl] {
9962 &self.chain_controls
9963 }
9964
9965 pub fn into_reactive_batch(self) -> ReactiveInputBatch<N> {
9971 let chain_id = self.chain_id;
9972 let chain_controls = self.chain_controls;
9973 let mut batch = ReactiveInputBatch::from_scoped_records_with_delivery_scope(
9974 self.records.into_iter().map(|scoped| {
9975 let source = scoped.record.context.source;
9976 let (audience, delivery_scope) = match scoped.scope {
9977 SubscriberInputScope::Canonical { .. } => (
9978 DeliveryAudience::All,
9979 if source == InputSource::Backfill {
9980 DeliveryScope::CanonicalProgress
9981 } else {
9982 DeliveryScope::Canonical
9983 },
9984 ),
9985 SubscriberInputScope::CanonicalResidual { excluded, .. } => (
9986 DeliveryAudience::AllExcept(excluded),
9987 if source == InputSource::Backfill {
9988 DeliveryScope::CanonicalProgress
9989 } else {
9990 DeliveryScope::Canonical
9991 },
9992 ),
9993 SubscriberInputScope::OwnerOnly { owners } => {
9994 let mut handler_ids = Vec::with_capacity(owners.len());
9995 for epoch in owners {
9996 if !handler_ids.contains(epoch.owner()) {
9997 handler_ids.push(epoch.owner().clone());
9998 }
9999 }
10000 (
10001 DeliveryAudience::Owners(handler_ids),
10002 DeliveryScope::OwnerCatchup,
10003 )
10004 }
10005 SubscriberInputScope::OwnerOnlyHandlers { owners } => (
10006 DeliveryAudience::Owners(owners),
10007 DeliveryScope::OwnerCatchup,
10008 ),
10009 SubscriberInputScope::Preconfirmed => {
10010 (DeliveryAudience::All, DeliveryScope::Preconfirmed)
10011 }
10012 };
10013 (scoped.record, audience, delivery_scope)
10014 }),
10015 )
10016 .with_chain_controls(chain_controls);
10017 if let Some(chain_id) = chain_id {
10018 batch = batch.with_chain_id(chain_id);
10019 }
10020 batch
10021 }
10022}
10023
10024impl SubscriberOwnerEpoch {
10025 pub const fn owner(&self) -> &HandlerId {
10027 &self.owner
10028 }
10029
10030 pub const fn sequence(&self) -> u64 {
10032 self.sequence
10033 }
10034}
10035
10036#[derive(Clone, Debug, PartialEq, Eq)]
10038#[non_exhaustive]
10039pub enum SubscriberOwnerStart {
10040 Live,
10042 PostBlock(BlockRef),
10049}
10050
10051#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10053#[non_exhaustive]
10054pub enum SubscriberOwnerState {
10055 Staged,
10058 Active,
10060 Removing,
10062}
10063
10064#[derive(Clone, Debug, PartialEq, Eq)]
10070pub struct SubscriberOwnerProgress {
10071 owner: SubscriberOwnerEpoch,
10072 through: BlockRef,
10073}
10074
10075impl SubscriberOwnerProgress {
10076 pub const fn owner(&self) -> &SubscriberOwnerEpoch {
10078 &self.owner
10079 }
10080
10081 pub const fn through(&self) -> &BlockRef {
10083 &self.through
10084 }
10085}
10086
10087#[derive(Debug, thiserror::Error)]
10089#[non_exhaustive]
10090pub enum SubscriberOwnerError {
10091 #[error(transparent)]
10093 Subscriber(#[from] SubscriberError),
10094 #[error("subscriber interest owner `{0}` is already registered")]
10096 AlreadyRegistered(HandlerId),
10097 #[error("post-block subscriber baseline {0} has no following block")]
10099 PostBlockOverflow(u64),
10100 #[error("subscriber owner epoch sequence exhausted")]
10102 EpochExhausted,
10103 #[error("subscriber owner epoch is not staged")]
10105 NotStaged,
10106 #[error("subscriber owner was staged live-only and has no catch-up baseline")]
10108 MissingBaseline,
10109 #[error("post-block subscriber owners support log interests only")]
10111 UnsupportedPostBlockInterest,
10112 #[error("subscriber reconcile target block {0} was not found")]
10114 BlockUnavailable(u64),
10115 #[error(
10117 "subscriber reconcile target mismatch: expected block {expected_number} {expected_hash}, got block {actual_number} {actual_hash}"
10118 )]
10119 BlockMismatch {
10120 expected_number: u64,
10122 expected_hash: B256,
10124 actual_number: u64,
10126 actual_hash: B256,
10128 },
10129 #[error("subscriber reconcile target block {target} precedes current owner position {current}")]
10131 ProgressRegression {
10132 current: u64,
10134 target: u64,
10136 },
10137 #[error(
10140 "subscriber reconcile conflicts with retained block {number} {current_hash}: target chain references {target_hash}"
10141 )]
10142 ProgressConflict {
10143 number: u64,
10145 current_hash: B256,
10147 target_hash: B256,
10149 },
10150 #[error("subscriber reconcile returned an invalid catch-up log: {0}")]
10152 InvalidBackfillLog(&'static str),
10153}
10154
10155pub trait InterestOwnerSubscriber<N: Network = Ethereum>: EventSubscriber<N> {
10170 fn upsert_interest_owners(
10183 &mut self,
10184 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
10185 ) -> SubscriberOperation<'_, ()> {
10186 Box::pin(async {
10187 Err(SubscriberError::Unsupported(
10188 "subscriber does not implement atomic bulk owner upsert",
10189 ))
10190 })
10191 }
10192
10193 fn replace_interest_owners(
10206 &mut self,
10207 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
10208 ) -> SubscriberOperation<'_, ()> {
10209 Box::pin(async {
10210 Err(SubscriberError::Unsupported(
10211 "subscriber does not implement atomic exact owner replacement",
10212 ))
10213 })
10214 }
10215
10216 fn replace_interest_owners_with_global_backfill(
10240 &mut self,
10241 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
10242 _backfill: SubscriberBackfill,
10243 ) -> SubscriberOperation<'_, ()> {
10244 Box::pin(async {
10245 Err(SubscriberError::Unsupported(
10246 "subscriber does not implement atomic owner replacement with global backfill",
10247 ))
10248 })
10249 }
10250
10251 fn add_interest_owner(
10263 &mut self,
10264 owner: HandlerId,
10265 interests: &[ReactiveInterest<N>],
10266 ) -> SubscriberOperation<'_, ()>;
10267
10268 fn add_interest_owner_with_backfill(
10276 &mut self,
10277 owner: HandlerId,
10278 interests: &[ReactiveInterest<N>],
10279 backfill: SubscriberBackfill,
10280 ) -> SubscriberOperation<'_, ()>;
10281
10282 fn add_interest_owner_with_canonical_catchup(
10300 &mut self,
10301 _owner: HandlerId,
10302 _interests: &[ReactiveInterest<N>],
10303 _retained: BlockRef,
10304 ) -> SubscriberOperation<'_, ()> {
10305 Box::pin(async {
10306 Err(SubscriberError::Unsupported(
10307 "subscriber does not implement coordinated canonical owner catch-up",
10308 ))
10309 })
10310 }
10311
10312 fn remove_interest_owner(
10323 &mut self,
10324 owner: &HandlerId,
10325 ) -> SubscriberOperation<'_, Option<Vec<ReactiveInterest<N>>>>;
10326
10327 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]>;
10329}
10330
10331pub struct ReactiveEngine<S, N: Network = Ethereum> {
10372 runtime: ReactiveRuntime<N>,
10373 subscriber: S,
10374 pending_acknowledgement: Option<PendingAcknowledgement<N>>,
10375 pending_checkpoint: Option<PendingCheckpoint<N>>,
10376 last_checkpoint_block: Option<DurableCheckpointBlock>,
10377 last_checkpoint_delivery_token: Option<SubscriberDeliveryToken>,
10378 last_checkpoint_delivery_witness: Option<B256>,
10379 last_subscriber_checkpoint: Option<SubscriberCheckpoint>,
10380 checkpoint_identity: Option<DurableCheckpointIdentity>,
10381}
10382
10383struct PendingAcknowledgement<N: Network> {
10384 token: SubscriberDeliveryToken,
10385 report: ReactiveBatchReport<N>,
10386}
10387
10388struct PendingCheckpoint<N: Network> {
10389 metadata: DurableCheckpointMetadata,
10390 delivery_token: Option<SubscriberDeliveryToken>,
10391 report: ReactiveBatchReport<N>,
10392 saved_to: Option<PathBuf>,
10393 staged_generation: u64,
10394}
10395
10396struct CheckpointStage<N: Network> {
10397 incoming_block: Option<DurableCheckpointBlock>,
10398 delivery_token: Option<SubscriberDeliveryToken>,
10399 delivery_witness: Option<B256>,
10400 subscriber_checkpoint: Option<SubscriberCheckpoint>,
10401 staged_generation: u64,
10402 report: ReactiveBatchReport<N>,
10403}
10404
10405struct DurableResumePlan {
10406 runtime: DurableRuntimeRestorePlan,
10407 position: SubscriberResumePosition,
10408 delivery_witness: Option<B256>,
10409}
10410
10411enum HandlerRegistrationCatchup {
10412 LiveOnly,
10413 OwnerBackfill(SubscriberBackfill),
10414 CoordinatedCanonical(BlockRef),
10415}
10416
10417const DELIVERY_WITNESS_VERSION: u32 = 1;
10418const DELIVERY_WITNESS_DOMAIN: &[u8] = b"evm-fork-cache/reactive-delivery-witness";
10419
10420#[derive(serde::Serialize)]
10421struct DeliveryWitnessEnvelope<'a> {
10422 version: u32,
10423 chain_id: Option<u64>,
10424 records: Vec<DeliveryRecordWitness<'a>>,
10425 chain_controls: &'a [ChainControl],
10426 subscriber_checkpoint: Option<&'a [u8]>,
10427 payload_commitment: Option<B256>,
10428}
10429
10430#[derive(serde::Serialize)]
10431struct DeliveryRecordWitness<'a> {
10432 identity: ReactiveInputIdentity,
10433 context: &'a ReactiveContext,
10434 audience: &'a DeliveryAudience,
10435 scope: DeliveryScope,
10436 payload: DeliveryPayloadWitness<'a>,
10437}
10438
10439#[derive(serde::Serialize)]
10440enum DeliveryPayloadWitness<'a> {
10441 Log {
10444 address: Address,
10445 topics: &'a [B256],
10446 data: &'a Bytes,
10447 block_hash: Option<B256>,
10448 block_number: Option<u64>,
10449 block_timestamp: Option<u64>,
10450 transaction_hash: Option<B256>,
10451 transaction_index: Option<u64>,
10452 log_index: Option<u64>,
10453 removed: bool,
10454 },
10455 IdentityCommitted,
10462}
10463
10464fn durable_delivery_witness<N: Network>(
10465 batch: &ReactiveInputBatch<N>,
10466) -> Result<B256, ReactiveEngineError> {
10467 let requires_payload_commitment = batch.records.iter().any(|record| {
10468 matches!(
10469 &record.input,
10470 ReactiveInput::BlockHeader(_)
10471 | ReactiveInput::FullBlock(_)
10472 | ReactiveInput::PendingTx(_)
10473 )
10474 });
10475 if requires_payload_commitment && batch.payload_commitment.is_none() {
10476 return Err(ReactiveEngineError::MissingPayloadCommitment);
10477 }
10478 let records = batch
10479 .records
10480 .iter()
10481 .enumerate()
10482 .map(|(index, record)| {
10483 let payload = match &record.input {
10484 ReactiveInput::Log(log) => DeliveryPayloadWitness::Log {
10485 address: log.address(),
10486 topics: log.topics(),
10487 data: &log.inner.data.data,
10488 block_hash: log.block_hash,
10489 block_number: log.block_number,
10490 block_timestamp: log.block_timestamp,
10491 transaction_hash: log.transaction_hash,
10492 transaction_index: log.transaction_index,
10493 log_index: log.log_index,
10494 removed: log.removed,
10495 },
10496 ReactiveInput::BlockHeader(_)
10497 | ReactiveInput::FullBlock(_)
10498 | ReactiveInput::PendingTxHash(_)
10499 | ReactiveInput::PendingTx(_) => DeliveryPayloadWitness::IdentityCommitted,
10500 };
10501 Ok(DeliveryRecordWitness {
10502 identity: record.validated_identity()?,
10503 context: &record.context,
10504 audience: batch
10505 .record_audience(index)
10506 .expect("enumerated record always has an audience"),
10507 scope: batch
10508 .record_delivery_scope(index)
10509 .expect("enumerated record always has a delivery scope"),
10510 payload,
10511 })
10512 })
10513 .collect::<Result<Vec<_>, ReactiveError>>()?;
10514 let envelope = DeliveryWitnessEnvelope {
10515 version: DELIVERY_WITNESS_VERSION,
10516 chain_id: batch.chain_id,
10517 records,
10518 chain_controls: &batch.chain_controls,
10519 subscriber_checkpoint: batch
10520 .subscriber_checkpoint
10521 .as_ref()
10522 .map(SubscriberCheckpoint::as_bytes),
10523 payload_commitment: batch
10524 .payload_commitment
10525 .as_ref()
10526 .map(SubscriberPayloadCommitment::digest),
10527 };
10528 let encoded = bincode::DefaultOptions::new()
10529 .with_fixint_encoding()
10530 .serialize(&envelope)
10531 .map_err(|error| ReactiveEngineError::DeliveryWitness(error.to_string()))?;
10532 let mut witness = Keccak256::new();
10533 witness.update(DELIVERY_WITNESS_DOMAIN);
10534 witness.update(encoded);
10535 Ok(witness.finalize())
10536}
10537
10538impl<S, N> ReactiveEngine<S, N>
10539where
10540 N: Network,
10541 S: EventSubscriber<N>,
10542{
10543 pub fn new(runtime: ReactiveRuntime<N>, subscriber: S) -> Self {
10545 Self {
10546 runtime,
10547 subscriber,
10548 pending_acknowledgement: None,
10549 pending_checkpoint: None,
10550 last_checkpoint_block: None,
10551 last_checkpoint_delivery_token: None,
10552 last_checkpoint_delivery_witness: None,
10553 last_subscriber_checkpoint: None,
10554 checkpoint_identity: None,
10555 }
10556 }
10557
10558 pub fn into_parts(self) -> Result<(ReactiveRuntime<N>, S), Box<Self>> {
10572 if self.pending_acknowledgement.is_some() || self.pending_checkpoint.is_some() {
10573 return Err(Box::new(self));
10574 }
10575 Ok((self.runtime, self.subscriber))
10576 }
10577
10578 fn durable_resume_plan(
10579 &self,
10580 metadata: &DurableCheckpointMetadata,
10581 ) -> Result<DurableResumePlan, ReactiveCheckpointRestoreError> {
10582 if !self.subscriber.capabilities().supports_durable_replay() {
10583 return Err(ReactiveCheckpointRestoreError::SubscriberNotDurable);
10584 }
10585 self.ensure_subscriber_restore_chain(metadata.identity.chain_id)?;
10586 if !self.runtime.is_pristine_for_checkpoint_restore()
10587 || self.pending_acknowledgement.is_some()
10588 || self.pending_checkpoint.is_some()
10589 || self.last_checkpoint_block.is_some()
10590 || self.last_checkpoint_delivery_token.is_some()
10591 || self.last_checkpoint_delivery_witness.is_some()
10592 || self.last_subscriber_checkpoint.is_some()
10593 || self.checkpoint_identity.is_some()
10594 {
10595 return Err(ReactiveCheckpointRestoreError::ActiveRuntime);
10596 }
10597
10598 let block = BlockRef {
10599 number: metadata.block.number,
10600 hash: metadata.block.hash,
10601 parent_hash: metadata.block.parent_hash,
10602 timestamp: metadata.block.timestamp,
10603 };
10604 let runtime = match metadata.runtime_checkpoint.as_deref() {
10605 Some(bytes) => self
10606 .runtime
10607 .plan_durable_checkpoint_restore(bytes, &block)?,
10608 None => DurableRuntimeRestorePlan {
10609 checkpoint: None,
10610 fallback_history: (self.runtime.config.journal_depth > 0)
10611 .then_some(block)
10612 .into_iter()
10613 .collect(),
10614 },
10615 };
10616 let delivery_token = metadata
10617 .delivery_token
10618 .clone()
10619 .map(SubscriberDeliveryToken::new);
10620 let subscriber_checkpoint = metadata
10621 .subscriber_checkpoint
10622 .clone()
10623 .map(SubscriberCheckpoint::new);
10624 let position = SubscriberResumePosition::new(
10625 metadata.identity.chain_id,
10626 block,
10627 runtime.canonical_history(),
10628 delivery_token,
10629 subscriber_checkpoint,
10630 );
10631 Ok(DurableResumePlan {
10632 runtime,
10633 position,
10634 delivery_witness: metadata.delivery_witness,
10635 })
10636 }
10637
10638 pub fn preview_durable_resume_position(
10667 &self,
10668 metadata: &DurableCheckpointMetadata,
10669 ) -> Result<SubscriberResumePosition, ReactiveCheckpointRestoreError> {
10670 Ok(self.durable_resume_plan(metadata)?.position)
10671 }
10672
10673 pub fn resume_from_durable_checkpoint(
10695 &mut self,
10696 metadata: &DurableCheckpointMetadata,
10697 ) -> Result<(), ReactiveCheckpointRestoreError> {
10698 let plan = self.durable_resume_plan(metadata)?;
10699 let prior_runtime = self.runtime.checkpoint_state();
10700
10701 let DurableResumePlan {
10702 runtime,
10703 position,
10704 delivery_witness,
10705 } = plan;
10706 self.runtime.apply_durable_checkpoint_restore(runtime);
10707 self.runtime.coverage_head = Some(position.coverage_head);
10708 if let Err(error) = self.subscriber.restore_position(&position) {
10709 self.runtime.restore_state(prior_runtime);
10710 return Err(ReactiveCheckpointRestoreError::Subscriber(error));
10711 }
10712 if let Err(error) = self.ensure_subscriber_restore_chain(metadata.identity.chain_id) {
10713 self.runtime.restore_state(prior_runtime);
10714 return Err(error);
10715 }
10716 self.last_checkpoint_block = Some(metadata.block.clone());
10717 self.last_checkpoint_delivery_token = position.delivery_token;
10718 self.last_checkpoint_delivery_witness = delivery_witness;
10719 self.last_subscriber_checkpoint = position.subscriber_checkpoint;
10720 self.checkpoint_identity = Some(metadata.identity.clone());
10721 Ok(())
10722 }
10723
10724 pub fn restore_durable_checkpoint(
10740 &mut self,
10741 cache: &mut EvmCache,
10742 loaded: LoadedDurableCheckpoint,
10743 expected: &DurableCheckpointIdentity,
10744 ) -> Result<DurableCheckpointMetadata, ReactiveCheckpointRestoreError> {
10745 if !self.subscriber.capabilities().supports_durable_replay() {
10746 return Err(ReactiveCheckpointRestoreError::SubscriberNotDurable);
10747 }
10748 self.ensure_subscriber_restore_chain(expected.chain_id)?;
10749 if !self.runtime.is_pristine_for_checkpoint_restore()
10750 || self.pending_acknowledgement.is_some()
10751 || self.pending_checkpoint.is_some()
10752 || self.last_checkpoint_block.is_some()
10753 || self.last_checkpoint_delivery_token.is_some()
10754 || self.last_checkpoint_delivery_witness.is_some()
10755 || self.last_subscriber_checkpoint.is_some()
10756 || self.checkpoint_identity.is_some()
10757 {
10758 return Err(ReactiveCheckpointRestoreError::ActiveRuntime);
10759 }
10760
10761 let prior_cache = EvmCacheStateSnapshot::capture(cache);
10762 let metadata = loaded.restore_into(cache, expected)?;
10763 if let Err(error) = self.resume_from_durable_checkpoint(&metadata) {
10764 prior_cache.restore(cache);
10765 return Err(error);
10766 }
10767 Ok(metadata)
10768 }
10769
10770 pub fn runtime(&self) -> &ReactiveRuntime<N> {
10772 &self.runtime
10773 }
10774
10775 pub fn runtime_mut(&mut self) -> &mut ReactiveRuntime<N> {
10777 &mut self.runtime
10778 }
10779
10780 pub fn subscriber(&self) -> &S {
10782 &self.subscriber
10783 }
10784
10785 pub fn subscriber_mut(&mut self) -> &mut S {
10787 &mut self.subscriber
10788 }
10789
10790 pub fn adopt_canonical_baseline(
10808 &mut self,
10809 cache: &EvmCache,
10810 baseline: ReactiveCanonicalBaseline,
10811 ) -> Result<(), ReactiveEngineError> {
10812 if self.pending_acknowledgement.is_some()
10813 || self.pending_checkpoint.is_some()
10814 || self.last_checkpoint_block.is_some()
10815 || self.last_checkpoint_delivery_token.is_some()
10816 || self.last_checkpoint_delivery_witness.is_some()
10817 || self.last_subscriber_checkpoint.is_some()
10818 || self.checkpoint_identity.is_some()
10819 {
10820 return Err(ReactiveBaselineError::ActiveRuntime.into());
10821 }
10822 self.runtime
10826 .validate_canonical_baseline_adoption(baseline.block)?;
10827 if baseline.chain_id != cache.chain_id() {
10828 return Err(ReactiveBaselineError::CacheChainMismatch {
10829 baseline_chain_id: baseline.chain_id,
10830 cache_chain_id: cache.chain_id(),
10831 }
10832 .into());
10833 }
10834 self.ensure_subscriber_chain(cache)?;
10835 let exact_selector = BlockId::from((baseline.block.hash, Some(true)));
10836 let context_matches = cache.block_number() == Some(baseline.block.number)
10837 && baseline
10838 .block
10839 .timestamp
10840 .is_none_or(|timestamp| cache.timestamp() == Some(timestamp));
10841 if cache.block() != exact_selector || !context_matches {
10842 return Err(ReactiveBaselineError::CacheBlockMismatch {
10843 number: baseline.block.number,
10844 hash: baseline.block.hash,
10845 }
10846 .into());
10847 }
10848 self.runtime.adopt_canonical_baseline(baseline.block)?;
10849 Ok(())
10850 }
10851
10852 pub fn next_batch(
10865 &mut self,
10866 cache: &EvmCache,
10867 ) -> Result<SubscriberNextBatch<'_, N>, ReactiveEngineError> {
10868 if self.pending_checkpoint.is_some() {
10869 return Err(ReactiveEngineError::PendingCheckpointCommit);
10870 }
10871 if self.pending_acknowledgement.is_some() {
10872 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
10873 }
10874 self.ensure_subscriber_chain(cache)?;
10875 Ok(self.subscriber.next_batch())
10876 }
10877
10878 pub fn ingest_batch(
10887 &mut self,
10888 cache: &mut EvmCache,
10889 batch: ReactiveInputBatch<N>,
10890 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
10891 self.ensure_raw_ingest_is_safe(cache, &batch)?;
10892 Ok(self.runtime.ingest_batch(cache, batch)?)
10893 }
10894
10895 pub fn ingest_batch_with_resync(
10905 &mut self,
10906 cache: &mut EvmCache,
10907 batch: ReactiveInputBatch<N>,
10908 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
10909 self.ensure_raw_ingest_is_safe(cache, &batch)?;
10910 Ok(self.runtime.ingest_batch_with_resync(cache, batch)?)
10911 }
10912
10913 fn ensure_raw_ingest_is_safe(
10914 &self,
10915 cache: &EvmCache,
10916 batch: &ReactiveInputBatch<N>,
10917 ) -> Result<(), ReactiveEngineError> {
10918 if self.pending_checkpoint.is_some() {
10919 return Err(ReactiveEngineError::PendingCheckpointCommit);
10920 }
10921 if self.pending_acknowledgement.is_some() {
10922 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
10923 }
10924 if batch.delivery_token().is_some() || batch.subscriber_checkpoint().is_some() {
10925 return Err(ReactiveEngineError::UncommittedDeliveryMetadata);
10926 }
10927 self.ensure_subscriber_chain(cache)?;
10928 Ok(())
10929 }
10930
10931 fn ensure_subscriber_chain(&self, cache: &EvmCache) -> Result<(), ReactiveEngineError> {
10932 if let Some(subscriber_chain_id) = self.subscriber.chain_id()
10933 && subscriber_chain_id != cache.chain_id()
10934 {
10935 return Err(ReactiveEngineError::SubscriberChainMismatch {
10936 subscriber_chain_id,
10937 cache_chain_id: cache.chain_id(),
10938 });
10939 }
10940 Ok(())
10941 }
10942
10943 fn ensure_subscriber_restore_chain(
10944 &self,
10945 checkpoint_chain_id: u64,
10946 ) -> Result<(), ReactiveCheckpointRestoreError> {
10947 if let Some(subscriber_chain_id) = self.subscriber.chain_id()
10948 && subscriber_chain_id != checkpoint_chain_id
10949 {
10950 return Err(ReactiveCheckpointRestoreError::SubscriberChainMismatch {
10951 subscriber_chain_id,
10952 checkpoint_chain_id,
10953 });
10954 }
10955 Ok(())
10956 }
10957
10958 pub async fn next_ingest(
10968 &mut self,
10969 cache: &mut EvmCache,
10970 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
10971 self.ensure_subscriber_chain(cache)?;
10972 if self.pending_checkpoint.is_some() {
10973 return Err(ReactiveEngineError::PendingCheckpointCommit);
10974 }
10975 if self.pending_acknowledgement.is_some() {
10976 return self.commit_pending_acknowledgement().await.map(Some);
10977 }
10978 let batch = self.subscriber.next_batch().await?;
10979 self.ensure_subscriber_chain(cache)?;
10980 let Some(mut batch) = batch else {
10981 return Ok(None);
10982 };
10983 let delivery_token = batch.take_delivery_token();
10984 let report = self.runtime.ingest_batch(cache, batch)?;
10985 self.stage_or_return_acknowledgement(delivery_token, report)
10986 .await
10987 }
10988
10989 pub async fn next_ingest_with_resync(
11000 &mut self,
11001 cache: &mut EvmCache,
11002 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
11003 self.ensure_subscriber_chain(cache)?;
11004 if self.pending_checkpoint.is_some() {
11005 return Err(ReactiveEngineError::PendingCheckpointCommit);
11006 }
11007 if self.pending_acknowledgement.is_some() {
11008 return self.commit_pending_acknowledgement().await.map(Some);
11009 }
11010 let batch = self.subscriber.next_batch().await?;
11011 self.ensure_subscriber_chain(cache)?;
11012 let Some(mut batch) = batch else {
11013 return Ok(None);
11014 };
11015 let delivery_token = batch.take_delivery_token();
11016 let report = self.runtime.ingest_batch_with_resync(cache, batch)?;
11017 self.stage_or_return_acknowledgement(delivery_token, report)
11018 .await
11019 }
11020
11021 pub async fn next_ingest_checkpointed(
11052 &mut self,
11053 cache: &mut EvmCache,
11054 store: &DurableCheckpointStore,
11055 identity: &DurableCheckpointIdentity,
11056 ) -> Result<Option<CheckpointedIngest<N>>, ReactiveEngineError> {
11057 if !self.subscriber.capabilities().supports_durable_replay() {
11058 return Err(ReactiveEngineError::SubscriberNotDurable);
11059 }
11060 self.ensure_subscriber_chain(cache)?;
11061 if self.pending_acknowledgement.is_some() {
11062 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11063 }
11064 self.ensure_checkpoint_identity(cache, identity)?;
11065 if self.pending_checkpoint.is_some() {
11066 return self.commit_pending_checkpoint(cache, store).await.map(Some);
11067 }
11068
11069 let batch = self.subscriber.next_batch().await?;
11070 self.ensure_subscriber_chain(cache)?;
11071 let Some(mut batch) = batch else {
11072 return Ok(None);
11073 };
11074 if batch_preconfirmation(&batch)?.is_some() {
11075 return Err(ReactiveEngineError::PreconfirmationNotCheckpointable);
11076 }
11077 self.runtime.discard_preconfirmed_branch(cache);
11078 let delivery_witness = batch
11079 .delivery_token()
11080 .map(|_| durable_delivery_witness(&batch))
11081 .transpose()?;
11082 let delivery_token = batch.take_delivery_token();
11083 let subscriber_checkpoint = batch.take_subscriber_checkpoint();
11084 if let (Some(replay_token), Some(committed_token)) = (
11085 delivery_token.as_ref(),
11086 self.last_checkpoint_delivery_token.as_ref(),
11087 ) && replay_token == committed_token
11088 {
11089 let committed_witness = self
11090 .last_checkpoint_delivery_witness
11091 .ok_or(ReactiveEngineError::MissingReplayWitness)?;
11092 if delivery_witness != Some(committed_witness) {
11093 return Err(ReactiveEngineError::ReplayDeliveryMismatch);
11094 }
11095 self.subscriber
11096 .acknowledge_delivery(replay_token.clone())
11097 .await
11098 .map_err(ReactiveEngineError::Acknowledgement)?;
11099 return Ok(Some(CheckpointedIngest::ReplayAcknowledged));
11100 }
11101
11102 self.ensure_checkpointable_reorgs(&batch)?;
11103
11104 let incoming_block = latest_canonical_batch_block(&batch);
11105 let cache_state = EvmCacheStateSnapshot::capture(cache);
11106 let runtime_state = self.runtime.checkpoint_state();
11107 let report = match self.runtime.ingest_batch_direct(cache, batch) {
11108 Ok(report) => report,
11109 Err(error) => {
11110 cache_state.restore(cache);
11111 self.runtime.restore_transaction_state(runtime_state);
11112 return Err(error.into());
11113 }
11114 };
11115 let reports = report.reports.clone();
11116 let stage = CheckpointStage {
11117 incoming_block,
11118 delivery_token,
11119 delivery_witness,
11120 subscriber_checkpoint,
11121 staged_generation: cache.snapshot_generation(),
11122 report,
11123 };
11124 if let Err(error) = self.stage_checkpoint(identity, stage) {
11125 cache_state.restore(cache);
11126 self.runtime.restore_transaction_state(runtime_state);
11127 return Err(error);
11128 }
11129 self.runtime.dispatch_reports(&reports);
11130 self.commit_pending_checkpoint(cache, store).await.map(Some)
11131 }
11132
11133 pub async fn next_ingest_with_resync_checkpointed(
11144 &mut self,
11145 cache: &mut EvmCache,
11146 store: &DurableCheckpointStore,
11147 identity: &DurableCheckpointIdentity,
11148 ) -> Result<Option<CheckpointedIngest<N>>, ReactiveEngineError> {
11149 if !self.subscriber.capabilities().supports_durable_replay() {
11150 return Err(ReactiveEngineError::SubscriberNotDurable);
11151 }
11152 self.ensure_subscriber_chain(cache)?;
11153 if self.pending_acknowledgement.is_some() {
11154 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11155 }
11156 self.ensure_checkpoint_identity(cache, identity)?;
11157 if self.pending_checkpoint.is_some() {
11158 return self.commit_pending_checkpoint(cache, store).await.map(Some);
11159 }
11160
11161 let batch = self.subscriber.next_batch().await?;
11162 self.ensure_subscriber_chain(cache)?;
11163 let Some(mut batch) = batch else {
11164 return Ok(None);
11165 };
11166 if batch_preconfirmation(&batch)?.is_some() {
11167 return Err(ReactiveEngineError::PreconfirmationNotCheckpointable);
11168 }
11169 self.runtime.discard_preconfirmed_branch(cache);
11170 let delivery_witness = batch
11171 .delivery_token()
11172 .map(|_| durable_delivery_witness(&batch))
11173 .transpose()?;
11174 let delivery_token = batch.take_delivery_token();
11175 let subscriber_checkpoint = batch.take_subscriber_checkpoint();
11176 if let (Some(replay_token), Some(committed_token)) = (
11177 delivery_token.as_ref(),
11178 self.last_checkpoint_delivery_token.as_ref(),
11179 ) && replay_token == committed_token
11180 {
11181 let committed_witness = self
11182 .last_checkpoint_delivery_witness
11183 .ok_or(ReactiveEngineError::MissingReplayWitness)?;
11184 if delivery_witness != Some(committed_witness) {
11185 return Err(ReactiveEngineError::ReplayDeliveryMismatch);
11186 }
11187 self.subscriber
11188 .acknowledge_delivery(replay_token.clone())
11189 .await
11190 .map_err(ReactiveEngineError::Acknowledgement)?;
11191 return Ok(Some(CheckpointedIngest::ReplayAcknowledged));
11192 }
11193
11194 self.ensure_checkpointable_reorgs(&batch)?;
11195
11196 let incoming_block = latest_canonical_batch_block(&batch);
11197 let cache_state = EvmCacheStateSnapshot::capture(cache);
11198 let runtime_state = self.runtime.checkpoint_state();
11199 let report = match self.runtime.ingest_batch_with_resync_direct(cache, batch) {
11200 Ok(report) => report,
11201 Err(error) => {
11202 cache_state.restore(cache);
11203 self.runtime.restore_transaction_state(runtime_state);
11204 return Err(error.into());
11205 }
11206 };
11207 let reports = report.reports.clone();
11208 let stage = CheckpointStage {
11209 incoming_block,
11210 delivery_token,
11211 delivery_witness,
11212 subscriber_checkpoint,
11213 staged_generation: cache.snapshot_generation(),
11214 report,
11215 };
11216 if let Err(error) = self.stage_checkpoint(identity, stage) {
11217 cache_state.restore(cache);
11218 self.runtime.restore_transaction_state(runtime_state);
11219 return Err(error);
11220 }
11221 self.runtime.dispatch_reports(&reports);
11222 self.commit_pending_checkpoint(cache, store).await.map(Some)
11223 }
11224
11225 fn stage_checkpoint(
11226 &mut self,
11227 identity: &DurableCheckpointIdentity,
11228 stage: CheckpointStage<N>,
11229 ) -> Result<(), ReactiveEngineError> {
11230 let CheckpointStage {
11231 incoming_block,
11232 delivery_token,
11233 delivery_witness,
11234 subscriber_checkpoint,
11235 staged_generation,
11236 report,
11237 } = stage;
11238 if delivery_token.is_some() != delivery_witness.is_some() {
11239 return Err(ReactiveEngineError::DeliveryWitness(
11240 "delivery token and witness must be staged together".into(),
11241 ));
11242 }
11243 let runtime_checkpoint = self.runtime.durable_checkpoint_bytes()?;
11244 let block = self
11245 .runtime
11246 .last_canonical_block()
11247 .map(|block| DurableCheckpointBlock {
11248 number: block.number,
11249 hash: block.hash,
11250 parent_hash: block.parent_hash,
11251 timestamp: block.timestamp,
11252 })
11253 .or(incoming_block)
11254 .or_else(|| self.last_checkpoint_block.clone())
11255 .ok_or(ReactiveEngineError::MissingCheckpointBlock)?;
11256 let metadata = DurableCheckpointMetadata {
11257 identity: identity.clone(),
11258 block,
11259 delivery_token: delivery_token
11260 .as_ref()
11261 .or(self.last_checkpoint_delivery_token.as_ref())
11262 .map(|token| token.as_bytes().to_vec()),
11263 delivery_witness: if delivery_token.is_some() {
11264 delivery_witness
11265 } else {
11266 self.last_checkpoint_delivery_witness
11267 },
11268 subscriber_checkpoint: subscriber_checkpoint
11269 .as_ref()
11270 .or(self.last_subscriber_checkpoint.as_ref())
11271 .map(|checkpoint| checkpoint.as_bytes().to_vec()),
11272 runtime_checkpoint: Some(runtime_checkpoint),
11273 };
11274 self.pending_checkpoint = Some(PendingCheckpoint {
11275 metadata,
11276 delivery_token,
11277 report,
11278 saved_to: None,
11279 staged_generation,
11280 });
11281 Ok(())
11282 }
11283
11284 fn ensure_checkpointable_reorgs(
11285 &self,
11286 batch: &ReactiveInputBatch<N>,
11287 ) -> Result<(), ReactiveEngineError> {
11288 let state = CanonicalSequenceState::new(
11289 self.runtime
11290 .journal
11291 .iter()
11292 .map(|entry| entry.block)
11293 .collect(),
11294 self.runtime.coverage_head,
11295 self.runtime.safe_head,
11296 self.runtime.finalized_head,
11297 );
11298 match validate_canonical_sequence_internal(
11299 &state,
11300 batch,
11301 CanonicalSequenceValidationPolicy::RequireCompleteRollback,
11302 ) {
11303 Ok(_) => Ok(()),
11304 Err(CanonicalSequenceError::Invalid(error)) => Err(error.into()),
11305 Err(CanonicalSequenceError::IncompleteRollback {
11306 common_ancestor,
11307 oldest_retained,
11308 ..
11309 }) => Err(ReactiveEngineError::CheckpointReorgOutsideJournal {
11310 common_ancestor,
11311 oldest_journaled: oldest_retained,
11312 journal_depth: self.runtime.config.journal_depth,
11313 }),
11314 }
11315 }
11316
11317 async fn stage_or_return_acknowledgement(
11318 &mut self,
11319 delivery_token: Option<SubscriberDeliveryToken>,
11320 report: ReactiveBatchReport<N>,
11321 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
11322 let Some(token) = delivery_token else {
11323 return Ok(Some(report));
11324 };
11325 self.pending_acknowledgement = Some(PendingAcknowledgement { token, report });
11326 self.commit_pending_acknowledgement().await.map(Some)
11327 }
11328
11329 async fn commit_pending_acknowledgement(
11330 &mut self,
11331 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
11332 let token = self
11333 .pending_acknowledgement
11334 .as_ref()
11335 .expect("caller checked pending acknowledgement")
11336 .token
11337 .clone();
11338 self.subscriber
11339 .acknowledge_delivery(token)
11340 .await
11341 .map_err(ReactiveEngineError::Acknowledgement)?;
11342 Ok(self
11343 .pending_acknowledgement
11344 .take()
11345 .expect("pending acknowledgement remains until commit")
11346 .report)
11347 }
11348
11349 async fn commit_pending_checkpoint(
11350 &mut self,
11351 cache: &EvmCache,
11352 store: &DurableCheckpointStore,
11353 ) -> Result<CheckpointedIngest<N>, ReactiveEngineError> {
11354 let pending = self
11355 .pending_checkpoint
11356 .as_mut()
11357 .expect("caller checked pending checkpoint");
11358 let cache_generation = cache.snapshot_generation();
11359 if cache_generation != pending.staged_generation {
11360 return Err(ReactiveEngineError::PendingCheckpointCacheChanged {
11361 staged_generation: pending.staged_generation,
11362 current_generation: cache_generation,
11363 });
11364 }
11365 if pending.saved_to.as_deref() != Some(store.path()) {
11366 store
11367 .save_async(cache, pending.metadata.clone())
11368 .await
11369 .map_err(ReactiveEngineError::Checkpoint)?;
11370 pending.saved_to = Some(store.path().to_path_buf());
11371 }
11372 if let Some(token) = pending.delivery_token.clone() {
11373 self.subscriber
11374 .acknowledge_delivery(token)
11375 .await
11376 .map_err(ReactiveEngineError::Acknowledgement)?;
11377 }
11378
11379 let pending = self
11380 .pending_checkpoint
11381 .take()
11382 .expect("pending checkpoint remains until commit");
11383 self.last_checkpoint_block = Some(pending.metadata.block);
11384 self.checkpoint_identity = Some(pending.metadata.identity);
11385 self.last_checkpoint_delivery_token = pending
11386 .metadata
11387 .delivery_token
11388 .map(SubscriberDeliveryToken::new);
11389 self.last_checkpoint_delivery_witness = pending.metadata.delivery_witness;
11390 self.last_subscriber_checkpoint = pending
11391 .metadata
11392 .subscriber_checkpoint
11393 .map(SubscriberCheckpoint::new);
11394 Ok(CheckpointedIngest::Applied(pending.report))
11395 }
11396
11397 fn ensure_checkpoint_identity(
11398 &self,
11399 cache: &EvmCache,
11400 identity: &DurableCheckpointIdentity,
11401 ) -> Result<(), ReactiveEngineError> {
11402 if identity.chain_id != cache.chain_id() {
11403 return Err(ReactiveEngineError::Checkpoint(
11404 DurableCheckpointError::CacheChainMismatch {
11405 cache_chain_id: cache.chain_id(),
11406 checkpoint_chain_id: identity.chain_id,
11407 },
11408 ));
11409 }
11410 if let Some(actual) = self.checkpoint_identity.as_ref()
11411 && actual != identity
11412 {
11413 return Err(ReactiveEngineError::Checkpoint(
11414 DurableCheckpointError::IdentityMismatch {
11415 expected: identity.clone(),
11416 actual: actual.clone(),
11417 },
11418 ));
11419 }
11420 if let Some(pending) = self.pending_checkpoint.as_ref()
11421 && &pending.metadata.identity != identity
11422 {
11423 return Err(ReactiveEngineError::Checkpoint(
11424 DurableCheckpointError::IdentityMismatch {
11425 expected: identity.clone(),
11426 actual: pending.metadata.identity.clone(),
11427 },
11428 ));
11429 }
11430 Ok(())
11431 }
11432}
11433
11434fn latest_canonical_batch_block<N: Network>(
11435 batch: &ReactiveInputBatch<N>,
11436) -> Option<DurableCheckpointBlock> {
11437 let record_block = batch
11438 .records()
11439 .iter()
11440 .enumerate()
11441 .filter(|(index, _)| {
11442 batch
11443 .record_delivery_scope(*index)
11444 .is_some_and(DeliveryScope::advances_canonical_state)
11445 })
11446 .filter_map(|(_, record)| canonical_record_block(record))
11447 .max_by_key(|block| block.number)
11448 .cloned();
11449 let control_block = batch
11450 .chain_controls()
11451 .iter()
11452 .filter_map(|control| match control {
11453 ChainControl::Reorg {
11454 common_ancestor, ..
11455 } => Some(common_ancestor),
11456 ChainControl::Barrier {
11457 block: Some(block), ..
11458 }
11459 | ChainControl::CanonicalProgress(block) => Some(block),
11460 ChainControl::Safe(_)
11461 | ChainControl::Finalized(_)
11462 | ChainControl::Barrier { block: None, .. } => None,
11463 })
11464 .max_by_key(|block| block.number)
11465 .cloned();
11466
11467 record_block
11468 .into_iter()
11469 .chain(control_block)
11470 .max_by_key(|block| block.number)
11471 .map(|block| DurableCheckpointBlock {
11472 number: block.number,
11473 hash: block.hash,
11474 parent_hash: block.parent_hash,
11475 timestamp: block.timestamp,
11476 })
11477}
11478
11479impl<S, N> ReactiveEngine<S, N>
11480where
11481 N: Network,
11482 S: InterestOwnerSubscriber<N>,
11483{
11484 pub async fn register_handler(
11510 &mut self,
11511 handler: Arc<dyn ReactiveHandler<N>>,
11512 ) -> Result<(), ReactiveEngineRegisterError> {
11513 let backfill = self
11514 .runtime
11515 .last_canonical_block()
11516 .filter(|retained| {
11517 self.runtime.journal.iter().any(|entry| {
11518 optional_block_refs_are_compatible(Some(&entry.block), Some(retained))
11519 })
11520 })
11521 .map(HandlerRegistrationCatchup::CoordinatedCanonical)
11522 .unwrap_or(HandlerRegistrationCatchup::LiveOnly);
11523 self.register_handler_inner(handler, backfill).await
11524 }
11525
11526 pub async fn register_handler_with_backfill(
11545 &mut self,
11546 handler: Arc<dyn ReactiveHandler<N>>,
11547 backfill: SubscriberBackfill,
11548 ) -> Result<(), ReactiveEngineRegisterError> {
11549 self.register_handler_inner(handler, HandlerRegistrationCatchup::OwnerBackfill(backfill))
11550 .await
11551 }
11552
11553 pub async fn register_handler_live_only(
11564 &mut self,
11565 handler: Arc<dyn ReactiveHandler<N>>,
11566 ) -> Result<(), ReactiveEngineRegisterError> {
11567 self.register_handler_inner(handler, HandlerRegistrationCatchup::LiveOnly)
11568 .await
11569 }
11570
11571 async fn register_handler_inner(
11572 &mut self,
11573 handler: Arc<dyn ReactiveHandler<N>>,
11574 catchup: HandlerRegistrationCatchup,
11575 ) -> Result<(), ReactiveEngineRegisterError> {
11576 let id = handler.id();
11577 if self.runtime.contains_handler(&id) {
11578 return Err(RegisterError::DuplicateHandler(id).into());
11579 }
11580 let interests = handler.interests();
11581
11582 if let HandlerRegistrationCatchup::OwnerBackfill(backfill) = &catchup {
11583 let retained_anchor = backfill.retained_anchor().copied();
11584 let is_exact_retained_block = retained_anchor.is_some_and(|anchor| {
11585 backfill.start_block() == anchor.number
11586 && backfill.end_block() == Some(anchor.number)
11587 && self.runtime.journal.iter().any(|entry| {
11588 optional_block_refs_are_compatible(Some(&entry.block), Some(&anchor))
11589 })
11590 });
11591 if !is_exact_retained_block {
11592 return Err(ReactiveEngineRegisterError::BackfillOutsideJournal {
11593 start_block: backfill.start_block(),
11594 end_block: backfill.end_block(),
11595 retained_anchor,
11596 });
11597 }
11598 }
11599
11600 let subscribed = match catchup {
11601 HandlerRegistrationCatchup::OwnerBackfill(backfill) => {
11602 self.subscriber
11603 .add_interest_owner_with_backfill(id.clone(), &interests, backfill)
11604 .await
11605 }
11606 HandlerRegistrationCatchup::CoordinatedCanonical(retained) => {
11607 self.subscriber
11608 .add_interest_owner_with_canonical_catchup(id.clone(), &interests, retained)
11609 .await
11610 }
11611 HandlerRegistrationCatchup::LiveOnly => {
11612 self.subscriber
11613 .add_interest_owner(id.clone(), &interests)
11614 .await
11615 }
11616 };
11617 if let Err(error) = subscribed {
11618 return Err(error.into());
11619 }
11620
11621 self.runtime
11626 .registry
11627 .insert_handler_prepared(id, handler, interests);
11628 Ok(())
11629 }
11630
11631 pub async fn sync_handler_interests(&mut self) -> Result<(), SubscriberError> {
11656 let owners = self
11657 .runtime
11658 .handler_ids()
11659 .into_iter()
11660 .map(|id| {
11661 let interests = self
11662 .runtime
11663 .handler_interests(&id)
11664 .map(<[ReactiveInterest<N>]>::to_vec)
11665 .unwrap_or_default();
11666 (id, interests)
11667 })
11668 .collect();
11669 self.subscriber.replace_interest_owners(owners).await
11670 }
11671
11672 pub async fn sync_handler_interests_with_backfill(&mut self) -> Result<(), SubscriberError> {
11700 let baseline =
11701 self.runtime
11702 .last_canonical_block()
11703 .ok_or(SubscriberError::InvalidConfig(
11704 "cannot continuity-sync handlers before a canonical runtime position exists",
11705 ))?;
11706 let backfill = SubscriberBackfill::after_canonical_block(baseline)?;
11707 let owners = self
11708 .runtime
11709 .handler_ids()
11710 .into_iter()
11711 .map(|id| {
11712 let interests = self
11713 .runtime
11714 .handler_interests(&id)
11715 .map(<[ReactiveInterest<N>]>::to_vec)
11716 .unwrap_or_default();
11717 (id, interests)
11718 })
11719 .collect();
11720 self.subscriber
11721 .replace_interest_owners_with_global_backfill(owners, backfill)
11722 .await
11723 }
11724
11725 pub async fn unregister_handler(
11757 &mut self,
11758 id: &HandlerId,
11759 ) -> Result<Option<Arc<dyn ReactiveHandler<N>>>, SubscriberError> {
11760 self.subscriber.remove_interest_owner(id).await?;
11761 Ok(self.runtime.unregister_handler(id))
11762 }
11763}
11764
11765pub struct AlloySubscriber<P, N: Network = Ethereum> {
11786 provider: P,
11787 provider_ref: Option<ProviderRef>,
11790 log_verification_provider: Option<P>,
11794 chain_id: Option<u64>,
11796 mode: SubscriberMode,
11797 config: SubscriberConfig,
11798 base_interests: Vec<ReactiveInterest<N>>,
11799 owned_interests: Vec<OwnedSubscriberInterests<N>>,
11800 next_owner_epoch: u64,
11801 interests: Vec<ReactiveInterest<N>>,
11802 log_source_ids: HashMap<Filter, usize>,
11807 next_log_source_id: usize,
11808 pending_backfills: VecDeque<QueuedSubscriberBackfill>,
11809 pending_source_backfills: VecDeque<SubscriberStreamSource>,
11814 sources_dirty: bool,
11817 stream_revision: u64,
11820 state: AlloySubscriberState<N>,
11821 pending_records: VecDeque<SubscriberInputRecord<N>>,
11822 pending_chain_controls: VecDeque<ChainControl>,
11823 pending_reconcile_owner_records: VecDeque<BufferedSubscriberOwnerRecord<N>>,
11828 resource_error: Option<String>,
11831 last_seen_log_blocks: HashMap<usize, u64>,
11832 verified_log_blocks: HashMap<(u64, B256), BlockRef>,
11833 verified_log_block_order: VecDeque<(u64, B256)>,
11834 recent_input_refs: VecDeque<InputRef>,
11835 recent_input_ref_set: HashSet<InputRef>,
11836 recent_owner_input_refs: HashMap<SubscriberOwnerEpoch, VecDeque<InputRef>>,
11837 recent_owner_input_ref_sets: HashMap<SubscriberOwnerEpoch, HashSet<InputRef>>,
11838 recent_compat_owner_input_refs: HashMap<HandlerId, VecDeque<InputRef>>,
11839 recent_compat_owner_input_ref_sets: HashMap<HandlerId, HashSet<InputRef>>,
11840 base_flashblock_header: Option<(FixedBytes<8>, BaseFlashblockBase)>,
11841 flashblocks_by_hash: HashMap<B256, FlashblockRef>,
11842 flashblock_hash_order: VecDeque<B256>,
11843 unmatched_pending_logs: VecDeque<(usize, Log)>,
11844 latest_preconfirmation: Option<FlashblockRef>,
11845 preconfirmed_seen_logs: HashSet<(B256, u64)>,
11846 _network: PhantomData<N>,
11847}
11848
11849struct OwnedSubscriberInterests<N: Network = Ethereum> {
11850 owner: HandlerId,
11851 interests: Vec<ReactiveInterest<N>>,
11852 epoch: Option<SubscriberOwnerEpoch>,
11853 state: SubscriberOwnerState,
11854 baseline: Option<BlockRef>,
11855 progress: Option<SubscriberOwnerProgress>,
11856 progress_stream_revision: Option<u64>,
11857}
11858
11859#[derive(Clone)]
11860struct SubscriberOwnerReconcilePlan<N: Network = Ethereum> {
11861 epoch: SubscriberOwnerEpoch,
11862 interests: Vec<ReactiveInterest<N>>,
11863 retained: BlockRef,
11864 from_block: u64,
11865}
11866
11867struct SubscriberOwnerCatchup {
11868 logs: Vec<Log>,
11869 certified: BlockRef,
11870}
11871
11872#[derive(Clone, Copy)]
11873struct SubscriberOwnerCatchupOptions {
11874 target_preverified: bool,
11875 max_logs: usize,
11876 max_log_bytes: usize,
11877 max_requests_in_flight: usize,
11878}
11879
11880struct SubscriberOwnerReconcileFilter {
11881 filter: Filter,
11882 from_block: u64,
11883}
11884
11885struct BufferedSubscriberOwnerRecord<N: Network = Ethereum> {
11886 record: ReactiveInputRecord<N>,
11887 owners: Vec<SubscriberOwnerEpoch>,
11888}
11889
11890const OWNER_RECONCILE_FILTERS_PER_CHUNK: usize = 256;
11891
11892struct QueuedSubscriberBackfill {
11893 owner: Option<HandlerId>,
11896 epoch: Option<SubscriberOwnerEpoch>,
11897 filters: Vec<Filter>,
11899 backfill: SubscriberBackfill,
11900}
11901
11902#[cfg(feature = "reactive-ws")]
11908fn ensure_ring_crypto_provider() {
11909 use std::sync::Once;
11910 static INSTALL: Once = Once::new();
11911 INSTALL.call_once(|| {
11912 let _ = rustls::crypto::ring::default_provider().install_default();
11913 });
11914}
11915
11916impl<P, N: Network> AlloySubscriber<P, N> {
11917 pub fn new(provider: P, mode: SubscriberMode, config: SubscriberConfig) -> Self {
11919 #[cfg(feature = "reactive-ws")]
11920 ensure_ring_crypto_provider();
11921 Self {
11922 provider,
11923 provider_ref: None,
11924 log_verification_provider: None,
11925 chain_id: None,
11926 mode,
11927 config,
11928 base_interests: Vec::new(),
11929 owned_interests: Vec::new(),
11930 next_owner_epoch: 0,
11931 interests: Vec::new(),
11932 log_source_ids: HashMap::new(),
11933 next_log_source_id: 0,
11934 pending_backfills: VecDeque::new(),
11935 pending_source_backfills: VecDeque::new(),
11936 sources_dirty: true,
11937 stream_revision: 0,
11938 state: AlloySubscriberState::Uninitialized,
11939 pending_records: VecDeque::new(),
11940 pending_chain_controls: VecDeque::new(),
11941 pending_reconcile_owner_records: VecDeque::new(),
11942 resource_error: None,
11943 last_seen_log_blocks: HashMap::new(),
11944 verified_log_blocks: HashMap::new(),
11945 verified_log_block_order: VecDeque::new(),
11946 recent_input_refs: VecDeque::new(),
11947 recent_input_ref_set: HashSet::new(),
11948 recent_owner_input_refs: HashMap::new(),
11949 recent_owner_input_ref_sets: HashMap::new(),
11950 recent_compat_owner_input_refs: HashMap::new(),
11951 recent_compat_owner_input_ref_sets: HashMap::new(),
11952 base_flashblock_header: None,
11953 flashblocks_by_hash: HashMap::new(),
11954 flashblock_hash_order: VecDeque::new(),
11955 unmatched_pending_logs: VecDeque::new(),
11956 latest_preconfirmation: None,
11957 preconfirmed_seen_logs: HashSet::new(),
11958 _network: PhantomData,
11959 }
11960 }
11961
11962 pub fn provider(&self) -> &P {
11964 &self.provider
11965 }
11966
11967 #[must_use]
11971 pub fn with_provider_ref(mut self, provider: ProviderRef) -> Self {
11972 self.provider_ref = Some(provider);
11973 self
11974 }
11975
11976 #[must_use]
11983 pub fn with_log_verification_provider(mut self, provider: P) -> Self {
11984 self.log_verification_provider = Some(provider);
11985 self
11986 }
11987
11988 pub fn mode(&self) -> SubscriberMode {
11990 self.mode
11991 }
11992
11993 pub fn config(&self) -> &SubscriberConfig {
11995 &self.config
11996 }
11997
11998 pub fn registered_interests(&self) -> &[ReactiveInterest<N>] {
12000 &self.interests
12001 }
12002
12003 pub fn stage_interest_owner(
12020 &mut self,
12021 owner: HandlerId,
12022 interests: &[ReactiveInterest<N>],
12023 start: SubscriberOwnerStart,
12024 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
12025 validate_subscriber_config(&self.config)?;
12026 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
12027 && interests
12028 .iter()
12029 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
12030 {
12031 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
12032 }
12033 if self
12034 .owned_interests
12035 .iter()
12036 .any(|entry| entry.owner == owner)
12037 {
12038 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
12039 }
12040
12041 let mut next_owned = self.clone_owned_interests();
12042 next_owned.push(OwnedSubscriberInterests {
12043 owner: owner.clone(),
12044 interests: interests.to_vec(),
12045 epoch: None,
12046 state: SubscriberOwnerState::Staged,
12047 baseline: None,
12048 progress: None,
12049 progress_stream_revision: None,
12050 });
12051 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
12052 validate_supported_interests(self.mode, &self.config, &next_registered)?;
12053
12054 let baseline = match start {
12055 SubscriberOwnerStart::Live => None,
12056 SubscriberOwnerStart::PostBlock(block) => {
12057 block
12058 .number
12059 .checked_add(1)
12060 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
12061 Some(block)
12062 }
12063 };
12064 let sequence = self
12065 .next_owner_epoch
12066 .checked_add(1)
12067 .ok_or(SubscriberOwnerError::EpochExhausted)?;
12068 let epoch = SubscriberOwnerEpoch {
12069 owner: owner.clone(),
12070 sequence,
12071 };
12072
12073 self.next_owner_epoch = sequence;
12074 let entry = next_owned
12075 .last_mut()
12076 .expect("staged owner was appended during preflight");
12077 entry.epoch = Some(epoch.clone());
12078 entry.baseline = baseline;
12079 self.owned_interests = next_owned;
12080 self.interests = next_registered;
12081 self.sources_dirty = true;
12082
12083 Ok(epoch)
12084 }
12085
12086 pub fn stage_interest_owner_replacement(
12100 &mut self,
12101 owner: HandlerId,
12102 interests: &[ReactiveInterest<N>],
12103 start: SubscriberOwnerStart,
12104 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
12105 validate_subscriber_config(&self.config)?;
12106 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
12107 && interests
12108 .iter()
12109 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
12110 {
12111 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
12112 }
12113 let active_count = self
12114 .owned_interests
12115 .iter()
12116 .filter(|entry| {
12117 entry.owner == owner
12118 && entry.state == SubscriberOwnerState::Active
12119 && entry.epoch.is_some()
12120 })
12121 .count();
12122 if active_count != 1
12123 || self
12124 .owned_interests
12125 .iter()
12126 .any(|entry| entry.owner == owner && entry.state != SubscriberOwnerState::Active)
12127 {
12128 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
12129 }
12130
12131 let mut next_owned = self.clone_owned_interests();
12132 next_owned.push(OwnedSubscriberInterests {
12133 owner: owner.clone(),
12134 interests: interests.to_vec(),
12135 epoch: None,
12136 state: SubscriberOwnerState::Staged,
12137 baseline: None,
12138 progress: None,
12139 progress_stream_revision: None,
12140 });
12141 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
12142 validate_supported_interests(self.mode, &self.config, &next_registered)?;
12143
12144 let baseline = match start {
12145 SubscriberOwnerStart::Live => None,
12146 SubscriberOwnerStart::PostBlock(block) => {
12147 block
12148 .number
12149 .checked_add(1)
12150 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
12151 Some(block)
12152 }
12153 };
12154 let sequence = self
12155 .next_owner_epoch
12156 .checked_add(1)
12157 .ok_or(SubscriberOwnerError::EpochExhausted)?;
12158 let epoch = SubscriberOwnerEpoch {
12159 owner: owner.clone(),
12160 sequence,
12161 };
12162
12163 self.next_owner_epoch = sequence;
12164 let entry = next_owned
12165 .last_mut()
12166 .expect("staged replacement owner was appended during preflight");
12167 entry.epoch = Some(epoch.clone());
12168 entry.baseline = baseline;
12169 self.owned_interests = next_owned;
12170 self.interests = next_registered;
12171 self.sources_dirty = true;
12172 Ok(epoch)
12173 }
12174
12175 pub fn interest_owner_state(
12177 &self,
12178 epoch: &SubscriberOwnerEpoch,
12179 ) -> Option<SubscriberOwnerState> {
12180 self.owned_interests
12181 .iter()
12182 .find(|entry| entry.epoch.as_ref() == Some(epoch))
12183 .map(|entry| entry.state)
12184 }
12185
12186 pub fn interest_owner_progress(
12188 &self,
12189 epoch: &SubscriberOwnerEpoch,
12190 ) -> Option<&SubscriberOwnerProgress> {
12191 self.owned_interests
12192 .iter()
12193 .find(|entry| entry.epoch.as_ref() == Some(epoch))
12194 .and_then(|entry| entry.progress.as_ref())
12195 }
12196
12197 pub fn activate_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
12201 let stream_revision = self.stream_revision;
12202 let sources_dirty = self.sources_dirty;
12203 let Some(entry) = self
12204 .owned_interests
12205 .iter_mut()
12206 .find(|entry| entry.epoch.as_ref() == Some(epoch))
12207 else {
12208 return false;
12209 };
12210 if entry.state != SubscriberOwnerState::Staged
12211 || (entry.baseline.is_some()
12212 && (entry.progress.is_none()
12213 || entry.progress_stream_revision != Some(stream_revision)
12214 || sources_dirty))
12215 {
12216 return false;
12217 }
12218 entry.state = SubscriberOwnerState::Active;
12219 true
12220 }
12221
12222 pub fn commit_interest_owner_replacement(
12224 &mut self,
12225 active: &SubscriberOwnerEpoch,
12226 replacement: &SubscriberOwnerEpoch,
12227 ) -> bool {
12228 let Some(active_index) = self
12229 .owned_interests
12230 .iter()
12231 .position(|entry| entry.epoch.as_ref() == Some(active))
12232 else {
12233 return false;
12234 };
12235 let Some(replacement_index) = self
12236 .owned_interests
12237 .iter()
12238 .position(|entry| entry.epoch.as_ref() == Some(replacement))
12239 else {
12240 return false;
12241 };
12242 if active_index == replacement_index
12243 || active.owner() != replacement.owner()
12244 || self.owned_interests[active_index].state != SubscriberOwnerState::Active
12245 || self.owned_interests[replacement_index].state != SubscriberOwnerState::Staged
12246 || (self.owned_interests[replacement_index].baseline.is_some()
12247 && (self.owned_interests[replacement_index].progress.is_none()
12248 || self.owned_interests[replacement_index].progress_stream_revision
12249 != Some(self.stream_revision)
12250 || self.sources_dirty))
12251 {
12252 return false;
12253 }
12254
12255 self.owned_interests[replacement_index].state = SubscriberOwnerState::Active;
12256 self.owned_interests.remove(active_index);
12257 self.purge_owner_epoch(active);
12258 self.rebuild_registered_interests();
12259 self.retire_unreferenced_filters();
12260 self.sources_dirty = true;
12261 true
12262 }
12263
12264 pub fn prepare_interest_owner_removal(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
12273 let Some(entry) = self
12274 .owned_interests
12275 .iter_mut()
12276 .find(|entry| entry.epoch.as_ref() == Some(epoch))
12277 else {
12278 return false;
12279 };
12280 if entry.state != SubscriberOwnerState::Active {
12281 return false;
12282 }
12283 entry.state = SubscriberOwnerState::Removing;
12284 true
12285 }
12286
12287 pub fn finalize_interest_owner_removal(
12293 &mut self,
12294 epoch: &SubscriberOwnerEpoch,
12295 ) -> Option<Vec<ReactiveInterest<N>>> {
12296 let index = self.owned_interests.iter().position(|entry| {
12297 entry.epoch.as_ref() == Some(epoch) && entry.state == SubscriberOwnerState::Removing
12298 })?;
12299 let removed = self.owned_interests.remove(index).interests;
12300 self.purge_owner_epoch(epoch);
12301 self.rebuild_registered_interests();
12302 self.retire_unreferenced_filters();
12303 self.sources_dirty = true;
12304 Some(removed)
12305 }
12306
12307 pub fn abort_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
12313 let Some(index) = self
12314 .owned_interests
12315 .iter()
12316 .position(|entry| entry.epoch.as_ref() == Some(epoch))
12317 else {
12318 return false;
12319 };
12320 match self.owned_interests[index].state {
12321 SubscriberOwnerState::Staged => {
12322 self.owned_interests.remove(index);
12323 self.purge_owner_epoch(epoch);
12324 self.rebuild_registered_interests();
12325 self.retire_unreferenced_filters();
12326 self.sources_dirty = true;
12327 true
12328 }
12329 SubscriberOwnerState::Removing => {
12330 self.owned_interests[index].state = SubscriberOwnerState::Active;
12331 true
12332 }
12333 SubscriberOwnerState::Active => false,
12334 }
12335 }
12336
12337 fn purge_owner_epoch(&mut self, epoch: &SubscriberOwnerEpoch) {
12338 self.pending_backfills
12339 .retain(|backfill| backfill.epoch.as_ref() != Some(epoch));
12340 self.pending_records
12341 .retain_mut(|pending| match &mut pending.scope {
12342 SubscriberInputScope::Canonical { owners }
12343 | SubscriberInputScope::CanonicalResidual { owners, .. } => {
12344 owners.retain(|owner| owner != epoch);
12345 true
12346 }
12347 SubscriberInputScope::OwnerOnly { owners } => {
12348 owners.retain(|owner| owner != epoch);
12349 !owners.is_empty()
12350 }
12351 SubscriberInputScope::OwnerOnlyHandlers { .. }
12352 | SubscriberInputScope::Preconfirmed => true,
12353 });
12354 self.pending_reconcile_owner_records.retain_mut(|pending| {
12355 pending.owners.retain(|owner| owner != epoch);
12356 !pending.owners.is_empty()
12357 });
12358 self.recent_owner_input_refs.remove(epoch);
12359 self.recent_owner_input_ref_sets.remove(epoch);
12360 }
12361
12362 pub fn upsert_interest_owners(
12370 &mut self,
12371 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
12372 ) -> Result<(), SubscriberError> {
12373 self.upsert_interest_owners_inner(owners, None)
12374 }
12375
12376 pub fn upsert_interest_owners_with_backfill(
12385 &mut self,
12386 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
12387 backfill: SubscriberBackfill,
12388 ) -> Result<(), SubscriberError> {
12389 self.upsert_interest_owners_inner(owners, Some(backfill))
12390 }
12391
12392 fn upsert_interest_owners_inner(
12393 &mut self,
12394 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
12395 explicit_backfill: Option<SubscriberBackfill>,
12396 ) -> Result<(), SubscriberError> {
12397 validate_subscriber_config(&self.config)?;
12398 let mut seen = HashSet::with_capacity(owners.len());
12399 let mut next_owned = self.clone_owned_interests();
12400 for (owner, interests) in &owners {
12401 if !seen.insert(owner.clone()) {
12402 return Err(SubscriberError::InvalidConfig(
12403 "bulk owner upsert contains a duplicate owner",
12404 ));
12405 }
12406 if self
12407 .owned_interests
12408 .iter()
12409 .any(|entry| &entry.owner == owner && entry.epoch.is_some())
12410 {
12411 return Err(SubscriberError::InvalidConfig(
12412 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
12413 ));
12414 }
12415 if let Some(entry) = next_owned.iter_mut().find(|entry| &entry.owner == owner) {
12416 entry.interests = interests.clone();
12417 entry.state = SubscriberOwnerState::Active;
12418 entry.baseline = None;
12419 entry.progress = None;
12420 entry.progress_stream_revision = None;
12421 } else {
12422 next_owned.push(OwnedSubscriberInterests {
12423 owner: owner.clone(),
12424 interests: interests.clone(),
12425 epoch: None,
12426 state: SubscriberOwnerState::Active,
12427 baseline: None,
12428 progress: None,
12429 progress_stream_revision: None,
12430 });
12431 }
12432 }
12433 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
12434 validate_supported_interests(self.mode, &self.config, &next_registered)?;
12435
12436 let mut replacement_backfills = Vec::new();
12441 for (owner, interests) in &owners {
12442 let previous_filters: Vec<Filter> = self
12443 .owner_interests(owner)
12444 .map(log_filters)
12445 .unwrap_or_default();
12446 let continuity_anchor = previous_filters
12447 .iter()
12448 .filter_map(|filter| self.log_anchor(filter))
12449 .min();
12450 let filters = log_filters(interests);
12451 if let Some(backfill) = explicit_backfill
12452 && !filters.is_empty()
12453 {
12454 replacement_backfills.push(QueuedSubscriberBackfill {
12455 owner: Some(owner.clone()),
12456 epoch: None,
12457 filters: filters.clone(),
12458 backfill,
12459 });
12460 }
12461 let explicit_covers = explicit_backfill.is_some_and(|explicit| {
12462 explicit.end_block().is_none()
12463 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
12464 });
12465 let continuity_filters: Vec<_> = filters
12466 .into_iter()
12467 .filter(|filter| !previous_filters.contains(filter))
12468 .collect();
12469 if let Some(anchor) = continuity_anchor
12470 && !continuity_filters.is_empty()
12471 && !explicit_covers
12472 {
12473 replacement_backfills.push(QueuedSubscriberBackfill {
12474 owner: Some(owner.clone()),
12475 epoch: None,
12476 filters: continuity_filters,
12477 backfill: SubscriberBackfill::from_block(anchor),
12478 });
12479 }
12480 }
12481
12482 let retained_backfills = self
12483 .pending_backfills
12484 .iter()
12485 .filter(|queued| {
12486 queued
12487 .owner
12488 .as_ref()
12489 .is_none_or(|owner| !seen.contains(owner))
12490 })
12491 .map(|queued| queued.filters.len())
12492 .sum::<usize>();
12493 let replacement_units = replacement_backfills
12494 .iter()
12495 .map(|queued| queued.filters.len())
12496 .sum::<usize>();
12497 if retained_backfills.saturating_add(replacement_units) > self.config.max_pending_backfills
12498 {
12499 return Err(SubscriberError::ResourceExhausted(format!(
12500 "bulk owner update would queue more than {} lazy backfills",
12501 self.config.max_pending_backfills
12502 )));
12503 }
12504
12505 self.owned_interests = next_owned;
12509 self.interests = next_registered;
12510 for owner in &seen {
12511 self.recent_compat_owner_input_refs.remove(owner);
12512 self.recent_compat_owner_input_ref_sets.remove(owner);
12513 }
12514 self.retire_unreferenced_filters();
12515 self.sources_dirty = true;
12516 self.pending_backfills.retain(|queued| {
12517 queued
12518 .owner
12519 .as_ref()
12520 .is_none_or(|owner| !seen.contains(owner))
12521 });
12522 self.pending_backfills.extend(replacement_backfills);
12523 Ok(())
12524 }
12525
12526 pub fn replace_interest_owners(
12535 &mut self,
12536 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
12537 ) -> Result<(), SubscriberError> {
12538 self.replace_interest_owners_inner(owners, None)
12539 }
12540
12541 pub fn replace_interest_owners_with_global_backfill(
12554 &mut self,
12555 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
12556 backfill: SubscriberBackfill,
12557 ) -> Result<(), SubscriberError> {
12558 self.replace_interest_owners_inner(owners, Some(backfill))
12559 }
12560
12561 fn replace_interest_owners_inner(
12562 &mut self,
12563 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
12564 backfill: Option<SubscriberBackfill>,
12565 ) -> Result<(), SubscriberError> {
12566 validate_subscriber_config(&self.config)?;
12567 if self
12568 .owned_interests
12569 .iter()
12570 .any(|entry| entry.epoch.is_some())
12571 {
12572 return Err(SubscriberError::InvalidConfig(
12573 "cannot replace compatibility owners while an epoch-scoped lifecycle exists",
12574 ));
12575 }
12576
12577 let mut seen = HashSet::with_capacity(owners.len());
12578 let mut next_owned = Vec::with_capacity(owners.len());
12579 for (owner, interests) in owners {
12580 if !seen.insert(owner.clone()) {
12581 return Err(SubscriberError::InvalidConfig(
12582 "owner replacement contains a duplicate owner",
12583 ));
12584 }
12585 next_owned.push(OwnedSubscriberInterests {
12586 owner,
12587 interests,
12588 epoch: None,
12589 state: SubscriberOwnerState::Active,
12590 baseline: None,
12591 progress: None,
12592 progress_stream_revision: None,
12593 });
12594 }
12595 let next_registered = aggregate_interests(&[], &next_owned);
12596 validate_supported_interests(self.mode, &self.config, &next_registered)?;
12597 let mut filters = log_filters(&next_registered);
12598 let mut unique_filters = Vec::with_capacity(filters.len());
12599 for filter in filters.drain(..) {
12600 if !unique_filters.contains(&filter) {
12601 unique_filters.push(filter);
12602 }
12603 }
12604 let replacement_backfills: VecDeque<_> = match backfill {
12605 Some(backfill) if !unique_filters.is_empty() => {
12606 VecDeque::from([QueuedSubscriberBackfill {
12607 owner: None,
12608 epoch: None,
12609 filters: unique_filters,
12610 backfill,
12611 }])
12612 }
12613 Some(_) | None => VecDeque::new(),
12614 };
12615 let replacement_units = replacement_backfills
12616 .iter()
12617 .map(|queued| queued.filters.len())
12618 .sum::<usize>();
12619 if replacement_units > self.config.max_pending_backfills {
12620 return Err(SubscriberError::ResourceExhausted(format!(
12621 "owner replacement would queue more than {} lazy backfills",
12622 self.config.max_pending_backfills
12623 )));
12624 }
12625
12626 self.base_interests.clear();
12631 self.owned_interests = next_owned;
12632 self.interests = next_registered;
12633 self.reset_delivery_state();
12634 self.pending_backfills = replacement_backfills;
12635 self.state = AlloySubscriberState::Uninitialized;
12636 Ok(())
12637 }
12638
12639 pub fn add_interest_owner(
12662 &mut self,
12663 owner: HandlerId,
12664 interests: &[ReactiveInterest<N>],
12665 ) -> Result<(), SubscriberError> {
12666 self.set_interest_owner(owner, interests, None)
12667 }
12668
12669 pub fn add_interest_owner_with_backfill(
12688 &mut self,
12689 owner: HandlerId,
12690 interests: &[ReactiveInterest<N>],
12691 backfill: SubscriberBackfill,
12692 ) -> Result<(), SubscriberError> {
12693 self.set_interest_owner(owner, interests, Some(backfill))
12694 }
12695
12696 pub fn add_interest_owner_with_canonical_catchup(
12715 &mut self,
12716 owner: HandlerId,
12717 interests: &[ReactiveInterest<N>],
12718 retained: BlockRef,
12719 ) -> Result<(), SubscriberError> {
12720 validate_subscriber_config(&self.config)?;
12721 if self
12722 .owned_interests
12723 .iter()
12724 .any(|entry| entry.owner == owner && entry.epoch.is_some())
12725 {
12726 return Err(SubscriberError::InvalidConfig(
12727 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
12728 ));
12729 }
12730
12731 let mut next_owned = self.clone_owned_interests();
12732 if let Some(entry) = next_owned.iter_mut().find(|entry| entry.owner == owner) {
12733 entry.interests = interests.to_vec();
12734 entry.state = SubscriberOwnerState::Active;
12735 entry.baseline = None;
12736 entry.progress = None;
12737 entry.progress_stream_revision = None;
12738 entry.epoch = None;
12739 } else {
12740 next_owned.push(OwnedSubscriberInterests {
12741 owner: owner.clone(),
12742 interests: interests.to_vec(),
12743 epoch: None,
12744 state: SubscriberOwnerState::Active,
12745 baseline: None,
12746 progress: None,
12747 progress_stream_revision: None,
12748 });
12749 }
12750 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
12751 validate_supported_interests(self.mode, &self.config, &next_registered)?;
12752 if next_registered
12753 .iter()
12754 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
12755 {
12756 return Err(SubscriberError::Unsupported(
12757 "Alloy coordinated registration supports log-only interest topologies",
12758 ));
12759 }
12760
12761 let mut owner_filters = Vec::new();
12762 for filter in log_filters(interests) {
12763 if !owner_filters.contains(&filter) {
12764 owner_filters.push(filter);
12765 }
12766 }
12767 let mut global_filters = Vec::new();
12768 for filter in log_filters(&next_registered) {
12769 if !global_filters.contains(&filter) {
12770 global_filters.push(filter);
12771 }
12772 }
12773 let owner_backfill =
12774 SubscriberBackfill::from_canonical_block_through(retained, retained.number)?;
12775 let global_backfill = SubscriberBackfill::after_canonical_block(retained)?;
12776 let replacement_units = owner_filters.len().saturating_add(global_filters.len());
12777 let retained_units = self
12778 .pending_backfills
12779 .iter()
12780 .filter(|queued| queued.owner.as_ref() != Some(&owner))
12781 .map(|queued| queued.filters.len())
12782 .sum::<usize>();
12783 if retained_units.saturating_add(replacement_units) > self.config.max_pending_backfills {
12784 return Err(SubscriberError::ResourceExhausted(format!(
12785 "coordinated owner registration would queue more than {} lazy backfills",
12786 self.config.max_pending_backfills
12787 )));
12788 }
12789
12790 let mut replacement_backfills = VecDeque::new();
12791 if !owner_filters.is_empty() {
12792 replacement_backfills.push_back(QueuedSubscriberBackfill {
12793 owner: Some(owner.clone()),
12794 epoch: None,
12795 filters: owner_filters,
12796 backfill: owner_backfill,
12797 });
12798 }
12799 replacement_backfills.push_back(QueuedSubscriberBackfill {
12802 owner: None,
12803 epoch: None,
12804 filters: global_filters,
12805 backfill: global_backfill,
12806 });
12807
12808 self.owned_interests = next_owned;
12811 self.interests = next_registered;
12812 self.recent_compat_owner_input_refs.remove(&owner);
12813 self.recent_compat_owner_input_ref_sets.remove(&owner);
12814 self.pending_backfills
12815 .retain(|queued| queued.owner.as_ref() != Some(&owner));
12816 self.pending_backfills.extend(replacement_backfills);
12817 self.retire_unreferenced_filters();
12818 self.sources_dirty = true;
12819 Ok(())
12820 }
12821
12822 pub fn remove_interest_owner(&mut self, owner: &HandlerId) -> Option<Vec<ReactiveInterest<N>>> {
12831 let index = self
12832 .owned_interests
12833 .iter()
12834 .position(|entry| &entry.owner == owner && entry.epoch.is_none())?;
12835 let removed = self.owned_interests.remove(index);
12836 if let Some(epoch) = &removed.epoch {
12837 self.purge_owner_epoch(epoch);
12838 } else {
12839 self.pending_backfills
12840 .retain(|backfill| backfill.owner.as_ref() != Some(owner));
12841 self.recent_compat_owner_input_refs.remove(owner);
12842 self.recent_compat_owner_input_ref_sets.remove(owner);
12843 }
12844 self.rebuild_registered_interests();
12845 self.retire_unreferenced_filters();
12846 self.sources_dirty = true;
12847 Some(removed.interests)
12848 }
12849
12850 pub fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
12852 self.owned_interests
12853 .iter()
12854 .find(|entry| &entry.owner == owner)
12855 .map(|entry| entry.interests.as_slice())
12856 }
12857
12858 fn set_interest_owner(
12859 &mut self,
12860 owner: HandlerId,
12861 interests: &[ReactiveInterest<N>],
12862 backfill: Option<SubscriberBackfill>,
12863 ) -> Result<(), SubscriberError> {
12864 validate_subscriber_config(&self.config)?;
12865 if self
12866 .owned_interests
12867 .iter()
12868 .any(|entry| entry.owner == owner && entry.epoch.is_some())
12869 {
12870 return Err(SubscriberError::InvalidConfig(
12871 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
12872 ));
12873 }
12874
12875 let mut next_owned = self.clone_owned_interests();
12876 let replaced_epoch = match next_owned.iter_mut().find(|entry| entry.owner == owner) {
12877 Some(entry) => {
12878 entry.interests = interests.to_vec();
12879 entry.state = SubscriberOwnerState::Active;
12880 entry.baseline = None;
12881 entry.progress = None;
12882 entry.progress_stream_revision = None;
12883 entry.epoch.take()
12884 }
12885 None => {
12886 next_owned.push(OwnedSubscriberInterests {
12887 owner: owner.clone(),
12888 interests: interests.to_vec(),
12889 epoch: None,
12890 state: SubscriberOwnerState::Active,
12891 baseline: None,
12892 progress: None,
12893 progress_stream_revision: None,
12894 });
12895 None
12896 }
12897 };
12898 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
12899 validate_supported_interests(self.mode, &self.config, &next_registered)?;
12900
12901 let previous_filters: Vec<Filter> = self
12908 .owner_interests(&owner)
12909 .map(log_filters)
12910 .unwrap_or_default();
12911 let continuity_anchor: Option<u64> = previous_filters
12912 .iter()
12913 .filter_map(|filter| self.log_anchor(filter))
12914 .min();
12915
12916 let mut replacement_backfills = Vec::new();
12920 let filters = log_filters(interests);
12921 if let Some(backfill) = backfill
12922 && !filters.is_empty()
12923 {
12924 replacement_backfills.push(QueuedSubscriberBackfill {
12925 owner: Some(owner.clone()),
12926 epoch: None,
12927 filters: filters.clone(),
12928 backfill,
12929 });
12930 }
12931 let explicit_covers = backfill.is_some_and(|explicit| {
12932 explicit.end_block().is_none()
12933 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
12934 });
12935 let continuity_filters: Vec<_> = filters
12936 .into_iter()
12937 .filter(|filter| !previous_filters.contains(filter))
12938 .collect();
12939 if let Some(anchor) = continuity_anchor
12940 && !continuity_filters.is_empty()
12941 && !explicit_covers
12942 {
12943 replacement_backfills.push(QueuedSubscriberBackfill {
12944 owner: Some(owner.clone()),
12945 epoch: None,
12946 filters: continuity_filters,
12947 backfill: SubscriberBackfill::from_block(anchor),
12948 });
12949 }
12950 let retained_backfills = self
12951 .pending_backfills
12952 .iter()
12953 .filter(|queued| queued.owner.as_ref() != Some(&owner))
12954 .map(|queued| queued.filters.len())
12955 .sum::<usize>();
12956 let replacement_units = replacement_backfills
12957 .iter()
12958 .map(|queued| queued.filters.len())
12959 .sum::<usize>();
12960 if retained_backfills.saturating_add(replacement_units) > self.config.max_pending_backfills
12961 {
12962 return Err(SubscriberError::ResourceExhausted(format!(
12963 "owner update would queue more than {} lazy backfills",
12964 self.config.max_pending_backfills
12965 )));
12966 }
12967
12968 self.owned_interests = next_owned;
12969 self.interests = next_registered;
12970 if let Some(epoch) = replaced_epoch {
12971 self.purge_owner_epoch(&epoch);
12972 } else {
12973 self.recent_compat_owner_input_refs.remove(&owner);
12974 self.recent_compat_owner_input_ref_sets.remove(&owner);
12975 }
12976 self.retire_unreferenced_filters();
12977 self.sources_dirty = true;
12978
12979 self.pending_backfills
12982 .retain(|queued| queued.owner.as_ref() != Some(&owner));
12983 self.pending_backfills.extend(replacement_backfills);
12984 Ok(())
12985 }
12986
12987 fn clone_owned_interests(&self) -> Vec<OwnedSubscriberInterests<N>> {
12988 self.owned_interests
12989 .iter()
12990 .map(|entry| OwnedSubscriberInterests {
12991 owner: entry.owner.clone(),
12992 interests: entry.interests.clone(),
12993 epoch: entry.epoch.clone(),
12994 state: entry.state,
12995 baseline: entry.baseline,
12996 progress: entry.progress.clone(),
12997 progress_stream_revision: entry.progress_stream_revision,
12998 })
12999 .collect()
13000 }
13001
13002 fn rebuild_registered_interests(&mut self) {
13003 self.interests = aggregate_interests(&self.base_interests, &self.owned_interests);
13004 }
13005
13006 fn log_anchor(&self, filter: &Filter) -> Option<u64> {
13009 if let Some(anchor) = self
13010 .log_source_ids
13011 .get(filter)
13012 .and_then(|id| self.last_seen_log_blocks.get(id))
13013 {
13014 return Some(*anchor);
13015 }
13016
13017 self.log_source_ids
13021 .values()
13022 .filter_map(|id| self.last_seen_log_blocks.get(id).copied())
13023 .min()
13024 }
13025
13026 #[allow(clippy::mutable_key_type)]
13033 fn logical_log_filters(&self) -> Vec<Filter> {
13034 let mut filters = log_filters(&self.base_interests);
13035 for entry in &self.owned_interests {
13036 filters.extend(log_filters(&entry.interests));
13037 }
13038 let mut seen = HashSet::new();
13039 filters.retain(|filter| seen.insert(filter.clone()));
13040 filters
13041 }
13042
13043 fn log_stream_filters(&self) -> Vec<Filter> {
13049 let mut merged = Vec::new();
13050 for filter in self.logical_log_filters() {
13051 merge_log_subscription_filter(&mut merged, &filter);
13052 }
13053
13054 let max_addresses = self.config.max_log_addresses_per_subscription.max(1);
13055 let mut planned = Vec::new();
13056 for filter in merged {
13057 let mut addresses: Vec<_> = filter.address.iter().copied().collect();
13058 if addresses.len() <= max_addresses {
13059 planned.push(filter);
13060 continue;
13061 }
13062 addresses.sort_unstable();
13063 for chunk in addresses.chunks(max_addresses) {
13064 let mut split = filter.clone();
13065 split.address = FilterSet::default();
13066 for address in chunk {
13067 split.address.insert(*address);
13068 }
13069 planned.push(split);
13070 }
13071 }
13072 planned
13073 }
13074
13075 #[allow(clippy::mutable_key_type)]
13082 fn retire_unreferenced_filters(&mut self) {
13083 let mut live: HashSet<Filter> = self.log_stream_filters().into_iter().collect();
13084 if let AlloySubscriberState::Active(streams) = &self.state {
13085 for entry in &streams.entries {
13086 match &entry.source {
13087 SubscriberStreamSource::PubSubLog { filter, .. }
13088 | SubscriberStreamSource::BasePendingLog { filter, .. }
13089 | SubscriberStreamSource::PollingLog { filter } => {
13090 live.insert(filter.clone());
13091 }
13092 SubscriberStreamSource::BaseFlashblocks
13093 | SubscriberStreamSource::OpPendingFlashblocks
13094 | SubscriberStreamSource::PubSubPendingHashes
13095 | SubscriberStreamSource::PubSubBlockHeaders
13096 | SubscriberStreamSource::PollingPendingHashes => {}
13097 }
13098 }
13099 }
13100 self.log_source_ids
13101 .retain(|filter, _| live.contains(filter));
13102 let live_ids: HashSet<usize> = self.log_source_ids.values().copied().collect();
13103 self.last_seen_log_blocks
13104 .retain(|id, _| live_ids.contains(id));
13105 }
13106
13107 fn drain_next_scoped_batch(&mut self) -> Option<SubscriberInputBatch<N>> {
13108 if self.pending_records.is_empty() && self.pending_chain_controls.is_empty() {
13109 return None;
13110 }
13111
13112 let first_preconfirmation = self.pending_records.front().and_then(|record| {
13113 if record.scope != SubscriberInputScope::Preconfirmed {
13114 return None;
13115 }
13116 match &record.record.context.chain_status {
13117 ChainStatus::Preconfirmed { flashblock } => Some(flashblock.clone()),
13118 _ => None,
13119 }
13120 });
13121 let len = self
13122 .pending_records
13123 .iter()
13124 .take(self.config.max_batch_size)
13125 .take_while(|record| match &first_preconfirmation {
13126 Some(expected) => {
13127 record.scope == SubscriberInputScope::Preconfirmed
13128 && matches!(
13129 &record.record.context.chain_status,
13130 ChainStatus::Preconfirmed { flashblock } if flashblock == expected
13131 )
13132 }
13133 None => record.scope != SubscriberInputScope::Preconfirmed,
13134 })
13135 .count();
13136 let records = self.pending_records.drain(..len).collect();
13137 let chain_controls = if first_preconfirmation.is_none() && self.pending_records.is_empty() {
13138 self.pending_chain_controls.drain(..).collect()
13139 } else {
13140 Vec::new()
13141 };
13142 Some(SubscriberInputBatch {
13143 records,
13144 chain_id: self.chain_id,
13145 chain_controls,
13146 })
13147 }
13148
13149 fn reset_delivery_state(&mut self) {
13150 self.pending_records.clear();
13151 self.pending_chain_controls.clear();
13152 self.pending_reconcile_owner_records.clear();
13153 self.resource_error = None;
13154 self.last_seen_log_blocks.clear();
13155 self.verified_log_blocks.clear();
13156 self.verified_log_block_order.clear();
13157 self.recent_input_refs.clear();
13158 self.recent_input_ref_set.clear();
13159 self.recent_owner_input_refs.clear();
13160 self.recent_owner_input_ref_sets.clear();
13161 self.recent_compat_owner_input_refs.clear();
13162 self.recent_compat_owner_input_ref_sets.clear();
13163 self.pending_backfills.clear();
13164 self.pending_source_backfills.clear();
13165 self.log_source_ids.clear();
13166 self.next_log_source_id = 0;
13167 self.sources_dirty = true;
13168 self.reset_flashblock_tracking();
13169 }
13170
13171 fn reset_flashblock_tracking(&mut self) {
13172 self.base_flashblock_header = None;
13173 self.flashblocks_by_hash.clear();
13174 self.flashblock_hash_order.clear();
13175 self.unmatched_pending_logs.clear();
13176 self.latest_preconfirmation = None;
13177 self.preconfirmed_seen_logs.clear();
13178 }
13179
13180 fn bump_stream_revision(&mut self) {
13181 self.stream_revision = self.stream_revision.saturating_add(1);
13182 }
13183}
13184
13185impl<P, N> InterestOwnerSubscriber<N> for AlloySubscriber<P, N>
13186where
13187 P: Provider<N> + Send + Sync,
13188 N: Network + 'static,
13189 N::HeaderResponse: Send + 'static,
13190{
13191 fn upsert_interest_owners(
13192 &mut self,
13193 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13194 ) -> SubscriberOperation<'_, ()> {
13195 Box::pin(async move {
13196 if !owners.is_empty() {
13197 self.ensure_chain_id().await?;
13198 }
13199 AlloySubscriber::upsert_interest_owners(self, owners)
13200 })
13201 }
13202
13203 fn replace_interest_owners(
13204 &mut self,
13205 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13206 ) -> SubscriberOperation<'_, ()> {
13207 Box::pin(async move {
13208 if owners.iter().any(|(_, interests)| !interests.is_empty()) {
13209 self.ensure_chain_id().await?;
13210 }
13211 AlloySubscriber::replace_interest_owners(self, owners)
13212 })
13213 }
13214
13215 fn replace_interest_owners_with_global_backfill(
13216 &mut self,
13217 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13218 backfill: SubscriberBackfill,
13219 ) -> SubscriberOperation<'_, ()> {
13220 Box::pin(async move {
13221 if owners.iter().any(|(_, interests)| !interests.is_empty()) {
13222 self.ensure_chain_id().await?;
13223 }
13224 AlloySubscriber::replace_interest_owners_with_global_backfill(self, owners, backfill)
13225 })
13226 }
13227
13228 fn add_interest_owner(
13229 &mut self,
13230 owner: HandlerId,
13231 interests: &[ReactiveInterest<N>],
13232 ) -> SubscriberOperation<'_, ()> {
13233 let interests = interests.to_vec();
13234 Box::pin(async move {
13235 if !interests.is_empty() {
13236 self.ensure_chain_id().await?;
13237 }
13238 AlloySubscriber::add_interest_owner(self, owner, &interests)
13239 })
13240 }
13241
13242 fn add_interest_owner_with_backfill(
13243 &mut self,
13244 owner: HandlerId,
13245 interests: &[ReactiveInterest<N>],
13246 backfill: SubscriberBackfill,
13247 ) -> SubscriberOperation<'_, ()> {
13248 let interests = interests.to_vec();
13249 Box::pin(async move {
13250 if !interests.is_empty() {
13251 self.ensure_chain_id().await?;
13252 }
13253 AlloySubscriber::add_interest_owner_with_backfill(self, owner, &interests, backfill)
13254 })
13255 }
13256
13257 fn add_interest_owner_with_canonical_catchup(
13258 &mut self,
13259 owner: HandlerId,
13260 interests: &[ReactiveInterest<N>],
13261 retained: BlockRef,
13262 ) -> SubscriberOperation<'_, ()> {
13263 let interests = interests.to_vec();
13264 Box::pin(async move {
13265 self.ensure_chain_id().await?;
13268 AlloySubscriber::add_interest_owner_with_canonical_catchup(
13269 self, owner, &interests, retained,
13270 )
13271 })
13272 }
13273
13274 fn remove_interest_owner(
13275 &mut self,
13276 owner: &HandlerId,
13277 ) -> SubscriberOperation<'_, Option<Vec<ReactiveInterest<N>>>> {
13278 let owner = owner.clone();
13279 Box::pin(async move { Ok(AlloySubscriber::remove_interest_owner(self, &owner)) })
13280 }
13281
13282 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
13283 AlloySubscriber::owner_interests(self, owner)
13284 }
13285}
13286
13287enum AlloySubscriberState<N: Network> {
13288 Uninitialized,
13289 Active(SubscriberStreams<N>),
13290 Empty,
13291}
13292
13293struct SubscriberStreams<N: Network> {
13294 entries: Vec<SubscriberStreamEntry<N>>,
13295 next_index: usize,
13296}
13297
13298struct SubscriberStreamEntry<N: Network> {
13299 source: SubscriberStreamSource,
13300 stream: BoxStream<'static, SubscriberEvent<N>>,
13301}
13302
13303impl<N: Network> SubscriberStreams<N> {
13304 fn new() -> Self {
13305 Self {
13306 entries: Vec::new(),
13307 next_index: 0,
13308 }
13309 }
13310
13311 fn is_empty(&self) -> bool {
13312 self.entries.is_empty()
13313 }
13314
13315 fn push(
13316 &mut self,
13317 source: SubscriberStreamSource,
13318 stream: BoxStream<'static, SubscriberEvent<N>>,
13319 ) {
13320 self.entries.push(SubscriberStreamEntry { source, stream });
13321 }
13322
13323 #[cfg(test)]
13324 fn len(&self) -> usize {
13325 self.entries.len()
13326 }
13327
13328 fn contains_source(&self, source: &SubscriberStreamSource) -> bool {
13329 self.entries
13330 .iter()
13331 .any(|entry| entry.source.same_key(source))
13332 }
13333
13334 fn retain_sources(&mut self, sources: &[SubscriberStreamSource]) {
13335 self.entries
13336 .retain(|entry| sources.iter().any(|source| entry.source.same_key(source)));
13337 self.normalize_next_index();
13338 }
13339
13340 fn normalize_next_index(&mut self) {
13341 if self.entries.is_empty() {
13342 self.next_index = 0;
13343 } else if self.next_index >= self.entries.len() {
13344 self.next_index %= self.entries.len();
13345 }
13346 }
13347
13348 async fn next(&mut self) -> Option<SubscriberEvent<N>> {
13349 poll_fn(|cx| {
13350 self.normalize_next_index();
13351 if self.entries.is_empty() {
13352 return std::task::Poll::Ready(None);
13353 }
13354
13355 let mut index = self.next_index;
13356 let mut checked = 0usize;
13357 while checked < self.entries.len() {
13358 if index >= self.entries.len() {
13359 index = 0;
13360 }
13361 match self.entries[index].stream.as_mut().poll_next(cx) {
13362 std::task::Poll::Ready(Some(event)) => {
13363 if matches!(event, SubscriberEvent::StreamTerminated(_)) {
13364 self.entries.remove(index);
13365 self.next_index = if self.entries.is_empty() {
13366 0
13367 } else {
13368 index % self.entries.len()
13369 };
13370 } else {
13371 self.next_index = (index + 1) % self.entries.len();
13372 }
13373 return std::task::Poll::Ready(Some(event));
13374 }
13375 std::task::Poll::Ready(None) => {
13376 self.entries.remove(index);
13377 if self.entries.is_empty() {
13378 self.next_index = 0;
13379 return std::task::Poll::Ready(None);
13380 }
13381 }
13382 std::task::Poll::Pending => {
13383 checked += 1;
13384 index += 1;
13385 }
13386 }
13387 }
13388
13389 if self.entries.is_empty() {
13390 std::task::Poll::Ready(None)
13391 } else {
13392 self.next_index = index % self.entries.len();
13393 std::task::Poll::Pending
13394 }
13395 })
13396 .await
13397 }
13398}
13399
13400#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13401#[allow(dead_code)]
13402enum SubscriberTransport {
13403 PubSub,
13404 Polling,
13405}
13406
13407#[derive(Clone, Debug)]
13408enum SubscriberStreamSource {
13409 PubSubLog { id: usize, filter: Filter },
13410 BasePendingLog { id: usize, filter: Filter },
13411 BaseFlashblocks,
13412 OpPendingFlashblocks,
13413 PubSubPendingHashes,
13414 PubSubBlockHeaders,
13415 PollingLog { filter: Filter },
13416 PollingPendingHashes,
13417}
13418
13419impl SubscriberStreamSource {
13420 fn label(&self) -> &'static str {
13421 match self {
13422 Self::PubSubLog { .. } => "pubsub log",
13423 Self::BasePendingLog { .. } => "Base pendingLogs",
13424 Self::BaseFlashblocks => "Base newFlashblocks",
13425 Self::OpPendingFlashblocks => "OP pending Flashblocks",
13426 Self::PubSubPendingHashes => "pubsub pending transaction hash",
13427 Self::PubSubBlockHeaders => "pubsub block header",
13428 Self::PollingLog { .. } => "polling log",
13429 Self::PollingPendingHashes => "polling pending transaction hash",
13430 }
13431 }
13432
13433 fn is_pubsub(&self) -> bool {
13434 matches!(
13435 self,
13436 Self::PubSubLog { .. }
13437 | Self::BasePendingLog { .. }
13438 | Self::BaseFlashblocks
13439 | Self::PubSubPendingHashes
13440 | Self::PubSubBlockHeaders
13441 )
13442 }
13443
13444 fn is_flashblocks(&self) -> bool {
13445 matches!(
13446 self,
13447 Self::BasePendingLog { .. } | Self::BaseFlashblocks | Self::OpPendingFlashblocks
13448 )
13449 }
13450
13451 fn same_key(&self, other: &Self) -> bool {
13452 match (self, other) {
13453 (Self::PubSubLog { filter: left, .. }, Self::PubSubLog { filter: right, .. })
13454 | (
13455 Self::BasePendingLog { filter: left, .. },
13456 Self::BasePendingLog { filter: right, .. },
13457 )
13458 | (Self::PollingLog { filter: left }, Self::PollingLog { filter: right }) => {
13459 left == right
13460 }
13461 (Self::BaseFlashblocks, Self::BaseFlashblocks)
13462 | (Self::OpPendingFlashblocks, Self::OpPendingFlashblocks)
13463 | (Self::PubSubPendingHashes, Self::PubSubPendingHashes)
13464 | (Self::PubSubBlockHeaders, Self::PubSubBlockHeaders)
13465 | (Self::PollingPendingHashes, Self::PollingPendingHashes) => true,
13466 _ => false,
13467 }
13468 }
13469}
13470
13471#[allow(dead_code)]
13472enum SubscriberEvent<N: Network> {
13473 Log {
13474 source_id: usize,
13475 log: Log,
13476 },
13477 BackfilledLogs {
13478 source_id: usize,
13479 logs: Vec<Log>,
13480 },
13481 Logs(Vec<Log>),
13482 BlockHeader(N::HeaderResponse),
13483 PendingHash(B256),
13484 PendingHashes(Vec<B256>),
13485 BasePendingLog {
13486 source_id: usize,
13487 log: Log,
13488 },
13489 BaseFlashblock(BaseFlashblockWirePayload),
13490 OpFlashblockTick,
13491 PreconfirmedLogs {
13492 flashblock: FlashblockRef,
13493 logs: Vec<Log>,
13494 },
13495 FlashblockObserved,
13496 StreamTerminated(SubscriberStreamSource),
13497}
13498
13499impl<P, N> EventSubscriber<N> for AlloySubscriber<P, N>
13500where
13501 P: Provider<N> + Send + Sync,
13502 N: Network + 'static,
13503 N::HeaderResponse: Send + 'static,
13504{
13505 fn chain_id(&self) -> Option<u64> {
13506 self.chain_id
13507 }
13508
13509 fn capabilities(&self) -> SubscriberCapabilities {
13510 let Ok(transport) = resolve_subscriber_transport(self.mode) else {
13511 return SubscriberCapabilities::default();
13512 };
13513 let mut capabilities = vec![
13514 SubscriberCapability::Logs,
13515 SubscriberCapability::PendingTransactionHashes,
13516 SubscriberCapability::HistoricalBackfill,
13517 SubscriberCapability::Live,
13518 SubscriberCapability::OwnerScopedDelivery,
13519 SubscriberCapability::DynamicInterests,
13520 ];
13521 if transport == SubscriberTransport::PubSub {
13522 capabilities.push(SubscriberCapability::BlockHeaders);
13523 }
13524 if self.config.preconfirmations != PreconfirmationMode::Disabled
13525 && self.provider_ref.is_some()
13526 && self.chain_id.and_then(flashblocks_adapter).is_some()
13527 {
13528 capabilities.push(SubscriberCapability::Preconfirmations);
13529 }
13530 SubscriberCapabilities::new(capabilities)
13531 }
13532
13533 fn register_interests(
13534 &mut self,
13535 interests: &[ReactiveInterest<N>],
13536 ) -> SubscriberOperation<'_, ()> {
13537 let interests = interests.to_vec();
13538 Box::pin(async move {
13539 validate_subscriber_config(&self.config)?;
13540 validate_supported_interests(self.mode, &self.config, &interests)?;
13541 if !interests.is_empty() {
13542 self.ensure_chain_id().await?;
13543 }
13544 self.validate_flashblocks_setup()?;
13545
13546 self.base_interests = interests;
13547 self.owned_interests.clear();
13548 self.rebuild_registered_interests();
13549 self.reset_delivery_state();
13550 self.state = AlloySubscriberState::Uninitialized;
13551 Ok(())
13552 })
13553 }
13554
13555 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N> {
13556 Box::pin(async {
13557 Ok(self
13558 .next_scoped_batch()
13559 .await?
13560 .map(SubscriberInputBatch::into_reactive_batch))
13561 })
13562 }
13563}
13564
13565impl<P, N> AlloySubscriber<P, N>
13566where
13567 P: Provider<N> + Send + Sync,
13568 N: Network + 'static,
13569 N::HeaderResponse: Send + 'static,
13570{
13571 async fn ensure_chain_id(&mut self) -> Result<u64, SubscriberError> {
13575 if let Some(chain_id) = self.chain_id {
13576 return Ok(chain_id);
13577 }
13578 let chain_id = self.provider.get_chain_id().await.map_err(provider_error)?;
13579 self.chain_id = Some(chain_id);
13580 Ok(chain_id)
13581 }
13582
13583 fn validate_flashblocks_setup(&self) -> Result<(), SubscriberError> {
13584 if self.config.preconfirmations == PreconfirmationMode::Disabled {
13585 return Ok(());
13586 }
13587 if self.provider_ref.is_none() {
13588 return Err(SubscriberError::InvalidConfig(
13589 "Flashblocks require a stable provider ref from a pinned provider lease",
13590 ));
13591 }
13592 let Some(chain_id) = self.chain_id else {
13593 return Ok(());
13594 };
13595 match flashblocks_adapter(chain_id) {
13596 Some(FlashblocksAdapter::BaseNative)
13597 if resolve_subscriber_transport(self.mode)? != SubscriberTransport::PubSub
13598 && self.config.preconfirmations == PreconfirmationMode::Required =>
13599 {
13600 return Err(SubscriberError::Unsupported(
13601 "Base Flashblocks require pubsub for newFlashblocks and pendingLogs",
13602 ));
13603 }
13604 Some(_) => {}
13605 None if self.config.preconfirmations == PreconfirmationMode::Required => {
13606 return Err(SubscriberError::Unsupported(
13607 "Flashblocks are currently implemented for Base and OP chains",
13608 ));
13609 }
13610 None => {}
13611 }
13612 Ok(())
13613 }
13614
13615 pub async fn reconcile_interest_owner(
13629 &mut self,
13630 epoch: &SubscriberOwnerEpoch,
13631 through: BlockRef,
13632 ) -> Result<SubscriberOwnerProgress, SubscriberOwnerError>
13633 where
13634 P: Clone,
13635 {
13636 self.reconcile_interest_owners(std::slice::from_ref(epoch), through)
13637 .await?
13638 .pop()
13639 .ok_or(SubscriberOwnerError::NotStaged)
13640 }
13641
13642 pub async fn reconcile_interest_owners(
13666 &mut self,
13667 epochs: &[SubscriberOwnerEpoch],
13668 through: BlockRef,
13669 ) -> Result<Vec<SubscriberOwnerProgress>, SubscriberOwnerError>
13670 where
13671 P: Clone,
13672 {
13673 if epochs.is_empty() {
13674 return Ok(Vec::new());
13675 }
13676
13677 self.ensure_chain_id().await?;
13678
13679 let mut seen = HashSet::new();
13680 let mut plans = Vec::with_capacity(epochs.len());
13681 for epoch in epochs {
13682 if !seen.insert(epoch.clone()) {
13683 continue;
13684 }
13685 let entry = self
13686 .owned_interests
13687 .iter()
13688 .find(|entry| {
13689 entry.epoch.as_ref() == Some(epoch)
13690 && entry.state == SubscriberOwnerState::Staged
13691 })
13692 .ok_or(SubscriberOwnerError::NotStaged)?;
13693 let position = entry
13694 .progress
13695 .as_ref()
13696 .map(|progress| &progress.through)
13697 .or(entry.baseline.as_ref())
13698 .ok_or(SubscriberOwnerError::MissingBaseline)?;
13699 let baseline = position.number;
13700 if through.number < baseline {
13701 return Err(SubscriberOwnerError::ProgressRegression {
13702 current: baseline,
13703 target: through.number,
13704 });
13705 }
13706 let from_block = baseline
13707 .checked_add(1)
13708 .ok_or(SubscriberOwnerError::PostBlockOverflow(baseline))?;
13709 if through.number == baseline && through.hash != position.hash {
13710 return Err(SubscriberOwnerError::ProgressConflict {
13711 number: baseline,
13712 current_hash: position.hash,
13713 target_hash: through.hash,
13714 });
13715 }
13716 if through.number == from_block
13717 && through
13718 .parent_hash
13719 .is_some_and(|parent| parent != position.hash)
13720 {
13721 return Err(SubscriberOwnerError::ProgressConflict {
13722 number: baseline,
13723 current_hash: position.hash,
13724 target_hash: through.parent_hash.expect("checked as present above"),
13725 });
13726 }
13727 if entry
13728 .interests
13729 .iter()
13730 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
13731 {
13732 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
13733 }
13734 plans.push(SubscriberOwnerReconcilePlan {
13735 epoch: epoch.clone(),
13736 interests: entry.interests.clone(),
13737 retained: *position,
13738 from_block,
13739 });
13740 }
13741
13742 self.ensure_streams().await?;
13745 let provider = self.provider.clone();
13746 let filters = merged_owner_reconcile_filters(&plans, through.number);
13747 let retained = plans.iter().map(|plan| plan.retained).collect();
13748 let target_epochs: HashSet<_> = plans.iter().map(|plan| plan.epoch.clone()).collect();
13749 let fetch = fetch_owner_catchup::<P, N>(
13750 provider,
13751 filters,
13752 retained,
13753 through,
13754 SubscriberOwnerCatchupOptions {
13755 target_preverified: false,
13756 max_logs: self.config.max_pending_records,
13757 max_log_bytes: self.config.max_backfill_log_bytes,
13758 max_requests_in_flight: self.config.max_reconcile_requests_in_flight,
13759 },
13760 );
13761 let SubscriberOwnerCatchup { logs, certified } =
13762 self.drive_reconcile_fetch(fetch, &target_epochs).await?;
13763
13764 let records = logs
13765 .into_iter()
13766 .map(|log| log_input_record(log, InputSource::Backfill))
13767 .collect();
13768 let mut routed_records = Vec::new();
13769 for record in dedupe_records(sort_records(records)).map_err(|error| {
13770 SubscriberError::InvalidBackfill(format!(
13771 "conflicting duplicate owner catch-up record: {error}"
13772 ))
13773 })? {
13774 let block_number = match &record.input {
13775 ReactiveInput::Log(log) => log
13776 .block_number
13777 .expect("bulk catch-up logs were validated before commit"),
13778 _ => unreachable!("bulk owner catch-up contains log records only"),
13779 };
13780 let owners: Vec<SubscriberOwnerEpoch> = plans
13781 .iter()
13782 .filter(|plan| block_number >= plan.from_block)
13783 .filter(|plan| {
13784 plan.interests
13785 .iter()
13786 .any(|interest| interest_matches(interest, &record.input))
13787 })
13788 .map(|plan| plan.epoch.clone())
13789 .collect();
13790 if !owners.is_empty() {
13791 routed_records.push((record, owners));
13792 }
13793 }
13794 self.ensure_pending_record_capacity(
13795 routed_records.len(),
13796 "owner reconciliation historical records",
13797 )?;
13798
13799 self.pending_backfills.retain(|queued| {
13803 queued
13804 .epoch
13805 .as_ref()
13806 .is_none_or(|epoch| !target_epochs.contains(epoch))
13807 });
13808 for (record, owners) in routed_records {
13809 self.enqueue_owner_record_for_owners_unmerged(record, owners);
13810 }
13811 self.promote_reconcile_owner_records(&target_epochs);
13812 self.seed_reconciled_filter_anchors(&plans, certified.number);
13813
13814 let stream_revision = self.stream_revision;
13815 let mut progress = Vec::with_capacity(plans.len());
13816 for plan in plans {
13817 let item = SubscriberOwnerProgress {
13818 owner: plan.epoch.clone(),
13819 through: certified,
13820 };
13821 let entry = self
13822 .owned_interests
13823 .iter_mut()
13824 .find(|entry| entry.epoch.as_ref() == Some(&plan.epoch))
13825 .expect("bulk reconcile holds exclusive access after epoch preflight");
13826 entry.progress = Some(item.clone());
13827 entry.progress_stream_revision = Some(stream_revision);
13828 progress.push(item);
13829 }
13830 Ok(progress)
13831 }
13832
13833 async fn drive_reconcile_fetch<T, F>(
13834 &mut self,
13835 fetch: F,
13836 target_epochs: &HashSet<SubscriberOwnerEpoch>,
13837 ) -> Result<T, SubscriberOwnerError>
13838 where
13839 F: Future<Output = Result<T, SubscriberOwnerError>>,
13840 {
13841 if !matches!(&self.state, AlloySubscriberState::Active(_)) {
13842 return fetch.await;
13843 }
13844 let mut fetch = Box::pin(fetch);
13845 loop {
13846 let event = {
13847 let live = Box::pin(self.next_event());
13848 match select(fetch, live).await {
13849 Either::Left((result, pending_live)) => {
13850 drop(pending_live);
13851 return result;
13852 }
13853 Either::Right((event, pending_fetch)) => {
13854 fetch = pending_fetch;
13855 event
13856 }
13857 }
13858 };
13859 let event = event?.ok_or_else(|| {
13860 SubscriberError::Provider(
13861 "Alloy subscriber streams ended during owner reconcile".to_owned(),
13862 )
13863 })?;
13864 self.buffer_reconcile_event_for_owners(&event, target_epochs);
13865 self.enqueue_event_excluding_owners(event, target_epochs);
13866 self.check_resource_error()?;
13867 }
13868 }
13869
13870 pub async fn next_scoped_batch_or<C, F>(
13889 &mut self,
13890 control: Pin<&mut F>,
13891 ) -> Result<SubscriberDriverPoll<C, N>, SubscriberError>
13892 where
13893 C: Send,
13894 F: Future<Output = C> + Send,
13895 {
13896 let batch = self.next_scoped_batch();
13897 match select(control, batch).await {
13898 Either::Left((control, pending_batch)) => {
13899 drop(pending_batch);
13900 Ok(SubscriberDriverPoll::Control(control))
13901 }
13902 Either::Right((batch, _pending_control)) => batch.map(SubscriberDriverPoll::Batch),
13903 }
13904 }
13905
13906 pub fn next_scoped_batch(&mut self) -> SubscriberNextScopedBatch<'_, N> {
13917 Box::pin(async {
13918 self.check_resource_error()?;
13919 if self.chain_id.is_none()
13920 && (!self.pending_records.is_empty()
13921 || !self.pending_chain_controls.is_empty()
13922 || !self.pending_backfills.is_empty()
13923 || !self.interests.is_empty())
13924 {
13925 self.ensure_chain_id().await?;
13926 }
13927 if let Some(batch) = self.drain_next_scoped_batch() {
13928 return Ok(Some(batch));
13929 }
13930
13931 self.ensure_streams().await?;
13936 self.check_resource_error()?;
13937 if let Some(batch) = self.drain_next_scoped_batch() {
13938 return Ok(Some(batch));
13939 }
13940
13941 self.drain_pending_backfills().await?;
13942 self.check_resource_error()?;
13943 if let Some(batch) = self.drain_next_scoped_batch() {
13944 return Ok(Some(batch));
13945 }
13946
13947 if self.interests.is_empty() {
13948 return Ok(None);
13949 }
13950
13951 loop {
13952 let Some(event) = self.next_event().await? else {
13953 return Ok(None);
13954 };
13955
13956 self.enqueue_event(event);
13957 self.check_resource_error()?;
13958 if let Some(batch) = self.drain_next_scoped_batch() {
13959 return Ok(Some(batch));
13960 }
13961 }
13962 })
13963 }
13964
13965 async fn ensure_streams(&mut self) -> Result<(), SubscriberError> {
13980 if !self.sources_dirty {
13981 return Ok(());
13982 }
13983 if matches!(self.state, AlloySubscriberState::Uninitialized) && self.interests.is_empty() {
13988 self.bump_stream_revision();
13989 self.sources_dirty = false;
13990 return Ok(());
13991 }
13992
13993 let desired = self.stream_sources()?;
13994 let missing: Vec<SubscriberStreamSource> = match &self.state {
13995 AlloySubscriberState::Active(streams) => desired
13996 .iter()
13997 .filter(|source| !streams.contains_source(source))
13998 .cloned()
13999 .collect(),
14000 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => desired.clone(),
14001 };
14002
14003 for source in missing {
14004 let stream = self.connect_source_stream(source.clone()).await?;
14005 self.install_source_stream(source.clone(), stream);
14009 if self.source_requires_backfill(&source) {
14010 self.queue_source_backfill(source);
14011 }
14012 }
14013
14014 while let Some(source) = self.pending_source_backfills.front().cloned() {
14015 let desired_and_live = desired.iter().any(|item| item.same_key(&source))
14016 && matches!(
14017 &self.state,
14018 AlloySubscriberState::Active(streams) if streams.contains_source(&source)
14019 );
14020 if !desired_and_live {
14021 self.pending_source_backfills.pop_front();
14022 continue;
14023 }
14024
14025 let event = self.backfill_reconnected_source(&source).await?;
14031 self.pending_source_backfills.pop_front();
14032 if let Some(event) = event {
14033 self.enqueue_event(event);
14034 }
14035 }
14036
14037 if let AlloySubscriberState::Active(streams) = &mut self.state {
14038 streams.retain_sources(&desired);
14039 if streams.is_empty() {
14040 self.state = AlloySubscriberState::Empty;
14041 }
14042 }
14043
14044 self.bump_stream_revision();
14045 self.sources_dirty = false;
14046 self.retire_unreferenced_filters();
14047 Ok(())
14048 }
14049
14050 fn install_source_stream(
14051 &mut self,
14052 source: SubscriberStreamSource,
14053 stream: BoxStream<'static, SubscriberEvent<N>>,
14054 ) {
14055 match &mut self.state {
14056 AlloySubscriberState::Active(streams) => {
14057 if streams.contains_source(&source) {
14058 return;
14059 }
14060 streams.push(source, stream);
14061 }
14062 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
14063 let mut streams = SubscriberStreams::new();
14064 streams.push(source, stream);
14065 self.state = AlloySubscriberState::Active(streams);
14066 }
14067 }
14068 self.bump_stream_revision();
14071 }
14072
14073 fn source_requires_backfill(&self, source: &SubscriberStreamSource) -> bool {
14074 matches!(source, SubscriberStreamSource::PubSubLog { id, .. }
14075 if self.last_seen_log_blocks.contains_key(id))
14076 }
14077
14078 fn queue_source_backfill(&mut self, source: SubscriberStreamSource) {
14079 if !self
14080 .pending_source_backfills
14081 .iter()
14082 .any(|pending| pending.same_key(&source))
14083 {
14084 self.pending_source_backfills.push_back(source);
14085 }
14086 }
14087
14088 async fn drain_pending_backfills(&mut self) -> Result<(), SubscriberError> {
14100 while let Some(queued) = self.pending_backfills.front() {
14101 let epoch = queued.epoch.clone();
14103 let owner = queued.owner.clone();
14104 let owner_exists = match (&epoch, &owner) {
14105 (Some(epoch), _) => self.interest_owner_state(epoch).is_some(),
14106 (None, Some(owner)) => self.owner_interests(owner).is_some(),
14107 (None, None) => true,
14108 };
14109 if !owner_exists {
14110 self.pending_backfills.pop_front();
14111 continue;
14112 }
14113 let filters = queued.filters.clone();
14114 let backfill = queued.backfill;
14115
14116 let to_block = match backfill.end_block() {
14117 Some(to_block) => to_block,
14118 None => self
14119 .provider
14120 .get_block_number()
14121 .await
14122 .map_err(provider_error)?,
14123 };
14124 if to_block < backfill.start_block() {
14125 let certified = if let Some(retained) = backfill.retained_anchor() {
14130 let actual =
14131 fetch_provider_block_ref::<P, N>(&self.provider, retained.number).await?;
14132 if !block_ref_satisfies_expected(&actual, retained) {
14133 return Err(SubscriberError::InvalidBackfill(format!(
14134 "retained anchor {}:{:?} conflicts with provider block {}:{:?}",
14135 retained.number, retained.hash, actual.number, actual.hash
14136 )));
14137 }
14138 if to_block < retained.number {
14139 return Err(SubscriberError::InvalidBackfill(format!(
14140 "backfill upper bound {to_block} precedes retained anchor {}",
14141 retained.number
14142 )));
14143 }
14144 Some(actual)
14145 } else {
14146 None
14147 };
14148 self.pending_backfills.pop_front();
14149 for filter in &filters {
14150 let source_id = self.log_source_id(filter);
14151 if let Some(certified) = certified {
14152 self.last_seen_log_blocks
14153 .entry(source_id)
14154 .and_modify(|anchor| *anchor = (*anchor).max(certified.number))
14155 .or_insert(certified.number);
14156 }
14157 }
14158 if owner.is_none()
14159 && let Some(certified) = certified
14160 {
14161 self.pending_chain_controls
14162 .push_back(global_backfill_barrier(backfill, certified));
14163 }
14164 if !self.pending_chain_controls.is_empty() {
14165 break;
14166 }
14167 continue;
14168 }
14169
14170 let through = fetch_provider_block_ref::<P, N>(&self.provider, to_block).await?;
14171 let request_filters =
14172 merged_lazy_backfill_filters(&filters, backfill.start_block(), through.number);
14173 let retained = backfill.retained_anchor().copied().into_iter().collect();
14174 let SubscriberOwnerCatchup {
14175 mut logs,
14176 certified,
14177 } = fetch_owner_catchup::<&P, N>(
14178 &self.provider,
14179 request_filters,
14180 retained,
14181 through,
14182 SubscriberOwnerCatchupOptions {
14183 target_preverified: true,
14184 max_logs: self.config.max_pending_records,
14185 max_log_bytes: self.config.max_backfill_log_bytes,
14186 max_requests_in_flight: self.config.max_reconcile_requests_in_flight,
14187 },
14188 )
14189 .await
14190 .map_err(lazy_backfill_error)?;
14191 logs.sort_by_key(|log| {
14192 (
14193 log.block_number.unwrap_or_default(),
14194 log.transaction_index.unwrap_or_default(),
14195 log.log_index.unwrap_or_default(),
14196 )
14197 });
14198 logs.dedup();
14199 self.ensure_pending_record_capacity(logs.len(), "lazy subscriber backfill records")?;
14200
14201 self.pending_backfills.pop_front();
14204 if let Some(epoch) = epoch.as_ref() {
14205 self.enqueue_backfilled_logs(logs, None, Some(epoch), Some(backfill));
14206 } else if let Some(owner) = owner.as_ref() {
14207 self.enqueue_compat_owner_backfilled_logs(logs, owner, backfill);
14208 } else {
14209 self.enqueue_backfilled_logs(logs, None, None, Some(backfill));
14210 self.pending_chain_controls
14211 .push_back(global_backfill_barrier(backfill, certified));
14212 }
14213 for filter in &filters {
14214 let source_id = self.log_source_id(filter);
14215 let anchor = self
14216 .last_seen_log_blocks
14217 .entry(source_id)
14218 .or_insert(certified.number);
14219 *anchor = (*anchor).max(certified.number);
14220 }
14221
14222 if !self.pending_records.is_empty() || !self.pending_chain_controls.is_empty() {
14223 break;
14224 }
14225 }
14226 Ok(())
14227 }
14228
14229 fn stream_sources(&mut self) -> Result<Vec<SubscriberStreamSource>, SubscriberError> {
14230 match resolve_subscriber_transport(self.mode)? {
14231 SubscriberTransport::PubSub => Ok(self.pubsub_stream_sources()),
14232 SubscriberTransport::Polling => Ok(self.polling_stream_sources()),
14233 }
14234 }
14235
14236 fn pubsub_stream_sources(&mut self) -> Vec<SubscriberStreamSource> {
14237 let mut sources = Vec::new();
14238 let inherited_anchor = self.last_seen_log_blocks.values().copied().min();
14239
14240 for filter in self.log_stream_filters() {
14241 let id = self.log_source_id(&filter);
14242 if let Some(anchor) = inherited_anchor {
14243 self.last_seen_log_blocks.entry(id).or_insert(anchor);
14244 }
14245 sources.push(SubscriberStreamSource::PubSubLog { id, filter });
14246 }
14247
14248 if needs_pending_hash_stream(&self.interests) {
14249 sources.push(SubscriberStreamSource::PubSubPendingHashes);
14250 }
14251
14252 if needs_header_block_stream(&self.interests) {
14253 sources.push(SubscriberStreamSource::PubSubBlockHeaders);
14254 }
14255
14256 if self.config.preconfirmations != PreconfirmationMode::Disabled {
14257 match self.chain_id.and_then(flashblocks_adapter) {
14258 Some(FlashblocksAdapter::BaseNative) => {
14259 sources.push(SubscriberStreamSource::BaseFlashblocks);
14260 for filter in self.log_stream_filters() {
14261 let id = self.log_source_id(&filter);
14262 sources.push(SubscriberStreamSource::BasePendingLog { id, filter });
14263 }
14264 }
14265 Some(FlashblocksAdapter::OpPending) => {
14266 sources.push(SubscriberStreamSource::OpPendingFlashblocks);
14267 }
14268 None => {}
14269 }
14270 }
14271
14272 sources
14273 }
14274
14275 fn polling_stream_sources(&self) -> Vec<SubscriberStreamSource> {
14276 let mut sources = Vec::new();
14277
14278 for filter in self.log_stream_filters() {
14279 sources.push(SubscriberStreamSource::PollingLog { filter });
14280 }
14281
14282 if needs_pending_hash_stream(&self.interests) {
14283 sources.push(SubscriberStreamSource::PollingPendingHashes);
14284 }
14285
14286 if self.config.preconfirmations != PreconfirmationMode::Disabled
14287 && self.chain_id.and_then(flashblocks_adapter) == Some(FlashblocksAdapter::OpPending)
14288 {
14289 sources.push(SubscriberStreamSource::OpPendingFlashblocks);
14290 }
14291
14292 sources
14293 }
14294
14295 fn log_source_id(&mut self, filter: &Filter) -> usize {
14296 if let Some(id) = self.log_source_ids.get(filter) {
14297 return *id;
14298 }
14299
14300 let id = self.next_log_source_id;
14301 self.next_log_source_id = self.next_log_source_id.saturating_add(1);
14302 self.log_source_ids.insert(filter.clone(), id);
14303 id
14304 }
14305
14306 async fn connect_source_stream(
14307 &mut self,
14308 source: SubscriberStreamSource,
14309 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
14310 match source {
14311 SubscriberStreamSource::PubSubLog { id, filter } => {
14312 self.connect_pubsub_log_stream(id, filter).await
14313 }
14314 SubscriberStreamSource::BasePendingLog { id, filter } => {
14315 self.connect_base_pending_log_stream(id, filter).await
14316 }
14317 SubscriberStreamSource::BaseFlashblocks => self.connect_base_flashblock_stream().await,
14318 SubscriberStreamSource::OpPendingFlashblocks => {
14319 self.connect_op_flashblock_tick_stream()
14320 }
14321 SubscriberStreamSource::PubSubPendingHashes => {
14322 self.connect_pubsub_pending_hash_stream().await
14323 }
14324 SubscriberStreamSource::PubSubBlockHeaders => {
14325 self.connect_pubsub_block_header_stream().await
14326 }
14327 SubscriberStreamSource::PollingLog { filter } => {
14328 self.connect_polling_log_stream(filter).await
14329 }
14330 SubscriberStreamSource::PollingPendingHashes => {
14331 self.connect_polling_pending_hash_stream().await
14332 }
14333 }
14334 }
14335
14336 async fn connect_pubsub_log_stream(
14337 &mut self,
14338 id: usize,
14339 filter: Filter,
14340 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
14341 #[cfg(feature = "reactive-ws")]
14342 {
14343 let source = SubscriberStreamSource::PubSubLog {
14344 id,
14345 filter: filter.clone(),
14346 };
14347 let stream = self
14348 .provider
14349 .subscribe_logs(&filter)
14350 .channel_size(self.config.max_batch_size.max(1))
14351 .await
14352 .map_err(provider_error)?
14353 .into_stream()
14354 .map(move |log| SubscriberEvent::Log { source_id: id, log });
14355 Ok(stream_with_termination(stream, source))
14356 }
14357
14358 #[cfg(not(feature = "reactive-ws"))]
14359 {
14360 let _ = (id, filter);
14361 Err(SubscriberError::Unsupported(
14362 "AlloySubscriber pubsub mode requires the reactive-ws feature",
14363 ))
14364 }
14365 }
14366
14367 async fn connect_base_pending_log_stream(
14368 &mut self,
14369 id: usize,
14370 filter: Filter,
14371 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
14372 #[cfg(feature = "reactive-ws")]
14373 {
14374 let source = SubscriberStreamSource::BasePendingLog {
14375 id,
14376 filter: filter.clone(),
14377 };
14378 let params = base_pending_log_filter(&filter)?;
14379 let stream = self
14380 .provider
14381 .subscribe::<_, Log>(("pendingLogs", params))
14382 .channel_size(self.config.max_batch_size.max(1))
14383 .await
14384 .map_err(provider_error)?
14385 .into_stream()
14386 .map(move |log| SubscriberEvent::BasePendingLog { source_id: id, log });
14387 Ok(stream_with_termination(stream, source))
14388 }
14389
14390 #[cfg(not(feature = "reactive-ws"))]
14391 {
14392 let _ = (id, filter);
14393 Err(SubscriberError::Unsupported(
14394 "Base Flashblocks require the reactive-ws feature",
14395 ))
14396 }
14397 }
14398
14399 async fn connect_base_flashblock_stream(
14400 &mut self,
14401 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
14402 #[cfg(feature = "reactive-ws")]
14403 {
14404 let stream = self
14405 .provider
14406 .subscribe::<_, BaseFlashblockWirePayload>(("newFlashblocks",))
14407 .channel_size(self.config.max_batch_size.max(1))
14408 .await
14409 .map_err(provider_error)?
14410 .into_stream()
14411 .map(SubscriberEvent::BaseFlashblock);
14412 Ok(stream_with_termination(
14413 stream,
14414 SubscriberStreamSource::BaseFlashblocks,
14415 ))
14416 }
14417
14418 #[cfg(not(feature = "reactive-ws"))]
14419 {
14420 Err(SubscriberError::Unsupported(
14421 "Base Flashblocks require the reactive-ws feature",
14422 ))
14423 }
14424 }
14425
14426 fn connect_op_flashblock_tick_stream(
14427 &self,
14428 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
14429 let interval = tokio::time::interval(self.config.flashblock_poll_interval);
14430 let stream = stream::unfold(interval, |mut interval| async move {
14431 interval.tick().await;
14432 Some((SubscriberEvent::OpFlashblockTick, interval))
14433 });
14434 Ok(stream_with_termination(
14435 stream,
14436 SubscriberStreamSource::OpPendingFlashblocks,
14437 ))
14438 }
14439
14440 async fn connect_pubsub_pending_hash_stream(
14441 &mut self,
14442 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
14443 #[cfg(feature = "reactive-ws")]
14444 {
14445 let stream = self
14446 .provider
14447 .subscribe_pending_transactions()
14448 .channel_size(self.config.max_batch_size.max(1))
14449 .await
14450 .map_err(provider_error)?
14451 .into_stream()
14452 .map(SubscriberEvent::PendingHash);
14453 Ok(stream_with_termination(
14454 stream,
14455 SubscriberStreamSource::PubSubPendingHashes,
14456 ))
14457 }
14458
14459 #[cfg(not(feature = "reactive-ws"))]
14460 {
14461 Err(SubscriberError::Unsupported(
14462 "AlloySubscriber pubsub mode requires the reactive-ws feature",
14463 ))
14464 }
14465 }
14466
14467 async fn connect_pubsub_block_header_stream(
14468 &mut self,
14469 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
14470 #[cfg(feature = "reactive-ws")]
14471 {
14472 let stream = self
14473 .provider
14474 .subscribe_blocks()
14475 .channel_size(self.config.max_batch_size.max(1))
14476 .await
14477 .map_err(provider_error)?
14478 .into_stream()
14479 .map(SubscriberEvent::BlockHeader);
14480 Ok(stream_with_termination(
14481 stream,
14482 SubscriberStreamSource::PubSubBlockHeaders,
14483 ))
14484 }
14485
14486 #[cfg(not(feature = "reactive-ws"))]
14487 {
14488 Err(SubscriberError::Unsupported(
14489 "AlloySubscriber pubsub mode requires the reactive-ws feature",
14490 ))
14491 }
14492 }
14493
14494 async fn connect_polling_log_stream(
14495 &mut self,
14496 filter: Filter,
14497 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
14498 #[cfg(feature = "reactive-polling")]
14499 {
14500 let source = SubscriberStreamSource::PollingLog {
14501 filter: filter.clone(),
14502 };
14503 let stream = self
14504 .provider
14505 .watch_logs(&filter)
14506 .await
14507 .map_err(provider_error)?
14508 .with_channel_size(self.config.max_batch_size.max(1))
14509 .into_stream()
14510 .map(SubscriberEvent::Logs);
14511 Ok(stream_with_termination(stream, source))
14512 }
14513
14514 #[cfg(not(feature = "reactive-polling"))]
14515 {
14516 let _ = filter;
14517 Err(SubscriberError::Unsupported(
14518 "AlloySubscriber polling mode requires the reactive-polling feature",
14519 ))
14520 }
14521 }
14522
14523 async fn connect_polling_pending_hash_stream(
14524 &mut self,
14525 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
14526 #[cfg(feature = "reactive-polling")]
14527 {
14528 let stream = self
14529 .provider
14530 .watch_pending_transactions()
14531 .await
14532 .map_err(provider_error)?
14533 .with_channel_size(self.config.max_batch_size.max(1))
14534 .into_stream()
14535 .map(SubscriberEvent::PendingHashes);
14536 Ok(stream_with_termination(
14537 stream,
14538 SubscriberStreamSource::PollingPendingHashes,
14539 ))
14540 }
14541
14542 #[cfg(not(feature = "reactive-polling"))]
14543 {
14544 Err(SubscriberError::Unsupported(
14545 "AlloySubscriber polling mode requires the reactive-polling feature",
14546 ))
14547 }
14548 }
14549
14550 async fn next_event(&mut self) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
14551 loop {
14552 let event = match &mut self.state {
14553 AlloySubscriberState::Active(streams) => streams.next().await,
14554 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
14555 return Ok(None);
14556 }
14557 };
14558
14559 let Some(event) = event else {
14560 return Err(SubscriberError::Provider(
14561 "Alloy subscriber streams terminated before the subscriber was stopped"
14562 .to_owned(),
14563 ));
14564 };
14565
14566 match event {
14567 SubscriberEvent::StreamTerminated(source) => {
14568 self.sources_dirty = true;
14572 self.bump_stream_revision();
14573 if source.is_flashblocks() {
14574 self.reset_flashblock_tracking();
14575 }
14576 if let Some(backfill_event) = self.reconnect_source_stream(source).await? {
14577 self.sources_dirty = false;
14578 if let Some(backfill_event) =
14579 self.normalize_flashblock_event(backfill_event).await?
14580 {
14581 self.verify_event_log_blocks(&backfill_event).await?;
14582 return Ok(Some(backfill_event));
14583 }
14584 }
14585 self.sources_dirty = false;
14586 }
14587 event => {
14588 let Some(event) = self.normalize_flashblock_event(event).await? else {
14589 continue;
14590 };
14591 self.verify_event_log_blocks(&event).await?;
14592 return Ok(Some(event));
14593 }
14594 }
14595 }
14596 }
14597
14598 async fn normalize_flashblock_event(
14599 &mut self,
14600 event: SubscriberEvent<N>,
14601 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
14602 match event {
14603 SubscriberEvent::BasePendingLog { source_id, log } => {
14604 let hash = log.block_hash.ok_or_else(|| {
14605 SubscriberError::Provider(
14606 "Base pendingLogs item is missing its partial block hash".into(),
14607 )
14608 })?;
14609 let Some(flashblock) = self.flashblocks_by_hash.get(&hash).cloned() else {
14610 if self.unmatched_pending_logs.len() >= self.config.max_pending_records {
14611 return Err(SubscriberError::ResourceExhausted(
14612 "unmatched Base pendingLogs exceeded max_pending_records".into(),
14613 ));
14614 }
14615 self.unmatched_pending_logs.push_back((source_id, log));
14616 return Ok(None);
14617 };
14618 let logs = self.filter_preconfirmed_logs(&flashblock, vec![log])?;
14619 Ok(Some(if logs.is_empty() {
14620 SubscriberEvent::FlashblockObserved
14621 } else {
14622 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
14623 }))
14624 }
14625 SubscriberEvent::BaseFlashblock(payload) => {
14626 let (flashblock, recover_pending_snapshot) =
14627 self.accept_base_flashblock(payload)?;
14628 let mut logs = Vec::new();
14629 let mut retained = VecDeque::new();
14630 while let Some((source_id, log)) = self.unmatched_pending_logs.pop_front() {
14631 if log.block_hash == Some(flashblock.block_hash) {
14632 let _ = source_id;
14633 logs.push(log);
14634 } else {
14635 retained.push_back((source_id, log));
14636 }
14637 }
14638 self.unmatched_pending_logs = retained;
14639
14640 if recover_pending_snapshot
14641 && let Some(event) = self.fetch_pending_flashblock().await?
14642 {
14643 return Ok(Some(event));
14644 }
14645 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
14646 Ok(Some(if logs.is_empty() {
14647 SubscriberEvent::FlashblockObserved
14648 } else {
14649 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
14650 }))
14651 }
14652 SubscriberEvent::OpFlashblockTick => self.fetch_pending_flashblock().await,
14653 SubscriberEvent::PreconfirmedLogs { flashblock, logs } => {
14654 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
14655 Ok(Some(if logs.is_empty() {
14656 SubscriberEvent::FlashblockObserved
14657 } else {
14658 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
14659 }))
14660 }
14661 SubscriberEvent::FlashblockObserved => Ok(None),
14662 event => Ok(Some(event)),
14663 }
14664 }
14665
14666 fn accept_base_flashblock(
14667 &mut self,
14668 payload: BaseFlashblockWirePayload,
14669 ) -> Result<(FlashblockRef, bool), SubscriberError> {
14670 let provider = self.provider_ref.clone().ok_or({
14671 SubscriberError::InvalidConfig(
14672 "Flashblocks require a stable provider ref from a pinned provider lease",
14673 )
14674 })?;
14675
14676 let (flashblock, recover_pending_snapshot) = match payload {
14677 BaseFlashblockWirePayload::Indexed(payload) => {
14678 if payload.index == 0 {
14679 let base = payload.base.clone().ok_or_else(|| {
14680 SubscriberError::Provider(
14681 "Base newFlashblocks index zero omitted its base header".into(),
14682 )
14683 })?;
14684 self.base_flashblock_header = Some((payload.payload_id, base));
14685 }
14686
14687 let base = self
14688 .base_flashblock_header
14689 .as_ref()
14690 .filter(|(payload_id, _)| *payload_id == payload.payload_id)
14691 .map(|(_, base)| base);
14692 let block_number = base.map(|base| base.block_number).or_else(|| {
14693 payload
14694 .metadata
14695 .as_ref()
14696 .map(|metadata| metadata.block_number)
14697 });
14698 let block_number = block_number.ok_or_else(|| {
14699 SubscriberError::Provider(
14700 "Base newFlashblocks payload omitted both base and metadata block number"
14701 .into(),
14702 )
14703 })?;
14704 let flashblock = FlashblockRef {
14705 provider,
14706 payload_id: Some(payload.payload_id),
14707 index: Some(payload.index),
14708 block_number,
14709 block_hash: payload.diff.block_hash,
14710 parent_hash: base.map(|base| base.parent_hash),
14711 state_root: Some(payload.diff.state_root),
14712 timestamp: base.map(|base| base.timestamp),
14713 };
14714 let recover = match self.latest_preconfirmation.as_ref() {
14715 Some(previous) if previous.same_payload(&flashblock) => {
14716 if let (Some(previous), Some(current)) = (previous.index, flashblock.index)
14717 {
14718 if current < previous {
14719 return Ok((flashblock, false));
14720 }
14721 current > previous.saturating_add(1)
14722 } else {
14723 false
14724 }
14725 }
14726 Some(_) => payload.index != 0,
14727 None => payload.index != 0,
14728 };
14729 (flashblock, recover)
14730 }
14731 BaseFlashblockWirePayload::Block(payload) => {
14732 let index = self
14733 .latest_preconfirmation
14734 .as_ref()
14735 .filter(|previous| {
14736 previous.block_number == payload.number
14737 && previous.parent_hash == Some(payload.parent_hash)
14738 })
14739 .and_then(|previous| previous.index)
14740 .map_or(0, |index| index.saturating_add(1));
14741 (
14742 FlashblockRef {
14743 provider,
14744 payload_id: None,
14745 index: Some(index),
14746 block_number: payload.number,
14747 block_hash: payload.hash,
14748 parent_hash: Some(payload.parent_hash),
14749 state_root: Some(payload.state_root),
14750 timestamp: Some(payload.timestamp),
14751 },
14752 false,
14753 )
14754 }
14755 };
14756
14757 self.flashblocks_by_hash
14758 .insert(flashblock.block_hash, flashblock.clone());
14759 self.flashblock_hash_order.push_back(flashblock.block_hash);
14760 while self.flashblock_hash_order.len() > 64 {
14761 if let Some(hash) = self.flashblock_hash_order.pop_front() {
14762 self.flashblocks_by_hash.remove(&hash);
14763 }
14764 }
14765 Ok((flashblock, recover_pending_snapshot))
14766 }
14767
14768 async fn fetch_pending_flashblock(
14769 &mut self,
14770 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
14771 let latest = self
14772 .provider
14773 .get_block_number()
14774 .await
14775 .map_err(provider_error)?;
14776 let Some(block) = self
14777 .provider
14778 .get_block_by_number(BlockNumberOrTag::Pending)
14779 .await
14780 .map_err(provider_error)?
14781 else {
14782 if self.config.preconfirmations == PreconfirmationMode::Required {
14783 return Err(SubscriberError::Provider(
14784 "Flashblocks provider returned no pending block".into(),
14785 ));
14786 }
14787 return Ok(None);
14788 };
14789 let header = block.header();
14790 if header.number() <= latest {
14791 if self.config.preconfirmations == PreconfirmationMode::Required {
14792 return Err(SubscriberError::Provider(
14793 "Flashblocks provider pending state did not advance beyond the canonical head"
14794 .into(),
14795 ));
14796 }
14797 return Ok(None);
14798 }
14799
14800 let provider = self.provider_ref.clone().ok_or({
14801 SubscriberError::InvalidConfig(
14802 "Flashblocks require a stable provider ref from a pinned provider lease",
14803 )
14804 })?;
14805 let parent_hash = Some(header.parent_hash());
14806 let index = self.latest_preconfirmation.as_ref().and_then(|previous| {
14807 (previous.block_number == header.number() && previous.parent_hash == parent_hash)
14808 .then(|| previous.index.unwrap_or(0).saturating_add(1))
14809 });
14810 let flashblock = FlashblockRef {
14811 provider,
14812 payload_id: None,
14813 index: Some(index.unwrap_or(0)),
14814 block_number: header.number(),
14815 block_hash: header.hash(),
14816 parent_hash,
14817 state_root: Some(header.state_root()),
14818 timestamp: Some(header.timestamp()),
14819 };
14820 if self
14821 .latest_preconfirmation
14822 .as_ref()
14823 .is_some_and(|previous| {
14824 previous.block_hash == flashblock.block_hash
14825 && previous.block_number == flashblock.block_number
14826 })
14827 {
14828 return Ok(None);
14829 }
14830
14831 let logs = self.fetch_pending_logs().await?;
14832 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
14833 Ok(Some(if logs.is_empty() {
14834 SubscriberEvent::FlashblockObserved
14835 } else {
14836 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
14837 }))
14838 }
14839
14840 async fn fetch_pending_logs(&mut self) -> Result<Vec<Log>, SubscriberError> {
14841 let mut logs = Vec::new();
14842 for filter in self.log_stream_filters() {
14843 let filter = filter
14844 .from_block(BlockNumberOrTag::Pending)
14845 .to_block(BlockNumberOrTag::Pending);
14846 logs.extend(
14847 self.provider
14848 .get_logs(&filter)
14849 .await
14850 .map_err(provider_error)?,
14851 );
14852 }
14853 Ok(logs)
14854 }
14855
14856 fn filter_preconfirmed_logs(
14857 &mut self,
14858 flashblock: &FlashblockRef,
14859 mut logs: Vec<Log>,
14860 ) -> Result<Vec<Log>, SubscriberError> {
14861 if self
14862 .latest_preconfirmation
14863 .as_ref()
14864 .is_none_or(|previous| !previous.same_payload(flashblock))
14865 {
14866 self.preconfirmed_seen_logs.clear();
14867 }
14868 if let Some(previous) = self.latest_preconfirmation.as_ref()
14869 && previous.same_payload(flashblock)
14870 && let (Some(previous_index), Some(current_index)) = (previous.index, flashblock.index)
14871 && current_index < previous_index
14872 {
14873 return Ok(Vec::new());
14874 }
14875 self.latest_preconfirmation = Some(flashblock.clone());
14876
14877 logs.sort_by_key(|log| (log.transaction_index.unwrap_or(u64::MAX), log.log_index));
14878 let mut filtered = Vec::new();
14879 for log in logs {
14880 if log.removed
14881 || log.block_number != Some(flashblock.block_number)
14882 || log.block_hash != Some(flashblock.block_hash)
14883 {
14884 return Err(SubscriberError::Provider(
14885 "pre-confirmed log disagrees with its Flashblock snapshot".into(),
14886 ));
14887 }
14888 let transaction_hash = log.transaction_hash.ok_or_else(|| {
14889 SubscriberError::Provider(
14890 "pre-confirmed log is missing its transaction hash".into(),
14891 )
14892 })?;
14893 let log_index = log.log_index.ok_or_else(|| {
14894 SubscriberError::Provider("pre-confirmed log is missing its log index".into())
14895 })?;
14896 if self
14897 .preconfirmed_seen_logs
14898 .insert((transaction_hash, log_index))
14899 && log_matches_any_interest(&log, &self.interests)
14900 {
14901 filtered.push(log);
14902 }
14903 }
14904 Ok(filtered)
14905 }
14906
14907 async fn verify_event_log_blocks(
14908 &mut self,
14909 event: &SubscriberEvent<N>,
14910 ) -> Result<(), SubscriberError> {
14911 if !self.config.verify_log_block_context {
14912 return Ok(());
14913 }
14914 match event {
14915 SubscriberEvent::Log { log, .. } => self.verify_log_block_context(log).await,
14916 SubscriberEvent::BackfilledLogs { logs, .. } | SubscriberEvent::Logs(logs) => {
14917 for log in logs {
14918 self.verify_log_block_context(log).await?;
14919 }
14920 Ok(())
14921 }
14922 SubscriberEvent::BlockHeader(_)
14923 | SubscriberEvent::PendingHash(_)
14924 | SubscriberEvent::PendingHashes(_)
14925 | SubscriberEvent::BasePendingLog { .. }
14926 | SubscriberEvent::BaseFlashblock(_)
14927 | SubscriberEvent::OpFlashblockTick
14928 | SubscriberEvent::PreconfirmedLogs { .. }
14929 | SubscriberEvent::FlashblockObserved
14930 | SubscriberEvent::StreamTerminated(_) => Ok(()),
14931 }
14932 }
14933
14934 async fn verify_log_block_context(&mut self, log: &Log) -> Result<(), SubscriberError> {
14935 if log.removed {
14936 return Ok(());
14937 }
14938 let number = log.block_number.ok_or_else(|| {
14939 SubscriberError::Provider(
14940 "canonical log is missing its block number during context verification".into(),
14941 )
14942 })?;
14943 let hash = log.block_hash.ok_or_else(|| {
14944 SubscriberError::Provider(
14945 "canonical log is missing its block hash during context verification".into(),
14946 )
14947 })?;
14948 let key = (number, hash);
14949 if self.verified_log_blocks.contains_key(&key) {
14950 return Ok(());
14951 }
14952 let provider = self
14953 .log_verification_provider
14954 .as_ref()
14955 .unwrap_or(&self.provider);
14956 let block = provider
14957 .get_block_by_number(BlockNumberOrTag::Number(number))
14958 .await
14959 .map_err(provider_error)?
14960 .ok_or_else(|| {
14961 SubscriberError::Provider(format!(
14962 "canonical log block {number} is unavailable during context verification"
14963 ))
14964 })?;
14965 let header = block.header();
14966 let verified = BlockRef {
14967 number: header.number(),
14968 hash: header.hash(),
14969 parent_hash: Some(header.parent_hash()),
14970 timestamp: Some(header.timestamp()),
14971 };
14972 if verified.number != number
14973 || verified.hash != hash
14974 || log
14975 .block_timestamp
14976 .is_some_and(|timestamp| verified.timestamp != Some(timestamp))
14977 {
14978 return Err(SubscriberError::Provider(format!(
14979 "canonical log block {number}:{hash:?} disagrees with the provider's current canonical identity"
14980 )));
14981 }
14982 self.verified_log_blocks.insert(key, verified);
14983 self.verified_log_block_order.push_back(key);
14984 let capacity = self.config.reconnect.dedupe_window.max(1);
14985 while self.verified_log_block_order.len() > capacity {
14986 if let Some(evicted) = self.verified_log_block_order.pop_front() {
14987 self.verified_log_blocks.remove(&evicted);
14988 }
14989 }
14990 Ok(())
14991 }
14992
14993 fn enqueue_event(&mut self, event: SubscriberEvent<N>) {
14994 self.enqueue_event_with_excluded_owners(event, None);
14995 }
14996
14997 fn buffer_reconcile_event_for_owners(
14998 &mut self,
14999 event: &SubscriberEvent<N>,
15000 target_epochs: &HashSet<SubscriberOwnerEpoch>,
15001 ) {
15002 match event {
15003 SubscriberEvent::Log { log, .. } => {
15004 self.buffer_reconcile_log_for_owners(log, InputSource::Subscription, target_epochs)
15005 }
15006 SubscriberEvent::BackfilledLogs { logs, .. } => {
15007 for log in logs {
15008 self.buffer_reconcile_log_for_owners(log, InputSource::Backfill, target_epochs);
15009 }
15010 }
15011 SubscriberEvent::Logs(logs) => {
15012 for log in logs {
15013 self.buffer_reconcile_log_for_owners(log, InputSource::Poll, target_epochs);
15014 }
15015 }
15016 SubscriberEvent::BlockHeader(_)
15017 | SubscriberEvent::PendingHash(_)
15018 | SubscriberEvent::PendingHashes(_)
15019 | SubscriberEvent::BasePendingLog { .. }
15020 | SubscriberEvent::BaseFlashblock(_)
15021 | SubscriberEvent::OpFlashblockTick
15022 | SubscriberEvent::PreconfirmedLogs { .. }
15023 | SubscriberEvent::FlashblockObserved
15024 | SubscriberEvent::StreamTerminated(_) => {}
15025 }
15026 }
15027
15028 fn buffer_reconcile_log_for_owners(
15029 &mut self,
15030 log: &Log,
15031 source: InputSource,
15032 target_epochs: &HashSet<SubscriberOwnerEpoch>,
15033 ) {
15034 let record = self.with_chain_id(log_input_record(log.clone(), source));
15035 let owners = self
15036 .staged_owners_for_record(&record)
15037 .into_iter()
15038 .filter(|owner| target_epochs.contains(owner))
15039 .collect::<Vec<_>>();
15040 if !owners.is_empty() {
15041 self.push_pending_reconcile_record(BufferedSubscriberOwnerRecord { record, owners });
15042 }
15043 }
15044
15045 fn promote_reconcile_owner_records(&mut self, target_epochs: &HashSet<SubscriberOwnerEpoch>) {
15046 let mut retained = VecDeque::new();
15047 while let Some(mut buffered) = self.pending_reconcile_owner_records.pop_front() {
15048 let mut promoted = Vec::new();
15049 buffered.owners.retain(|owner| {
15050 if target_epochs.contains(owner) {
15051 promoted.push(owner.clone());
15052 false
15053 } else {
15054 true
15055 }
15056 });
15057 if promoted.is_empty() {
15058 retained.push_back(buffered);
15059 continue;
15060 }
15061 let promoted_record = if buffered.owners.is_empty() {
15062 buffered.record
15063 } else {
15064 let record = buffered.record.clone();
15065 retained.push_back(buffered);
15066 record
15067 };
15068 self.enqueue_owner_record_for_owners_unmerged(promoted_record, promoted);
15069 }
15070 self.pending_reconcile_owner_records = retained;
15071 }
15072
15073 fn seed_reconciled_filter_anchors(
15074 &mut self,
15075 plans: &[SubscriberOwnerReconcilePlan<N>],
15076 through: u64,
15077 ) {
15078 for filter in plans.iter().flat_map(|plan| log_filters(&plan.interests)) {
15079 let Some(source_id) = self.log_source_ids.get(&filter).copied() else {
15080 continue;
15081 };
15082 let anchor = self
15083 .last_seen_log_blocks
15084 .entry(source_id)
15085 .or_insert(through);
15086 *anchor = (*anchor).max(through);
15087 }
15088 }
15089
15090 fn enqueue_event_excluding_owners(
15091 &mut self,
15092 event: SubscriberEvent<N>,
15093 excluded: &HashSet<SubscriberOwnerEpoch>,
15094 ) {
15095 self.enqueue_event_with_excluded_owners(event, Some(excluded));
15096 }
15097
15098 fn enqueue_event_with_excluded_owners(
15099 &mut self,
15100 event: SubscriberEvent<N>,
15101 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
15102 ) {
15103 match event {
15104 SubscriberEvent::Log { source_id, log } => {
15105 if log_matches_any_interest(&log, &self.interests) {
15106 let record = log_input_record(log, InputSource::Subscription);
15107 self.note_log_block(source_id, &record);
15108 self.enqueue_record_with_excluded_owners(record, excluded);
15109 }
15110 }
15111 SubscriberEvent::BackfilledLogs { source_id, logs } => {
15112 self.enqueue_backfilled_logs_with_excluded_owners(
15113 logs,
15114 Some(source_id),
15115 None,
15116 None,
15117 excluded,
15118 );
15119 }
15120 SubscriberEvent::Logs(logs) => {
15121 for log in logs {
15122 if log_matches_any_interest(&log, &self.interests) {
15123 self.enqueue_record_with_excluded_owners(
15124 log_input_record(log, InputSource::Poll),
15125 excluded,
15126 );
15127 }
15128 }
15129 }
15130 SubscriberEvent::BlockHeader(header) => {
15131 if needs_header_block_stream(&self.interests) {
15132 let record = block_header_input_record::<N>(header);
15133 self.enqueue_record_with_excluded_owners(record, excluded);
15134 }
15135 }
15136 SubscriberEvent::PendingHash(hash) => {
15137 let record = pending_hash_input_record::<N>(hash, InputSource::Subscription);
15138 self.enqueue_record_with_excluded_owners(record, excluded);
15139 }
15140 SubscriberEvent::PendingHashes(hashes) => {
15141 for hash in hashes {
15142 self.enqueue_record_with_excluded_owners(
15143 pending_hash_input_record::<N>(hash, InputSource::Poll),
15144 excluded,
15145 );
15146 }
15147 }
15148 SubscriberEvent::PreconfirmedLogs { flashblock, logs } => {
15149 for log in logs {
15150 let record = self
15151 .with_chain_id(preconfirmed_log_input_record::<N>(log, flashblock.clone()));
15152 self.push_pending_record(SubscriberInputRecord {
15153 record,
15154 scope: SubscriberInputScope::Preconfirmed,
15155 });
15156 }
15157 }
15158 SubscriberEvent::BasePendingLog { .. }
15159 | SubscriberEvent::BaseFlashblock(_)
15160 | SubscriberEvent::OpFlashblockTick
15161 | SubscriberEvent::FlashblockObserved => {}
15162 SubscriberEvent::StreamTerminated(_) => {}
15163 }
15164 }
15165
15166 fn enqueue_backfilled_logs(
15167 &mut self,
15168 logs: Vec<Log>,
15169 source_id: Option<usize>,
15170 owner: Option<&SubscriberOwnerEpoch>,
15171 range: Option<SubscriberBackfill>,
15172 ) {
15173 self.enqueue_backfilled_logs_with_excluded_owners(logs, source_id, owner, range, None);
15174 }
15175
15176 fn enqueue_backfilled_logs_with_excluded_owners(
15177 &mut self,
15178 logs: Vec<Log>,
15179 source_id: Option<usize>,
15180 owner: Option<&SubscriberOwnerEpoch>,
15181 range: Option<SubscriberBackfill>,
15182 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
15183 ) {
15184 for log in logs {
15185 if range.as_ref().is_some_and(|range| {
15186 log.block_number.is_some_and(|block| {
15187 block < range.start_block() || range.end_block().is_some_and(|end| block > end)
15188 })
15189 }) {
15190 continue;
15191 }
15192 let matches = match owner {
15193 Some(epoch) => self
15194 .owned_interests
15195 .iter()
15196 .find(|entry| entry.epoch.as_ref() == Some(epoch))
15197 .is_some_and(|entry| log_matches_any_interest(&log, &entry.interests)),
15198 None => log_matches_any_interest(&log, &self.interests),
15199 };
15200 if matches {
15201 let record = log_input_record(log, InputSource::Backfill);
15202 if let Some(epoch) = owner {
15203 self.enqueue_owner_record(record, epoch.clone());
15204 } else {
15205 if let Some(source_id) = source_id {
15206 self.note_log_block(source_id, &record);
15207 }
15208 self.enqueue_record_with_excluded_owners(record, excluded);
15209 }
15210 }
15211 }
15212 }
15213
15214 fn enqueue_compat_owner_backfilled_logs(
15215 &mut self,
15216 logs: Vec<Log>,
15217 owner: &HandlerId,
15218 range: SubscriberBackfill,
15219 ) {
15220 let interests = self
15221 .owned_interests
15222 .iter()
15223 .find(|entry| {
15224 &entry.owner == owner
15225 && entry.epoch.is_none()
15226 && entry.state == SubscriberOwnerState::Active
15227 })
15228 .map(|entry| entry.interests.clone());
15229 let Some(interests) = interests else {
15230 return;
15231 };
15232 for log in logs {
15233 if log.block_number.is_some_and(|block| {
15234 block < range.start_block() || range.end_block().is_some_and(|end| block > end)
15235 }) || !log_matches_any_interest(&log, &interests)
15236 {
15237 continue;
15238 }
15239 let record = log_input_record(log, InputSource::Backfill);
15240 self.enqueue_compat_owner_record(record, owner.clone());
15241 }
15242 }
15243
15244 async fn reconnect_source_stream(
15245 &mut self,
15246 source: SubscriberStreamSource,
15247 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
15248 if !source.is_pubsub() {
15249 return Err(stream_terminated_error(&source));
15250 }
15251
15252 if !self.config.reconnect.enabled {
15253 return Err(SubscriberError::Provider(format!(
15254 "Alloy subscriber {} stream terminated and reconnect is disabled",
15255 source.label()
15256 )));
15257 }
15258
15259 let mut attempts = 0usize;
15260 let mut delay = self.config.reconnect.initial_delay;
15261 let mut retry_delay = self.config.reconnect.retry_delay;
15262
15263 loop {
15264 attempts = attempts.saturating_add(1);
15265 if !delay.is_zero() {
15266 tokio::time::sleep(delay).await;
15267 }
15268
15269 match self.reconnect_source_once(source.clone()).await {
15270 Ok(backfill_event) => return Ok(backfill_event),
15271 Err(error) if reconnect_attempts_exhausted(attempts, &self.config.reconnect) => {
15272 return Err(SubscriberError::Provider(format!(
15273 "Alloy subscriber {} stream terminated and reconnect failed after {attempts} attempt(s): {error}",
15274 source.label()
15275 )));
15276 }
15277 Err(error) => {
15278 tracing::warn!(
15279 stream = source.label(),
15280 attempts,
15281 error = %error,
15282 "Alloy subscriber reconnect attempt failed"
15283 );
15284 delay = retry_delay;
15285 retry_delay =
15286 next_reconnect_delay(retry_delay, self.config.reconnect.max_delay);
15287 }
15288 }
15289 }
15290 }
15291
15292 async fn reconnect_source_once(
15293 &mut self,
15294 source: SubscriberStreamSource,
15295 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
15296 if matches!(
15297 &self.state,
15298 AlloySubscriberState::Active(streams) if streams.contains_source(&source)
15299 ) {
15300 let backfill_event = self.backfill_reconnected_source(&source).await?;
15304 self.pending_source_backfills
15305 .retain(|pending| !pending.same_key(&source));
15306 return Ok(backfill_event);
15307 }
15308 let stream = self.connect_source_stream(source.clone()).await?;
15309 if !matches!(self.state, AlloySubscriberState::Active(_)) {
15310 return Err(SubscriberError::Provider(
15311 "Alloy subscriber state changed before reconnect completed".to_owned(),
15312 ));
15313 }
15314 self.install_source_stream(source.clone(), stream);
15315 if self.source_requires_backfill(&source) {
15316 self.queue_source_backfill(source.clone());
15317 }
15318 let backfill_event = self.backfill_reconnected_source(&source).await?;
15319 self.pending_source_backfills
15320 .retain(|pending| !pending.same_key(&source));
15321
15322 Ok(backfill_event)
15323 }
15324
15325 async fn backfill_reconnected_source(
15326 &mut self,
15327 source: &SubscriberStreamSource,
15328 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
15329 if source.is_flashblocks() {
15330 return self.fetch_pending_flashblock().await;
15331 }
15332 let SubscriberStreamSource::PubSubLog { id, filter } = source else {
15333 return Ok(None);
15334 };
15335 let Some(from_block) = self.last_seen_log_blocks.get(id).copied() else {
15336 return Ok(None);
15337 };
15338
15339 let latest = self
15340 .provider
15341 .get_block_number()
15342 .await
15343 .map_err(provider_error)?;
15344 if latest < from_block {
15345 return Ok(None);
15346 }
15347
15348 let logs = self
15349 .provider
15350 .get_logs(&filter.clone().from_block(from_block).to_block(latest))
15351 .await
15352 .map_err(provider_error)?;
15353 Ok(Some(SubscriberEvent::BackfilledLogs {
15354 source_id: *id,
15355 logs,
15356 }))
15357 }
15358
15359 fn note_log_block(&mut self, source_id: usize, record: &ReactiveInputRecord<N>) {
15360 if let Some(block) = record.context.block.as_ref() {
15361 self.last_seen_log_blocks.insert(source_id, block.number);
15362 }
15363 }
15364
15365 fn enqueue_record_with_excluded_owners(
15366 &mut self,
15367 record: ReactiveInputRecord<N>,
15368 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
15369 ) {
15370 let record = self.with_chain_id(record);
15371 let mut owners = self.staged_owners_for_record(&record);
15372 if let Some(excluded) = excluded {
15373 owners.retain(|owner| !excluded.contains(owner));
15374 }
15375 let canonical_duplicate = self.should_skip_recent_duplicate(&record);
15376 let owners = self.filter_recent_owner_duplicates(&record, owners);
15377 let compatibility_owners = self.compatibility_owners_for_record(&record);
15378 let (already_served, newly_served): (Vec<_>, Vec<_>) = compatibility_owners
15379 .into_iter()
15380 .partition(|owner| self.compatibility_owner_has_seen(&record, owner));
15381 if canonical_duplicate {
15382 if !owners.is_empty() {
15383 self.push_pending_record(SubscriberInputRecord {
15384 record: record.clone(),
15385 scope: SubscriberInputScope::OwnerOnly { owners },
15386 });
15387 }
15388 if !newly_served.is_empty() {
15389 for owner in &newly_served {
15390 self.remember_compatibility_owner_record(&record, owner);
15391 }
15392 self.push_pending_record(SubscriberInputRecord {
15393 record,
15394 scope: SubscriberInputScope::OwnerOnlyHandlers {
15395 owners: newly_served,
15396 },
15397 });
15398 }
15399 return;
15400 }
15401 self.remember_record(&record);
15402 for owner in already_served.iter().chain(&newly_served) {
15403 self.remember_compatibility_owner_record(&record, owner);
15404 }
15405 self.push_pending_record(SubscriberInputRecord {
15406 record,
15407 scope: if already_served.is_empty() {
15408 SubscriberInputScope::Canonical { owners }
15409 } else {
15410 SubscriberInputScope::CanonicalResidual {
15411 owners,
15412 excluded: already_served,
15413 }
15414 },
15415 });
15416 }
15417
15418 fn enqueue_compat_owner_record(&mut self, record: ReactiveInputRecord<N>, owner: HandlerId) {
15419 let record = self.with_chain_id(record);
15420 if self.compatibility_owner_has_seen(&record, &owner) {
15421 return;
15422 }
15423 self.remember_compatibility_owner_record(&record, &owner);
15424 self.push_pending_record(SubscriberInputRecord {
15425 record,
15426 scope: SubscriberInputScope::OwnerOnlyHandlers {
15427 owners: vec![owner],
15428 },
15429 });
15430 }
15431
15432 fn compatibility_owners_for_record(&self, record: &ReactiveInputRecord<N>) -> Vec<HandlerId> {
15433 self.owned_interests
15434 .iter()
15435 .filter(|entry| entry.epoch.is_none() && entry.state == SubscriberOwnerState::Active)
15436 .filter(|entry| {
15437 entry
15438 .interests
15439 .iter()
15440 .any(|interest| interest_matches(interest, &record.input))
15441 })
15442 .map(|entry| entry.owner.clone())
15443 .collect()
15444 }
15445
15446 fn compatibility_owner_has_seen(
15447 &self,
15448 record: &ReactiveInputRecord<N>,
15449 owner: &HandlerId,
15450 ) -> bool {
15451 should_dedupe_record(record)
15452 && self
15453 .recent_compat_owner_input_ref_sets
15454 .get(owner)
15455 .is_some_and(|seen| seen.contains(&record.input_ref()))
15456 }
15457
15458 fn remember_compatibility_owner_record(
15459 &mut self,
15460 record: &ReactiveInputRecord<N>,
15461 owner: &HandlerId,
15462 ) {
15463 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
15464 return;
15465 }
15466 let input_ref = record.input_ref();
15467 let seen = self
15468 .recent_compat_owner_input_ref_sets
15469 .entry(owner.clone())
15470 .or_default();
15471 if !seen.insert(input_ref) {
15472 return;
15473 }
15474 let recent = self
15475 .recent_compat_owner_input_refs
15476 .entry(owner.clone())
15477 .or_default();
15478 recent.push_back(input_ref);
15479 while recent.len() > self.config.reconnect.dedupe_window {
15480 if let Some(evicted) = recent.pop_front() {
15481 seen.remove(&evicted);
15482 }
15483 }
15484 }
15485
15486 fn enqueue_owner_record(
15487 &mut self,
15488 record: ReactiveInputRecord<N>,
15489 owner: SubscriberOwnerEpoch,
15490 ) {
15491 self.enqueue_owner_record_for_owners(record, vec![owner]);
15492 }
15493
15494 fn enqueue_owner_record_for_owners(
15495 &mut self,
15496 record: ReactiveInputRecord<N>,
15497 owners: Vec<SubscriberOwnerEpoch>,
15498 ) {
15499 self.enqueue_owner_record_for_owners_inner(record, owners, true);
15500 }
15501
15502 fn enqueue_owner_record_for_owners_unmerged(
15503 &mut self,
15504 record: ReactiveInputRecord<N>,
15505 owners: Vec<SubscriberOwnerEpoch>,
15506 ) {
15507 self.enqueue_owner_record_for_owners_inner(record, owners, false);
15508 }
15509
15510 fn enqueue_owner_record_for_owners_inner(
15511 &mut self,
15512 record: ReactiveInputRecord<N>,
15513 owners: Vec<SubscriberOwnerEpoch>,
15514 merge_pending: bool,
15515 ) {
15516 let record = self.with_chain_id(record);
15517 let owners = self.filter_recent_owner_duplicates(&record, owners);
15518 if owners.is_empty() {
15519 return;
15520 }
15521 if merge_pending
15522 && should_dedupe_record(&record)
15523 && self.config.reconnect.dedupe_window != 0
15524 {
15525 let input_ref = record.input_ref();
15526 if let Some(pending) = self
15527 .pending_records
15528 .iter_mut()
15529 .rev()
15530 .find(|pending| pending.record.input_ref() == input_ref)
15531 {
15532 let pending_owners = match &mut pending.scope {
15533 SubscriberInputScope::Canonical { owners }
15534 | SubscriberInputScope::CanonicalResidual { owners, .. }
15535 | SubscriberInputScope::OwnerOnly { owners } => Some(owners),
15536 SubscriberInputScope::OwnerOnlyHandlers { .. }
15537 | SubscriberInputScope::Preconfirmed => None,
15538 };
15539 if let Some(pending_owners) = pending_owners {
15540 for owner in owners {
15541 if !pending_owners.contains(&owner) {
15542 pending_owners.push(owner);
15543 }
15544 }
15545 return;
15546 }
15547 }
15548 }
15549 self.push_pending_record(SubscriberInputRecord {
15550 record,
15551 scope: SubscriberInputScope::OwnerOnly { owners },
15552 });
15553 }
15554
15555 fn push_pending_record(&mut self, record: SubscriberInputRecord<N>) {
15556 if self.pending_record_count() >= self.config.max_pending_records {
15557 self.note_resource_error(format!(
15558 "pending record queues reached the configured limit of {}",
15559 self.config.max_pending_records
15560 ));
15561 return;
15562 }
15563 self.pending_records.push_back(record);
15564 }
15565
15566 fn ensure_pending_record_capacity(
15567 &mut self,
15568 additional: usize,
15569 operation: &str,
15570 ) -> Result<(), SubscriberError> {
15571 let required = self.pending_record_count().saturating_add(additional);
15572 if required > self.config.max_pending_records {
15573 self.note_resource_error(format!(
15574 "{operation} require {required} pending records, above the configured limit of {}",
15575 self.config.max_pending_records
15576 ));
15577 return self.check_resource_error();
15578 }
15579 Ok(())
15580 }
15581
15582 fn push_pending_reconcile_record(&mut self, record: BufferedSubscriberOwnerRecord<N>) {
15583 if self.pending_record_count() >= self.config.max_pending_records {
15584 self.note_resource_error(format!(
15585 "pending record queues reached the configured limit of {}",
15586 self.config.max_pending_records
15587 ));
15588 return;
15589 }
15590 self.pending_reconcile_owner_records.push_back(record);
15591 }
15592
15593 fn pending_record_count(&self) -> usize {
15594 self.pending_records
15595 .len()
15596 .saturating_add(self.pending_reconcile_owner_records.len())
15597 }
15598
15599 fn note_resource_error(&mut self, message: String) {
15600 if self.resource_error.is_none() {
15601 self.resource_error = Some(message);
15602 }
15603 }
15604
15605 fn check_resource_error(&self) -> Result<(), SubscriberError> {
15606 match &self.resource_error {
15607 Some(message) => Err(SubscriberError::ResourceExhausted(message.clone())),
15608 None => Ok(()),
15609 }
15610 }
15611
15612 fn with_chain_id(&self, mut record: ReactiveInputRecord<N>) -> ReactiveInputRecord<N> {
15613 record.context.chain_id = self.chain_id;
15614 if self.config.verify_log_block_context
15615 && let ReactiveInput::Log(log) = &record.input
15616 && !log.removed
15617 && let (Some(number), Some(hash)) = (log.block_number, log.block_hash)
15618 && let Some(verified) = self.verified_log_blocks.get(&(number, hash)).copied()
15619 {
15620 record.context.block = Some(verified);
15621 record.context.chain_status = ChainStatus::Included {
15622 block: verified,
15623 confirmations: 0,
15624 };
15625 }
15626 record
15627 }
15628
15629 fn staged_owners_for_record(
15630 &self,
15631 record: &ReactiveInputRecord<N>,
15632 ) -> Vec<SubscriberOwnerEpoch> {
15633 self.owned_interests
15634 .iter()
15635 .filter(|entry| entry.state == SubscriberOwnerState::Staged)
15636 .filter(|entry| {
15637 entry
15638 .interests
15639 .iter()
15640 .any(|interest| interest_matches(interest, &record.input))
15641 })
15642 .filter_map(|entry| entry.epoch.clone())
15643 .collect()
15644 }
15645
15646 fn filter_recent_owner_duplicates(
15647 &mut self,
15648 record: &ReactiveInputRecord<N>,
15649 owners: Vec<SubscriberOwnerEpoch>,
15650 ) -> Vec<SubscriberOwnerEpoch> {
15651 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
15652 return owners;
15653 }
15654 let input_ref = record.input_ref();
15655 let window = self.config.reconnect.dedupe_window;
15656 owners
15657 .into_iter()
15658 .filter(|owner| {
15659 let seen = self
15660 .recent_owner_input_ref_sets
15661 .entry(owner.clone())
15662 .or_default();
15663 if !seen.insert(input_ref) {
15664 return false;
15665 }
15666 let recent = self
15667 .recent_owner_input_refs
15668 .entry(owner.clone())
15669 .or_default();
15670 recent.push_back(input_ref);
15671 while recent.len() > window {
15672 if let Some(evicted) = recent.pop_front() {
15673 seen.remove(&evicted);
15674 }
15675 }
15676 true
15677 })
15678 .collect()
15679 }
15680
15681 fn should_skip_recent_duplicate(&self, record: &ReactiveInputRecord<N>) -> bool {
15682 if !should_dedupe_record(record) {
15683 return false;
15684 }
15685 self.recent_input_ref_set.contains(&record.input_ref())
15686 }
15687
15688 fn remember_record(&mut self, record: &ReactiveInputRecord<N>) {
15689 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
15690 return;
15691 }
15692
15693 let input_ref = record.input_ref();
15694 if !self.recent_input_ref_set.insert(input_ref) {
15695 return;
15696 }
15697 self.recent_input_refs.push_back(input_ref);
15698
15699 while self.recent_input_refs.len() > self.config.reconnect.dedupe_window {
15700 if let Some(evicted) = self.recent_input_refs.pop_front() {
15701 self.recent_input_ref_set.remove(&evicted);
15702 }
15703 }
15704 }
15705}
15706
15707fn stream_with_termination<N, S>(
15708 stream: S,
15709 source: SubscriberStreamSource,
15710) -> BoxStream<'static, SubscriberEvent<N>>
15711where
15712 N: Network + 'static,
15713 S: futures::Stream<Item = SubscriberEvent<N>> + Send + 'static,
15714{
15715 stream
15716 .chain(stream::once(async move {
15717 SubscriberEvent::StreamTerminated(source)
15718 }))
15719 .boxed()
15720}
15721
15722fn aggregate_interests<N: Network>(
15723 base: &[ReactiveInterest<N>],
15724 owned: &[OwnedSubscriberInterests<N>],
15725) -> Vec<ReactiveInterest<N>> {
15726 base.iter()
15727 .cloned()
15728 .chain(
15729 owned
15730 .iter()
15731 .flat_map(|entry| entry.interests.iter().cloned()),
15732 )
15733 .collect()
15734}
15735
15736fn stream_terminated_error(source: &SubscriberStreamSource) -> SubscriberError {
15737 SubscriberError::Provider(format!(
15738 "Alloy subscriber {} stream terminated before the subscriber was stopped",
15739 source.label()
15740 ))
15741}
15742
15743fn reconnect_attempts_exhausted(attempts: usize, config: &SubscriberReconnectConfig) -> bool {
15744 config
15745 .max_attempts
15746 .is_some_and(|max_attempts| attempts >= max_attempts)
15747}
15748
15749fn next_reconnect_delay(current: Duration, max: Duration) -> Duration {
15750 if current.is_zero() {
15751 return current;
15752 }
15753 current.checked_mul(2).unwrap_or(max).min(max)
15754}
15755
15756fn should_dedupe_record<N: Network>(record: &ReactiveInputRecord<N>) -> bool {
15757 match &record.input {
15758 ReactiveInput::Log(log) => {
15759 is_canonical_status(&record.context.chain_status) && !log.removed
15760 }
15761 ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_) => true,
15762 ReactiveInput::FullBlock(_) | ReactiveInput::PendingTx(_) => false,
15763 }
15764}
15765
15766#[cfg(test)]
15767mod subscriber_helper_tests {
15768 use super::*;
15769 use alloy_provider::ProviderBuilder;
15770 use alloy_transport::mock::Asserter;
15771
15772 #[test]
15773 fn base_flashblock_wire_decodes_cumulative_block_shape() {
15774 let payload: BaseFlashblockWirePayload = serde_json::from_str(
15775 r#"{
15776 "hash":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
15777 "number":"0x2ef403b",
15778 "parentHash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
15779 "stateRoot":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
15780 "timestamp":"0x6a68dd59",
15781 "transactions":[]
15782 }"#,
15783 )
15784 .expect("decode current Base newFlashblocks shape");
15785 let BaseFlashblockWirePayload::Block(payload) = payload else {
15786 panic!("expected cumulative block-shaped payload")
15787 };
15788 assert_eq!(payload.number, 49_233_979);
15789 assert_eq!(payload.timestamp, 1_785_257_305);
15790 assert_eq!(payload.hash, B256::repeat_byte(0xaa));
15791 assert_eq!(payload.parent_hash, B256::repeat_byte(0xbb));
15792 assert_eq!(payload.state_root, B256::repeat_byte(0xcc));
15793 }
15794
15795 #[test]
15796 fn unproven_parent_replacement_rewind_discards_every_unauthenticated_identity() {
15797 let parent = BlockRef {
15798 number: 79,
15799 hash: B256::repeat_byte(0x79),
15800 parent_hash: Some(B256::repeat_byte(0x78)),
15801 timestamp: Some(1_700_000_079),
15802 };
15803 let old_tip = BlockRef {
15804 number: 80,
15805 hash: B256::repeat_byte(0x80),
15806 parent_hash: Some(parent.hash),
15807 timestamp: Some(1_700_000_080),
15808 };
15809 let replacement = BlockRef {
15810 hash: B256::repeat_byte(0xe0),
15811 parent_hash: Some(B256::repeat_byte(0xdf)),
15812 ..old_tip
15813 };
15814 let mut state =
15815 CanonicalSequenceState::new(vec![parent, old_tip], Some(old_tip), Some(parent), None);
15816
15817 let rewind = apply_sequence_canonical_block(&mut state, &replacement, false)
15818 .expect("replacement metadata is structurally valid")
15819 .expect("unknown parent is an observable rewind");
15820
15821 assert_eq!(rewind.common_ancestor, None);
15822 assert_eq!(rewind.dropped, vec![parent, old_tip]);
15823 assert_eq!(state.retained_canonical_history(), &[replacement]);
15824 assert_eq!(state.coverage_head(), Some(&replacement));
15825 assert_eq!(state.safe_head(), None);
15826 assert_eq!(state.finalized_head(), None);
15827 }
15828
15829 #[test]
15830 fn handler_ids_are_non_empty_across_construction_and_deserialization() {
15831 assert_eq!(HandlerId::try_new("").unwrap_err(), HandlerIdError);
15832 let valid = HandlerId::try_new("owner-1").expect("non-empty id");
15833 let encoded = serde_json::to_string(&valid).expect("serialize id");
15834 assert_eq!(
15835 serde_json::from_str::<HandlerId>(&encoded).expect("deserialize valid id"),
15836 valid
15837 );
15838 assert!(serde_json::from_str::<HandlerId>(r#"""#).is_err());
15839 }
15840
15841 fn rpc_log(removed: bool) -> Log {
15842 Log {
15843 inner: alloy_primitives::Log::new_unchecked(
15844 Address::repeat_byte(0x42),
15845 vec![B256::repeat_byte(0x01)],
15846 Bytes::new(),
15847 ),
15848 block_hash: Some(B256::repeat_byte(0x02)),
15849 block_number: Some(7),
15850 block_timestamp: Some(1_700_000_000),
15851 transaction_hash: Some(B256::repeat_byte(0x03)),
15852 transaction_index: Some(4),
15853 log_index: Some(5),
15854 removed,
15855 }
15856 }
15857
15858 fn rpc_transaction(chain_id: Option<u64>) -> alloy_rpc_types_eth::Transaction {
15859 use alloy_consensus::SignableTransaction as _;
15860
15861 let envelope: alloy_consensus::TxEnvelope = alloy_consensus::TxLegacy {
15862 chain_id,
15863 ..Default::default()
15864 }
15865 .into_signed(alloy_primitives::Signature::test_signature())
15866 .into();
15867 alloy_rpc_types_eth::Transaction {
15868 inner: alloy_consensus::transaction::Recovered::new_unchecked(envelope, Address::ZERO),
15869 block_hash: None,
15870 block_number: None,
15871 transaction_index: None,
15872 effective_gas_price: None,
15873 }
15874 }
15875
15876 #[cfg(feature = "reactive-ws")]
15877 fn rpc_log_at(block_number: u64, transaction_index: u64, log_index: u64) -> Log {
15878 Log {
15879 inner: alloy_primitives::Log::new_unchecked(
15880 Address::repeat_byte(0x42),
15881 vec![B256::repeat_byte(0x01)],
15882 Bytes::new(),
15883 ),
15884 block_hash: Some(B256::repeat_byte(block_number as u8)),
15885 block_number: Some(block_number),
15886 block_timestamp: Some(1_700_000_000 + block_number),
15887 transaction_hash: Some(B256::repeat_byte(0x20 + transaction_index as u8)),
15888 transaction_index: Some(transaction_index),
15889 log_index: Some(log_index),
15890 removed: false,
15891 }
15892 }
15893
15894 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
15895 fn rpc_block(number: u64, hash: B256) -> alloy_rpc_types_eth::Block {
15896 alloy_rpc_types_eth::Block::empty(alloy_rpc_types_eth::Header {
15897 hash,
15898 inner: alloy_consensus::Header {
15899 number,
15900 parent_hash: B256::repeat_byte(number.saturating_sub(1) as u8),
15901 timestamp: 1_700_000_000 + number,
15902 ..Default::default()
15903 },
15904 total_difficulty: None,
15905 size: None,
15906 })
15907 }
15908
15909 #[tokio::test(flavor = "multi_thread")]
15910 #[cfg(feature = "reactive-ws")]
15911 async fn verified_log_context_fetches_and_caches_exact_parent_identity() {
15912 let stream_asserter = Asserter::new();
15913 let provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
15914 let verification_asserter = Asserter::new();
15915 verification_asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(7))));
15916 let verification_provider =
15917 ProviderBuilder::new().connect_mocked_client(verification_asserter.clone());
15918 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
15919 provider,
15920 SubscriberMode::PubSub,
15921 SubscriberConfig {
15922 verify_log_block_context: true,
15923 ..SubscriberConfig::default()
15924 },
15925 )
15926 .with_log_verification_provider(verification_provider);
15927 let log = rpc_log_at(7, 0, 0);
15928
15929 subscriber
15930 .verify_log_block_context(&log)
15931 .await
15932 .expect("verify live log block");
15933 subscriber
15934 .verify_log_block_context(&log)
15935 .await
15936 .expect("reuse verified block cache");
15937 let record = subscriber.with_chain_id(log_input_record(log, InputSource::Subscription));
15938
15939 assert_eq!(
15940 record.context.block.expect("verified block").parent_hash,
15941 Some(B256::repeat_byte(6))
15942 );
15943 assert!(
15944 verification_asserter.read_q().is_empty(),
15945 "one provider lookup should verify every log in the same block"
15946 );
15947 assert!(
15948 stream_asserter.read_q().is_empty(),
15949 "verification must not use the high-volume stream provider"
15950 );
15951 }
15952
15953 #[tokio::test(flavor = "multi_thread")]
15954 async fn stream_with_termination_yields_terminal_source_marker() {
15955 let mut stream = stream_with_termination::<Ethereum, _>(
15956 stream::iter([SubscriberEvent::<Ethereum>::PendingHash(B256::repeat_byte(
15957 0xaa,
15958 ))]),
15959 SubscriberStreamSource::PubSubPendingHashes,
15960 );
15961
15962 assert!(matches!(
15963 stream.next().await,
15964 Some(SubscriberEvent::PendingHash(hash)) if hash == B256::repeat_byte(0xaa)
15965 ));
15966 assert!(matches!(
15967 stream.next().await,
15968 Some(SubscriberEvent::StreamTerminated(source)) if source.is_pubsub()
15969 ));
15970 assert!(stream.next().await.is_none());
15971 }
15972
15973 #[test]
15974 fn reconnect_delay_doubles_until_capped() {
15975 assert_eq!(
15976 next_reconnect_delay(Duration::from_millis(250), Duration::from_secs(1)),
15977 Duration::from_millis(500)
15978 );
15979 assert_eq!(
15980 next_reconnect_delay(Duration::from_millis(750), Duration::from_secs(1)),
15981 Duration::from_secs(1)
15982 );
15983 assert_eq!(
15984 next_reconnect_delay(Duration::ZERO, Duration::from_secs(1)),
15985 Duration::ZERO
15986 );
15987 }
15988
15989 #[test]
15990 fn canonical_logs_are_deduped_but_removed_logs_are_not() {
15991 let included = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
15992 let removed = log_input_record::<Ethereum>(rpc_log(true), InputSource::Subscription);
15993
15994 assert!(should_dedupe_record(&included));
15995 assert!(!should_dedupe_record(&removed));
15996 }
15997
15998 #[test]
15999 fn owner_reconcile_dedupe_rejects_conflicts_and_preserves_compatible_enrichment() {
16000 let set_context_timestamp = |record: &mut ReactiveInputRecord<Ethereum>,
16001 timestamp: Option<u64>| {
16002 record.context.block.as_mut().expect("block").timestamp = timestamp;
16003 match &mut record.context.chain_status {
16004 ChainStatus::Included { block, .. }
16005 | ChainStatus::Safe { block }
16006 | ChainStatus::Finalized { block }
16007 | ChainStatus::Reorged {
16008 dropped_from: block,
16009 } => block.timestamp = timestamp,
16010 ChainStatus::Pending | ChainStatus::Preconfirmed { .. } => {
16011 panic!("log record is canonical")
16012 }
16013 }
16014 };
16015
16016 let mut payload_only = log_input_record::<Ethereum>(rpc_log(false), InputSource::Backfill);
16017 let payload_timestamp = match &payload_only.input {
16018 ReactiveInput::Log(log) => log.block_timestamp.expect("timestamp"),
16019 _ => unreachable!(),
16020 };
16021 set_context_timestamp(&mut payload_only, None);
16022 let mut context_only = payload_only.clone();
16023 if let ReactiveInput::Log(log) = &mut context_only.input {
16024 log.block_timestamp = None;
16025 }
16026 set_context_timestamp(&mut context_only, Some(payload_timestamp + 1));
16027 assert!(matches!(
16028 dedupe_records(vec![payload_only, context_only]),
16029 Err(ReactiveError::InvalidInputRecord { .. })
16030 ));
16031
16032 let mut partial = log_input_record::<Ethereum>(rpc_log(false), InputSource::Backfill);
16033 if let ReactiveInput::Log(log) = &mut partial.input {
16034 log.block_timestamp = None;
16035 }
16036 set_context_timestamp(&mut partial, None);
16037 let complete = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
16038 let deduped =
16039 dedupe_records(vec![partial, complete]).expect("compatible metadata enriches");
16040 assert_eq!(deduped.len(), 1);
16041 deduped[0]
16042 .validated_identity()
16043 .expect("merged record remains coherent");
16044 let resolved = resolve_record_block_payload_metadata(
16045 &deduped[0],
16046 *canonical_record_block(&deduped[0]).expect("canonical"),
16047 )
16048 .expect("effective block");
16049 assert_eq!(resolved.timestamp, Some(payload_timestamp));
16050 }
16051
16052 #[test]
16053 fn full_block_bodies_are_never_suppressed_from_header_hash_alone() {
16054 use alloy_rpc_types_eth::{Block, Header};
16055
16056 let block_ref = BlockRef {
16057 number: 7,
16058 hash: B256::repeat_byte(0x77),
16059 parent_hash: Some(B256::repeat_byte(0x66)),
16060 timestamp: Some(1_700_000_007),
16061 };
16062 let block = Block::empty(Header {
16063 hash: block_ref.hash,
16064 inner: alloy_consensus::Header {
16065 number: block_ref.number,
16066 parent_hash: block_ref.parent_hash.expect("parent"),
16067 timestamp: block_ref.timestamp.expect("timestamp"),
16068 ..Default::default()
16069 },
16070 total_difficulty: None,
16071 size: None,
16072 });
16073 let record = ReactiveInputRecord::<Ethereum>::new(
16074 ReactiveInput::FullBlock(block),
16075 ReactiveContext {
16076 chain_id: Some(1),
16077 source: InputSource::Subscription,
16078 chain_status: ChainStatus::Included {
16079 block: block_ref,
16080 confirmations: 0,
16081 },
16082 block: Some(block_ref),
16083 transaction_index: None,
16084 log_index: None,
16085 },
16086 );
16087
16088 assert!(!record.is_payload_deduplicable());
16089 assert!(!record.same_deduplicable_payload(&record));
16090 let retained = dedupe_scoped_records(vec![
16091 (
16092 record.clone(),
16093 DeliveryAudience::All,
16094 DeliveryScope::Canonical,
16095 ),
16096 (record, DeliveryAudience::All, DeliveryScope::Canonical),
16097 ])
16098 .expect("non-deduplicable bodies are preserved, not treated as conflicts");
16099 assert_eq!(retained.len(), 2);
16100 }
16101
16102 #[test]
16103 fn hydrated_transaction_wrappers_reject_inclusion_and_chain_identity_conflicts() {
16104 let pending_context = ReactiveContext {
16105 chain_id: Some(1),
16106 source: InputSource::Batch,
16107 chain_status: ChainStatus::Pending,
16108 block: None,
16109 transaction_index: None,
16110 log_index: None,
16111 };
16112 let mut included_pending = rpc_transaction(Some(1));
16113 included_pending.block_hash = Some(B256::repeat_byte(0xaa));
16114 assert!(matches!(
16115 ReactiveInputRecord::<Ethereum>::new(
16116 ReactiveInput::PendingTx(included_pending),
16117 pending_context.clone(),
16118 )
16119 .validated_identity(),
16120 Err(ReactiveError::InvalidInputRecord { .. })
16121 ));
16122 assert!(matches!(
16123 ReactiveInputRecord::<Ethereum>::new(
16124 ReactiveInput::PendingTx(rpc_transaction(Some(2))),
16125 pending_context,
16126 )
16127 .validated_identity(),
16128 Err(ReactiveError::InvalidInputRecord { .. })
16129 ));
16130
16131 let block_ref = BlockRef {
16132 number: 8,
16133 hash: B256::repeat_byte(0x88),
16134 parent_hash: Some(B256::repeat_byte(0x77)),
16135 timestamp: Some(1_700_000_008),
16136 };
16137 let header = alloy_rpc_types_eth::Header {
16138 hash: block_ref.hash,
16139 inner: alloy_consensus::Header {
16140 number: block_ref.number,
16141 parent_hash: block_ref.parent_hash.expect("parent"),
16142 timestamp: block_ref.timestamp.expect("timestamp"),
16143 ..Default::default()
16144 },
16145 total_difficulty: None,
16146 size: None,
16147 };
16148 let context = ReactiveContext {
16149 chain_id: Some(1),
16150 source: InputSource::Batch,
16151 chain_status: ChainStatus::Included {
16152 block: block_ref,
16153 confirmations: 0,
16154 },
16155 block: Some(block_ref),
16156 transaction_index: None,
16157 log_index: None,
16158 };
16159 for transaction in [
16160 alloy_rpc_types_eth::Transaction {
16161 block_hash: Some(B256::repeat_byte(0xff)),
16162 ..rpc_transaction(Some(1))
16163 },
16164 alloy_rpc_types_eth::Transaction {
16165 block_hash: Some(block_ref.hash),
16166 block_number: Some(block_ref.number),
16167 transaction_index: Some(1),
16168 ..rpc_transaction(Some(1))
16169 },
16170 rpc_transaction(Some(2)),
16171 ] {
16172 let block = alloy_rpc_types_eth::Block::new(
16173 header.clone(),
16174 alloy_network::primitives::BlockTransactions::Full(vec![transaction]),
16175 );
16176 assert!(matches!(
16177 ReactiveInputRecord::<Ethereum>::new(
16178 ReactiveInput::FullBlock(block),
16179 context.clone(),
16180 )
16181 .validated_identity(),
16182 Err(ReactiveError::InvalidInputRecord { .. })
16183 ));
16184 }
16185 }
16186
16187 #[test]
16188 fn compatibility_owner_backfill_and_live_overlap_split_exact_audiences() {
16189 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
16190 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
16191 provider,
16192 SubscriberMode::Auto,
16193 SubscriberConfig::default(),
16194 );
16195 let owner = HandlerId::new("compat-owner");
16196 subscriber
16197 .add_interest_owner(
16198 owner.clone(),
16199 &[ReactiveInterest::Logs(LogInterest {
16200 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
16201 local_matcher: None,
16202 route_key: None,
16203 })],
16204 )
16205 .unwrap();
16206 let log = rpc_log(false);
16207
16208 subscriber.enqueue_compat_owner_record(
16209 log_input_record(log.clone(), InputSource::Backfill),
16210 owner.clone(),
16211 );
16212 subscriber.enqueue_event(SubscriberEvent::Log { source_id: 0, log });
16213
16214 let batch = subscriber
16215 .drain_next_scoped_batch()
16216 .expect("owner catch-up and residual live copies");
16217 assert_eq!(batch.records.len(), 2);
16218 assert_eq!(
16219 batch.records[0].scope,
16220 SubscriberInputScope::OwnerOnlyHandlers {
16221 owners: vec![owner.clone()]
16222 }
16223 );
16224 assert_eq!(
16225 batch.records[1].scope,
16226 SubscriberInputScope::CanonicalResidual {
16227 owners: Vec::new(),
16228 excluded: vec![owner.clone()]
16229 }
16230 );
16231
16232 let reactive = batch.into_reactive_batch();
16233 assert_eq!(
16234 reactive.record_audience(0),
16235 Some(&DeliveryAudience::Owners(vec![owner.clone()]))
16236 );
16237 assert_eq!(
16238 reactive.record_delivery_scope(0),
16239 Some(DeliveryScope::OwnerCatchup)
16240 );
16241 assert_eq!(
16242 reactive.record_audience(1),
16243 Some(&DeliveryAudience::AllExcept(vec![owner]))
16244 );
16245 assert_eq!(
16246 reactive.record_delivery_scope(1),
16247 Some(DeliveryScope::Canonical)
16248 );
16249 }
16250
16251 #[test]
16252 fn active_owner_replacement_commits_atomically_to_one_new_epoch() {
16253 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
16254 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
16255 provider,
16256 SubscriberMode::Auto,
16257 SubscriberConfig::default(),
16258 );
16259 let owner = HandlerId::new("replace-owner");
16260 let original = ReactiveInterest::Logs(LogInterest {
16261 provider_filter: Filter::new().address(Address::repeat_byte(0x41)),
16262 local_matcher: None,
16263 route_key: None,
16264 });
16265 let replacement_interest = ReactiveInterest::Logs(LogInterest {
16266 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
16267 local_matcher: None,
16268 route_key: None,
16269 });
16270 let active = subscriber
16271 .stage_interest_owner(owner.clone(), &[original], SubscriberOwnerStart::Live)
16272 .unwrap();
16273 assert!(subscriber.activate_interest_owner(&active));
16274 let replacement = subscriber
16275 .stage_interest_owner_replacement(
16276 owner,
16277 &[replacement_interest],
16278 SubscriberOwnerStart::Live,
16279 )
16280 .unwrap();
16281
16282 assert!(subscriber.commit_interest_owner_replacement(&active, &replacement));
16283 assert_eq!(subscriber.interest_owner_state(&active), None);
16284 assert_eq!(
16285 subscriber.interest_owner_state(&replacement),
16286 Some(SubscriberOwnerState::Active)
16287 );
16288 assert_eq!(subscriber.registered_interests().len(), 1);
16289 }
16290
16291 #[test]
16292 fn compatibility_and_epoch_owner_lifecycles_cannot_mix() {
16293 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
16294 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
16295 provider,
16296 SubscriberMode::Auto,
16297 SubscriberConfig::default(),
16298 );
16299 let owner = HandlerId::new("one-lifecycle");
16300 let interest = ReactiveInterest::Logs(LogInterest {
16301 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
16302 local_matcher: None,
16303 route_key: None,
16304 });
16305 let epoch = subscriber
16306 .stage_interest_owner(
16307 owner.clone(),
16308 std::slice::from_ref(&interest),
16309 SubscriberOwnerStart::Live,
16310 )
16311 .expect("stage epoch owner");
16312
16313 assert!(matches!(
16314 subscriber.add_interest_owner(owner.clone(), std::slice::from_ref(&interest)),
16315 Err(SubscriberError::InvalidConfig(_))
16316 ));
16317 assert_eq!(
16318 subscriber.interest_owner_state(&epoch),
16319 Some(SubscriberOwnerState::Staged)
16320 );
16321 assert!(subscriber.abort_interest_owner(&epoch));
16322 subscriber
16323 .add_interest_owner(owner.clone(), std::slice::from_ref(&interest))
16324 .expect("compatibility owner after epoch abort");
16325 assert!(matches!(
16326 subscriber.stage_interest_owner_replacement(
16327 owner,
16328 std::slice::from_ref(&interest),
16329 SubscriberOwnerStart::Live,
16330 ),
16331 Err(SubscriberOwnerError::AlreadyRegistered(_))
16332 ));
16333 }
16334
16335 #[test]
16336 fn pending_record_overflow_is_sticky_and_fail_closed() {
16337 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
16338 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
16339 provider,
16340 SubscriberMode::Polling,
16341 SubscriberConfig {
16342 max_pending_records: 1,
16343 ..SubscriberConfig::default()
16344 },
16345 );
16346 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
16347 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
16348 local_matcher: None,
16349 route_key: None,
16350 })];
16351 subscriber.enqueue_event(SubscriberEvent::Log {
16352 source_id: 0,
16353 log: rpc_log(false),
16354 });
16355 let mut second = rpc_log(false);
16356 second.log_index = Some(6);
16357 second.transaction_hash = Some(B256::repeat_byte(0x04));
16358 subscriber.enqueue_event(SubscriberEvent::Log {
16359 source_id: 0,
16360 log: second,
16361 });
16362
16363 assert_eq!(subscriber.pending_records.len(), 1);
16364 assert!(matches!(
16365 subscriber.check_resource_error(),
16366 Err(SubscriberError::ResourceExhausted(_))
16367 ));
16368 subscriber.reset_delivery_state();
16369 assert!(subscriber.check_resource_error().is_ok());
16370 }
16371
16372 #[test]
16373 fn historical_log_payload_bytes_are_bounded_independently_of_log_count() {
16374 let baseline = rpc_log(false);
16375 let fixed_bytes =
16376 validate_backfill_resource_limits(std::slice::from_ref(&baseline), 1, usize::MAX)
16377 .expect("measure fixed log accounting");
16378 let mut large = baseline;
16379 large.inner = alloy_primitives::Log::new_unchecked(
16380 Address::repeat_byte(0x42),
16381 vec![B256::repeat_byte(0x01)],
16382 Bytes::from(vec![0u8; 256]),
16383 );
16384
16385 assert!(matches!(
16386 validate_backfill_resource_limits(&[large], 1, fixed_bytes + 255),
16387 Err(SubscriberError::ResourceExhausted(_))
16388 ));
16389 }
16390
16391 #[tokio::test(flavor = "multi_thread")]
16392 #[cfg(feature = "reactive-polling")]
16393 async fn reconcile_capacity_failure_does_not_publish_progress_or_partial_history() {
16394 use alloy_rpc_types_eth::{Block, Header};
16395
16396 let asserter = Asserter::new();
16397 let baseline = BlockRef {
16398 number: 6,
16399 hash: B256::repeat_byte(6),
16400 parent_hash: Some(B256::repeat_byte(5)),
16401 timestamp: Some(1_700_000_006),
16402 };
16403 let through = BlockRef {
16404 number: 7,
16405 hash: B256::repeat_byte(7),
16406 parent_hash: Some(baseline.hash),
16407 timestamp: Some(1_700_000_007),
16408 };
16409 let rpc_block = || -> Block {
16410 Block::empty(Header {
16411 hash: through.hash,
16412 inner: alloy_consensus::Header {
16413 number: through.number,
16414 parent_hash: through.parent_hash.expect("parent"),
16415 timestamp: through.timestamp.expect("timestamp"),
16416 ..Default::default()
16417 },
16418 total_difficulty: None,
16419 size: None,
16420 })
16421 };
16422 let mut historical = rpc_log(false);
16423 historical.block_hash = Some(through.hash);
16424 historical.block_timestamp = through.timestamp;
16425 asserter.push_success(&Some(rpc_block()));
16426 asserter.push_success(&vec![historical]);
16427 asserter.push_success(&Some(rpc_block()));
16428 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
16429 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
16430 provider,
16431 SubscriberMode::Polling,
16432 SubscriberConfig {
16433 max_pending_records: 1,
16434 ..SubscriberConfig::default()
16435 },
16436 );
16437 subscriber.chain_id = Some(1);
16438 let interest = ReactiveInterest::Logs(LogInterest {
16439 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
16440 local_matcher: None,
16441 route_key: None,
16442 });
16443 let epoch = subscriber
16444 .stage_interest_owner(
16445 HandlerId::new("capacity-owner"),
16446 std::slice::from_ref(&interest),
16447 SubscriberOwnerStart::PostBlock(baseline),
16448 )
16449 .expect("stage owner");
16450 subscriber.sources_dirty = false;
16453 subscriber.state = AlloySubscriberState::Empty;
16454 subscriber.push_pending_record(SubscriberInputRecord {
16455 record: log_input_record(rpc_log(false), InputSource::Poll),
16456 scope: SubscriberInputScope::Canonical { owners: Vec::new() },
16457 });
16458
16459 let error = subscriber
16460 .reconcile_interest_owner(&epoch, through)
16461 .await
16462 .expect_err("historical delivery cannot displace the queued live record");
16463 assert!(matches!(
16464 error,
16465 SubscriberOwnerError::Subscriber(SubscriberError::ResourceExhausted(_))
16466 ));
16467 assert!(subscriber.interest_owner_progress(&epoch).is_none());
16468 assert_eq!(subscriber.pending_records.len(), 1);
16469 assert!(matches!(
16470 subscriber.pending_records[0].scope,
16471 SubscriberInputScope::Canonical { .. }
16472 ));
16473 }
16474
16475 #[test]
16476 fn lazy_backfill_queue_capacity_failure_is_atomic() {
16477 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
16478 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
16479 provider,
16480 SubscriberMode::Auto,
16481 SubscriberConfig {
16482 max_pending_backfills: 1,
16483 ..SubscriberConfig::default()
16484 },
16485 );
16486 let interest = |address| {
16487 ReactiveInterest::Logs(LogInterest {
16488 provider_filter: Filter::new().address(address),
16489 local_matcher: None,
16490 route_key: None,
16491 })
16492 };
16493 subscriber
16494 .add_interest_owner_with_backfill(
16495 HandlerId::new("owner-a"),
16496 &[interest(Address::repeat_byte(0x41))],
16497 SubscriberBackfill::from_block(10),
16498 )
16499 .expect("first queued backfill");
16500
16501 let error = subscriber
16502 .add_interest_owner_with_backfill(
16503 HandlerId::new("owner-b"),
16504 &[interest(Address::repeat_byte(0x42))],
16505 SubscriberBackfill::from_block(10),
16506 )
16507 .expect_err("second backfill must exceed capacity");
16508
16509 assert!(matches!(error, SubscriberError::ResourceExhausted(_)));
16510 assert!(
16511 subscriber
16512 .owner_interests(&HandlerId::new("owner-b"))
16513 .is_none()
16514 );
16515 assert_eq!(subscriber.pending_backfills.len(), 1);
16516 }
16517
16518 #[test]
16519 fn exact_owner_replacement_is_atomic_and_removes_crash_stale_owners() {
16520 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
16521 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
16522 provider,
16523 SubscriberMode::Auto,
16524 SubscriberConfig {
16525 max_pending_backfills: 1,
16526 ..SubscriberConfig::default()
16527 },
16528 );
16529 let interest = |address| {
16530 ReactiveInterest::Logs(LogInterest {
16531 provider_filter: Filter::new().address(address),
16532 local_matcher: None,
16533 route_key: None,
16534 })
16535 };
16536 subscriber
16537 .add_interest_owner(
16538 HandlerId::new("crash-stale"),
16539 &[interest(Address::repeat_byte(0xee))],
16540 )
16541 .expect("seed stale owner");
16542 subscriber.base_interests = vec![interest(Address::repeat_byte(0xdd))];
16543 subscriber.rebuild_registered_interests();
16544 subscriber.push_pending_record(SubscriberInputRecord {
16545 record: log_input_record(rpc_log(false), InputSource::Poll),
16546 scope: SubscriberInputScope::Canonical { owners: Vec::new() },
16547 });
16548 let baseline = BlockRef {
16549 number: 100,
16550 hash: B256::repeat_byte(100),
16551 parent_hash: Some(B256::repeat_byte(99)),
16552 timestamp: Some(1_700_000_100),
16553 };
16554 let backfill = SubscriberBackfill::after_canonical_block(baseline).expect("C + 1");
16555
16556 let error = subscriber
16557 .replace_interest_owners_with_global_backfill(
16558 vec![
16559 (
16560 HandlerId::new("pool-a"),
16561 vec![interest(Address::repeat_byte(0xa1))],
16562 ),
16563 (
16564 HandlerId::new("pool-b"),
16565 vec![ReactiveInterest::Logs(LogInterest {
16566 provider_filter: Filter::new()
16569 .address(Address::repeat_byte(0xb2))
16570 .from_block(7),
16571 local_matcher: None,
16572 route_key: None,
16573 })],
16574 ),
16575 ],
16576 backfill,
16577 )
16578 .expect_err("two backfills exceed atomic capacity");
16579 assert!(matches!(error, SubscriberError::ResourceExhausted(_)));
16580 assert!(
16581 subscriber
16582 .owner_interests(&HandlerId::new("crash-stale"))
16583 .is_some(),
16584 "failed replacement must preserve the prior topology"
16585 );
16586 assert!(
16587 subscriber
16588 .owner_interests(&HandlerId::new("pool-a"))
16589 .is_none()
16590 );
16591 assert_eq!(subscriber.base_interests.len(), 1);
16592 assert_eq!(subscriber.pending_records.len(), 1);
16593
16594 subscriber
16595 .replace_interest_owners_with_global_backfill(
16596 vec![(
16597 HandlerId::new("pool-a"),
16598 vec![interest(Address::repeat_byte(0xa1))],
16599 )],
16600 backfill,
16601 )
16602 .expect("replacement within capacity");
16603 assert!(
16604 subscriber
16605 .owner_interests(&HandlerId::new("crash-stale"))
16606 .is_none(),
16607 "successful exact replacement removes stale owners"
16608 );
16609 assert!(
16610 subscriber.base_interests.is_empty(),
16611 "successful exact replacement removes stale unowned interests"
16612 );
16613 assert!(
16614 subscriber.drain_next_scoped_batch().is_none(),
16615 "stale canonical delivery must not escape before C + 1 recovery"
16616 );
16617 assert!(
16618 subscriber
16619 .owner_interests(&HandlerId::new("pool-a"))
16620 .is_some()
16621 );
16622 assert_eq!(subscriber.pending_backfills.len(), 1);
16623 assert_eq!(subscriber.pending_backfills[0].backfill, backfill);
16624 assert!(
16625 subscriber.pending_backfills[0].owner.is_none(),
16626 "startup history must be global canonical catch-up, not owner-only"
16627 );
16628 }
16629
16630 #[test]
16631 fn exclusive_canonical_backfill_rejects_block_number_overflow() {
16632 let baseline = BlockRef {
16633 number: u64::MAX,
16634 hash: B256::repeat_byte(0xff),
16635 parent_hash: None,
16636 timestamp: None,
16637 };
16638 assert!(matches!(
16639 SubscriberBackfill::after_canonical_block(baseline),
16640 Err(SubscriberError::InvalidConfig(_))
16641 ));
16642 }
16643
16644 #[tokio::test(flavor = "multi_thread")]
16645 async fn exclusive_canonical_backfill_validates_the_retained_baseline_hash() {
16646 let asserter = Asserter::new();
16647 asserter.push_success(&101u64);
16648 asserter.push_success(&Some(rpc_block(101, B256::repeat_byte(101))));
16649 asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(0xee))));
16650 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
16651 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
16652 provider,
16653 SubscriberMode::Auto,
16654 SubscriberConfig::default(),
16655 );
16656 let baseline = BlockRef {
16657 number: 100,
16658 hash: B256::repeat_byte(0xaa),
16659 parent_hash: None,
16660 timestamp: None,
16661 };
16662 let backfill = SubscriberBackfill::after_canonical_block(baseline).expect("C + 1");
16663 subscriber
16664 .add_interest_owner_with_backfill(
16665 HandlerId::new("pool"),
16666 &[ReactiveInterest::Logs(LogInterest {
16667 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
16668 local_matcher: None,
16669 route_key: None,
16670 })],
16671 backfill,
16672 )
16673 .expect("queue post-baseline backfill");
16674
16675 let error = subscriber
16676 .drain_pending_backfills()
16677 .await
16678 .expect_err("provider branch differs at retained baseline");
16679 assert!(matches!(error, SubscriberError::InvalidBackfill(_)));
16680 assert_eq!(subscriber.pending_backfills.len(), 1);
16681 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 101);
16682 assert!(subscriber.pending_records.is_empty());
16683 }
16684
16685 #[tokio::test(flavor = "multi_thread")]
16686 #[cfg(feature = "reactive-ws")]
16687 async fn coordinated_multifilter_windows_are_globally_sorted_for_owner_and_canonical_delivery()
16688 {
16689 let asserter = Asserter::new();
16690 let retained = BlockRef {
16691 number: 10,
16692 hash: B256::repeat_byte(10),
16693 parent_hash: Some(B256::repeat_byte(9)),
16694 timestamp: Some(1_700_000_010),
16695 };
16696 let activation = BlockRef {
16697 number: 12,
16698 hash: B256::repeat_byte(12),
16699 parent_hash: Some(B256::repeat_byte(11)),
16700 timestamp: Some(1_700_000_012),
16701 };
16702
16703 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
16707 asserter.push_success(&vec![rpc_log_at(10, 2, 2)]);
16708 asserter.push_success(&vec![rpc_log_at(10, 1, 1)]);
16709 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
16710 asserter.push_success(&activation.number);
16711 asserter.push_success(&Some(rpc_block(activation.number, activation.hash)));
16712 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
16713 asserter.push_success(&vec![rpc_log_at(12, 2, 2)]);
16714 asserter.push_success(&vec![rpc_log_at(11, 1, 1)]);
16715 asserter.push_success(&Some(rpc_block(activation.number, activation.hash)));
16716 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
16717 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
16718 provider,
16719 SubscriberMode::Auto,
16720 SubscriberConfig::default(),
16721 );
16722 let interests = (0..257)
16723 .map(|start| {
16724 ReactiveInterest::Logs(LogInterest {
16725 provider_filter: Filter::new()
16726 .address(Address::repeat_byte(0x42))
16727 .event_signature(B256::repeat_byte(0x01))
16728 .from_block(start),
16729 local_matcher: None,
16730 route_key: None,
16731 })
16732 })
16733 .collect::<Vec<_>>();
16734 subscriber
16735 .add_interest_owner_with_canonical_catchup(
16736 HandlerId::new("many-filters"),
16737 &interests,
16738 retained,
16739 )
16740 .expect("queue coordinated windows");
16741 assert_eq!(subscriber.pending_backfills.len(), 2);
16742 assert_eq!(subscriber.pending_backfills[0].filters.len(), 257);
16743 assert_eq!(subscriber.pending_backfills[1].filters.len(), 257);
16744
16745 subscriber
16746 .drain_pending_backfills()
16747 .await
16748 .expect("owner filter group");
16749 let owner = subscriber
16750 .drain_next_scoped_batch()
16751 .expect("owner ordered batch");
16752 assert_eq!(owner.records.len(), 2);
16753 assert_eq!(owner.records[0].record.context.transaction_index, Some(1));
16754 assert_eq!(owner.records[1].record.context.transaction_index, Some(2));
16755 assert!(
16756 owner.records.iter().all(|record| matches!(
16757 record.scope,
16758 SubscriberInputScope::OwnerOnlyHandlers { .. }
16759 ))
16760 );
16761
16762 subscriber
16763 .drain_pending_backfills()
16764 .await
16765 .expect("global filter group");
16766 let global = subscriber
16767 .drain_next_scoped_batch()
16768 .expect("global ordered batch");
16769 assert_eq!(global.records.len(), 2);
16770 assert_eq!(
16771 global.records[0].record.context.block.map(|b| b.number),
16772 Some(11)
16773 );
16774 assert_eq!(
16775 global.records[1].record.context.block.map(|b| b.number),
16776 Some(12)
16777 );
16778 assert!(
16779 global
16780 .records
16781 .iter()
16782 .all(|record| record.scope.is_canonical())
16783 );
16784 assert!(matches!(
16785 global.chain_controls.as_slice(),
16786 [ChainControl::Barrier {
16787 block: Some(block),
16788 ..
16789 }] if block == &activation
16790 ));
16791 }
16792
16793 #[tokio::test(flavor = "multi_thread")]
16794 #[cfg(feature = "reactive-ws")]
16795 async fn aborting_staged_epoch_purges_only_its_buffered_delivery() {
16796 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
16797 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
16798 provider,
16799 SubscriberMode::PubSub,
16800 SubscriberConfig::default(),
16801 );
16802 subscriber.chain_id = Some(1);
16803 let interest = ReactiveInterest::Logs(LogInterest {
16804 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
16805 local_matcher: None,
16806 route_key: None,
16807 });
16808 let owner_a = subscriber
16809 .stage_interest_owner(
16810 HandlerId::new("owner-a"),
16811 std::slice::from_ref(&interest),
16812 SubscriberOwnerStart::Live,
16813 )
16814 .unwrap();
16815 let owner_b = subscriber
16816 .stage_interest_owner(
16817 HandlerId::new("owner-b"),
16818 &[interest],
16819 SubscriberOwnerStart::Live,
16820 )
16821 .unwrap();
16822
16823 subscriber.enqueue_event(SubscriberEvent::Log {
16824 source_id: 0,
16825 log: rpc_log(false),
16826 });
16827 assert!(subscriber.abort_interest_owner(&owner_a));
16828
16829 let batch = subscriber
16830 .next_scoped_batch()
16831 .await
16832 .unwrap()
16833 .expect("shared canonical delivery remains queued");
16834 assert_eq!(batch.records.len(), 1);
16835 assert_eq!(
16836 batch.records[0].scope,
16837 SubscriberInputScope::Canonical {
16838 owners: vec![owner_b]
16839 }
16840 );
16841 }
16842
16843 #[tokio::test(flavor = "multi_thread")]
16844 #[cfg(feature = "reactive-ws")]
16845 async fn owner_backfill_dedupe_never_suppresses_canonical_delivery() {
16846 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
16847 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
16848 provider,
16849 SubscriberMode::PubSub,
16850 SubscriberConfig::default(),
16851 );
16852 subscriber.chain_id = Some(1);
16853 let epoch = subscriber
16854 .stage_interest_owner(
16855 HandlerId::new("owner"),
16856 &[ReactiveInterest::Logs(LogInterest {
16857 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
16858 local_matcher: None,
16859 route_key: None,
16860 })],
16861 SubscriberOwnerStart::Live,
16862 )
16863 .unwrap();
16864 let log = rpc_log(false);
16865
16866 subscriber.enqueue_owner_record(
16867 log_input_record(log.clone(), InputSource::Backfill),
16868 epoch.clone(),
16869 );
16870 subscriber.enqueue_event(SubscriberEvent::Log { source_id: 0, log });
16871
16872 let batch = subscriber
16873 .next_scoped_batch()
16874 .await
16875 .unwrap()
16876 .expect("owner backfill and canonical live delivery");
16877 assert_eq!(batch.records.len(), 2);
16878 assert_eq!(
16879 batch.records[0].scope,
16880 SubscriberInputScope::OwnerOnly {
16881 owners: vec![epoch]
16882 }
16883 );
16884 assert_eq!(
16885 batch.records[1].scope,
16886 SubscriberInputScope::Canonical { owners: Vec::new() },
16887 "owner replay dedupe must not suppress the global live record"
16888 );
16889 }
16890
16891 #[tokio::test(flavor = "multi_thread")]
16892 #[cfg(feature = "reactive-polling")]
16893 async fn reconcile_fetch_drains_live_burst_beyond_output_batch_capacity() {
16894 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
16895 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
16896 provider,
16897 SubscriberMode::Polling,
16898 SubscriberConfig {
16899 max_batch_size: 2,
16900 ..SubscriberConfig::default()
16901 },
16902 );
16903 let interest = ReactiveInterest::Logs(LogInterest {
16904 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
16905 local_matcher: None,
16906 route_key: None,
16907 });
16908 let epoch = subscriber
16909 .stage_interest_owner(
16910 HandlerId::new("owner"),
16911 std::slice::from_ref(&interest),
16912 SubscriberOwnerStart::Live,
16913 )
16914 .unwrap();
16915 subscriber.sources_dirty = false;
16916
16917 let mut duplicate = rpc_log(false);
16918 duplicate.transaction_hash = Some(B256::repeat_byte(1));
16919 duplicate.log_index = Some(0);
16920 let events = (0u8..10).map(|index| {
16921 let mut log = rpc_log(false);
16922 log.transaction_hash = Some(B256::repeat_byte(index.saturating_add(1)));
16923 log.log_index = Some(index as u64);
16924 SubscriberEvent::Log { source_id: 0, log }
16925 });
16926 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
16927 let mut streams = SubscriberStreams::new();
16928 streams.push(
16929 SubscriberStreamSource::PollingLog { filter },
16930 stream::iter(events).boxed(),
16931 );
16932 subscriber.state = AlloySubscriberState::Active(streams);
16933
16934 let mut polls = 0usize;
16935 let fetched_duplicate = duplicate.clone();
16936 let fetch = poll_fn(move |cx| {
16937 polls += 1;
16938 if polls > 10 {
16939 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>(fetched_duplicate.clone()))
16940 } else {
16941 cx.waker().wake_by_ref();
16942 std::task::Poll::Pending
16943 }
16944 });
16945 let target_epochs = HashSet::from([epoch.clone()]);
16946 let fetched_duplicate = subscriber
16947 .drive_reconcile_fetch(fetch, &target_epochs)
16948 .await
16949 .unwrap();
16950 subscriber.enqueue_owner_record_for_owners_unmerged(
16951 log_input_record(fetched_duplicate, InputSource::Backfill),
16952 vec![epoch.clone()],
16953 );
16954 subscriber.promote_reconcile_owner_records(&target_epochs);
16955
16956 assert_eq!(subscriber.pending_records.len(), 20);
16957 assert!(subscriber.pending_records.iter().take(10).all(|record| {
16958 record.scope == SubscriberInputScope::Canonical { owners: Vec::new() }
16959 }));
16960 assert!(subscriber.pending_records.iter().skip(10).all(|record| {
16961 record.scope
16962 == SubscriberInputScope::OwnerOnly {
16963 owners: vec![epoch.clone()],
16964 }
16965 }));
16966 }
16967
16968 #[tokio::test(flavor = "multi_thread")]
16969 #[cfg(feature = "reactive-polling")]
16970 async fn reconcile_fetch_waits_for_provider_when_live_topology_is_empty() {
16971 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
16972 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
16973 provider,
16974 SubscriberMode::Polling,
16975 SubscriberConfig::default(),
16976 );
16977 subscriber.chain_id = Some(1);
16978 subscriber.sources_dirty = false;
16979 let mut first_poll = true;
16980 let fetch = poll_fn(move |cx| {
16981 if first_poll {
16982 first_poll = false;
16983 cx.waker().wake_by_ref();
16984 std::task::Poll::Pending
16985 } else {
16986 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>("certified"))
16987 }
16988 });
16989
16990 let result = subscriber
16991 .drive_reconcile_fetch(fetch, &HashSet::new())
16992 .await
16993 .expect("an empty live topology must not be mistaken for termination");
16994 assert_eq!(result, "certified");
16995 }
16996
16997 #[tokio::test(flavor = "multi_thread")]
16998 #[cfg(all(feature = "reactive-polling", feature = "reactive-ws"))]
16999 async fn successful_owner_reconcile_seeds_its_live_filter_reconnect_anchor() {
17000 use alloy_rpc_types_eth::{Block, Header};
17001
17002 let asserter = Asserter::new();
17003 let baseline = BlockRef {
17004 number: 100,
17005 hash: B256::repeat_byte(0x64),
17006 parent_hash: Some(B256::repeat_byte(0x63)),
17007 timestamp: Some(1_700_000_100),
17008 };
17009 let through = BlockRef {
17010 number: 101,
17011 hash: B256::repeat_byte(0x65),
17012 parent_hash: Some(baseline.hash),
17013 timestamp: Some(1_700_000_101),
17014 };
17015 let rpc_block = || -> Block {
17016 Block::empty(Header {
17017 hash: through.hash,
17018 inner: alloy_consensus::Header {
17019 number: through.number,
17020 parent_hash: through.parent_hash.unwrap(),
17021 timestamp: through.timestamp.unwrap(),
17022 ..Default::default()
17023 },
17024 total_difficulty: None,
17025 size: None,
17026 })
17027 };
17028 asserter.push_success(&Some(rpc_block()));
17029 asserter.push_success(&Vec::<Log>::new());
17030 asserter.push_success(&Some(rpc_block()));
17031 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
17032 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17033 provider,
17034 SubscriberMode::PubSub,
17035 SubscriberConfig::default(),
17036 );
17037 subscriber.chain_id = Some(1);
17038 let interest = ReactiveInterest::Logs(LogInterest {
17039 provider_filter: Filter::new().address(Address::repeat_byte(0xac)),
17040 local_matcher: None,
17041 route_key: None,
17042 });
17043 let epoch = subscriber
17044 .stage_interest_owner(
17045 HandlerId::new("reconnect-anchor"),
17046 std::slice::from_ref(&interest),
17047 SubscriberOwnerStart::PostBlock(baseline),
17048 )
17049 .unwrap();
17050 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
17051 let source = SubscriberStreamSource::PubSubLog {
17052 id: subscriber.log_source_id(&filter),
17053 filter: filter.clone(),
17054 };
17055 let mut streams = SubscriberStreams::new();
17056 streams.push(source, stream::pending().boxed());
17057 subscriber.state = AlloySubscriberState::Active(streams);
17058 subscriber.sources_dirty = false;
17059
17060 subscriber
17061 .reconcile_interest_owner(&epoch, through)
17062 .await
17063 .unwrap();
17064 assert_eq!(subscriber.log_anchor(&filter), Some(through.number));
17065 assert!(asserter.read_q().is_empty());
17066 }
17067
17068 #[tokio::test(flavor = "multi_thread")]
17069 #[cfg(feature = "reactive-polling")]
17070 async fn cancelled_reconcile_retains_hidden_owner_live_delivery_for_retry() {
17071 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17072 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17073 provider,
17074 SubscriberMode::Polling,
17075 SubscriberConfig::default(),
17076 );
17077 let interest = ReactiveInterest::Logs(LogInterest {
17078 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
17079 local_matcher: None,
17080 route_key: None,
17081 });
17082 let epoch = subscriber
17083 .stage_interest_owner(
17084 HandlerId::new("owner"),
17085 std::slice::from_ref(&interest),
17086 SubscriberOwnerStart::PostBlock(BlockRef {
17087 number: 100,
17088 hash: B256::repeat_byte(0x64),
17089 parent_hash: None,
17090 timestamp: None,
17091 }),
17092 )
17093 .unwrap();
17094 subscriber.sources_dirty = false;
17095
17096 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
17097 let event = SubscriberEvent::Log {
17098 source_id: 0,
17099 log: rpc_log(false),
17100 };
17101 let mut streams = SubscriberStreams::new();
17102 streams.push(
17103 SubscriberStreamSource::PollingLog { filter },
17104 stream::once(async move { event })
17105 .chain(stream::pending())
17106 .boxed(),
17107 );
17108 subscriber.state = AlloySubscriberState::Active(streams);
17109
17110 let targets = HashSet::from([epoch.clone()]);
17111 {
17112 let fetch = futures::future::pending::<Result<(), SubscriberOwnerError>>();
17113 let drive = subscriber.drive_reconcile_fetch(fetch, &targets);
17114 futures::pin_mut!(drive);
17115 poll_fn(|cx| {
17116 assert!(drive.as_mut().poll(cx).is_pending());
17117 std::task::Poll::Ready(())
17118 })
17119 .await;
17120 }
17121
17122 assert_eq!(subscriber.pending_records.len(), 1);
17123 assert_eq!(
17124 subscriber.pending_records[0].scope,
17125 SubscriberInputScope::Canonical { owners: Vec::new() },
17126 "canonical delivery commits immediately at a cancellation-safe boundary"
17127 );
17128 assert_eq!(subscriber.pending_reconcile_owner_records.len(), 1);
17129
17130 subscriber
17131 .drive_reconcile_fetch(futures::future::ready(Ok(())), &targets)
17132 .await
17133 .unwrap();
17134 subscriber.promote_reconcile_owner_records(&targets);
17135 assert!(subscriber.pending_reconcile_owner_records.is_empty());
17136 assert_eq!(subscriber.pending_records.len(), 2);
17137 assert_eq!(
17138 subscriber.pending_records[0].scope,
17139 SubscriberInputScope::Canonical { owners: Vec::new() },
17140 "canonical delivery remains target-excluded"
17141 );
17142 assert_eq!(
17143 subscriber.pending_records[1].scope,
17144 SubscriberInputScope::OwnerOnly {
17145 owners: vec![epoch]
17146 },
17147 "retry commit appends hidden owner delivery after historical catch-up"
17148 );
17149 }
17150
17151 #[tokio::test(flavor = "multi_thread")]
17152 #[cfg(feature = "reactive-ws")]
17153 async fn control_cancellation_preserves_terminated_source_reconcile_intent() {
17154 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17155 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17156 provider,
17157 SubscriberMode::PubSub,
17158 SubscriberConfig {
17159 reconnect: SubscriberReconnectConfig {
17160 initial_delay: Duration::from_secs(60),
17161 ..SubscriberReconnectConfig::default()
17162 },
17163 ..SubscriberConfig::default()
17164 },
17165 );
17166 subscriber.chain_id = Some(1);
17167 let interest = ReactiveInterest::Logs(LogInterest {
17168 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
17169 local_matcher: None,
17170 route_key: None,
17171 });
17172 let epoch = subscriber
17173 .stage_interest_owner(
17174 HandlerId::new("owner"),
17175 std::slice::from_ref(&interest),
17176 SubscriberOwnerStart::PostBlock(BlockRef {
17177 number: 7,
17178 hash: B256::repeat_byte(0x07),
17179 parent_hash: Some(B256::repeat_byte(0x06)),
17180 timestamp: Some(1_700_000_007),
17181 }),
17182 )
17183 .unwrap();
17184 subscriber.sources_dirty = false;
17185 subscriber.stream_revision = 1;
17186 let entry = subscriber
17187 .owned_interests
17188 .iter_mut()
17189 .find(|entry| entry.epoch.as_ref() == Some(&epoch))
17190 .unwrap();
17191 entry.progress = Some(SubscriberOwnerProgress {
17192 owner: epoch.clone(),
17193 through: entry.baseline.unwrap(),
17194 });
17195 entry.progress_stream_revision = Some(1);
17196
17197 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
17198 let source = SubscriberStreamSource::PubSubLog {
17199 id: subscriber.log_source_id(&filter),
17200 filter,
17201 };
17202 let mut streams = SubscriberStreams::new();
17203 streams.push(
17204 source.clone(),
17205 stream::iter([SubscriberEvent::StreamTerminated(source)]).boxed(),
17206 );
17207 subscriber.state = AlloySubscriberState::Active(streams);
17208 let prior_revision = subscriber.stream_revision;
17209
17210 let mut first_poll = true;
17211 let control = poll_fn(move |cx| {
17212 if first_poll {
17213 first_poll = false;
17214 cx.waker().wake_by_ref();
17215 std::task::Poll::Pending
17216 } else {
17217 std::task::Poll::Ready("stop")
17218 }
17219 });
17220 futures::pin_mut!(control);
17221 let outcome = subscriber
17222 .next_scoped_batch_or(control.as_mut())
17223 .await
17224 .unwrap();
17225
17226 assert!(matches!(outcome, SubscriberDriverPoll::Control("stop")));
17227 assert!(subscriber.sources_dirty);
17228 assert!(subscriber.stream_revision > prior_revision);
17229 assert!(
17230 !subscriber.activate_interest_owner(&epoch),
17231 "progress certified against the terminated stream revision is stale"
17232 );
17233 }
17234
17235 #[tokio::test]
17236 #[cfg(feature = "reactive-ws")]
17237 async fn pubsub_sources_assign_stable_log_ids_before_shared_streams() {
17238 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17239 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17240 provider,
17241 SubscriberMode::PubSub,
17242 SubscriberConfig::default(),
17243 );
17244 subscriber.chain_id = Some(1);
17245 subscriber
17246 .register_interests(&[
17247 ReactiveInterest::Logs(LogInterest {
17248 provider_filter: Filter::new().address(Address::repeat_byte(0x01)),
17249 local_matcher: None,
17250 route_key: None,
17251 }),
17252 ReactiveInterest::Logs(LogInterest {
17253 provider_filter: Filter::new().address(Address::repeat_byte(0x02)),
17254 local_matcher: None,
17255 route_key: None,
17256 }),
17257 ReactiveInterest::PendingTransactions(PendingTxInterest::default()),
17258 ])
17259 .await
17260 .expect("register base interests");
17261
17262 let sources = subscriber.stream_sources().expect("stream sources");
17266 assert_eq!(sources.len(), 2);
17267 assert!(matches!(
17268 &sources[0],
17269 SubscriberStreamSource::PubSubLog { id: 0, .. }
17270 ));
17271 assert!(matches!(
17272 sources[1],
17273 SubscriberStreamSource::PubSubPendingHashes
17274 ));
17275
17276 let again = subscriber.stream_sources().expect("stream sources again");
17278 assert!(again[0].same_key(&sources[0]));
17279 }
17280
17281 #[tokio::test(flavor = "multi_thread")]
17282 #[cfg(feature = "reactive-ws")]
17283 async fn pubsub_stream_termination_attempts_reconnect_before_error() {
17284 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17285 let mut subscriber = AlloySubscriber::new(
17286 provider,
17287 SubscriberMode::PubSub,
17288 SubscriberConfig {
17289 reconnect: SubscriberReconnectConfig {
17290 initial_delay: Duration::ZERO,
17291 retry_delay: Duration::ZERO,
17292 max_delay: Duration::ZERO,
17293 max_attempts: Some(1),
17294 ..SubscriberReconnectConfig::default()
17295 },
17296 ..SubscriberConfig::default()
17297 },
17298 );
17299 subscriber.chain_id = Some(1);
17300 subscriber.interests = vec![ReactiveInterest::PendingTransactions(
17301 PendingTxInterest::default(),
17302 )];
17303
17304 let mut streams = SubscriberStreams::new();
17305 let source = SubscriberStreamSource::PubSubPendingHashes;
17306 streams.push(
17307 source,
17308 stream::once(async {
17309 SubscriberEvent::<Ethereum>::StreamTerminated(
17310 SubscriberStreamSource::PubSubPendingHashes,
17311 )
17312 })
17313 .boxed(),
17314 );
17315 subscriber.state = AlloySubscriberState::Active(streams);
17316
17317 let result = subscriber.next_batch().await;
17318 assert!(
17319 matches!(result, Err(SubscriberError::Provider(ref message)) if message.contains("reconnect failed after 1 attempt")),
17320 "terminated pubsub streams should attempt reconnect before surfacing failure: {result:?}"
17321 );
17322 }
17323
17324 #[test]
17325 fn backfilled_logs_skip_recent_subscription_duplicates() {
17326 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17327 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17328 provider,
17329 SubscriberMode::PubSub,
17330 SubscriberConfig::default(),
17331 );
17332 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
17333 provider_filter: Filter::new()
17334 .address(Address::repeat_byte(0x42))
17335 .event_signature(B256::repeat_byte(0x01)),
17336 local_matcher: None,
17337 route_key: None,
17338 })];
17339
17340 let log = rpc_log(false);
17341 subscriber.enqueue_event(SubscriberEvent::Log {
17342 source_id: 0,
17343 log: log.clone(),
17344 });
17345 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
17346 source_id: 0,
17347 logs: vec![log],
17348 });
17349
17350 assert_eq!(subscriber.pending_records.len(), 1);
17351 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
17352 assert_eq!(
17353 subscriber.pending_records[0].context.source,
17354 InputSource::Subscription
17355 );
17356 }
17357
17358 #[test]
17359 fn backfilled_logs_surface_with_backfill_source() {
17360 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17365 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17366 provider,
17367 SubscriberMode::PubSub,
17368 SubscriberConfig::default(),
17369 );
17370 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
17371 provider_filter: Filter::new()
17372 .address(Address::repeat_byte(0x42))
17373 .event_signature(B256::repeat_byte(0x01)),
17374 local_matcher: None,
17375 route_key: None,
17376 })];
17377
17378 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
17379 source_id: 0,
17380 logs: vec![rpc_log(false)],
17381 });
17382
17383 assert_eq!(subscriber.pending_records.len(), 1);
17384 assert_eq!(
17385 subscriber.pending_records[0].context.source,
17386 InputSource::Backfill
17387 );
17388 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
17389 }
17390
17391 #[test]
17392 #[cfg(feature = "reactive-ws")]
17393 fn owner_removal_preserves_delivery_and_dedupe_state() {
17394 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17395 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17396 provider,
17397 SubscriberMode::PubSub,
17398 SubscriberConfig::default(),
17399 );
17400 subscriber
17401 .add_interest_owner(
17402 HandlerId::new("pool-a"),
17403 &[ReactiveInterest::Logs(LogInterest {
17404 provider_filter: Filter::new()
17405 .address(Address::repeat_byte(0x42))
17406 .event_signature(B256::repeat_byte(0x01)),
17407 local_matcher: None,
17408 route_key: None,
17409 })],
17410 )
17411 .expect("register pool-a owner");
17412 subscriber
17413 .add_interest_owner(
17414 HandlerId::new("pool-b"),
17415 &[ReactiveInterest::Logs(LogInterest {
17416 provider_filter: Filter::new()
17417 .address(Address::repeat_byte(0x24))
17418 .event_signature(B256::repeat_byte(0x02)),
17419 local_matcher: None,
17420 route_key: None,
17421 })],
17422 )
17423 .expect("register pool-b owner");
17424
17425 let sources = subscriber.stream_sources().expect("stream sources");
17428 subscriber.enqueue_event(SubscriberEvent::Log {
17429 source_id: 0,
17430 log: rpc_log(false),
17431 });
17432 let mut streams = SubscriberStreams::new();
17433 streams.push(
17434 sources[0].clone(),
17435 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
17436 );
17437 subscriber.state = AlloySubscriberState::Active(streams);
17438 assert_eq!(subscriber.pending_records.len(), 1);
17439 assert_eq!(subscriber.recent_input_refs.len(), 1);
17440 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
17441
17442 let removed = subscriber
17443 .remove_interest_owner(&HandlerId::new("pool-b"))
17444 .expect("pool-b should be removed");
17445
17446 assert_eq!(removed.len(), 1);
17447 assert_eq!(subscriber.pending_records.len(), 1);
17448 assert_eq!(subscriber.recent_input_refs.len(), 1);
17449 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
17450 assert!(
17451 subscriber
17452 .owner_interests(&HandlerId::new("pool-a"))
17453 .is_some()
17454 );
17455 assert!(
17456 subscriber
17457 .owner_interests(&HandlerId::new("pool-b"))
17458 .is_none()
17459 );
17460 assert_eq!(subscriber.registered_interests().len(), 1);
17461 }
17462
17463 #[test]
17464 #[cfg(feature = "reactive-ws")]
17465 fn owner_log_sources_fan_in_across_owners() {
17466 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17467 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17468 provider,
17469 SubscriberMode::PubSub,
17470 SubscriberConfig::default(),
17471 );
17472 subscriber
17473 .add_interest_owner(
17474 HandlerId::new("pool-a"),
17475 &[ReactiveInterest::Logs(LogInterest {
17476 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
17477 local_matcher: None,
17478 route_key: None,
17479 })],
17480 )
17481 .expect("register pool-a owner");
17482
17483 let initial_sources = subscriber.stream_sources().expect("initial sources");
17484 assert_eq!(initial_sources.len(), 1);
17485 let pool_a_source = initial_sources[0].clone();
17486 assert!(matches!(
17487 &pool_a_source,
17488 SubscriberStreamSource::PubSubLog { id: 0, .. }
17489 ));
17490
17491 subscriber
17492 .add_interest_owner(
17493 HandlerId::new("pool-b"),
17494 &[ReactiveInterest::Logs(LogInterest {
17495 provider_filter: Filter::new().address(Address::repeat_byte(0xb2)),
17496 local_matcher: None,
17497 route_key: None,
17498 })],
17499 )
17500 .expect("register pool-b owner");
17501
17502 let expanded_sources = subscriber.stream_sources().expect("expanded sources");
17503 assert_eq!(
17504 expanded_sources.len(),
17505 1,
17506 "compatible owner filters should share one provider subscription"
17507 );
17508 assert!(
17509 !expanded_sources[0].same_key(&pool_a_source),
17510 "the provider-facing superset changes while owner routing remains exact"
17511 );
17512
17513 subscriber
17514 .remove_interest_owner(&HandlerId::new("pool-b"))
17515 .expect("pool-b should be removed");
17516 let trimmed_sources = subscriber.stream_sources().expect("trimmed sources");
17517 assert_eq!(trimmed_sources.len(), 1);
17518 assert!(trimmed_sources[0].same_key(&pool_a_source));
17519 }
17520
17521 #[test]
17522 #[cfg(feature = "reactive-ws")]
17523 fn provider_log_fan_in_respects_address_ceiling() {
17524 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17525 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17526 provider,
17527 SubscriberMode::PubSub,
17528 SubscriberConfig {
17529 max_log_addresses_per_subscription: 2,
17530 ..SubscriberConfig::default()
17531 },
17532 );
17533 for index in 0..5 {
17534 subscriber
17535 .add_interest_owner(
17536 HandlerId::new(format!("pool-{index}")),
17537 &[log_interest_for(index + 1)],
17538 )
17539 .expect("register pool owner");
17540 }
17541
17542 let sources = subscriber.stream_sources().expect("stream sources");
17543 assert_eq!(sources.len(), 3);
17544 let mut address_counts: Vec<_> = sources
17545 .iter()
17546 .map(|source| match source {
17547 SubscriberStreamSource::PubSubLog { filter, .. } => filter.address.iter().count(),
17548 _ => panic!("expected log source"),
17549 })
17550 .collect();
17551 address_counts.sort_unstable();
17552 assert_eq!(address_counts, vec![1, 2, 2]);
17553 }
17554
17555 #[tokio::test(flavor = "multi_thread")]
17556 #[cfg(feature = "reactive-ws")]
17557 async fn owner_backfill_seeds_reconnect_anchor_before_live_log() {
17558 let asserter = Asserter::new();
17559 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
17560 asserter.push_success(&vec![rpc_log(false)]);
17561 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
17562 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
17563 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17564 provider,
17565 SubscriberMode::PubSub,
17566 SubscriberConfig::default(),
17567 );
17568 subscriber
17569 .add_interest_owner_with_backfill(
17570 HandlerId::new("pool-a"),
17571 &[ReactiveInterest::Logs(LogInterest {
17572 provider_filter: Filter::new()
17573 .address(Address::repeat_byte(0x42))
17574 .event_signature(B256::repeat_byte(0x01)),
17575 local_matcher: None,
17576 route_key: None,
17577 })],
17578 SubscriberBackfill::range(1, 7),
17579 )
17580 .expect("register pool-a with backfill");
17581
17582 subscriber
17583 .drain_pending_backfills()
17584 .await
17585 .expect("owner backfill should drain");
17586
17587 assert_eq!(subscriber.pending_records.len(), 1);
17588 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
17589 }
17590
17591 #[tokio::test(flavor = "multi_thread")]
17592 async fn subscriber_streams_poll_ready_sources_round_robin() {
17593 let first_hash = B256::repeat_byte(0x01);
17594 let second_hash = B256::repeat_byte(0x02);
17595 let mut streams = SubscriberStreams::new();
17596 streams.push(
17597 SubscriberStreamSource::PubSubPendingHashes,
17598 stream::iter([
17599 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
17600 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
17601 ])
17602 .boxed(),
17603 );
17604 streams.push(
17605 SubscriberStreamSource::PubSubBlockHeaders,
17606 stream::once(async move { SubscriberEvent::<Ethereum>::PendingHash(second_hash) })
17607 .boxed(),
17608 );
17609
17610 assert!(matches!(
17611 streams.next().await,
17612 Some(SubscriberEvent::PendingHash(hash)) if hash == first_hash
17613 ));
17614 assert!(matches!(
17615 streams.next().await,
17616 Some(SubscriberEvent::PendingHash(hash)) if hash == second_hash
17617 ));
17618 }
17619
17620 #[tokio::test(flavor = "multi_thread")]
17621 #[cfg(feature = "reactive-ws")]
17622 async fn owner_updates_ensure_streams_without_full_reset() {
17623 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17624 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17625 provider,
17626 SubscriberMode::PubSub,
17627 SubscriberConfig::default(),
17628 );
17629 subscriber.chain_id = Some(1);
17630 subscriber
17631 .register_interests(&[ReactiveInterest::PendingTransactions(
17632 PendingTxInterest::default(),
17633 )])
17634 .await
17635 .expect("register base pending interest");
17636 subscriber
17637 .add_interest_owner(
17638 HandlerId::new("headers"),
17639 &[ReactiveInterest::Blocks(BlockInterest::default())],
17640 )
17641 .expect("register header owner");
17642
17643 let mut streams = SubscriberStreams::new();
17644 streams.push(
17645 SubscriberStreamSource::PubSubPendingHashes,
17646 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
17647 );
17648 streams.push(
17649 SubscriberStreamSource::PubSubBlockHeaders,
17650 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
17651 );
17652 subscriber.state = AlloySubscriberState::Active(streams);
17653
17654 subscriber
17655 .remove_interest_owner(&HandlerId::new("headers"))
17656 .expect("header owner should be removed");
17657 assert!(matches!(
17658 &subscriber.state,
17659 AlloySubscriberState::Active(streams) if streams.len() == 2
17660 ));
17661
17662 subscriber
17663 .ensure_streams()
17664 .await
17665 .expect("pure removal reconciliation should not touch provider");
17666
17667 assert!(matches!(
17668 &subscriber.state,
17669 AlloySubscriberState::Active(streams)
17670 if streams.len() == 1
17671 && streams.contains_source(&SubscriberStreamSource::PubSubPendingHashes)
17672 && !streams.contains_source(&SubscriberStreamSource::PubSubBlockHeaders)
17673 ));
17674
17675 subscriber
17676 .add_interest_owner(
17677 HandlerId::new("headers"),
17678 &[ReactiveInterest::Blocks(BlockInterest::default())],
17679 )
17680 .expect("re-add header owner");
17681 assert!(matches!(
17682 &subscriber.state,
17683 AlloySubscriberState::Active(streams) if streams.len() == 1
17684 ));
17685 }
17686
17687 #[tokio::test(flavor = "multi_thread")]
17688 #[cfg(feature = "reactive-polling")]
17689 async fn ensure_streams_retains_each_successful_connection_across_later_failure() {
17690 let asserter = Asserter::new();
17691 asserter.push_success(&U256::from(1));
17692 asserter.push_failure_msg("second filter connection failed");
17693 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
17694 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17695 provider,
17696 SubscriberMode::Polling,
17697 SubscriberConfig {
17698 max_log_addresses_per_subscription: 1,
17699 ..SubscriberConfig::default()
17700 },
17701 );
17702 subscriber.chain_id = Some(1);
17703 subscriber
17704 .register_interests(&[log_interest_for(0x41), log_interest_for(0x42)])
17705 .await
17706 .expect("register two independently connected filters");
17707
17708 let error = subscriber
17709 .ensure_streams()
17710 .await
17711 .expect_err("second provider connection is forced to fail");
17712 assert!(matches!(error, SubscriberError::Provider(_)));
17713 assert!(subscriber.sources_dirty);
17714 let retained_streams = match &subscriber.state {
17715 AlloySubscriberState::Active(streams) => Some(streams.len()),
17716 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => None,
17717 };
17718 assert_eq!(
17719 retained_streams,
17720 Some(1),
17721 "first connection must survive later error {error:?}; revision {}",
17722 subscriber.stream_revision
17723 );
17724
17725 asserter.push_success(&U256::from(2));
17726 subscriber
17727 .ensure_streams()
17728 .await
17729 .expect("retry connects only the missing source");
17730 assert!(!subscriber.sources_dirty);
17731 assert!(matches!(
17732 &subscriber.state,
17733 AlloySubscriberState::Active(streams) if streams.len() == 2
17734 ));
17735 assert!(asserter.read_q().is_empty());
17736 }
17737
17738 #[tokio::test(flavor = "multi_thread")]
17739 #[cfg(feature = "reactive-ws")]
17740 async fn cancelled_post_install_backfill_is_retried_without_reconnecting() {
17741 let asserter = Asserter::new();
17742 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
17743 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17744 provider,
17745 SubscriberMode::PubSub,
17746 SubscriberConfig::default(),
17747 );
17748 subscriber.chain_id = Some(1);
17749 subscriber
17750 .register_interests(&[log_interest_for(0x43)])
17751 .await
17752 .expect("register log source");
17753 let source = subscriber
17754 .stream_sources()
17755 .expect("one desired source")
17756 .pop()
17757 .expect("log source");
17758 let SubscriberStreamSource::PubSubLog { id, .. } = source else {
17759 panic!("expected pubsub log source")
17760 };
17761 subscriber.last_seen_log_blocks.insert(id, 6);
17762
17763 {
17764 let source = SubscriberStreamSource::PubSubLog {
17765 id,
17766 filter: subscriber
17767 .log_stream_filters()
17768 .pop()
17769 .expect("provider filter"),
17770 };
17771 let interrupted = async {
17772 subscriber.install_source_stream(
17773 source.clone(),
17774 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
17775 );
17776 subscriber.queue_source_backfill(source);
17777 subscriber.sources_dirty = true;
17778 futures::future::pending::<()>().await;
17779 };
17780 futures::pin_mut!(interrupted);
17781 poll_fn(|cx| {
17782 assert!(interrupted.as_mut().poll(cx).is_pending());
17783 std::task::Poll::Ready(())
17784 })
17785 .await;
17786 }
17787
17788 assert_eq!(subscriber.pending_source_backfills.len(), 1);
17789 assert!(matches!(
17790 &subscriber.state,
17791 AlloySubscriberState::Active(streams) if streams.len() == 1
17792 ));
17793
17794 asserter.push_success(&7u64);
17795 asserter.push_success(&Vec::<Log>::new());
17796 subscriber
17797 .ensure_streams()
17798 .await
17799 .expect("retry completes only the pending historical window");
17800
17801 assert!(subscriber.pending_source_backfills.is_empty());
17802 assert!(!subscriber.sources_dirty);
17803 assert!(matches!(
17804 &subscriber.state,
17805 AlloySubscriberState::Active(streams) if streams.len() == 1
17806 ));
17807 assert!(asserter.read_q().is_empty());
17808 }
17809
17810 #[cfg(feature = "reactive-ws")]
17812 fn log_interest_matching_rpc_log() -> ReactiveInterest<Ethereum> {
17813 ReactiveInterest::Logs(LogInterest {
17814 provider_filter: Filter::new()
17815 .address(Address::repeat_byte(0x42))
17816 .event_signature(B256::repeat_byte(0x01)),
17817 local_matcher: None,
17818 route_key: None,
17819 })
17820 }
17821
17822 #[cfg(any(feature = "reactive-ws", feature = "reactive-polling"))]
17823 fn log_interest_for(address: u8) -> ReactiveInterest<Ethereum> {
17824 ReactiveInterest::Logs(LogInterest {
17825 provider_filter: Filter::new().address(Address::repeat_byte(address)),
17826 local_matcher: None,
17827 route_key: None,
17828 })
17829 }
17830
17831 #[tokio::test(flavor = "multi_thread")]
17834 #[cfg(feature = "reactive-ws")]
17835 async fn drain_backfill_retains_queue_entry_on_provider_error() {
17836 let asserter = Asserter::new();
17837 asserter.push_failure_msg("rate limited");
17838 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
17839 asserter.push_success(&vec![rpc_log(false)]);
17840 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
17841 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
17842 let mut subscriber = AlloySubscriber::new(
17843 provider,
17844 SubscriberMode::PubSub,
17845 SubscriberConfig::default(),
17846 );
17847 subscriber
17848 .add_interest_owner_with_backfill(
17849 HandlerId::new("pool"),
17850 &[log_interest_matching_rpc_log()],
17851 SubscriberBackfill::range(1, 7),
17852 )
17853 .expect("register owner with backfill");
17854 assert_eq!(subscriber.pending_backfills.len(), 1);
17855
17856 let first = subscriber.drain_pending_backfills().await;
17857 assert!(first.is_err(), "provider failure should surface");
17858 assert_eq!(
17859 subscriber.pending_backfills.len(),
17860 1,
17861 "failed fetch must leave the backfill queued for retry"
17862 );
17863 assert!(subscriber.pending_records.is_empty());
17864
17865 subscriber
17866 .drain_pending_backfills()
17867 .await
17868 .expect("retry should succeed");
17869 assert!(subscriber.pending_backfills.is_empty());
17870 assert_eq!(subscriber.pending_records.len(), 1);
17871 }
17872
17873 #[tokio::test(flavor = "multi_thread")]
17876 #[cfg(feature = "reactive-ws")]
17877 async fn drain_backfill_seeds_anchor_on_empty_window() {
17878 let asserter = Asserter::new();
17879 asserter.push_success(&Some(rpc_block(42, B256::repeat_byte(42))));
17880 asserter.push_success(&Vec::<Log>::new());
17881 asserter.push_success(&Some(rpc_block(42, B256::repeat_byte(42))));
17882 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
17883 let mut subscriber = AlloySubscriber::new(
17884 provider,
17885 SubscriberMode::PubSub,
17886 SubscriberConfig::default(),
17887 );
17888 subscriber
17889 .add_interest_owner_with_backfill(
17890 HandlerId::new("pool"),
17891 &[log_interest_matching_rpc_log()],
17892 SubscriberBackfill::range(1, 42),
17893 )
17894 .expect("register owner with backfill");
17895
17896 subscriber
17897 .drain_pending_backfills()
17898 .await
17899 .expect("empty backfill should drain");
17900
17901 assert!(subscriber.pending_records.is_empty());
17902 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
17903 .pop()
17904 .unwrap();
17905 assert_eq!(
17906 subscriber.log_anchor(&filter),
17907 Some(42),
17908 "empty window must still seed the anchor at its upper bound"
17909 );
17910 }
17911
17912 #[tokio::test(flavor = "multi_thread")]
17915 #[cfg(feature = "reactive-ws")]
17916 async fn drain_backfill_open_ended_resolves_head_and_seeds_anchor() {
17917 let asserter = Asserter::new();
17918 asserter.push_success(&100u64); asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(100))));
17920 asserter.push_success(&Vec::<Log>::new()); asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(100))));
17922 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
17923 let mut subscriber = AlloySubscriber::new(
17924 provider,
17925 SubscriberMode::PubSub,
17926 SubscriberConfig::default(),
17927 );
17928 subscriber
17929 .add_interest_owner_with_backfill(
17930 HandlerId::new("pool"),
17931 &[log_interest_matching_rpc_log()],
17932 SubscriberBackfill::from_block(10),
17933 )
17934 .expect("register owner with open-ended backfill");
17935
17936 subscriber
17937 .drain_pending_backfills()
17938 .await
17939 .expect("open-ended backfill should drain");
17940
17941 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
17942 .pop()
17943 .unwrap();
17944 assert_eq!(subscriber.log_anchor(&filter), Some(100));
17945 }
17946
17947 #[test]
17950 #[cfg(feature = "reactive-ws")]
17951 fn duplicate_filters_across_owners_map_to_single_source() {
17952 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17953 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17954 provider,
17955 SubscriberMode::PubSub,
17956 SubscriberConfig::default(),
17957 );
17958 subscriber
17959 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
17960 .expect("register pool-a");
17961 subscriber
17962 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xaa)])
17963 .expect("register pool-b with identical filter");
17964
17965 assert_eq!(
17966 subscriber.log_stream_filters().len(),
17967 1,
17968 "identical filters across owners must collapse to one"
17969 );
17970 let sources = subscriber.stream_sources().expect("stream sources");
17971 assert_eq!(sources.len(), 1);
17972 }
17973
17974 #[test]
17977 #[cfg(feature = "reactive-ws")]
17978 fn owner_removal_prunes_source_ids_and_anchors() {
17979 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17980 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17981 provider,
17982 SubscriberMode::PubSub,
17983 SubscriberConfig::default(),
17984 );
17985 subscriber
17986 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
17987 .expect("register pool-a");
17988 subscriber
17989 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xbb)])
17990 .expect("register pool-b");
17991
17992 let _ = subscriber.stream_sources().expect("stream sources");
17994 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
17995 let filter_b = log_filters(&[log_interest_for(0xbb)]).pop().unwrap();
17996 let id_a = subscriber.log_source_id(&filter_a);
17997 let id_b = subscriber.log_source_id(&filter_b);
17998 subscriber.last_seen_log_blocks.insert(id_a, 10);
17999 subscriber.last_seen_log_blocks.insert(id_b, 20);
18000 assert_eq!(
18001 subscriber.log_source_ids.len(),
18002 3,
18003 "one provider fan-in id plus two explicitly seeded logical ids"
18004 );
18005
18006 subscriber
18007 .remove_interest_owner(&HandlerId::new("pool-b"))
18008 .expect("remove pool-b");
18009
18010 assert_eq!(
18011 subscriber.log_source_ids.len(),
18012 1,
18013 "pool-b's filter id should be retired"
18014 );
18015 assert!(subscriber.log_source_ids.contains_key(&filter_a));
18016 assert_eq!(subscriber.last_seen_log_blocks.get(&id_a), Some(&10));
18017 assert_eq!(
18018 subscriber.last_seen_log_blocks.get(&id_b),
18019 None,
18020 "pool-b's anchor should be pruned"
18021 );
18022 }
18023
18024 #[test]
18029 #[cfg(feature = "reactive-ws")]
18030 fn owner_filter_growth_queues_continuity_backfill_from_prior_anchor() {
18031 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18032 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18033 provider,
18034 SubscriberMode::PubSub,
18035 SubscriberConfig::default(),
18036 );
18037 subscriber
18038 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
18039 .expect("register amm with pool A");
18040
18041 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
18044 let id_a = subscriber.log_source_id(&filter_a);
18045 subscriber.last_seen_log_blocks.insert(id_a, 50);
18046
18047 subscriber
18050 .add_interest_owner(
18051 HandlerId::new("amm"),
18052 &[log_interest_for(0xaa), log_interest_for(0xbb)],
18053 )
18054 .expect("grow amm to pools A+B");
18055
18056 assert_eq!(
18057 subscriber.pending_backfills.len(),
18058 1,
18059 "the changed merged filter should queue exactly one continuity backfill"
18060 );
18061 let queued = &subscriber.pending_backfills[0];
18062 assert_eq!(queued.owner, Some(HandlerId::new("amm")));
18063 assert_eq!(queued.backfill.start_block(), 50);
18064 assert_eq!(
18065 queued.backfill.end_block(),
18066 None,
18067 "continuity backfill runs open-ended to the current head"
18068 );
18069 }
18070
18071 #[test]
18074 #[cfg(feature = "reactive-ws")]
18075 fn unchanged_owner_filter_does_not_queue_continuity_backfill() {
18076 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18077 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18078 provider,
18079 SubscriberMode::PubSub,
18080 SubscriberConfig::default(),
18081 );
18082 subscriber
18083 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
18084 .expect("register amm");
18085 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
18086 let id_a = subscriber.log_source_id(&filter_a);
18087 subscriber.last_seen_log_blocks.insert(id_a, 50);
18088
18089 subscriber
18090 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
18091 .expect("re-register identical interests");
18092
18093 assert!(
18094 subscriber.pending_backfills.is_empty(),
18095 "an unchanged filter shape must not queue continuity backfill"
18096 );
18097 }
18098
18099 #[test]
18103 #[cfg(feature = "reactive-ws")]
18104 fn explicit_open_ended_backfill_below_anchor_suppresses_continuity() {
18105 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18106 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18107 provider,
18108 SubscriberMode::PubSub,
18109 SubscriberConfig::default(),
18110 );
18111 subscriber
18112 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
18113 .expect("register amm");
18114 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
18115 let id_a = subscriber.log_source_id(&filter_a);
18116 subscriber.last_seen_log_blocks.insert(id_a, 50);
18117
18118 subscriber
18120 .add_interest_owner_with_backfill(
18121 HandlerId::new("amm"),
18122 &[log_interest_for(0xaa), log_interest_for(0xbb)],
18123 SubscriberBackfill::from_block(10),
18124 )
18125 .expect("grow amm with explicit deep backfill");
18126
18127 assert_eq!(
18128 subscriber.pending_backfills.len(),
18129 1,
18130 "only the explicit backfill should be queued; continuity is subsumed"
18131 );
18132 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 10);
18133 }
18134
18135 #[tokio::test(flavor = "multi_thread")]
18138 #[cfg(feature = "reactive-ws")]
18139 async fn ensure_streams_is_noop_when_not_dirty() {
18140 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18141 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18142 provider,
18143 SubscriberMode::PubSub,
18144 SubscriberConfig::default(),
18145 );
18146 subscriber
18148 .add_interest_owner(
18149 HandlerId::new("headers"),
18150 &[ReactiveInterest::Blocks(BlockInterest::default())],
18151 )
18152 .expect("register header owner");
18153 subscriber.state = AlloySubscriberState::Empty;
18155 subscriber.sources_dirty = false;
18156
18157 subscriber
18158 .ensure_streams()
18159 .await
18160 .expect("clean reconcile must be a no-op");
18161
18162 assert!(
18163 matches!(subscriber.state, AlloySubscriberState::Empty),
18164 "not-dirty ensure_streams must not connect new sources"
18165 );
18166 }
18167}
18168
18169fn resolve_subscriber_transport(
18170 mode: SubscriberMode,
18171) -> Result<SubscriberTransport, SubscriberError> {
18172 match mode {
18173 SubscriberMode::PubSub => {
18174 #[cfg(feature = "reactive-ws")]
18175 {
18176 Ok(SubscriberTransport::PubSub)
18177 }
18178 #[cfg(not(feature = "reactive-ws"))]
18179 {
18180 Err(SubscriberError::Unsupported(
18181 "AlloySubscriber pubsub mode requires the reactive-ws feature",
18182 ))
18183 }
18184 }
18185 SubscriberMode::Polling => {
18186 #[cfg(feature = "reactive-polling")]
18187 {
18188 Ok(SubscriberTransport::Polling)
18189 }
18190 #[cfg(not(feature = "reactive-polling"))]
18191 {
18192 Err(SubscriberError::Unsupported(
18193 "AlloySubscriber polling mode requires the reactive-polling feature",
18194 ))
18195 }
18196 }
18197 SubscriberMode::Auto => resolve_auto_subscriber_transport(),
18198 }
18199}
18200
18201fn resolve_auto_subscriber_transport() -> Result<SubscriberTransport, SubscriberError> {
18202 #[cfg(feature = "reactive-ws")]
18203 {
18204 Ok(SubscriberTransport::PubSub)
18205 }
18206
18207 #[cfg(all(not(feature = "reactive-ws"), feature = "reactive-polling"))]
18208 {
18209 Ok(SubscriberTransport::Polling)
18210 }
18211
18212 #[cfg(not(any(feature = "reactive-ws", feature = "reactive-polling")))]
18213 {
18214 Err(SubscriberError::Unsupported(
18215 "AlloySubscriber requires either reactive-ws or reactive-polling",
18216 ))
18217 }
18218}
18219
18220fn validate_subscriber_config(config: &SubscriberConfig) -> Result<(), SubscriberError> {
18221 if config.preconfirmations != PreconfirmationMode::Disabled
18222 && config.flashblock_poll_interval.is_zero()
18223 {
18224 return Err(SubscriberError::InvalidConfig(
18225 "SubscriberConfig::flashblock_poll_interval must be greater than zero",
18226 ));
18227 }
18228 if config.max_batch_size == 0 {
18229 return Err(SubscriberError::InvalidConfig(
18230 "SubscriberConfig::max_batch_size must be greater than zero",
18231 ));
18232 }
18233 if config.max_log_addresses_per_subscription == 0 {
18234 return Err(SubscriberError::InvalidConfig(
18235 "SubscriberConfig::max_log_addresses_per_subscription must be greater than zero",
18236 ));
18237 }
18238 if config.max_pending_records == 0 {
18239 return Err(SubscriberError::InvalidConfig(
18240 "SubscriberConfig::max_pending_records must be greater than zero",
18241 ));
18242 }
18243 if config.max_pending_backfills == 0 {
18244 return Err(SubscriberError::InvalidConfig(
18245 "SubscriberConfig::max_pending_backfills must be greater than zero",
18246 ));
18247 }
18248 if config.max_backfill_log_bytes == 0 {
18249 return Err(SubscriberError::InvalidConfig(
18250 "SubscriberConfig::max_backfill_log_bytes must be greater than zero",
18251 ));
18252 }
18253 if config.max_reconcile_requests_in_flight == 0 {
18254 return Err(SubscriberError::InvalidConfig(
18255 "SubscriberConfig::max_reconcile_requests_in_flight must be greater than zero",
18256 ));
18257 }
18258 if config.reconnect.enabled {
18259 if config.reconnect.retry_delay > config.reconnect.max_delay {
18260 return Err(SubscriberError::InvalidConfig(
18261 "SubscriberReconnectConfig::retry_delay must be less than or equal to max_delay",
18262 ));
18263 }
18264 if matches!(config.reconnect.max_attempts, Some(0)) {
18265 return Err(SubscriberError::InvalidConfig(
18266 "SubscriberReconnectConfig::max_attempts must be greater than zero when set",
18267 ));
18268 }
18269 }
18270 Ok(())
18271}
18272
18273fn validate_supported_interests<N: Network>(
18274 mode: SubscriberMode,
18275 config: &SubscriberConfig,
18276 interests: &[ReactiveInterest<N>],
18277) -> Result<(), SubscriberError> {
18278 let transport = resolve_subscriber_transport(mode)?;
18279
18280 for interest in interests {
18281 match interest {
18282 ReactiveInterest::Logs(_) => {}
18283 ReactiveInterest::PendingTransactions(interest)
18284 if !config.hydrate_pending_transactions && interest.matches_hash_only() => {}
18285 ReactiveInterest::PendingTransactions(_) => {
18286 return Err(SubscriberError::Unsupported(
18287 "AlloySubscriber currently supports pending transaction hash interests only (full pending-tx hydration is unimplemented)",
18288 ));
18289 }
18290 ReactiveInterest::Blocks(interest) => match (transport, interest.mode) {
18291 (SubscriberTransport::PubSub, BlockInterestMode::Header) => {}
18292 (_, BlockInterestMode::FullBlock) => {
18293 return Err(SubscriberError::Unsupported(
18294 "AlloySubscriber full block streams are not implemented in this transport slice",
18295 ));
18296 }
18297 (SubscriberTransport::Polling, BlockInterestMode::Header) => {
18298 return Err(SubscriberError::Unsupported(
18299 "AlloySubscriber polling block streams are not implemented in this transport slice",
18300 ));
18301 }
18302 },
18303 }
18304 }
18305
18306 Ok(())
18307}
18308
18309fn log_filters<N: Network>(interests: &[ReactiveInterest<N>]) -> Vec<Filter> {
18310 let mut filters = Vec::new();
18311 for interest in interests {
18312 if let ReactiveInterest::Logs(interest) = interest {
18313 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
18314 }
18315 }
18316 filters
18317}
18318
18319fn needs_header_block_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
18320 interests.iter().any(|interest| {
18321 matches!(
18322 interest,
18323 ReactiveInterest::Blocks(BlockInterest {
18324 mode: BlockInterestMode::Header,
18325 })
18326 )
18327 })
18328}
18329
18330fn needs_pending_hash_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
18331 interests.iter().any(|interest| {
18332 matches!(
18333 interest,
18334 ReactiveInterest::PendingTransactions(interest) if interest.matches_hash_only()
18335 )
18336 })
18337}
18338
18339fn log_matches_any_interest<N: Network>(log: &Log, interests: &[ReactiveInterest<N>]) -> bool {
18340 interests.iter().any(|interest| {
18341 matches!(
18342 interest,
18343 ReactiveInterest::Logs(interest) if interest.matches(log)
18344 )
18345 })
18346}
18347
18348fn validate_owner_backfill_logs(
18349 logs: &[Log],
18350 from_block: u64,
18351 through: &BlockRef,
18352) -> Result<(), SubscriberOwnerError> {
18353 for log in logs {
18354 if log.removed {
18355 return Err(SubscriberOwnerError::InvalidBackfillLog(
18356 "removed log in canonical catch-up",
18357 ));
18358 }
18359 let number = log
18360 .block_number
18361 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
18362 "log missing block number",
18363 ))?;
18364 let hash = log
18365 .block_hash
18366 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
18367 "log missing block hash",
18368 ))?;
18369 log.transaction_hash
18370 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
18371 "log missing transaction hash",
18372 ))?;
18373 log.transaction_index
18374 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
18375 "log missing transaction index",
18376 ))?;
18377 log.log_index
18378 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
18379 "log missing log index",
18380 ))?;
18381 if number < from_block || number > through.number {
18382 return Err(SubscriberOwnerError::InvalidBackfillLog(
18383 "log outside requested block range",
18384 ));
18385 }
18386 if number == through.number && hash != through.hash {
18387 return Err(SubscriberOwnerError::InvalidBackfillLog(
18388 "target-block log hash mismatch",
18389 ));
18390 }
18391 }
18392 Ok(())
18393}
18394
18395fn validate_backfill_resource_limits(
18396 logs: &[Log],
18397 max_logs: usize,
18398 max_log_bytes: usize,
18399) -> Result<usize, SubscriberError> {
18400 if logs.len() > max_logs {
18401 return Err(SubscriberError::ResourceExhausted(format!(
18402 "historical response returned {} logs, above the configured limit of {max_logs}",
18403 logs.len()
18404 )));
18405 }
18406 let bytes = logs.iter().fold(0usize, |total, log| {
18407 let fixed = 20usize + (32 * 3) + (8 * 4) + 1;
18411 total
18412 .saturating_add(fixed)
18413 .saturating_add(log.topics().len().saturating_mul(32))
18414 .saturating_add(log.inner.data.data.len())
18415 });
18416 if bytes > max_log_bytes {
18417 return Err(SubscriberError::ResourceExhausted(format!(
18418 "historical response retained approximately {bytes} log bytes, above the configured limit of {max_log_bytes}"
18419 )));
18420 }
18421 Ok(bytes)
18422}
18423
18424async fn fetch_provider_block_ref<P, N>(
18425 provider: &P,
18426 number: u64,
18427) -> Result<BlockRef, SubscriberError>
18428where
18429 P: Provider<N> + Send + Sync,
18430 N: Network,
18431{
18432 let block = provider
18433 .get_block_by_number(BlockNumberOrTag::Number(number))
18434 .await
18435 .map_err(provider_error)?
18436 .ok_or_else(|| {
18437 SubscriberError::InvalidBackfill(format!(
18438 "canonical target block {number} is unavailable"
18439 ))
18440 })?;
18441 let header = block.header();
18442 Ok(BlockRef {
18443 number: header.number(),
18444 hash: header.hash(),
18445 parent_hash: Some(header.parent_hash()),
18446 timestamp: Some(header.timestamp()),
18447 })
18448}
18449
18450fn block_ref_satisfies_expected(actual: &BlockRef, expected: &BlockRef) -> bool {
18451 actual.number == expected.number
18452 && actual.hash == expected.hash
18453 && optional_metadata_compatible(actual.parent_hash.as_ref(), expected.parent_hash.as_ref())
18454 && optional_metadata_compatible(actual.timestamp.as_ref(), expected.timestamp.as_ref())
18455}
18456
18457fn validate_owner_backfill_log_set(logs: &[Log]) -> Result<(), SubscriberOwnerError> {
18458 let mut positions = HashMap::new();
18459 let mut block_hashes = HashMap::new();
18460 let mut transaction_hashes = HashMap::new();
18461 let mut transaction_positions = HashMap::new();
18462 let mut ordering = BTreeMap::<u64, Vec<(u64, u64)>>::new();
18463 for log in logs {
18464 let number = log
18465 .block_number
18466 .expect("individual owner catch-up logs are validated before set validation");
18467 let block_hash = log
18468 .block_hash
18469 .expect("individual owner catch-up logs are validated before set validation");
18470 let transaction_hash = log
18471 .transaction_hash
18472 .expect("individual owner catch-up logs are validated before set validation");
18473 let transaction_index = log
18474 .transaction_index
18475 .expect("individual owner catch-up logs are validated before set validation");
18476 let log_index = log
18477 .log_index
18478 .expect("individual owner catch-up logs are validated before set validation");
18479 if block_hashes
18480 .insert(number, block_hash)
18481 .is_some_and(|prior| prior != block_hash)
18482 {
18483 return Err(SubscriberOwnerError::InvalidBackfillLog(
18484 "conflicting block identity in canonical catch-up",
18485 ));
18486 }
18487 if let Some(previous) = positions.insert((number, log_index), log)
18488 && previous != log
18489 {
18490 return Err(SubscriberOwnerError::InvalidBackfillLog(
18491 "conflicting logs at one canonical block position",
18492 ));
18493 }
18494 let conflicting_transaction = transaction_hashes
18495 .insert((number, transaction_index), transaction_hash)
18496 .is_some_and(|prior| prior != transaction_hash)
18497 || transaction_positions
18498 .insert((number, transaction_hash), transaction_index)
18499 .is_some_and(|prior| prior != transaction_index);
18500 if conflicting_transaction {
18501 return Err(SubscriberOwnerError::InvalidBackfillLog(
18502 "conflicting transaction identity at one canonical block position",
18503 ));
18504 }
18505 ordering
18506 .entry(number)
18507 .or_default()
18508 .push((log_index, transaction_index));
18509 }
18510 for positions in ordering.values_mut() {
18511 positions.sort_unstable();
18512 if positions.windows(2).any(|pair| pair[0].1 > pair[1].1) {
18513 return Err(SubscriberOwnerError::InvalidBackfillLog(
18514 "transaction and log positions disagree on canonical order",
18515 ));
18516 }
18517 }
18518 Ok(())
18519}
18520
18521fn merged_owner_reconcile_filters<N: Network>(
18522 plans: &[SubscriberOwnerReconcilePlan<N>],
18523 through: u64,
18524) -> Vec<SubscriberOwnerReconcileFilter> {
18525 let mut by_start = BTreeMap::<u64, Vec<Filter>>::new();
18526 for plan in plans.iter().filter(|plan| plan.from_block <= through) {
18527 let filters = by_start.entry(plan.from_block).or_default();
18528 filters.extend(
18529 log_filters(&plan.interests)
18530 .into_iter()
18531 .map(|filter| filter.from_block(plan.from_block).to_block(through)),
18532 );
18533 }
18534
18535 let mut chunks = Vec::new();
18536 for (from_block, filters) in by_start {
18537 for filters in filters.chunks(OWNER_RECONCILE_FILTERS_PER_CHUNK) {
18538 let mut merged = Vec::new();
18539 for filter in filters {
18540 merge_log_subscription_filter(&mut merged, filter);
18541 }
18542 chunks.extend(
18543 merged
18544 .into_iter()
18545 .map(|filter| SubscriberOwnerReconcileFilter { filter, from_block }),
18546 );
18547 }
18548 }
18549 chunks
18550}
18551
18552fn merged_lazy_backfill_filters(
18553 filters: &[Filter],
18554 from_block: u64,
18555 through: u64,
18556) -> Vec<SubscriberOwnerReconcileFilter> {
18557 let mut requests = Vec::new();
18558 for filters in filters.chunks(OWNER_RECONCILE_FILTERS_PER_CHUNK) {
18559 let mut merged = Vec::new();
18560 for filter in filters {
18561 merge_log_subscription_filter(
18562 &mut merged,
18563 &filter.clone().from_block(from_block).to_block(through),
18564 );
18565 }
18566 requests.extend(
18567 merged
18568 .into_iter()
18569 .map(|filter| SubscriberOwnerReconcileFilter { filter, from_block }),
18570 );
18571 }
18572 requests
18573}
18574
18575fn lazy_backfill_error(error: SubscriberOwnerError) -> SubscriberError {
18576 match error {
18577 SubscriberOwnerError::Subscriber(error) => error,
18578 error => SubscriberError::InvalidBackfill(error.to_string()),
18579 }
18580}
18581
18582fn global_backfill_barrier(backfill: SubscriberBackfill, certified: BlockRef) -> ChainControl {
18583 let mut id = b"alloy-global-backfill-v1".to_vec();
18584 id.extend_from_slice(&backfill.start_block().to_be_bytes());
18585 id.extend_from_slice(&certified.number.to_be_bytes());
18586 id.extend_from_slice(certified.hash.as_slice());
18587 ChainControl::Barrier {
18588 id,
18589 block: Some(certified),
18590 }
18591}
18592
18593async fn fetch_owner_catchup<P, N>(
18594 provider: P,
18595 filters: Vec<SubscriberOwnerReconcileFilter>,
18596 retained: Vec<BlockRef>,
18597 through: BlockRef,
18598 options: SubscriberOwnerCatchupOptions,
18599) -> Result<SubscriberOwnerCatchup, SubscriberOwnerError>
18600where
18601 P: Provider<N> + Send + Sync,
18602 N: Network,
18603{
18604 if !options.target_preverified {
18605 let _ = verify_provider_reconcile_target::<P, N>(&provider, &through).await?;
18606 }
18607 let mut certified_positions = HashSet::new();
18608 for position in retained {
18609 let target_certifies_position = position == through
18610 || (position.number.checked_add(1) == Some(through.number)
18611 && through.parent_hash == Some(position.hash));
18612 if !target_certifies_position && certified_positions.insert(position) {
18613 let _ = verify_provider_reconcile_target::<P, N>(&provider, &position).await?;
18614 }
18615 }
18616 let mut logs = Vec::new();
18617 let mut total_log_bytes = 0usize;
18618 let requests = stream::iter(filters.into_iter().map(|filter| {
18619 let provider = &provider;
18620 async move {
18621 let logs = provider
18622 .get_logs(&filter.filter)
18623 .await
18624 .map_err(provider_error)?;
18625 Ok::<_, SubscriberOwnerError>((filter.from_block, logs))
18626 }
18627 }))
18628 .buffer_unordered(options.max_requests_in_flight);
18629 futures::pin_mut!(requests);
18630 while let Some(result) = requests.next().await {
18631 let (from_block, fetched) = result?;
18632 let fetched_bytes =
18633 validate_backfill_resource_limits(&fetched, options.max_logs, options.max_log_bytes)?;
18634 validate_owner_backfill_logs(&fetched, from_block, &through)?;
18635 if logs.len().saturating_add(fetched.len()) > options.max_logs {
18636 return Err(SubscriberError::ResourceExhausted(format!(
18637 "bulk reconcile returned more than {} logs",
18638 options.max_logs
18639 ))
18640 .into());
18641 }
18642 total_log_bytes = total_log_bytes.saturating_add(fetched_bytes);
18643 if total_log_bytes > options.max_log_bytes {
18644 return Err(SubscriberError::ResourceExhausted(format!(
18645 "bulk reconcile retained approximately {total_log_bytes} log bytes, above the configured limit of {}",
18646 options.max_log_bytes
18647 ))
18648 .into());
18649 }
18650 logs.extend(fetched);
18651 }
18652 validate_owner_backfill_log_set(&logs)?;
18653 let certified = verify_provider_reconcile_target::<P, N>(&provider, &through).await?;
18654 Ok(SubscriberOwnerCatchup { logs, certified })
18655}
18656
18657async fn verify_provider_reconcile_target<P, N>(
18658 provider: &P,
18659 expected: &BlockRef,
18660) -> Result<BlockRef, SubscriberOwnerError>
18661where
18662 P: Provider<N> + Send + Sync,
18663 N: Network,
18664{
18665 let block = provider
18666 .get_block_by_number(BlockNumberOrTag::Number(expected.number))
18667 .await
18668 .map_err(provider_error)?
18669 .ok_or(SubscriberOwnerError::BlockUnavailable(expected.number))?;
18670 let header = block.header();
18671 let actual = BlockRef {
18672 number: header.number(),
18673 hash: header.hash(),
18674 parent_hash: Some(header.parent_hash()),
18675 timestamp: Some(header.timestamp()),
18676 };
18677 let exact_parent = expected
18678 .parent_hash
18679 .is_none_or(|parent| Some(parent) == actual.parent_hash);
18680 let exact_timestamp = expected
18681 .timestamp
18682 .is_none_or(|timestamp| Some(timestamp) == actual.timestamp);
18683 if actual.number != expected.number
18684 || actual.hash != expected.hash
18685 || !exact_parent
18686 || !exact_timestamp
18687 {
18688 return Err(SubscriberOwnerError::BlockMismatch {
18689 expected_number: expected.number,
18690 expected_hash: expected.hash,
18691 actual_number: actual.number,
18692 actual_hash: actual.hash,
18693 });
18694 }
18695 Ok(actual)
18696}
18697
18698fn log_input_record<N: Network>(log: Log, source: InputSource) -> ReactiveInputRecord<N> {
18699 let context = log_reactive_context(&log);
18700 ReactiveInputRecord::new(
18701 ReactiveInput::Log(log),
18702 ReactiveContext { source, ..context },
18703 )
18704}
18705
18706fn preconfirmed_log_input_record<N: Network>(
18707 log: Log,
18708 flashblock: FlashblockRef,
18709) -> ReactiveInputRecord<N> {
18710 let block = flashblock.block_ref();
18711 let provider = flashblock.provider.clone();
18712 ReactiveInputRecord::new(
18713 ReactiveInput::Log(log.clone()),
18714 ReactiveContext {
18715 chain_id: None,
18716 source: InputSource::Flashblocks,
18717 chain_status: ChainStatus::Preconfirmed { flashblock },
18718 block: Some(block),
18719 transaction_index: log.transaction_index,
18720 log_index: log.log_index,
18721 },
18722 )
18723 .with_provider(provider)
18724}
18725
18726fn log_reactive_context(log: &Log) -> ReactiveContext {
18727 let block = match (log.block_hash, log.block_number) {
18728 (Some(hash), Some(number)) => Some(BlockRef {
18729 number,
18730 hash,
18731 parent_hash: None,
18732 timestamp: log.block_timestamp,
18733 }),
18734 _ => None,
18735 };
18736
18737 let chain_status = match (&block, log.removed) {
18738 (Some(block), true) => ChainStatus::Reorged {
18739 dropped_from: *block,
18740 },
18741 (Some(block), false) => ChainStatus::Included {
18742 block: *block,
18743 confirmations: 0,
18744 },
18745 (None, _) => ChainStatus::Pending,
18746 };
18747
18748 ReactiveContext {
18749 chain_id: None,
18750 source: InputSource::Poll,
18751 chain_status,
18752 block,
18753 transaction_index: log.transaction_index,
18754 log_index: log.log_index,
18755 }
18756}
18757
18758fn block_header_input_record<N>(header: N::HeaderResponse) -> ReactiveInputRecord<N>
18759where
18760 N: Network,
18761{
18762 let block = BlockRef {
18763 number: header.number(),
18764 hash: HeaderResponseTrait::hash(&header),
18765 parent_hash: Some(header.parent_hash()),
18766 timestamp: Some(header.timestamp()),
18767 };
18768 ReactiveInputRecord::new(
18769 ReactiveInput::BlockHeader(header),
18770 ReactiveContext {
18771 chain_id: None,
18772 source: InputSource::Subscription,
18773 chain_status: ChainStatus::Included {
18774 block,
18775 confirmations: 0,
18776 },
18777 block: Some(block),
18778 transaction_index: None,
18779 log_index: None,
18780 },
18781 )
18782}
18783
18784fn pending_hash_input_record<N: Network>(
18785 hash: B256,
18786 source: InputSource,
18787) -> ReactiveInputRecord<N> {
18788 ReactiveInputRecord::new(
18789 ReactiveInput::PendingTxHash(hash),
18790 ReactiveContext {
18791 chain_id: None,
18792 source,
18793 chain_status: ChainStatus::Pending,
18794 block: None,
18795 transaction_index: None,
18796 log_index: None,
18797 },
18798 )
18799}
18800
18801#[cfg(feature = "reactive-ws")]
18802fn base_pending_log_filter(filter: &Filter) -> Result<serde_json::Value, SubscriberError> {
18803 let encoded = serde_json::to_value(filter)
18804 .map_err(|error| SubscriberError::Provider(error.to_string()))?;
18805 let serde_json::Value::Object(mut fields) = encoded else {
18806 return Err(SubscriberError::Provider(
18807 "Alloy log filter did not serialize as an object".into(),
18808 ));
18809 };
18810 fields.retain(|key, _| key == "address" || key == "topics");
18811 Ok(serde_json::Value::Object(fields))
18812}
18813
18814fn provider_error(error: impl fmt::Display) -> SubscriberError {
18815 SubscriberError::Provider(error.to_string())
18816}
18817
18818#[derive(Debug, thiserror::Error)]
18820#[non_exhaustive]
18821pub enum SubscriberError {
18822 #[error("{0}")]
18824 InvalidConfig(&'static str),
18825 #[error("{0}")]
18827 Unsupported(&'static str),
18828 #[error("provider error: {0}")]
18830 Provider(String),
18831 #[error("invalid canonical backfill: {0}")]
18834 InvalidBackfill(String),
18835 #[error("subscriber resource limit exceeded: {0}")]
18837 ResourceExhausted(String),
18838}