1use std::{
15 any::Any,
16 borrow::Cow,
17 collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
18 fmt,
19 future::Future,
20 hash::Hash,
21 marker::PhantomData,
22 num::NonZeroU64,
23 path::PathBuf,
24 pin::Pin,
25 sync::{
26 Arc,
27 atomic::{AtomicU64, Ordering},
28 },
29 time::{Duration, Instant},
30};
31
32use alloy_consensus::{BlockHeader as _, Transaction as _};
33use alloy_eips::{BlockId, BlockNumberOrTag};
34use alloy_network::{
35 Ethereum, Network,
36 primitives::{
37 BlockResponse as _, HeaderResponse as HeaderResponseTrait,
38 TransactionResponse as TransactionResponseTrait,
39 },
40};
41use alloy_primitives::{Address, B256, Bytes, FixedBytes, Keccak256, U256};
42use alloy_provider::{Provider, RootProvider};
43use alloy_rpc_client::BatchRequest;
44use alloy_rpc_types_eth::{Filter, FilterSet, Log};
45pub use alloy_transport_balancer::EndpointId;
46use bincode::Options;
47use futures::{StreamExt, stream};
48use futures::{
49 future::{Either, poll_fn, select},
50 stream::{BoxStream, FuturesUnordered},
51};
52
53use crate::{
54 cache::{
55 AccountProof, BlockStateDiff, DurableCheckpointBlock, DurableCheckpointError,
56 DurableCheckpointIdentity, DurableCheckpointMetadata, DurableCheckpointStore, EvmCache,
57 EvmCacheStateSnapshot, LoadedDurableCheckpoint,
58 },
59 errors::{BlockContextError, StorageFetchResult},
60 events::{EventDecoder, StateView},
61 freshness::FreshnessRegistry,
62 state_update::{AccountPatch, PurgeScope, StateDiff, StateUpdate},
63};
64
65#[derive(Clone, Debug, PartialEq, Eq)]
67pub enum ReactiveInput<N: Network = Ethereum> {
68 Log(Log),
70 BlockHeader(N::HeaderResponse),
72 FullBlock(N::BlockResponse),
74 PendingTxHash(B256),
76 PendingTx(N::TransactionResponse),
78}
79
80#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
82pub struct ReactiveContext {
83 pub chain_id: Option<u64>,
85 pub source: InputSource,
87 pub chain_status: ChainStatus,
89 pub block: Option<BlockRef>,
91 pub transaction_index: Option<u64>,
93 pub log_index: Option<u64>,
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
99pub struct BlockRef {
100 pub number: u64,
102 pub hash: B256,
104 pub parent_hash: Option<B256>,
106 pub timestamp: Option<u64>,
108}
109
110#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
117pub struct ProviderRef {
118 pub endpoint: EndpointId,
120 pub generation: u64,
122}
123
124impl ProviderRef {
125 pub fn new(endpoint: impl Into<EndpointId>, generation: u64) -> Self {
127 Self {
128 endpoint: endpoint.into(),
129 generation,
130 }
131 }
132}
133
134#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
136pub struct FlashblockRef {
137 pub provider: ProviderRef,
139 pub payload_id: Option<FixedBytes<8>>,
143 pub index: Option<u64>,
145 pub block_number: u64,
147 pub content_hash: B256,
153 pub partial_block_hash: Option<B256>,
155 pub parent_hash: Option<B256>,
157 pub state_root: Option<B256>,
159 pub transactions_root: Option<B256>,
161 pub transaction_hashes: Vec<B256>,
163 pub timestamp: Option<u64>,
165 pub base_fee_per_gas: Option<u64>,
167 pub beneficiary: Option<Address>,
169 pub prevrandao: Option<B256>,
171 pub gas_limit: Option<u64>,
173}
174
175impl FlashblockRef {
176 pub const fn block_ref(&self) -> BlockRef {
181 BlockRef {
182 number: self.block_number,
183 hash: self.content_hash,
184 parent_hash: self.parent_hash,
185 timestamp: self.timestamp,
186 }
187 }
188
189 pub fn contains_transaction(&self, transaction_hash: &B256) -> bool {
191 self.transaction_hashes.contains(transaction_hash)
192 }
193
194 fn transaction_index(&self, transaction_hash: &B256) -> Option<u64> {
195 self.transaction_hashes
196 .iter()
197 .position(|candidate| candidate == transaction_hash)
198 .and_then(|index| u64::try_from(index).ok())
199 }
200
201 fn same_payload(&self, other: &Self) -> bool {
202 self.provider == other.provider
203 && match (self.payload_id, other.payload_id) {
204 (Some(left), Some(right)) => left == right,
205 _ => {
206 self.block_number == other.block_number && self.parent_hash == other.parent_hash
207 }
208 }
209 }
210
211 fn is_cumulative_successor_of(&self, previous: &Self) -> bool {
212 self.same_payload(previous)
213 && self.transaction_hashes.len() >= previous.transaction_hashes.len()
214 && self
215 .transaction_hashes
216 .starts_with(&previous.transaction_hashes)
217 && match (previous.index, self.index) {
218 (Some(previous), Some(current)) => current >= previous,
219 _ => true,
220 }
221 }
222}
223
224#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
226pub enum PreconfirmationMode {
227 #[default]
229 Disabled,
230 Preferred,
233 Required,
235}
236
237#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
239pub struct BaseFlashblockPayload {
240 pub payload_id: FixedBytes<8>,
242 pub index: u64,
244 pub base: Option<BaseFlashblockBase>,
246 pub diff: BaseFlashblockDiff,
248 #[serde(default)]
250 pub metadata: Option<BaseFlashblockMetadata>,
251}
252
253#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
255pub struct BaseFlashblockBase {
256 pub parent_hash: B256,
258 #[serde(deserialize_with = "deserialize_rpc_u64")]
260 pub block_number: u64,
261 #[serde(deserialize_with = "deserialize_rpc_u64")]
263 pub timestamp: u64,
264 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
266 pub gas_limit: Option<u64>,
267 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
269 pub base_fee_per_gas: Option<u64>,
270 #[serde(default, alias = "fee_recipient", alias = "feeRecipient")]
272 pub beneficiary: Option<Address>,
273 #[serde(
275 default,
276 alias = "prev_randao",
277 alias = "prevRandao",
278 alias = "mixHash"
279 )]
280 pub prevrandao: Option<B256>,
281}
282
283#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
285pub struct BaseFlashblockDiff {
286 pub state_root: B256,
288 pub block_hash: B256,
290 #[serde(default)]
292 pub transactions: Vec<serde_json::Value>,
293 #[serde(default)]
295 pub transactions_root: Option<B256>,
296}
297
298#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
301pub struct BaseFlashblockMetadata {
302 #[serde(deserialize_with = "deserialize_rpc_u64")]
304 pub block_number: u64,
305}
306
307#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
310#[serde(rename_all = "camelCase")]
311struct BaseFlashblockBlockPayload {
312 hash: B256,
313 #[serde(deserialize_with = "deserialize_rpc_u64")]
314 number: u64,
315 parent_hash: B256,
316 state_root: B256,
317 #[serde(default)]
318 transactions_root: Option<B256>,
319 #[serde(default)]
320 transactions: Vec<serde_json::Value>,
321 #[serde(deserialize_with = "deserialize_rpc_u64")]
322 timestamp: u64,
323 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
324 base_fee_per_gas: Option<u64>,
325 #[serde(default, alias = "beneficiary", alias = "feeRecipient")]
326 miner: Option<Address>,
327 #[serde(default, alias = "prevRandao")]
328 mix_hash: Option<B256>,
329 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
330 gas_limit: Option<u64>,
331}
332
333#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
337#[serde(untagged)]
338enum BaseFlashblockWirePayload {
339 Indexed(BaseFlashblockPayload),
340 Block(BaseFlashblockBlockPayload),
341}
342
343fn deserialize_rpc_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
344where
345 D: serde::Deserializer<'de>,
346{
347 #[derive(serde::Deserialize)]
348 #[serde(untagged)]
349 enum RpcU64 {
350 Number(u64),
351 String(String),
352 }
353
354 match <RpcU64 as serde::Deserialize>::deserialize(deserializer)? {
355 RpcU64::Number(number) => Ok(number),
356 RpcU64::String(value) => {
357 let value = value.strip_prefix("0x").unwrap_or(&value);
358 u64::from_str_radix(value, 16).map_err(serde::de::Error::custom)
359 }
360 }
361}
362
363fn deserialize_optional_rpc_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
364where
365 D: serde::Deserializer<'de>,
366{
367 #[derive(serde::Deserialize)]
368 #[serde(untagged)]
369 enum RpcU64 {
370 Number(u64),
371 String(String),
372 }
373
374 let Some(value) = <Option<RpcU64> as serde::Deserialize>::deserialize(deserializer)? else {
375 return Ok(None);
376 };
377 match value {
378 RpcU64::Number(number) => Ok(Some(number)),
379 RpcU64::String(value) => {
380 let value = value.strip_prefix("0x").unwrap_or(&value);
381 u64::from_str_radix(value, 16)
382 .map(Some)
383 .map_err(serde::de::Error::custom)
384 }
385 }
386}
387
388fn non_placeholder_hash(hash: B256) -> Option<B256> {
389 (!hash.is_zero()).then_some(hash)
390}
391
392fn flashblock_transaction_hashes(
393 transactions: &[serde_json::Value],
394) -> Result<Vec<B256>, SubscriberError> {
395 let hashes: Vec<B256> = transactions
396 .iter()
397 .map(|transaction| {
398 let value = match transaction {
399 serde_json::Value::String(value) => value.as_str(),
400 serde_json::Value::Object(object) => object
401 .get("hash")
402 .or_else(|| object.get("transactionHash"))
403 .and_then(serde_json::Value::as_str)
404 .ok_or_else(|| {
405 SubscriberError::Provider(
406 "Flashblock transaction object is missing its hash".into(),
407 )
408 })?,
409 _ => {
410 return Err(SubscriberError::Provider(
411 "Flashblock transaction must be a hash, raw transaction, or object".into(),
412 ));
413 }
414 };
415 if value.len() == 66 {
416 return value.parse::<B256>().map_err(|error| {
417 SubscriberError::Provider(format!(
418 "Flashblock transaction hash is invalid: {error}"
419 ))
420 });
421 }
422 let encoded = value.strip_prefix("0x").unwrap_or(value);
423 let raw = alloy_primitives::hex::decode(encoded).map_err(|error| {
424 SubscriberError::Provider(format!(
425 "Flashblock raw transaction is invalid hex: {error}"
426 ))
427 })?;
428 Ok(alloy_primitives::keccak256(raw))
429 })
430 .collect::<Result<_, _>>()?;
431 let mut unique = HashSet::with_capacity(hashes.len());
432 if hashes.iter().any(|hash| !unique.insert(*hash)) {
433 return Err(SubscriberError::Provider(
434 "Flashblock cumulative transaction membership contains a duplicate hash".into(),
435 ));
436 }
437 Ok(hashes)
438}
439
440struct FlashblockContentCommitment<'a> {
441 provider: &'a ProviderRef,
442 payload_id: Option<FixedBytes<8>>,
443 index: Option<u64>,
444 block_number: u64,
445 partial_block_hash: Option<B256>,
446 parent_hash: Option<B256>,
447 state_root: Option<B256>,
448 transactions_root: Option<B256>,
449 transaction_hashes: &'a [B256],
450 timestamp: Option<u64>,
451 base_fee_per_gas: Option<u64>,
452 beneficiary: Option<Address>,
453 prevrandao: Option<B256>,
454 gas_limit: Option<u64>,
455}
456
457fn flashblock_content_hash(content: FlashblockContentCommitment<'_>) -> B256 {
458 let mut commitment = Keccak256::new();
459 commitment.update(b"evm-fork-cache/flashblock-content/v1");
460 let endpoint = content.provider.endpoint.as_str().as_bytes();
461 commitment.update((endpoint.len() as u64).to_be_bytes());
462 commitment.update(endpoint);
463 commitment.update(content.provider.generation.to_be_bytes());
464 commitment.update(content.block_number.to_be_bytes());
465 commit_optional_bytes(
466 &mut commitment,
467 content.payload_id.as_ref().map(FixedBytes::as_slice),
468 );
469 commit_optional_u64(&mut commitment, content.index);
470 commit_optional_bytes(
471 &mut commitment,
472 content
473 .partial_block_hash
474 .as_ref()
475 .map(FixedBytes::as_slice),
476 );
477 commit_optional_bytes(
478 &mut commitment,
479 content.parent_hash.as_ref().map(FixedBytes::as_slice),
480 );
481 commit_optional_bytes(
482 &mut commitment,
483 content.state_root.as_ref().map(FixedBytes::as_slice),
484 );
485 commit_optional_bytes(
486 &mut commitment,
487 content.transactions_root.as_ref().map(FixedBytes::as_slice),
488 );
489 commitment.update((content.transaction_hashes.len() as u64).to_be_bytes());
490 for transaction_hash in content.transaction_hashes {
491 commitment.update(transaction_hash);
492 }
493 commit_optional_u64(&mut commitment, content.timestamp);
494 commit_optional_u64(&mut commitment, content.base_fee_per_gas);
495 commit_optional_bytes(
496 &mut commitment,
497 content
498 .beneficiary
499 .as_ref()
500 .map(|address| address.as_slice()),
501 );
502 commit_optional_bytes(
503 &mut commitment,
504 content.prevrandao.as_ref().map(FixedBytes::as_slice),
505 );
506 commit_optional_u64(&mut commitment, content.gas_limit);
507 let hash = commitment.finalize();
508 if hash.is_zero() {
509 B256::with_last_byte(1)
510 } else {
511 hash
512 }
513}
514
515fn commit_optional_bytes(commitment: &mut Keccak256, value: Option<&[u8]>) {
516 match value {
517 Some(value) => {
518 commitment.update([1]);
519 commitment.update((value.len() as u64).to_be_bytes());
520 commitment.update(value);
521 }
522 None => commitment.update([0]),
523 }
524}
525
526fn commit_optional_u64(commitment: &mut Keccak256, value: Option<u64>) {
527 match value {
528 Some(value) => {
529 commitment.update([1]);
530 commitment.update(value.to_be_bytes());
531 }
532 None => commitment.update([0]),
533 }
534}
535
536#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
539pub struct ReactiveCanonicalBaseline {
540 pub chain_id: u64,
542 pub block: BlockRef,
544}
545
546impl ReactiveCanonicalBaseline {
547 pub const fn new(chain_id: u64, block: BlockRef) -> Self {
549 Self { chain_id, block }
550 }
551}
552
553#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
562#[non_exhaustive]
563pub enum ChainControl {
564 Reorg {
566 common_ancestor: BlockRef,
568 old_tip: BlockRef,
570 new_tip: BlockRef,
572 },
573 Safe(BlockRef),
575 Finalized(BlockRef),
577 CanonicalProgress(BlockRef),
584 Barrier {
586 id: Vec<u8>,
588 block: Option<BlockRef>,
590 },
591}
592
593#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
612pub struct CanonicalSequenceState {
613 retained_canonical_history: Vec<BlockRef>,
614 coverage_head: Option<BlockRef>,
615 safe_head: Option<BlockRef>,
616 finalized_head: Option<BlockRef>,
617}
618
619impl CanonicalSequenceState {
620 pub fn new(
626 retained_canonical_history: Vec<BlockRef>,
627 coverage_head: Option<BlockRef>,
628 safe_head: Option<BlockRef>,
629 finalized_head: Option<BlockRef>,
630 ) -> Self {
631 Self {
632 retained_canonical_history,
633 coverage_head,
634 safe_head,
635 finalized_head,
636 }
637 }
638
639 pub fn retained_canonical_history(&self) -> &[BlockRef] {
641 &self.retained_canonical_history
642 }
643
644 pub const fn coverage_head(&self) -> Option<&BlockRef> {
646 self.coverage_head.as_ref()
647 }
648
649 pub const fn safe_head(&self) -> Option<&BlockRef> {
651 self.safe_head.as_ref()
652 }
653
654 pub const fn finalized_head(&self) -> Option<&BlockRef> {
656 self.finalized_head.as_ref()
657 }
658
659 pub fn retain_recent_history(&mut self, max_entries: usize) {
668 let remove = self
669 .retained_canonical_history
670 .len()
671 .saturating_sub(max_entries);
672 self.retained_canonical_history.drain(..remove);
673 }
674
675 pub fn validate(&self) -> Result<(), ReactiveError> {
689 validate_canonical_sequence_snapshot(self)
690 }
691}
692
693#[derive(Clone, Debug, PartialEq, Eq)]
695#[non_exhaustive]
696pub enum CanonicalSequenceMutation {
697 Rewind {
699 common_ancestor: Option<BlockRef>,
705 dropped: Vec<BlockRef>,
707 },
708 Canonical(BlockRef),
710 Safe(BlockRef),
712 Finalized(BlockRef),
714}
715
716#[derive(Clone, Debug, PartialEq, Eq)]
718pub struct CanonicalSequenceValidation {
719 pre_record_state: CanonicalSequenceState,
720 next_state: CanonicalSequenceState,
721 mutations: Vec<CanonicalSequenceMutation>,
722 normalized_chain_controls: Vec<ChainControl>,
723}
724
725impl CanonicalSequenceValidation {
726 pub const fn pre_record_state(&self) -> &CanonicalSequenceState {
728 &self.pre_record_state
729 }
730
731 pub const fn next_state(&self) -> &CanonicalSequenceState {
733 &self.next_state
734 }
735
736 pub fn mutations(&self) -> &[CanonicalSequenceMutation] {
738 &self.mutations
739 }
740
741 pub fn normalized_chain_controls(&self) -> &[ChainControl] {
751 &self.normalized_chain_controls
752 }
753}
754
755#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
757#[non_exhaustive]
758pub enum ChainStatus {
759 Pending,
761 Preconfirmed {
766 flashblock: Arc<FlashblockRef>,
770 },
771 Included {
773 block: BlockRef,
775 confirmations: u64,
777 },
778 Safe {
780 block: BlockRef,
782 },
783 Finalized {
785 block: BlockRef,
787 },
788 Reorged {
790 dropped_from: BlockRef,
792 },
793}
794
795#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
797#[non_exhaustive]
798pub enum InputSource {
799 Batch,
801 Subscription,
803 Poll,
805 Backfill,
807 Flashblocks,
809 Synthetic,
811}
812
813#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
815pub enum InputRef {
816 Log {
818 chain_id: Option<u64>,
820 block_hash: B256,
822 transaction_hash: B256,
824 log_index: u64,
826 },
827 PendingTx {
829 chain_id: Option<u64>,
831 hash: B256,
833 },
834 Block {
836 chain_id: Option<u64>,
838 hash: B256,
840 number: u64,
842 },
843}
844
845#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
852#[non_exhaustive]
853pub enum ReactiveInputKind {
854 CanonicalLog,
856 ReorgSignalLog,
858 BlockHeader,
860 FullBlock,
862 PendingTxHash,
864 PendingTx,
866}
867
868#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
875pub struct ReactiveInputIdentity {
876 input_ref: InputRef,
877 kind: ReactiveInputKind,
878}
879
880impl ReactiveInputIdentity {
881 pub fn try_from_parts(
895 input_ref: InputRef,
896 kind: ReactiveInputKind,
897 ) -> Result<Self, ReactiveInputIdentityError> {
898 let compatible = matches!(
899 (input_ref, kind),
900 (
901 InputRef::Log { .. },
902 ReactiveInputKind::CanonicalLog | ReactiveInputKind::ReorgSignalLog
903 ) | (
904 InputRef::Block { .. },
905 ReactiveInputKind::BlockHeader | ReactiveInputKind::FullBlock
906 ) | (
907 InputRef::PendingTx { .. },
908 ReactiveInputKind::PendingTxHash | ReactiveInputKind::PendingTx
909 )
910 );
911 if !compatible {
912 return Err(ReactiveInputIdentityError { input_ref, kind });
913 }
914 Ok(Self { input_ref, kind })
915 }
916
917 pub const fn input_ref(&self) -> InputRef {
919 self.input_ref
920 }
921
922 pub const fn kind(&self) -> ReactiveInputKind {
924 self.kind
925 }
926}
927
928#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
931#[error("reactive input kind {kind:?} is incompatible with input reference {input_ref:?}")]
932pub struct ReactiveInputIdentityError {
933 input_ref: InputRef,
934 kind: ReactiveInputKind,
935}
936
937impl ReactiveInputIdentityError {
938 pub const fn input_ref(&self) -> InputRef {
940 self.input_ref
941 }
942
943 pub const fn kind(&self) -> ReactiveInputKind {
945 self.kind
946 }
947}
948
949#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
951pub enum StateEffectQuality {
952 ExactFromInput,
954 AppliedWithPendingResync,
956 ResyncedAuthoritatively,
958 RequiresRepair,
960 NoStateEffect,
962}
963
964#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize)]
966pub struct HandlerId(String);
967
968impl HandlerId {
969 pub fn new(id: impl Into<String>) -> Self {
976 Self::try_new(id).expect("handler id must not be empty")
977 }
978
979 pub fn try_new(id: impl Into<String>) -> Result<Self, HandlerIdError> {
986 let id = id.into();
987 if id.is_empty() {
988 return Err(HandlerIdError);
989 }
990 Ok(Self(id))
991 }
992
993 pub fn as_str(&self) -> &str {
995 &self.0
996 }
997}
998
999impl<'de> serde::Deserialize<'de> for HandlerId {
1000 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1001 where
1002 D: serde::Deserializer<'de>,
1003 {
1004 let id = <String as serde::Deserialize>::deserialize(deserializer)?;
1005 Self::try_new(id).map_err(serde::de::Error::custom)
1006 }
1007}
1008
1009#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
1012#[error("handler id must not be empty")]
1013pub struct HandlerIdError;
1014
1015impl fmt::Display for HandlerId {
1016 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1017 self.0.fmt(f)
1018 }
1019}
1020
1021#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1023pub struct ReportTag {
1024 pub key: String,
1026 pub value: String,
1028}
1029
1030impl ReportTag {
1031 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1033 Self {
1034 key: key.into(),
1035 value: value.into(),
1036 }
1037 }
1038}
1039
1040#[derive(Clone)]
1042pub struct HookSignal {
1043 pub namespace: Cow<'static, str>,
1045 pub kind: Cow<'static, str>,
1047 pub labels: Vec<ReportTag>,
1049 pub payload: Option<Arc<dyn Any + Send + Sync>>,
1051}
1052
1053impl fmt::Debug for HookSignal {
1054 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1055 f.debug_struct("HookSignal")
1056 .field("namespace", &self.namespace)
1057 .field("kind", &self.kind)
1058 .field("labels", &self.labels)
1059 .field("payload", &self.payload.as_ref().map(|_| "<payload>"))
1060 .finish()
1061 }
1062}
1063
1064#[derive(Clone, Debug)]
1066pub enum ReactiveEffect {
1067 StateUpdate(StateUpdate),
1069 Resync(ResyncRequest),
1071 Invalidate(InvalidationRequest),
1073 Hook(HookSignal),
1075 Speculative(SpeculativeRequest),
1077}
1078
1079#[derive(Clone, Debug)]
1081pub struct HandlerOutcome {
1082 pub effects: Vec<ReactiveEffect>,
1084 pub quality: StateEffectQuality,
1086 pub tags: Vec<ReportTag>,
1088}
1089
1090impl HandlerOutcome {
1091 pub fn empty(quality: StateEffectQuality) -> Self {
1093 Self {
1094 effects: Vec::new(),
1095 quality,
1096 tags: Vec::new(),
1097 }
1098 }
1099}
1100
1101#[derive(Clone, Debug)]
1103pub struct ReactiveInputRecord<N: Network = Ethereum> {
1104 pub input: ReactiveInput<N>,
1106 pub context: ReactiveContext,
1108 pub provider: Option<ProviderRef>,
1111}
1112
1113impl<N: Network> ReactiveInputRecord<N> {
1114 pub fn new(input: ReactiveInput<N>, context: ReactiveContext) -> Self {
1116 Self {
1117 input,
1118 context,
1119 provider: None,
1120 }
1121 }
1122
1123 #[must_use]
1125 pub fn with_provider(mut self, provider: ProviderRef) -> Self {
1126 self.provider = Some(provider);
1127 self
1128 }
1129
1130 pub fn input_ref(&self) -> InputRef {
1132 input_ref(&self.input, &self.context)
1133 }
1134
1135 pub fn validated_identity(&self) -> Result<ReactiveInputIdentity, ReactiveError> {
1152 validate_input_record(self)?;
1153 let kind = match &self.input {
1154 ReactiveInput::Log(log)
1155 if log.removed
1156 || matches!(self.context.chain_status, ChainStatus::Reorged { .. }) =>
1157 {
1158 ReactiveInputKind::ReorgSignalLog
1159 }
1160 ReactiveInput::Log(_) => ReactiveInputKind::CanonicalLog,
1161 ReactiveInput::BlockHeader(_) => ReactiveInputKind::BlockHeader,
1162 ReactiveInput::FullBlock(_) => ReactiveInputKind::FullBlock,
1163 ReactiveInput::PendingTxHash(_) => ReactiveInputKind::PendingTxHash,
1164 ReactiveInput::PendingTx(_) => ReactiveInputKind::PendingTx,
1165 };
1166 ReactiveInputIdentity::try_from_parts(self.input_ref(), kind).map_err(|error| {
1167 ReactiveError::InvalidInputRecord {
1168 message: error.to_string(),
1169 }
1170 })
1171 }
1172
1173 pub fn same_deduplicable_payload(&self, other: &Self) -> bool {
1187 match (&self.input, &other.input) {
1188 (ReactiveInput::Log(left), ReactiveInput::Log(right)) => {
1189 left.inner == right.inner
1190 && left.block_hash == right.block_hash
1191 && left.block_number == right.block_number
1192 && optional_metadata_compatible(
1193 left.block_timestamp.as_ref(),
1194 right.block_timestamp.as_ref(),
1195 )
1196 && left.transaction_hash == right.transaction_hash
1197 && left.transaction_index == right.transaction_index
1198 && left.log_index == right.log_index
1199 && left.removed == right.removed
1200 }
1201 (ReactiveInput::BlockHeader(left), ReactiveInput::BlockHeader(right)) => {
1202 left.hash() == right.hash()
1203 }
1204 (ReactiveInput::FullBlock(_), ReactiveInput::FullBlock(_)) => false,
1205 (ReactiveInput::PendingTxHash(left), ReactiveInput::PendingTxHash(right)) => {
1206 left == right
1207 }
1208 (ReactiveInput::PendingTx(_), ReactiveInput::PendingTx(_)) => false,
1209 _ => false,
1210 }
1211 }
1212
1213 pub fn is_payload_deduplicable(&self) -> bool {
1219 matches!(
1220 &self.input,
1221 ReactiveInput::Log(_) | ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_)
1222 )
1223 }
1224
1225 pub fn merge_compatible_duplicate(&mut self, other: &Self) -> Result<bool, ReactiveError> {
1240 let identity = self.validated_identity()?;
1241 let other_identity = other.validated_identity()?;
1242 if identity != other_identity
1243 || !self.is_payload_deduplicable()
1244 || !other.is_payload_deduplicable()
1245 {
1246 return Ok(false);
1247 }
1248 if !self.same_deduplicable_payload(other) || !self.dedupe_context_is_compatible(other) {
1249 return Err(ReactiveError::InvalidInputRecord {
1250 message: format!(
1251 "conflicting payload or semantic context for identity {identity:?}"
1252 ),
1253 });
1254 }
1255 let mut merged = self.clone();
1256 merge_deduplicable_record(&mut merged, other);
1257 merged.validated_identity()?;
1258 *self = merged;
1259 Ok(true)
1260 }
1261
1262 pub fn dedupe_context_is_compatible(&self, other: &Self) -> bool {
1270 let left = &self.context;
1271 let right = &other.context;
1272 left.chain_id == right.chain_id
1273 && optional_block_refs_are_compatible(left.block.as_ref(), right.block.as_ref())
1274 && left.transaction_index == right.transaction_index
1275 && left.log_index == right.log_index
1276 && chain_statuses_are_dedupe_compatible(&left.chain_status, &right.chain_status)
1277 }
1278}
1279
1280fn chain_statuses_are_dedupe_compatible(left: &ChainStatus, right: &ChainStatus) -> bool {
1281 match (left, right) {
1282 (ChainStatus::Pending, ChainStatus::Pending)
1283 | (ChainStatus::Reorged { .. }, ChainStatus::Reorged { .. }) => true,
1284 (
1285 ChainStatus::Preconfirmed { flashblock: left },
1286 ChainStatus::Preconfirmed { flashblock: right },
1287 ) => left == right,
1288 (
1289 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. },
1290 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. },
1291 ) => true,
1292 _ => false,
1293 }
1294}
1295
1296fn optional_metadata_compatible<T: PartialEq>(left: Option<&T>, right: Option<&T>) -> bool {
1297 left.zip(right).is_none_or(|(left, right)| left == right)
1298}
1299
1300fn optional_block_refs_are_compatible(left: Option<&BlockRef>, right: Option<&BlockRef>) -> bool {
1301 match (left, right) {
1302 (None, None) => true,
1303 (Some(left), Some(right)) => {
1304 left.number == right.number
1305 && left.hash == right.hash
1306 && optional_metadata_compatible(
1307 left.parent_hash.as_ref(),
1308 right.parent_hash.as_ref(),
1309 )
1310 && optional_metadata_compatible(left.timestamp.as_ref(), right.timestamp.as_ref())
1311 }
1312 _ => false,
1313 }
1314}
1315
1316fn merge_deduplicable_record<N: Network>(
1317 retained: &mut ReactiveInputRecord<N>,
1318 incoming: &ReactiveInputRecord<N>,
1319) {
1320 if let (ReactiveInput::Log(retained), ReactiveInput::Log(incoming)) =
1321 (&mut retained.input, &incoming.input)
1322 && retained.block_timestamp.is_none()
1323 {
1324 retained.block_timestamp = incoming.block_timestamp;
1325 }
1326 if let (Some(retained), Some(incoming)) =
1327 (&mut retained.context.block, incoming.context.block.as_ref())
1328 {
1329 enrich_block_ref(retained, incoming);
1330 }
1331 retained.context.chain_status = merged_chain_status(
1332 &retained.context.chain_status,
1333 &incoming.context.chain_status,
1334 );
1335 if input_source_rank(incoming.context.source) > input_source_rank(retained.context.source) {
1336 retained.context.source = incoming.context.source;
1337 }
1338 if retained.provider.is_none() {
1339 retained.provider = incoming.provider.clone();
1340 }
1341}
1342
1343fn enrich_block_ref(retained: &mut BlockRef, incoming: &BlockRef) {
1344 if retained.parent_hash.is_none() {
1345 retained.parent_hash = incoming.parent_hash;
1346 }
1347 if retained.timestamp.is_none() {
1348 retained.timestamp = incoming.timestamp;
1349 }
1350}
1351
1352fn merged_chain_status(retained: &ChainStatus, incoming: &ChainStatus) -> ChainStatus {
1353 let merged_block = |left: &BlockRef, right: &BlockRef| {
1354 let mut block = *left;
1355 enrich_block_ref(&mut block, right);
1356 block
1357 };
1358 match (retained, incoming) {
1359 (ChainStatus::Pending, ChainStatus::Pending) => ChainStatus::Pending,
1360 (
1361 ChainStatus::Preconfirmed { flashblock: left },
1362 ChainStatus::Preconfirmed { flashblock: right },
1363 ) => {
1364 debug_assert_eq!(left, right, "compatible pre-confirmed records agree");
1365 ChainStatus::Preconfirmed {
1366 flashblock: left.clone(),
1367 }
1368 }
1369 (
1370 ChainStatus::Reorged { dropped_from: left },
1371 ChainStatus::Reorged {
1372 dropped_from: right,
1373 },
1374 ) => ChainStatus::Reorged {
1375 dropped_from: merged_block(left, right),
1376 },
1377 (left, right) => {
1378 let (left_block, left_rank, left_confirmations) = canonical_status_parts(left)
1379 .expect("compatible duplicate has a canonical lifecycle");
1380 let (right_block, right_rank, right_confirmations) = canonical_status_parts(right)
1381 .expect("compatible duplicate has a canonical lifecycle");
1382 let block = merged_block(left_block, right_block);
1383 let rank = left_rank.max(right_rank);
1384 match rank {
1385 3 => ChainStatus::Finalized { block },
1386 2 => ChainStatus::Safe { block },
1387 _ => ChainStatus::Included {
1388 block,
1389 confirmations: left_confirmations.max(right_confirmations),
1390 },
1391 }
1392 }
1393 }
1394}
1395
1396fn canonical_status_parts(status: &ChainStatus) -> Option<(&BlockRef, u8, u64)> {
1397 match status {
1398 ChainStatus::Included {
1399 block,
1400 confirmations,
1401 } => Some((block, 1, *confirmations)),
1402 ChainStatus::Safe { block } => Some((block, 2, 0)),
1403 ChainStatus::Finalized { block } => Some((block, 3, 0)),
1404 ChainStatus::Pending | ChainStatus::Preconfirmed { .. } | ChainStatus::Reorged { .. } => {
1405 None
1406 }
1407 }
1408}
1409
1410fn input_source_rank(source: InputSource) -> u8 {
1411 match source {
1412 InputSource::Backfill => 0,
1413 InputSource::Poll => 1,
1414 InputSource::Subscription => 2,
1415 InputSource::Flashblocks => 3,
1416 InputSource::Batch => 4,
1417 InputSource::Synthetic => 5,
1418 }
1419}
1420
1421#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1431pub struct SubscriberDeliveryToken(Vec<u8>);
1432
1433impl SubscriberDeliveryToken {
1434 pub fn new(bytes: Vec<u8>) -> Self {
1436 Self(bytes)
1437 }
1438
1439 pub fn as_bytes(&self) -> &[u8] {
1441 &self.0
1442 }
1443
1444 pub fn into_bytes(self) -> Vec<u8> {
1446 self.0
1447 }
1448}
1449
1450#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1456pub struct SubscriberCheckpoint(Vec<u8>);
1457
1458impl SubscriberCheckpoint {
1459 pub fn new(bytes: Vec<u8>) -> Self {
1461 Self(bytes)
1462 }
1463
1464 pub fn as_bytes(&self) -> &[u8] {
1466 &self.0
1467 }
1468
1469 pub fn into_bytes(self) -> Vec<u8> {
1471 self.0
1472 }
1473}
1474
1475#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1485pub struct SubscriberPayloadCommitment(B256);
1486
1487impl SubscriberPayloadCommitment {
1488 pub const fn new(commitment: B256) -> Self {
1490 Self(commitment)
1491 }
1492
1493 pub const fn digest(&self) -> B256 {
1495 self.0
1496 }
1497}
1498
1499#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1505#[non_exhaustive]
1506pub struct SubscriberResumePosition {
1507 pub chain_id: u64,
1509 pub coverage_head: BlockRef,
1511 pub canonical_history: Vec<BlockRef>,
1513 pub delivery_token: Option<SubscriberDeliveryToken>,
1517 pub subscriber_checkpoint: Option<SubscriberCheckpoint>,
1519}
1520
1521impl SubscriberResumePosition {
1522 pub fn new(
1524 chain_id: u64,
1525 coverage_head: BlockRef,
1526 canonical_history: Vec<BlockRef>,
1527 delivery_token: Option<SubscriberDeliveryToken>,
1528 subscriber_checkpoint: Option<SubscriberCheckpoint>,
1529 ) -> Self {
1530 Self {
1531 chain_id,
1532 coverage_head,
1533 canonical_history,
1534 delivery_token,
1535 subscriber_checkpoint,
1536 }
1537 }
1538}
1539
1540#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1548#[non_exhaustive]
1549pub enum DeliveryAudience {
1550 #[default]
1552 All,
1553 Owners(Vec<HandlerId>),
1555 AllExcept(Vec<HandlerId>),
1560}
1561
1562#[derive(
1570 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
1571)]
1572#[non_exhaustive]
1573pub enum DeliveryScope {
1574 #[default]
1576 Canonical,
1577 CanonicalProgress,
1579 OwnerCatchup,
1581 Preconfirmed,
1584}
1585
1586impl DeliveryScope {
1587 const fn advances_canonical_state(self) -> bool {
1588 matches!(self, Self::Canonical | Self::CanonicalProgress)
1589 }
1590}
1591
1592#[derive(Clone, Debug)]
1594pub struct ReactiveInputDelivery<N: Network = Ethereum> {
1595 record: ReactiveInputRecord<N>,
1596 audience: DeliveryAudience,
1597 scope: DeliveryScope,
1598}
1599
1600impl<N: Network> ReactiveInputDelivery<N> {
1601 pub fn new(
1603 record: ReactiveInputRecord<N>,
1604 audience: DeliveryAudience,
1605 scope: DeliveryScope,
1606 ) -> Self {
1607 Self {
1608 record,
1609 audience,
1610 scope,
1611 }
1612 }
1613
1614 pub const fn record(&self) -> &ReactiveInputRecord<N> {
1616 &self.record
1617 }
1618
1619 pub const fn audience(&self) -> &DeliveryAudience {
1621 &self.audience
1622 }
1623
1624 pub const fn scope(&self) -> DeliveryScope {
1626 self.scope
1627 }
1628
1629 pub fn into_parts(self) -> (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope) {
1631 (self.record, self.audience, self.scope)
1632 }
1633}
1634
1635#[derive(Clone, Debug)]
1640#[non_exhaustive]
1641pub struct ReactiveInputBatchParts<N: Network = Ethereum> {
1642 pub chain_id: Option<u64>,
1644 pub deliveries: Vec<ReactiveInputDelivery<N>>,
1646 pub delivery_token: Option<SubscriberDeliveryToken>,
1648 pub subscriber_checkpoint: Option<SubscriberCheckpoint>,
1650 pub payload_commitment: Option<SubscriberPayloadCommitment>,
1652 pub chain_controls: Vec<ChainControl>,
1654}
1655
1656#[derive(Clone, Debug)]
1658pub struct ReactiveInputBatch<N: Network = Ethereum> {
1659 records: Vec<ReactiveInputRecord<N>>,
1660 chain_id: Option<u64>,
1661 delivery_token: Option<SubscriberDeliveryToken>,
1662 subscriber_checkpoint: Option<SubscriberCheckpoint>,
1663 payload_commitment: Option<SubscriberPayloadCommitment>,
1664 audience: DeliveryAudience,
1665 record_audiences: Option<Vec<DeliveryAudience>>,
1666 delivery_scope: DeliveryScope,
1667 record_delivery_scopes: Option<Vec<DeliveryScope>>,
1668 chain_controls: Vec<ChainControl>,
1669}
1670
1671type RuntimeInputDelivery<N> = (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope);
1672
1673impl<N: Network> ReactiveInputBatch<N> {
1674 pub fn new(records: Vec<ReactiveInputRecord<N>>) -> Self {
1676 let chain_id = common_record_chain_id(&records);
1677 Self {
1678 records,
1679 chain_id,
1680 delivery_token: None,
1681 subscriber_checkpoint: None,
1682 payload_commitment: None,
1683 audience: DeliveryAudience::All,
1684 record_audiences: None,
1685 delivery_scope: DeliveryScope::Canonical,
1686 record_delivery_scopes: None,
1687 chain_controls: Vec::new(),
1688 }
1689 }
1690
1691 pub fn with_chain_id(mut self, chain_id: u64) -> Self {
1694 self.chain_id = Some(chain_id);
1695 self
1696 }
1697
1698 pub const fn chain_id(&self) -> Option<u64> {
1701 self.chain_id
1702 }
1703
1704 pub fn with_delivery_token(mut self, token: SubscriberDeliveryToken) -> Self {
1706 self.delivery_token = Some(token);
1707 self
1708 }
1709
1710 pub fn delivery_token(&self) -> Option<&SubscriberDeliveryToken> {
1712 self.delivery_token.as_ref()
1713 }
1714
1715 pub fn with_subscriber_checkpoint(mut self, checkpoint: SubscriberCheckpoint) -> Self {
1717 self.subscriber_checkpoint = Some(checkpoint);
1718 self
1719 }
1720
1721 pub fn subscriber_checkpoint(&self) -> Option<&SubscriberCheckpoint> {
1723 self.subscriber_checkpoint.as_ref()
1724 }
1725
1726 pub fn with_payload_commitment(mut self, commitment: SubscriberPayloadCommitment) -> Self {
1729 self.payload_commitment = Some(commitment);
1730 self
1731 }
1732
1733 pub const fn payload_commitment(&self) -> Option<&SubscriberPayloadCommitment> {
1735 self.payload_commitment.as_ref()
1736 }
1737
1738 pub fn with_audience(mut self, audience: DeliveryAudience) -> Self {
1740 self.audience = audience;
1741 self.record_audiences = None;
1742 self
1743 }
1744
1745 pub const fn audience(&self) -> &DeliveryAudience {
1747 &self.audience
1748 }
1749
1750 pub fn from_scoped_records(
1752 records: impl IntoIterator<Item = (ReactiveInputRecord<N>, DeliveryAudience)>,
1753 ) -> Self {
1754 let (records, record_audiences): (Vec<_>, Vec<_>) = records.into_iter().unzip();
1755 let chain_id = common_record_chain_id(&records);
1756 Self {
1757 records,
1758 chain_id,
1759 delivery_token: None,
1760 subscriber_checkpoint: None,
1761 payload_commitment: None,
1762 audience: DeliveryAudience::All,
1763 record_audiences: Some(record_audiences),
1764 delivery_scope: DeliveryScope::Canonical,
1765 record_delivery_scopes: None,
1766 chain_controls: Vec::new(),
1767 }
1768 }
1769
1770 pub fn from_deliveries(deliveries: impl IntoIterator<Item = ReactiveInputDelivery<N>>) -> Self {
1772 Self::from_scoped_records_with_delivery_scope(
1773 deliveries
1774 .into_iter()
1775 .map(ReactiveInputDelivery::into_parts),
1776 )
1777 }
1778
1779 pub fn record_audience(&self, index: usize) -> Option<&DeliveryAudience> {
1781 if index >= self.records.len() {
1782 return None;
1783 }
1784 Some(
1785 self.record_audiences
1786 .as_ref()
1787 .and_then(|audiences| audiences.get(index))
1788 .unwrap_or(&self.audience),
1789 )
1790 }
1791
1792 pub fn with_delivery_scope(mut self, scope: DeliveryScope) -> Self {
1794 self.delivery_scope = scope;
1795 self.record_delivery_scopes = None;
1796 self
1797 }
1798
1799 pub fn record_delivery_scope(&self, index: usize) -> Option<DeliveryScope> {
1801 if index >= self.records.len() {
1802 return None;
1803 }
1804 Some(
1805 self.record_delivery_scopes
1806 .as_ref()
1807 .and_then(|scopes| scopes.get(index))
1808 .copied()
1809 .unwrap_or(self.delivery_scope),
1810 )
1811 }
1812
1813 fn from_scoped_records_with_delivery_scope(
1814 records: impl IntoIterator<Item = (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
1815 ) -> Self {
1816 let mut input_records = Vec::new();
1817 let mut audiences = Vec::new();
1818 let mut scopes = Vec::new();
1819 for (record, audience, scope) in records {
1820 input_records.push(record);
1821 audiences.push(audience);
1822 scopes.push(scope);
1823 }
1824 let chain_id = common_record_chain_id(&input_records);
1825 Self {
1826 records: input_records,
1827 chain_id,
1828 delivery_token: None,
1829 subscriber_checkpoint: None,
1830 payload_commitment: None,
1831 audience: DeliveryAudience::All,
1832 record_audiences: Some(audiences),
1833 delivery_scope: DeliveryScope::Canonical,
1834 record_delivery_scopes: Some(scopes),
1835 chain_controls: Vec::new(),
1836 }
1837 }
1838
1839 pub fn with_chain_controls(mut self, controls: impl IntoIterator<Item = ChainControl>) -> Self {
1846 self.chain_controls = controls.into_iter().collect();
1847 self
1848 }
1849
1850 pub fn chain_controls(&self) -> &[ChainControl] {
1852 &self.chain_controls
1853 }
1854
1855 pub fn records(&self) -> &[ReactiveInputRecord<N>] {
1857 &self.records
1858 }
1859
1860 pub fn into_records(self) -> Vec<ReactiveInputRecord<N>> {
1867 self.records
1868 }
1869
1870 pub fn into_parts(self) -> ReactiveInputBatchParts<N> {
1872 let chain_id = self.chain_id;
1873 let delivery_token = self.delivery_token;
1874 let subscriber_checkpoint = self.subscriber_checkpoint;
1875 let payload_commitment = self.payload_commitment;
1876 let chain_controls = self.chain_controls;
1877 let audiences = self
1878 .record_audiences
1879 .unwrap_or_else(|| vec![self.audience; self.records.len()]);
1880 let scopes = self
1881 .record_delivery_scopes
1882 .unwrap_or_else(|| vec![self.delivery_scope; self.records.len()]);
1883 let deliveries = self
1884 .records
1885 .into_iter()
1886 .zip(audiences)
1887 .zip(scopes)
1888 .map(|((record, audience), scope)| ReactiveInputDelivery::new(record, audience, scope))
1889 .collect();
1890 ReactiveInputBatchParts {
1891 chain_id,
1892 deliveries,
1893 delivery_token,
1894 subscriber_checkpoint,
1895 payload_commitment,
1896 chain_controls,
1897 }
1898 }
1899
1900 fn into_runtime_parts(self) -> (Vec<RuntimeInputDelivery<N>>, Vec<ChainControl>, Option<u64>) {
1901 let audiences = self
1902 .record_audiences
1903 .unwrap_or_else(|| vec![self.audience; self.records.len()]);
1904 let scopes = self
1905 .record_delivery_scopes
1906 .unwrap_or_else(|| vec![self.delivery_scope; self.records.len()]);
1907 let records = self
1908 .records
1909 .into_iter()
1910 .zip(audiences)
1911 .zip(scopes)
1912 .map(|((record, audience), scope)| (record, audience, scope))
1913 .collect();
1914 (records, self.chain_controls, self.chain_id)
1915 }
1916
1917 fn take_delivery_token(&mut self) -> Option<SubscriberDeliveryToken> {
1918 self.delivery_token.take()
1919 }
1920
1921 fn take_subscriber_checkpoint(&mut self) -> Option<SubscriberCheckpoint> {
1922 self.subscriber_checkpoint.take()
1923 }
1924}
1925
1926fn common_record_chain_id<N: Network>(records: &[ReactiveInputRecord<N>]) -> Option<u64> {
1927 let chain_id = records.first()?.context.chain_id?;
1928 records
1929 .iter()
1930 .all(|record| record.context.chain_id == Some(chain_id))
1931 .then_some(chain_id)
1932}
1933
1934pub trait ReactiveHandler<N: Network = Ethereum>: Send + Sync {
1936 fn id(&self) -> HandlerId;
1938
1939 fn interests(&self) -> Vec<ReactiveInterest<N>>;
1941
1942 fn log_route_index(&self) -> Option<LogRouteIndex> {
1949 None
1950 }
1951
1952 fn handle(
1954 &self,
1955 ctx: &ReactiveContext,
1956 input: &ReactiveInput<N>,
1957 state: &dyn StateView,
1958 ) -> Result<HandlerOutcome, HandlerError>;
1959}
1960
1961pub trait ReactiveHook<N: Network = Ethereum>: Send + Sync {
1971 fn on_report(&self, report: Arc<ReactiveReport<N>>);
1973}
1974
1975#[allow(clippy::large_enum_variant)]
1977#[derive(Clone)]
1978pub enum ReactiveInterest<N: Network = Ethereum> {
1979 Logs(LogInterest),
1981 Blocks(BlockInterest),
1983 PendingTransactions(PendingTxInterest<N>),
1985}
1986
1987impl<N: Network> fmt::Debug for ReactiveInterest<N> {
1988 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1989 match self {
1990 Self::Logs(interest) => f.debug_tuple("Logs").field(interest).finish(),
1991 Self::Blocks(interest) => f.debug_tuple("Blocks").field(interest).finish(),
1992 Self::PendingTransactions(interest) => f
1993 .debug_tuple("PendingTransactions")
1994 .field(interest)
1995 .finish(),
1996 }
1997 }
1998}
1999
2000#[derive(Clone)]
2002pub struct LogInterest {
2003 pub provider_filter: Filter,
2005 pub local_matcher: Option<Arc<dyn LogMatcher>>,
2007 pub route_key: Option<RouteKeySpec>,
2009}
2010
2011impl LogInterest {
2012 pub fn matches(&self, log: &Log) -> bool {
2014 self.provider_filter.rpc_matches(log)
2015 && self
2016 .local_matcher
2017 .as_ref()
2018 .is_none_or(|matcher| matcher.matches(log))
2019 }
2020
2021 pub fn route_key(&self, log: &Log) -> Option<RouteKey> {
2023 self.route_key.as_ref().and_then(|spec| spec.extract(log))
2024 }
2025}
2026
2027impl fmt::Debug for LogInterest {
2028 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2029 f.debug_struct("LogInterest")
2030 .field("provider_filter", &self.provider_filter)
2031 .field(
2032 "local_matcher",
2033 &self.local_matcher.as_ref().map(|_| "<matcher>"),
2034 )
2035 .field("route_key", &self.route_key)
2036 .finish()
2037 }
2038}
2039
2040pub trait LogMatcher: Send + Sync {
2042 fn matches(&self, log: &Log) -> bool;
2044}
2045
2046#[derive(Clone)]
2048pub enum RouteKeySpec {
2049 EmitterAddress,
2051 Topic {
2053 index: usize,
2055 },
2056 DataSlice {
2058 offset: usize,
2060 len: usize,
2062 },
2063 Custom(Arc<dyn RouteKeyExtractor>),
2065}
2066
2067impl RouteKeySpec {
2068 pub fn extract(&self, log: &Log) -> Option<RouteKey> {
2070 match self {
2071 Self::EmitterAddress => Some(RouteKey::Address(log.address())),
2072 Self::Topic { index } => log.topics().get(*index).copied().map(RouteKey::Bytes32),
2073 Self::DataSlice { offset, len } => {
2074 let data = log.inner.data.data.as_ref();
2075 let end = offset.checked_add(*len)?;
2076 data.get(*offset..end)
2077 .map(|bytes| RouteKey::Bytes(bytes.to_vec()))
2078 }
2079 Self::Custom(extractor) => extractor.extract(log),
2080 }
2081 }
2082}
2083
2084impl fmt::Debug for RouteKeySpec {
2085 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2086 match self {
2087 Self::EmitterAddress => f.write_str("EmitterAddress"),
2088 Self::Topic { index } => f.debug_struct("Topic").field("index", index).finish(),
2089 Self::DataSlice { offset, len } => f
2090 .debug_struct("DataSlice")
2091 .field("offset", offset)
2092 .field("len", len)
2093 .finish(),
2094 Self::Custom(_) => f.write_str("Custom(<extractor>)"),
2095 }
2096 }
2097}
2098
2099pub trait RouteKeyExtractor: Send + Sync {
2101 fn extract(&self, log: &Log) -> Option<RouteKey>;
2103}
2104
2105#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2107pub enum RouteKey {
2108 Address(Address),
2110 Bytes32(B256),
2112 Bytes(Vec<u8>),
2114}
2115
2116#[non_exhaustive]
2118#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2119pub enum LogRouteKey {
2120 Emitter(Address),
2122 Topic {
2124 index: usize,
2126 value: B256,
2128 },
2129 DataSlice {
2131 offset: usize,
2133 value: Vec<u8>,
2135 },
2136}
2137
2138#[derive(Clone, Debug, PartialEq, Eq)]
2140pub struct LogRouteIndex {
2141 keys: Vec<LogRouteKey>,
2142}
2143
2144impl LogRouteIndex {
2145 pub fn new(primary: LogRouteKey, additional: impl IntoIterator<Item = LogRouteKey>) -> Self {
2147 let mut keys = vec![primary];
2148 for key in additional {
2149 if !keys.contains(&key) {
2150 keys.push(key);
2151 }
2152 }
2153 Self { keys }
2154 }
2155
2156 pub fn single(key: LogRouteKey) -> Self {
2158 Self { keys: vec![key] }
2159 }
2160
2161 pub fn keys(&self) -> &[LogRouteKey] {
2163 &self.keys
2164 }
2165}
2166
2167#[derive(Clone, Debug, PartialEq, Eq)]
2169pub struct ReactiveLogRoute {
2170 pub handler_id: HandlerId,
2172 pub route_key: Option<RouteKey>,
2174}
2175
2176#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2178pub struct BlockInterest {
2179 pub mode: BlockInterestMode,
2181}
2182
2183impl Default for BlockInterest {
2184 fn default() -> Self {
2185 Self {
2186 mode: BlockInterestMode::Header,
2187 }
2188 }
2189}
2190
2191#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2193pub enum BlockInterestMode {
2194 Header,
2196 FullBlock,
2198}
2199
2200#[derive(Clone)]
2202pub struct PendingTxInterest<N: Network = Ethereum> {
2203 pub full_transactions: bool,
2205 pub from: AddressMatcher,
2207 pub to: AddressMatcher,
2209 pub selectors: SelectorMatcher,
2211 pub local_matcher: Option<Arc<dyn PendingTxMatcher<N>>>,
2213}
2214
2215impl<N: Network> Default for PendingTxInterest<N> {
2216 fn default() -> Self {
2217 Self {
2218 full_transactions: false,
2219 from: AddressMatcher::Any,
2220 to: AddressMatcher::Any,
2221 selectors: SelectorMatcher::Any,
2222 local_matcher: None,
2223 }
2224 }
2225}
2226
2227impl<N: Network> PendingTxInterest<N> {
2228 fn matches_hash_only(&self) -> bool {
2229 !self.full_transactions
2230 && self.from.is_any()
2231 && self.to.is_any()
2232 && self.selectors.is_any()
2233 && self.local_matcher.is_none()
2234 }
2235
2236 fn matches_tx(&self, tx: &N::TransactionResponse) -> bool {
2237 self.from.matches(tx.from())
2238 && self.to.matches_option(tx.to())
2239 && self.selectors.matches(tx.input())
2240 && self
2241 .local_matcher
2242 .as_ref()
2243 .is_none_or(|matcher| matcher.matches(tx))
2244 }
2245}
2246
2247impl<N: Network> fmt::Debug for PendingTxInterest<N> {
2248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2249 f.debug_struct("PendingTxInterest")
2250 .field("full_transactions", &self.full_transactions)
2251 .field("from", &self.from)
2252 .field("to", &self.to)
2253 .field("selectors", &self.selectors)
2254 .field(
2255 "local_matcher",
2256 &self.local_matcher.as_ref().map(|_| "<matcher>"),
2257 )
2258 .finish()
2259 }
2260}
2261
2262#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2264pub enum AddressMatcher {
2265 Any,
2267 Exact(Address),
2269 AnyOf(Vec<Address>),
2271}
2272
2273impl AddressMatcher {
2274 pub fn is_any(&self) -> bool {
2276 matches!(self, Self::Any)
2277 }
2278
2279 pub fn matches(&self, address: Address) -> bool {
2281 match self {
2282 Self::Any => true,
2283 Self::Exact(expected) => *expected == address,
2284 Self::AnyOf(addresses) => addresses.contains(&address),
2285 }
2286 }
2287
2288 pub fn matches_option(&self, address: Option<Address>) -> bool {
2290 match (self, address) {
2291 (Self::Any, _) => true,
2292 (_, Some(address)) => self.matches(address),
2293 _ => false,
2294 }
2295 }
2296}
2297
2298#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2300pub enum SelectorMatcher {
2301 Any,
2303 AnyOf(Vec<[u8; 4]>),
2305}
2306
2307impl SelectorMatcher {
2308 pub fn is_any(&self) -> bool {
2310 matches!(self, Self::Any)
2311 }
2312
2313 pub fn matches(&self, input: &Bytes) -> bool {
2315 match self {
2316 Self::Any => true,
2317 Self::AnyOf(selectors) => input
2318 .get(..4)
2319 .and_then(|bytes| bytes.try_into().ok())
2320 .is_some_and(|selector| selectors.contains(&selector)),
2321 }
2322 }
2323}
2324
2325pub trait PendingTxMatcher<N: Network = Ethereum>: Send + Sync {
2327 fn matches(&self, tx: &N::TransactionResponse) -> bool;
2329}
2330
2331#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
2349#[non_exhaustive]
2350pub enum TrackingPolicy {
2351 Slots {
2356 slots: Vec<U256>,
2358 },
2359 WholeAccount,
2364 Scalars,
2369}
2370
2371#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2384pub enum RootGateCadence {
2385 EveryNBlocks(NonZeroU64),
2389 Disabled,
2391}
2392
2393impl RootGateCadence {
2394 pub fn every_n_blocks(n: u64) -> Self {
2396 Self::EveryNBlocks(NonZeroU64::new(n.max(1)).expect("clamped to at least 1"))
2397 }
2398}
2399
2400impl Default for RootGateCadence {
2401 fn default() -> Self {
2405 Self::every_n_blocks(16)
2406 }
2407}
2408
2409#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
2416struct TrackedRoot {
2417 last_root: B256,
2418 last_block: u64,
2419 balance: U256,
2420 nonce: u64,
2421 code_hash: B256,
2422}
2423
2424#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2426pub struct ResyncRequest {
2427 pub id: ResyncId,
2429 pub reason: ResyncReason,
2431 pub block: ResyncBlock,
2433 pub targets: Vec<ResyncTarget>,
2435 pub priority: ResyncPriority,
2437}
2438
2439#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2441pub struct ResyncId(String);
2442
2443impl ResyncId {
2444 pub fn new(id: impl Into<String>) -> Self {
2446 Self(id.into())
2447 }
2448}
2449
2450#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2452#[non_exhaustive]
2453pub enum ResyncReason {
2454 HandlerRequested,
2456 SkippedStateEffect,
2458 MissedBlockRange,
2465 RootMoved,
2475 Custom(String),
2477}
2478
2479#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2481pub enum ResyncBlock {
2482 Latest,
2484 Pending,
2486 Safe,
2488 Finalized,
2490 Number(u64),
2492 Hash {
2494 number: u64,
2496 hash: B256,
2498 require_canonical: bool,
2500 },
2501}
2502
2503#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2505pub enum ResyncTarget {
2506 StorageSlot {
2508 address: Address,
2510 slot: U256,
2512 },
2513 StorageSlots {
2515 address: Address,
2517 slots: Vec<U256>,
2519 },
2520 Account {
2522 address: Address,
2524 fields: AccountFieldMask,
2526 },
2527}
2528
2529#[derive(
2531 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
2532)]
2533pub struct AccountFieldMask {
2534 pub balance: bool,
2536 pub nonce: bool,
2538 pub code: bool,
2540}
2541
2542#[derive(
2544 Clone,
2545 Copy,
2546 Debug,
2547 Default,
2548 PartialEq,
2549 Eq,
2550 Hash,
2551 PartialOrd,
2552 Ord,
2553 serde::Serialize,
2554 serde::Deserialize,
2555)]
2556pub enum ResyncPriority {
2557 Low,
2559 #[default]
2561 Normal,
2562 High,
2564}
2565
2566#[derive(Clone, Debug, PartialEq, Eq)]
2568pub struct InvalidationRequest {
2569 pub scope: PurgeScope,
2571 pub address: Address,
2573 pub reason: InvalidationReason,
2575}
2576
2577#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2579pub enum InvalidationReason {
2580 HandlerRequested,
2582 Reorg,
2584 Custom(String),
2586}
2587
2588#[derive(Clone, Debug, PartialEq, Eq)]
2590pub struct SpeculativeRequest {
2591 pub id: SpeculativeId,
2593 pub input_ref: InputRef,
2595 pub labels: Vec<ReportTag>,
2597}
2598
2599#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2601pub struct SpeculativeId(String);
2602
2603impl SpeculativeId {
2604 pub fn new(id: impl Into<String>) -> Self {
2606 Self(id.into())
2607 }
2608}
2609
2610#[derive(Clone, Debug, PartialEq, Eq)]
2612pub struct ReactiveConfig {
2613 pub hook_backpressure: HookBackpressure,
2618 pub journal_depth: usize,
2635}
2636
2637impl Default for ReactiveConfig {
2638 fn default() -> Self {
2639 Self {
2640 hook_backpressure: HookBackpressure::Block,
2641 journal_depth: 64,
2642 }
2643 }
2644}
2645
2646#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2648pub enum HookBackpressure {
2649 Block,
2651 DropNewest,
2653 DropOldest,
2655 Error,
2657}
2658
2659#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2667#[non_exhaustive]
2668pub enum CacheHealth {
2669 #[default]
2671 Healthy,
2672 Degraded {
2676 since_block: u64,
2678 },
2679 Unhealthy {
2682 since_block: u64,
2684 },
2685}
2686
2687#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2694#[non_exhaustive]
2695pub struct CacheMetricsSnapshot {
2696 pub deep_reorgs: u64,
2699 pub reorgs_recovered: u64,
2701 pub resync_requests: u64,
2703 pub resync_failures: u64,
2705 pub missed_ranges: u64,
2707 pub coverage_gaps: u64,
2709 pub pending_contamination: u64,
2711 pub stale_verdicts: u64,
2713}
2714
2715#[derive(Debug, Default)]
2721struct CacheMetrics {
2722 deep_reorgs: AtomicU64,
2723 reorgs_recovered: AtomicU64,
2724 resync_requests: AtomicU64,
2725 resync_failures: AtomicU64,
2726 missed_ranges: AtomicU64,
2727 coverage_gaps: AtomicU64,
2728 pending_contamination: AtomicU64,
2729 stale_verdicts: AtomicU64,
2730}
2731
2732impl CacheMetrics {
2733 fn snapshot(&self) -> CacheMetricsSnapshot {
2734 CacheMetricsSnapshot {
2735 deep_reorgs: self.deep_reorgs.load(Ordering::Relaxed),
2736 reorgs_recovered: self.reorgs_recovered.load(Ordering::Relaxed),
2737 resync_requests: self.resync_requests.load(Ordering::Relaxed),
2738 resync_failures: self.resync_failures.load(Ordering::Relaxed),
2739 missed_ranges: self.missed_ranges.load(Ordering::Relaxed),
2740 coverage_gaps: self.coverage_gaps.load(Ordering::Relaxed),
2741 pending_contamination: self.pending_contamination.load(Ordering::Relaxed),
2742 stale_verdicts: self.stale_verdicts.load(Ordering::Relaxed),
2743 }
2744 }
2745
2746 fn restore(&self, snapshot: CacheMetricsSnapshot) {
2747 self.deep_reorgs
2748 .store(snapshot.deep_reorgs, Ordering::Relaxed);
2749 self.reorgs_recovered
2750 .store(snapshot.reorgs_recovered, Ordering::Relaxed);
2751 self.resync_requests
2752 .store(snapshot.resync_requests, Ordering::Relaxed);
2753 self.resync_failures
2754 .store(snapshot.resync_failures, Ordering::Relaxed);
2755 self.missed_ranges
2756 .store(snapshot.missed_ranges, Ordering::Relaxed);
2757 self.coverage_gaps
2758 .store(snapshot.coverage_gaps, Ordering::Relaxed);
2759 self.pending_contamination
2760 .store(snapshot.pending_contamination, Ordering::Relaxed);
2761 self.stale_verdicts
2762 .store(snapshot.stale_verdicts, Ordering::Relaxed);
2763 }
2764}
2765
2766#[derive(Clone, Debug)]
2768#[non_exhaustive]
2769pub enum ReactiveReport<N: Network = Ethereum> {
2770 Input(InputReport<N>),
2772 Decoded(DecodedReport<N>),
2774 Applied(AppliedReport<N>),
2776 Resynced(ResyncReport),
2778 BlockCommitted(BlockReport<N>),
2780 Reorg(ReorgReport<N>),
2782 ChainControl(ChainControlReport),
2784 MissedBlockRange(MissedRangeReport<N>),
2787 Health(HealthReport<N>),
2789 CoverageGap(CoverageGapReport<N>),
2792 Error(ReactiveErrorReport<N>),
2794}
2795
2796#[derive(Clone, Debug, PartialEq, Eq)]
2798pub struct ChainControlReport {
2799 pub control: ChainControl,
2801}
2802
2803#[derive(Clone, Debug)]
2805pub struct InputReport<N: Network = Ethereum> {
2806 pub input_ref: InputRef,
2808 pub context: ReactiveContext,
2810 pub provider: Option<ProviderRef>,
2812 pub _network: PhantomData<N>,
2814}
2815
2816#[derive(Clone, Debug)]
2818pub struct DecodedReport<N: Network = Ethereum> {
2819 pub input_ref: InputRef,
2821 pub handler_ids: Vec<HandlerId>,
2823 pub _network: PhantomData<N>,
2825}
2826
2827#[derive(Clone, Debug)]
2829pub struct AppliedReport<N: Network = Ethereum> {
2830 pub input_ref: InputRef,
2832 pub handler_id: HandlerId,
2834 pub quality: StateEffectQuality,
2836 pub tags: Vec<ReportTag>,
2838 pub diff: StateDiff,
2840 pub state_updates: Vec<StateUpdate>,
2842 pub invalidations: Vec<InvalidationRequest>,
2844 pub resyncs: Vec<ResyncRequest>,
2846 pub speculative: Vec<SpeculativeRequest>,
2848 pub hook_signals: Vec<HookSignal>,
2850 pub _network: PhantomData<N>,
2852}
2853
2854#[derive(Clone, Debug, Default, PartialEq, Eq)]
2858pub struct ResyncReport {
2859 pub requested: Vec<ResyncRequest>,
2861 pub state_updates: Vec<StateUpdate>,
2863 pub diff: StateDiff,
2865 pub failed: Vec<ResyncFailure>,
2867}
2868
2869#[derive(Clone, Debug, PartialEq, Eq)]
2871pub struct ResyncFailure {
2872 pub request_id: ResyncId,
2874 pub block: ResyncBlock,
2876 pub target: ResyncTarget,
2878 pub kind: ResyncFailureKind,
2880 pub message: String,
2882}
2883
2884#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2886#[non_exhaustive]
2887pub enum ResyncFailureKind {
2888 MissingStorageFetcher,
2890 StorageFetchFailed,
2892 StorageFetchOmitted,
2894 MissingAccountFetcher,
2896 AccountFetchFailed,
2898 AccountFetchOmitted,
2900}
2901
2902#[derive(Clone, Debug)]
2904pub struct BlockReport<N: Network = Ethereum> {
2905 pub block: Option<BlockRef>,
2907 pub inputs: Vec<InputRef>,
2909 pub _network: PhantomData<N>,
2911}
2912
2913#[derive(Clone, Debug)]
2929pub struct ReorgReport<N: Network = Ethereum> {
2930 pub dropped: Option<BlockRef>,
2932 pub dropped_blocks: Vec<BlockRef>,
2934 pub dropped_inputs: Vec<InputRef>,
2936 pub rollback_updates: Vec<StateUpdate>,
2938 pub rollback_diff: StateDiff,
2940 pub purge_updates: Vec<StateUpdate>,
2942 pub purge_diff: StateDiff,
2944 pub canceled_resyncs: Vec<ResyncRequest>,
2946 pub reason: ReorgReason,
2948 pub _network: PhantomData<N>,
2950}
2951
2952#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2954pub enum ReorgReason {
2955 RemovedLog,
2957 ReorgedInput,
2959 ParentMismatch,
2961 Explicit,
2963}
2964
2965#[derive(Clone, Debug)]
2973pub struct MissedRangeReport<N: Network = Ethereum> {
2974 pub from: u64,
2976 pub to: u64,
2978 pub block: u64,
2980 pub _network: PhantomData<N>,
2982}
2983
2984#[derive(Clone, Debug)]
2987pub struct HealthReport<N: Network = Ethereum> {
2988 pub from: CacheHealth,
2990 pub to: CacheHealth,
2992 pub block: Option<u64>,
2994 pub _network: PhantomData<N>,
2996}
2997
2998#[derive(Clone, Debug)]
3011pub struct CoverageGapReport<N: Network = Ethereum> {
3012 pub address: Address,
3014 pub block: u64,
3016 pub _network: PhantomData<N>,
3018}
3019
3020#[derive(Clone, Debug)]
3023pub struct ReactiveErrorReport<N: Network = Ethereum> {
3024 pub input_ref: Option<InputRef>,
3026 pub message: String,
3028 pub _network: PhantomData<N>,
3030}
3031
3032#[derive(Clone, Debug)]
3035pub struct ReactiveBatchReport<N: Network = Ethereum> {
3036 pub applied: Vec<AppliedReport<N>>,
3038 pub resyncs: Vec<ResyncRequest>,
3040 pub speculative: Vec<SpeculativeRequest>,
3042 pub reports: Vec<Arc<ReactiveReport<N>>>,
3044}
3045
3046impl<N: Network> Default for ReactiveBatchReport<N> {
3047 fn default() -> Self {
3048 Self {
3049 applied: Vec::new(),
3050 resyncs: Vec::new(),
3051 speculative: Vec::new(),
3052 reports: Vec::new(),
3053 }
3054 }
3055}
3056
3057#[derive(Clone, Debug, PartialEq, Eq)]
3059pub struct HandlerError {
3060 message: String,
3061}
3062
3063impl HandlerError {
3064 pub fn new(message: impl Into<String>) -> Self {
3066 Self {
3067 message: message.into(),
3068 }
3069 }
3070}
3071
3072impl fmt::Display for HandlerError {
3073 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3074 self.message.fmt(f)
3075 }
3076}
3077
3078impl std::error::Error for HandlerError {}
3079
3080impl From<String> for HandlerError {
3081 fn from(message: String) -> Self {
3082 Self::new(message)
3083 }
3084}
3085
3086impl From<&str> for HandlerError {
3087 fn from(message: &str) -> Self {
3088 Self::new(message)
3089 }
3090}
3091
3092#[derive(Debug, thiserror::Error)]
3094#[non_exhaustive]
3095pub enum ReactiveError {
3096 #[error("handler `{handler_id}` failed: {source}")]
3098 HandlerFailed {
3099 handler_id: HandlerId,
3101 source: HandlerError,
3103 },
3104 #[error(
3106 "conflicting effects for input {input_ref:?} on target {target:?}: `{first}` vs `{second}`"
3107 )]
3108 ConflictingEffects {
3109 input_ref: Box<InputRef>,
3111 target: Box<EffectTarget>,
3113 first: HandlerId,
3115 second: HandlerId,
3117 },
3118 #[error(
3120 "pending input {input_ref:?} emitted invalid canonical effect `{effect_kind}` from `{handler_id}`"
3121 )]
3122 InvalidPendingEffect {
3123 input_ref: Box<InputRef>,
3125 handler_id: HandlerId,
3127 effect_kind: &'static str,
3129 },
3130 #[error("invalid reactive input record: {message}")]
3133 InvalidInputRecord {
3134 message: String,
3136 },
3137 #[error("invalid chain control: {message}")]
3139 InvalidChainControl {
3140 message: String,
3142 },
3143 #[error(
3146 "owner catch-up block {number} {hash} is outside the retained canonical rollback journal"
3147 )]
3148 OwnerCatchupOutsideJournal {
3149 number: u64,
3151 hash: B256,
3153 },
3154 #[error(transparent)]
3156 Register(#[from] RegisterError),
3157}
3158
3159#[derive(Debug, thiserror::Error)]
3161#[non_exhaustive]
3162pub enum RegisterError {
3163 #[error("handler id `{0}` is already registered")]
3165 DuplicateHandler(HandlerId),
3166}
3167
3168#[derive(Debug, thiserror::Error)]
3171#[non_exhaustive]
3172pub enum ReactiveEngineRegisterError {
3173 #[error(transparent)]
3175 Register(#[from] RegisterError),
3176 #[error(transparent)]
3178 Subscriber(#[from] SubscriberError),
3179 #[error(
3182 "owner backfill {start_block}..={end_block:?} must target exactly one hash-certified block in the retained rollback journal"
3183 )]
3184 BackfillOutsideJournal {
3185 start_block: u64,
3187 end_block: Option<u64>,
3189 retained_anchor: Option<BlockRef>,
3191 },
3192}
3193
3194#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
3197#[non_exhaustive]
3198pub enum ReactiveBaselineError {
3199 #[error("cannot adopt a canonical baseline after reactive processing has started")]
3201 ActiveRuntime,
3202 #[error(
3205 "canonical baseline conflicts with existing block {existing_number} {existing_hash} (requested {requested_number} {requested_hash})"
3206 )]
3207 ConflictingBaseline {
3208 existing_number: u64,
3210 existing_hash: B256,
3212 requested_number: u64,
3214 requested_hash: B256,
3216 },
3217 #[error("baseline chain id {baseline_chain_id} does not match cache chain id {cache_chain_id}")]
3219 CacheChainMismatch {
3220 baseline_chain_id: u64,
3222 cache_chain_id: u64,
3224 },
3225 #[error("cache block selector is not canonically hash-pinned to baseline {number} {hash}")]
3227 CacheBlockMismatch {
3228 number: u64,
3230 hash: B256,
3232 },
3233}
3234
3235#[derive(Debug, thiserror::Error)]
3238#[non_exhaustive]
3239pub enum ReactiveEngineError {
3240 #[error(transparent)]
3242 Subscriber(#[from] SubscriberError),
3243 #[error(transparent)]
3245 Runtime(ReactiveError),
3246 #[error(transparent)]
3248 Baseline(#[from] ReactiveBaselineError),
3249 #[error("runtime ingestion succeeded but subscriber acknowledgement failed: {0}")]
3252 Acknowledgement(#[source] SubscriberError),
3253 #[error("runtime ingestion succeeded but durable checkpoint commit failed: {0}")]
3257 Checkpoint(#[source] DurableCheckpointError),
3258 #[error("cannot durably checkpoint reactive state before observing a canonical block")]
3260 MissingCheckpointBlock,
3261 #[error("pre-confirmed Flashblock batches cannot be durably checkpointed")]
3264 PreconfirmationNotCheckpointable,
3265 #[error("failed to encode durable reactive runtime state: {0}")]
3267 RuntimeCheckpoint(String),
3268 #[error("cannot use ordinary ingestion while a durable checkpoint commit is pending")]
3271 PendingCheckpointCommit,
3272 #[error("cannot use checkpointed ingestion while an ordinary acknowledgement is pending")]
3275 PendingAcknowledgementCommit,
3276 #[error(
3280 "raw engine ingestion cannot consume delivery tokens or subscriber checkpoints; use a combined next_ingest helper"
3281 )]
3282 UncommittedDeliveryMetadata,
3283 #[error(
3285 "subscriber chain id {subscriber_chain_id} does not match cache chain id {cache_chain_id}"
3286 )]
3287 SubscriberChainMismatch {
3288 subscriber_chain_id: u64,
3290 cache_chain_id: u64,
3292 },
3293 #[error("subscriber does not advertise durable replay support")]
3295 SubscriberNotDurable,
3296 #[error(
3299 "committed delivery token has no delivery witness; replay cannot be acknowledged safely"
3300 )]
3301 MissingReplayWitness,
3302 #[error("replayed delivery token does not match its committed delivery witness")]
3305 ReplayDeliveryMismatch,
3306 #[error("failed to encode durable delivery witness: {0}")]
3308 DeliveryWitness(String),
3309 #[error(
3312 "tokened block-header, full-block, or hydrated-transaction delivery requires an exact payload commitment"
3313 )]
3314 MissingPayloadCommitment,
3315 #[error(
3318 "cache changed while durable checkpoint commit was pending (staged generation {staged_generation}, current generation {current_generation})"
3319 )]
3320 PendingCheckpointCacheChanged {
3321 staged_generation: u64,
3323 current_generation: u64,
3325 },
3326 #[error(
3329 "reorg after block {common_ancestor} exceeds the retained rollback journal (oldest retained block {oldest_journaled:?}, configured depth {journal_depth})"
3330 )]
3331 CheckpointReorgOutsideJournal {
3332 common_ancestor: u64,
3334 oldest_journaled: Option<u64>,
3336 journal_depth: usize,
3338 },
3339 #[error(
3342 "owner catch-up block {number} {hash} is outside the retained canonical rollback journal"
3343 )]
3344 OwnerCatchupOutsideJournal {
3345 number: u64,
3347 hash: B256,
3349 },
3350}
3351
3352impl From<ReactiveError> for ReactiveEngineError {
3353 fn from(error: ReactiveError) -> Self {
3354 match error {
3355 ReactiveError::OwnerCatchupOutsideJournal { number, hash } => {
3356 Self::OwnerCatchupOutsideJournal { number, hash }
3357 }
3358 error => Self::Runtime(error),
3359 }
3360 }
3361}
3362
3363#[derive(Debug, thiserror::Error)]
3365#[non_exhaustive]
3366pub enum ReactiveCheckpointRestoreError {
3367 #[error("cannot restore a durable checkpoint into a runtime with canonical journal state")]
3369 ActiveRuntime,
3370 #[error("invalid durable reactive runtime state: {0}")]
3372 InvalidRuntimeCheckpoint(String),
3373 #[error(transparent)]
3375 Checkpoint(#[from] DurableCheckpointError),
3376 #[error("subscriber rejected durable resume position: {0}")]
3378 Subscriber(#[source] SubscriberError),
3379 #[error(
3381 "subscriber chain id {subscriber_chain_id} does not match checkpoint chain id {checkpoint_chain_id}"
3382 )]
3383 SubscriberChainMismatch {
3384 subscriber_chain_id: u64,
3386 checkpoint_chain_id: u64,
3388 },
3389 #[error("subscriber does not advertise durable replay support")]
3391 SubscriberNotDurable,
3392}
3393
3394#[derive(Clone, Debug)]
3396#[non_exhaustive]
3397pub enum CheckpointedIngest<N: Network = Ethereum> {
3398 Applied(ReactiveBatchReport<N>),
3400 ReplayAcknowledged,
3403}
3404
3405#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3407pub enum EffectTarget {
3408 StorageSlot {
3410 address: Address,
3412 slot: U256,
3414 },
3415 AccountBalance {
3417 address: Address,
3419 },
3420 AccountNonce {
3422 address: Address,
3424 },
3425 AccountCode {
3427 address: Address,
3429 },
3430 MaskedStorageSlot {
3432 address: Address,
3434 slot: U256,
3436 mask: U256,
3438 },
3439}
3440
3441#[derive(Clone, Debug, PartialEq, Eq)]
3442enum AbsoluteValue {
3443 U256(U256),
3444 U64(u64),
3445 Bytes(Bytes),
3446}
3447
3448pub struct ReactiveRuntime<N: Network = Ethereum> {
3450 registry: ReactiveRegistry<N>,
3451 hooks: Vec<Arc<dyn ReactiveHook<N>>>,
3452 config: ReactiveConfig,
3453 journal: VecDeque<BlockJournal<N>>,
3454 coverage_head: Option<BlockRef>,
3455 pending_resyncs: Vec<ResyncRequest>,
3456 health: CacheHealth,
3457 safe_head: Option<BlockRef>,
3458 finalized_head: Option<BlockRef>,
3459 metrics: CacheMetrics,
3460 freshness: Option<FreshnessRegistry>,
3469 tracking: HashMap<Address, TrackingPolicy>,
3473 tracked_roots: HashMap<Address, TrackedRoot>,
3476 root_gate_cadence: RootGateCadence,
3478 last_gate_block: Option<u64>,
3482 touched_since_gate: HashSet<Address>,
3488 preconfirmed_branch: Option<PreconfirmedBranch>,
3491}
3492
3493#[derive(Clone)]
3494struct PreconfirmedBranch {
3495 flashblock: FlashblockRef,
3496 canonical_cache: EvmCacheStateSnapshot,
3497}
3498
3499#[derive(Clone, Debug)]
3500struct BlockJournal<N: Network = Ethereum> {
3501 block: BlockRef,
3502 inputs: Vec<InputRef>,
3503 applied: Vec<AppliedReport<N>>,
3504 handler_ids: Vec<HandlerId>,
3505 resynced: Vec<ResyncReport>,
3506 rollback_diffs: Vec<StateDiff>,
3507}
3508
3509const DURABLE_RUNTIME_CHECKPOINT_VERSION: u32 = 3;
3510
3511#[derive(serde::Serialize, serde::Deserialize)]
3512struct DurableRuntimeCheckpoint {
3513 version: u32,
3514 safe_head: Option<BlockRef>,
3515 finalized_head: Option<BlockRef>,
3516 health: CacheHealth,
3517 pending_resyncs: Vec<ResyncRequest>,
3518 coverage_head: Option<BlockRef>,
3519 journal: Vec<DurableBlockJournal>,
3520 freshness: Option<FreshnessRegistry>,
3521 tracking: HashMap<Address, TrackingPolicy>,
3522 tracked_roots: HashMap<Address, TrackedRoot>,
3523 root_gate_cadence: RootGateCadence,
3524 last_gate_block: Option<u64>,
3525 touched_since_gate: HashSet<Address>,
3526 metrics: CacheMetricsSnapshot,
3527}
3528
3529#[derive(serde::Serialize, serde::Deserialize)]
3530struct DurableBlockJournal {
3531 block: BlockRef,
3532 handler_ids: Vec<HandlerId>,
3533 rollback_diffs: Vec<StateDiff>,
3534}
3535
3536struct DurableRuntimeRestorePlan {
3537 checkpoint: Option<DurableRuntimeCheckpoint>,
3538 fallback_history: Vec<BlockRef>,
3539}
3540
3541impl DurableRuntimeRestorePlan {
3542 fn canonical_history(&self) -> Vec<BlockRef> {
3543 self.checkpoint.as_ref().map_or_else(
3544 || self.fallback_history.clone(),
3545 |checkpoint| checkpoint.journal.iter().map(|entry| entry.block).collect(),
3546 )
3547 }
3548}
3549
3550#[derive(Clone)]
3551struct ReactiveRuntimeState<N: Network> {
3552 journal: VecDeque<BlockJournal<N>>,
3553 coverage_head: Option<BlockRef>,
3554 pending_resyncs: Vec<ResyncRequest>,
3555 health: CacheHealth,
3556 safe_head: Option<BlockRef>,
3557 finalized_head: Option<BlockRef>,
3558 freshness: Option<FreshnessRegistry>,
3559 tracking: HashMap<Address, TrackingPolicy>,
3560 tracked_roots: HashMap<Address, TrackedRoot>,
3561 root_gate_cadence: RootGateCadence,
3562 last_gate_block: Option<u64>,
3563 touched_since_gate: HashSet<Address>,
3564 metrics: CacheMetricsSnapshot,
3565}
3566
3567#[derive(Clone)]
3568struct ChainControlState {
3569 journal_invalidated_from: Option<u64>,
3570 resolved_canonical_blocks: HashMap<(u64, B256), BlockRef>,
3571}
3572
3573#[derive(Default)]
3582struct BatchDroppedCanonical {
3583 identities: HashSet<(u64, B256)>,
3584 implicit_spans: Vec<(u64, u64)>,
3585}
3586
3587impl BatchDroppedCanonical {
3588 fn covers_implicit_number(&self, number: u64) -> bool {
3589 self.implicit_spans
3590 .iter()
3591 .any(|(from, through)| number >= *from && number <= *through)
3592 }
3593
3594 fn contains(&self, block: &BlockRef) -> bool {
3595 self.identities.contains(&(block.number, block.hash))
3596 }
3597
3598 fn record_identity(&mut self, block: &BlockRef) {
3599 self.identities.insert((block.number, block.hash));
3600 }
3601
3602 fn record_explicit(&mut self, _common_ancestor: &BlockRef, old_tip: &BlockRef) {
3603 self.identities.insert((old_tip.number, old_tip.hash));
3604 }
3605
3606 fn record_drained(&mut self, blocks: &[BlockRef]) {
3607 let Some(from) = blocks.iter().map(|block| block.number).min() else {
3608 return;
3609 };
3610 let through = blocks
3611 .iter()
3612 .map(|block| block.number)
3613 .max()
3614 .expect("a non-empty drained set has a maximum");
3615 self.implicit_spans.push((from, through));
3616 self.identities
3617 .extend(blocks.iter().map(|block| (block.number, block.hash)));
3618 }
3619}
3620
3621pub struct ReactiveRegistry<N: Network = Ethereum> {
3629 handlers: BTreeMap<u128, RegisteredHandler<N>>,
3630 handler_positions: HashMap<HandlerId, u128>,
3631 next_handler_position: u128,
3632 indexed_log_handlers: HashMap<LogRouteKey, BTreeSet<u128>>,
3633 fallback_log_handlers: BTreeSet<u128>,
3634 data_slice_shapes: HashMap<(usize, usize), usize>,
3635}
3636
3637struct RegisteredHandler<N: Network = Ethereum> {
3638 id: HandlerId,
3639 handler: Arc<dyn ReactiveHandler<N>>,
3640 interests: Vec<ReactiveInterest<N>>,
3641 has_log_interests: bool,
3642 log_route_index: Option<LogRouteIndex>,
3643}
3644
3645impl<N: Network> Default for ReactiveRegistry<N> {
3646 fn default() -> Self {
3647 Self::new()
3648 }
3649}
3650
3651impl<N: Network> ReactiveRegistry<N> {
3652 pub fn new() -> Self {
3654 Self {
3655 handlers: BTreeMap::new(),
3656 handler_positions: HashMap::new(),
3657 next_handler_position: 0,
3658 indexed_log_handlers: HashMap::new(),
3659 fallback_log_handlers: BTreeSet::new(),
3660 data_slice_shapes: HashMap::new(),
3661 }
3662 }
3663
3664 pub fn register_handler(
3674 &mut self,
3675 handler: Arc<dyn ReactiveHandler<N>>,
3676 ) -> Result<(), RegisterError> {
3677 let id = handler.id();
3678 if self.handler_positions.contains_key(&id) {
3679 return Err(RegisterError::DuplicateHandler(id));
3680 }
3681 let interests = handler.interests();
3682 self.insert_handler_prepared(id, handler, interests);
3683 Ok(())
3684 }
3685
3686 fn insert_handler_prepared(
3687 &mut self,
3688 id: HandlerId,
3689 handler: Arc<dyn ReactiveHandler<N>>,
3690 interests: Vec<ReactiveInterest<N>>,
3691 ) {
3692 debug_assert!(!self.handler_positions.contains_key(&id));
3693 let has_log_interests = interests
3694 .iter()
3695 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)));
3696 let log_route_index = handler.log_route_index();
3697 if self.next_handler_position == u128::MAX {
3698 self.compact_handler_positions();
3699 }
3700 let position = self.next_handler_position;
3701 self.next_handler_position += 1;
3702 self.handler_positions.insert(id.clone(), position);
3703 if let Some(index) = &log_route_index {
3704 for key in index.keys() {
3705 if let LogRouteKey::DataSlice { offset, value } = key {
3706 *self
3707 .data_slice_shapes
3708 .entry((*offset, value.len()))
3709 .or_default() += 1;
3710 }
3711 self.indexed_log_handlers
3712 .entry(key.clone())
3713 .or_default()
3714 .insert(position);
3715 }
3716 } else if has_log_interests {
3717 self.fallback_log_handlers.insert(position);
3718 }
3719 self.handlers.insert(
3720 position,
3721 RegisteredHandler {
3722 id,
3723 handler,
3724 interests,
3725 has_log_interests,
3726 log_route_index,
3727 },
3728 );
3729 }
3730
3731 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
3737 let position = self.handler_positions.remove(id)?;
3738 let registered = self.handlers.remove(&position)?;
3739 if let Some(index) = ®istered.log_route_index {
3740 for key in index.keys() {
3741 let remove_bucket = self
3742 .indexed_log_handlers
3743 .get_mut(key)
3744 .is_some_and(|owners| {
3745 owners.remove(&position);
3746 owners.is_empty()
3747 });
3748 if remove_bucket {
3749 self.indexed_log_handlers.remove(key);
3750 }
3751 if let LogRouteKey::DataSlice { offset, value } = key {
3752 let shape = (*offset, value.len());
3753 let remove_shape =
3754 self.data_slice_shapes.get_mut(&shape).is_some_and(|count| {
3755 *count -= 1;
3756 *count == 0
3757 });
3758 if remove_shape {
3759 self.data_slice_shapes.remove(&shape);
3760 }
3761 }
3762 }
3763 } else {
3764 self.fallback_log_handlers.remove(&position);
3765 }
3766 Some(registered.handler)
3767 }
3768
3769 pub fn contains_handler(&self, id: &HandlerId) -> bool {
3771 self.handler_positions.contains_key(id)
3772 }
3773
3774 pub fn handler_ids(&self) -> Vec<HandlerId> {
3776 self.handlers
3777 .values()
3778 .map(|handler| handler.id.clone())
3779 .collect()
3780 }
3781
3782 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
3784 self.handler_positions
3785 .get(id)
3786 .and_then(|position| self.handlers.get(position))
3787 .map(|registered| registered.interests.as_slice())
3788 }
3789
3790 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
3792 self.handlers
3793 .values()
3794 .flat_map(|handler| handler.interests.clone())
3795 .collect()
3796 }
3797
3798 pub fn log_subscription_filters(&self) -> Vec<Filter> {
3805 let mut filters = Vec::new();
3806 for interest in self.log_interests() {
3807 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
3808 }
3809 filters
3810 }
3811
3812 pub fn route_log(&self, log: &Log) -> Vec<ReactiveLogRoute> {
3818 self.log_handler_candidates(log)
3819 .into_iter()
3820 .filter_map(|handler| handler.route_log(log))
3821 .collect()
3822 }
3823
3824 fn log_handler_candidates(&self, log: &Log) -> Vec<&RegisteredHandler<N>> {
3825 let mut indexed_positions = Vec::new();
3826 if let Some(indexed) = self
3827 .indexed_log_handlers
3828 .get(&LogRouteKey::Emitter(log.address()))
3829 {
3830 indexed_positions.extend(indexed.iter().copied());
3831 }
3832 for (index, value) in log.topics().iter().copied().enumerate() {
3833 if let Some(indexed) = self
3834 .indexed_log_handlers
3835 .get(&LogRouteKey::Topic { index, value })
3836 {
3837 indexed_positions.extend(indexed.iter().copied());
3838 }
3839 }
3840 let data = log.inner.data.data.as_ref();
3841 for &(offset, len) in self.data_slice_shapes.keys() {
3842 let Some(end) = offset.checked_add(len) else {
3843 continue;
3844 };
3845 let Some(value) = data.get(offset..end) else {
3846 continue;
3847 };
3848 if let Some(indexed) = self.indexed_log_handlers.get(&LogRouteKey::DataSlice {
3849 offset,
3850 value: value.to_vec(),
3851 }) {
3852 indexed_positions.extend(indexed.iter().copied());
3853 }
3854 }
3855 if indexed_positions.is_empty() {
3856 if self.fallback_log_handlers.is_empty() {
3857 return Vec::new();
3858 }
3859 if !self.indexed_log_handlers.is_empty() {
3860 return self
3861 .fallback_log_handlers
3862 .iter()
3863 .filter_map(|position| self.handlers.get(position))
3864 .collect();
3865 }
3866 return self
3867 .handlers
3868 .values()
3869 .filter(|handler| handler.has_log_interests && handler.log_route_index.is_none())
3870 .collect();
3871 }
3872
3873 indexed_positions.extend(self.fallback_log_handlers.iter().copied());
3874 indexed_positions.sort_unstable();
3875 indexed_positions.dedup();
3876 indexed_positions
3877 .into_iter()
3878 .filter_map(|position| self.handlers.get(&position))
3879 .collect()
3880 }
3881
3882 fn handlers(&self) -> impl Iterator<Item = &RegisteredHandler<N>> {
3883 self.handlers.values()
3884 }
3885
3886 fn log_interests(&self) -> impl Iterator<Item = &LogInterest> {
3887 self.handlers.values().flat_map(|handler| {
3888 handler
3889 .interests
3890 .iter()
3891 .filter_map(|interest| match interest {
3892 ReactiveInterest::Logs(interest) => Some(interest),
3893 ReactiveInterest::Blocks(_) | ReactiveInterest::PendingTransactions(_) => None,
3894 })
3895 })
3896 }
3897
3898 fn compact_handler_positions(&mut self) {
3899 let handlers = std::mem::take(&mut self.handlers);
3900 self.handler_positions.clear();
3901 self.indexed_log_handlers.clear();
3902 self.fallback_log_handlers.clear();
3903 self.data_slice_shapes.clear();
3904
3905 for (position, (_, handler)) in handlers.into_iter().enumerate() {
3906 let position = position as u128;
3907 self.handler_positions.insert(handler.id.clone(), position);
3908 if let Some(index) = &handler.log_route_index {
3909 for key in index.keys() {
3910 if let LogRouteKey::DataSlice { offset, value } = key {
3911 *self
3912 .data_slice_shapes
3913 .entry((*offset, value.len()))
3914 .or_default() += 1;
3915 }
3916 self.indexed_log_handlers
3917 .entry(key.clone())
3918 .or_default()
3919 .insert(position);
3920 }
3921 } else if handler.has_log_interests {
3922 self.fallback_log_handlers.insert(position);
3923 }
3924 self.handlers.insert(position, handler);
3925 }
3926 self.next_handler_position = self.handlers.len() as u128;
3927 }
3928}
3929
3930impl<N: Network> ReactiveRuntime<N> {
3931 pub fn new(config: ReactiveConfig) -> Self {
3933 Self {
3934 registry: ReactiveRegistry::new(),
3935 hooks: Vec::new(),
3936 config,
3937 journal: VecDeque::new(),
3938 coverage_head: None,
3939 pending_resyncs: Vec::new(),
3940 health: CacheHealth::Healthy,
3941 safe_head: None,
3942 finalized_head: None,
3943 metrics: CacheMetrics::default(),
3944 freshness: None,
3945 tracking: HashMap::new(),
3946 tracked_roots: HashMap::new(),
3947 root_gate_cadence: RootGateCadence::default(),
3948 last_gate_block: None,
3949 touched_since_gate: HashSet::new(),
3950 preconfirmed_branch: None,
3951 }
3952 }
3953
3954 fn checkpoint_state(&self) -> ReactiveRuntimeState<N> {
3955 ReactiveRuntimeState {
3956 journal: self.journal.clone(),
3957 coverage_head: self.coverage_head,
3958 pending_resyncs: self.pending_resyncs.clone(),
3959 health: self.health,
3960 safe_head: self.safe_head,
3961 finalized_head: self.finalized_head,
3962 freshness: self.freshness.clone(),
3963 tracking: self.tracking.clone(),
3964 tracked_roots: self.tracked_roots.clone(),
3965 root_gate_cadence: self.root_gate_cadence,
3966 last_gate_block: self.last_gate_block,
3967 touched_since_gate: self.touched_since_gate.clone(),
3968 metrics: self.metrics.snapshot(),
3969 }
3970 }
3971
3972 fn is_pristine_for_checkpoint_restore(&self) -> bool {
3973 self.preconfirmed_branch.is_none()
3974 && self.journal.is_empty()
3975 && self.coverage_head.is_none()
3976 && self.pending_resyncs.is_empty()
3977 && self.health == CacheHealth::Healthy
3978 && self.safe_head.is_none()
3979 && self.finalized_head.is_none()
3980 && self.tracked_roots.is_empty()
3981 && self.last_gate_block.is_none()
3982 && self.touched_since_gate.is_empty()
3983 && self.metrics.snapshot() == CacheMetricsSnapshot::default()
3984 }
3985
3986 fn adopted_baseline_only(&self) -> Option<BlockRef> {
3987 let baseline = self.coverage_head?;
3988 let journal_is_baseline_only = if self.config.journal_depth == 0 {
3989 self.journal.is_empty()
3990 } else {
3991 self.journal.len() == 1
3992 && self.journal.front().is_some_and(|entry| {
3993 entry.block == baseline
3994 && entry.inputs.is_empty()
3995 && entry.applied.is_empty()
3996 && entry.handler_ids.is_empty()
3997 && entry.resynced.is_empty()
3998 && entry.rollback_diffs.is_empty()
3999 })
4000 };
4001 (self.preconfirmed_branch.is_none()
4002 && journal_is_baseline_only
4003 && self.pending_resyncs.is_empty()
4004 && self.health == CacheHealth::Healthy
4005 && self.safe_head.is_none()
4006 && self.finalized_head.is_none()
4007 && self.tracked_roots.is_empty()
4008 && self.last_gate_block.is_none()
4009 && self.touched_since_gate.is_empty()
4010 && self.metrics.snapshot() == CacheMetricsSnapshot::default())
4011 .then_some(baseline)
4012 }
4013
4014 fn restore_state(&mut self, state: ReactiveRuntimeState<N>) {
4015 self.journal = state.journal;
4016 self.coverage_head = state.coverage_head;
4017 self.pending_resyncs = state.pending_resyncs;
4018 self.health = state.health;
4019 self.safe_head = state.safe_head;
4020 self.finalized_head = state.finalized_head;
4021 self.freshness = state.freshness;
4022 self.tracking = state.tracking;
4023 self.tracked_roots = state.tracked_roots;
4024 self.root_gate_cadence = state.root_gate_cadence;
4025 self.last_gate_block = state.last_gate_block;
4026 self.touched_since_gate = state.touched_since_gate;
4027 self.metrics.restore(state.metrics);
4028 }
4029
4030 fn restore_transaction_state(&mut self, state: ReactiveRuntimeState<N>) {
4031 let metrics = self.metrics.snapshot();
4036 self.restore_state(state);
4037 self.metrics.restore(metrics);
4038 }
4039
4040 fn durable_checkpoint_bytes(&self) -> Result<Vec<u8>, ReactiveEngineError> {
4041 let checkpoint = DurableRuntimeCheckpoint {
4042 version: DURABLE_RUNTIME_CHECKPOINT_VERSION,
4043 safe_head: self.safe_head,
4044 finalized_head: self.finalized_head,
4045 health: self.health,
4046 pending_resyncs: self.pending_resyncs.clone(),
4047 coverage_head: self.coverage_head,
4048 journal: self
4049 .journal
4050 .iter()
4051 .map(|entry| DurableBlockJournal {
4052 block: entry.block,
4053 handler_ids: entry.handler_ids.clone(),
4054 rollback_diffs: entry.rollback_diffs.clone(),
4055 })
4056 .collect(),
4057 freshness: self.freshness.clone(),
4058 tracking: self.tracking.clone(),
4059 tracked_roots: self.tracked_roots.clone(),
4060 root_gate_cadence: self.root_gate_cadence,
4061 last_gate_block: self.last_gate_block,
4062 touched_since_gate: self.touched_since_gate.clone(),
4063 metrics: self.metrics.snapshot(),
4064 };
4065 bincode::serialize(&checkpoint)
4066 .map_err(|error| ReactiveEngineError::RuntimeCheckpoint(error.to_string()))
4067 }
4068
4069 fn plan_durable_checkpoint_restore(
4070 &self,
4071 bytes: &[u8],
4072 expected_coverage: &BlockRef,
4073 ) -> Result<DurableRuntimeRestorePlan, ReactiveCheckpointRestoreError> {
4074 let mut cursor = std::io::Cursor::new(bytes);
4075 let mut checkpoint: DurableRuntimeCheckpoint = bincode::DefaultOptions::new()
4076 .with_fixint_encoding()
4077 .with_limit(bytes.len() as u64)
4078 .deserialize_from(&mut cursor)
4079 .map_err(|error| {
4080 ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(error.to_string())
4081 })?;
4082 if cursor.position() != bytes.len() as u64 {
4083 return Err(ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(
4084 "runtime checkpoint has trailing bytes".to_owned(),
4085 ));
4086 }
4087 if checkpoint.version != DURABLE_RUNTIME_CHECKPOINT_VERSION {
4088 return Err(ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(
4089 format!(
4090 "unsupported runtime checkpoint version {}",
4091 checkpoint.version
4092 ),
4093 ));
4094 }
4095 self.validate_durable_runtime_checkpoint(&checkpoint, expected_coverage)?;
4096
4097 let retained = self.config.journal_depth.min(checkpoint.journal.len());
4098 let discard = checkpoint.journal.len() - retained;
4099 checkpoint.journal.drain(..discard);
4100 Ok(DurableRuntimeRestorePlan {
4101 checkpoint: Some(checkpoint),
4102 fallback_history: Vec::new(),
4103 })
4104 }
4105
4106 fn apply_durable_checkpoint_restore(&mut self, plan: DurableRuntimeRestorePlan) {
4107 let Some(checkpoint) = plan.checkpoint else {
4108 self.journal = plan
4109 .fallback_history
4110 .into_iter()
4111 .map(|block| BlockJournal {
4112 block,
4113 inputs: Vec::new(),
4114 applied: Vec::new(),
4115 handler_ids: Vec::new(),
4116 resynced: Vec::new(),
4117 rollback_diffs: Vec::new(),
4118 })
4119 .collect();
4120 return;
4121 };
4122 self.safe_head = checkpoint.safe_head;
4123 self.finalized_head = checkpoint.finalized_head;
4124 self.health = checkpoint.health;
4125 self.pending_resyncs = checkpoint.pending_resyncs;
4126 self.coverage_head = checkpoint.coverage_head;
4127 self.journal = checkpoint
4128 .journal
4129 .into_iter()
4130 .map(|entry| BlockJournal {
4131 block: entry.block,
4132 inputs: Vec::new(),
4133 applied: Vec::new(),
4134 handler_ids: entry.handler_ids,
4135 resynced: Vec::new(),
4136 rollback_diffs: entry.rollback_diffs,
4137 })
4138 .collect();
4139 self.freshness = checkpoint.freshness;
4140 self.tracking = checkpoint.tracking;
4141 self.tracked_roots = checkpoint.tracked_roots;
4142 self.root_gate_cadence = checkpoint.root_gate_cadence;
4143 self.last_gate_block = checkpoint.last_gate_block;
4144 self.touched_since_gate = checkpoint.touched_since_gate;
4145 self.metrics.restore(checkpoint.metrics);
4146 }
4147
4148 fn validate_durable_runtime_checkpoint(
4149 &self,
4150 checkpoint: &DurableRuntimeCheckpoint,
4151 expected_coverage: &BlockRef,
4152 ) -> Result<(), ReactiveCheckpointRestoreError> {
4153 let invalid =
4154 |message: String| ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(message);
4155 let Some(coverage) = checkpoint.coverage_head.as_ref() else {
4156 return Err(invalid(
4157 "runtime checkpoint is missing its canonical coverage head".into(),
4158 ));
4159 };
4160 if !optional_block_refs_are_compatible(Some(coverage), Some(expected_coverage)) {
4161 return Err(invalid(format!(
4162 "runtime coverage {}:{:?} conflicts with checkpoint metadata {}:{:?}",
4163 coverage.number, coverage.hash, expected_coverage.number, expected_coverage.hash
4164 )));
4165 }
4166 for (label, head) in [
4167 ("safe", checkpoint.safe_head.as_ref()),
4168 ("finalized", checkpoint.finalized_head.as_ref()),
4169 ] {
4170 let Some(head) = head else { continue };
4171 if head.number > coverage.number
4172 || (head.number == coverage.number && head.hash != coverage.hash)
4173 {
4174 return Err(invalid(format!(
4175 "{label} head {}:{:?} lies beyond or conflicts with canonical coverage {}:{:?}",
4176 head.number, head.hash, coverage.number, coverage.hash
4177 )));
4178 }
4179 if head.number.checked_add(1) == Some(coverage.number)
4180 && coverage
4181 .parent_hash
4182 .is_some_and(|parent| parent != head.hash)
4183 {
4184 return Err(invalid(format!(
4185 "canonical coverage does not descend from adjacent {label} head"
4186 )));
4187 }
4188 }
4189 if let (Some(finalized), Some(safe)) = (
4190 checkpoint.finalized_head.as_ref(),
4191 checkpoint.safe_head.as_ref(),
4192 ) {
4193 if finalized.number > safe.number
4194 || (finalized.number == safe.number && finalized.hash != safe.hash)
4195 {
4196 return Err(invalid(
4197 "finalized head is above or conflicts with the safe head".into(),
4198 ));
4199 }
4200 if finalized.number.checked_add(1) == Some(safe.number)
4201 && safe.parent_hash != Some(finalized.hash)
4202 {
4203 return Err(invalid(
4204 "adjacent safe head does not descend from finalized head".into(),
4205 ));
4206 }
4207 }
4208
4209 let mut previous: Option<&DurableBlockJournal> = None;
4210 for entry in &checkpoint.journal {
4211 if entry.block.number > coverage.number
4212 || (entry.block.number == coverage.number && entry.block.hash != coverage.hash)
4213 {
4214 return Err(invalid(format!(
4215 "journal block {}:{:?} lies beyond or conflicts with canonical coverage",
4216 entry.block.number, entry.block.hash
4217 )));
4218 }
4219 if let Some(previous) = previous {
4220 if entry.block.number <= previous.block.number {
4221 return Err(invalid(
4222 "runtime journal block numbers are not strictly increasing".into(),
4223 ));
4224 }
4225 if previous.block.number.checked_add(1) == Some(entry.block.number)
4226 && entry.block.parent_hash.is_some()
4227 && entry.block.parent_hash != Some(previous.block.hash)
4228 {
4229 return Err(invalid(
4230 "adjacent runtime journal blocks are not parent-linked".into(),
4231 ));
4232 }
4233 }
4234 for (label, head) in [
4235 ("safe", checkpoint.safe_head.as_ref()),
4236 ("finalized", checkpoint.finalized_head.as_ref()),
4237 ] {
4238 if let Some(head) = head
4239 && head.number == entry.block.number
4240 && !optional_block_refs_are_compatible(Some(head), Some(&entry.block))
4241 {
4242 return Err(invalid(format!(
4243 "{label} head conflicts with the retained journal at block {}",
4244 head.number
4245 )));
4246 }
4247 }
4248 let mut handler_ids = HashSet::new();
4249 if entry
4250 .handler_ids
4251 .iter()
4252 .any(|handler_id| !handler_ids.insert(handler_id))
4253 {
4254 return Err(invalid(
4255 "runtime journal contains duplicate handler generation ids".into(),
4256 ));
4257 }
4258 previous = Some(entry);
4259 }
4260 if let Some(tail) = checkpoint.journal.last()
4261 && tail.block.number == coverage.number
4262 && !optional_block_refs_are_compatible(Some(&tail.block), Some(coverage))
4263 {
4264 return Err(invalid(format!(
4265 "runtime journal tail conflicts with canonical coverage at block {}",
4266 coverage.number
4267 )));
4268 }
4269 if let Some(tail) = checkpoint.journal.last()
4270 && tail.block.number.checked_add(1) == Some(coverage.number)
4271 && coverage
4272 .parent_hash
4273 .is_some_and(|parent_hash| parent_hash != tail.block.hash)
4274 {
4275 return Err(invalid(format!(
4276 "canonical coverage does not descend from adjacent runtime journal tail at block {}",
4277 tail.block.number
4278 )));
4279 }
4280
4281 if let Some(last_gate_block) = checkpoint.last_gate_block {
4282 if last_gate_block > coverage.number {
4283 return Err(invalid(
4284 "root-gate cursor lies beyond canonical coverage".into(),
4285 ));
4286 }
4287 } else if !checkpoint.tracked_roots.is_empty() {
4288 return Err(invalid(
4289 "root-gate baselines exist without a completed gate cursor".into(),
4290 ));
4291 }
4292 for (address, baseline) in &checkpoint.tracked_roots {
4293 let Some(policy) = checkpoint.tracking.get(address) else {
4294 return Err(invalid(
4295 "root-gate baseline has no corresponding tracking policy".into(),
4296 ));
4297 };
4298 if matches!(policy, TrackingPolicy::Slots { .. }) {
4299 return Err(invalid(
4300 "slot-only tracking cannot carry an account root baseline".into(),
4301 ));
4302 }
4303 if baseline.last_block > coverage.number
4304 || checkpoint
4305 .last_gate_block
4306 .is_some_and(|last_gate| baseline.last_block > last_gate)
4307 {
4308 return Err(invalid(
4309 "root-gate baseline lies beyond the committed gate window".into(),
4310 ));
4311 }
4312 }
4313 Ok(())
4314 }
4315
4316 pub fn track_account(&mut self, address: Address, policy: TrackingPolicy) {
4330 self.tracking.insert(address, policy);
4331 self.tracked_roots.remove(&address);
4332 }
4333
4334 pub fn untrack_account(&mut self, address: Address) -> bool {
4338 self.tracked_roots.remove(&address);
4339 self.tracking.remove(&address).is_some()
4340 }
4341
4342 pub fn set_root_gate_cadence(&mut self, cadence: RootGateCadence) {
4350 self.root_gate_cadence = cadence;
4351 self.last_gate_block = None;
4352 self.touched_since_gate.clear();
4353 }
4354
4355 pub fn root_gate_cadence(&self) -> RootGateCadence {
4357 self.root_gate_cadence
4358 }
4359
4360 pub fn enable_freshness_stamping(&mut self) {
4372 if self.freshness.is_none() {
4373 self.freshness = Some(FreshnessRegistry::new());
4374 }
4375 }
4376
4377 pub fn freshness(&self) -> Option<&FreshnessRegistry> {
4382 self.freshness.as_ref()
4383 }
4384
4385 pub fn freshness_mut(&mut self) -> Option<&mut FreshnessRegistry> {
4390 self.freshness.as_mut()
4391 }
4392
4393 pub fn health(&self) -> CacheHealth {
4395 self.health
4396 }
4397
4398 pub fn metrics(&self) -> CacheMetricsSnapshot {
4400 self.metrics.snapshot()
4401 }
4402
4403 pub fn reset_health(&mut self) {
4413 self.health = CacheHealth::Healthy;
4414 }
4415
4416 fn escalate_trust(&mut self, block: u64) -> Option<Arc<ReactiveReport<N>>> {
4430 let to = match self.health {
4431 CacheHealth::Healthy => CacheHealth::Degraded { since_block: block },
4432 CacheHealth::Degraded { .. } => CacheHealth::Unhealthy { since_block: block },
4433 CacheHealth::Unhealthy { .. } => return None,
4434 };
4435 self.transition_health(to, Some(block))
4436 }
4437
4438 fn transition_health(
4446 &mut self,
4447 to: CacheHealth,
4448 block: Option<u64>,
4449 ) -> Option<Arc<ReactiveReport<N>>> {
4450 if to == self.health {
4451 return None;
4452 }
4453 let from = self.health;
4454 self.health = to;
4455 Some(Arc::new(ReactiveReport::Health(HealthReport {
4456 from,
4457 to,
4458 block,
4459 _network: PhantomData,
4460 })))
4461 }
4462
4463 pub fn register_handler(
4470 &mut self,
4471 handler: Arc<dyn ReactiveHandler<N>>,
4472 ) -> Result<(), RegisterError> {
4473 self.registry.register_handler(handler)
4474 }
4475
4476 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
4484 self.registry.unregister_handler(id)
4485 }
4486
4487 pub fn contains_handler(&self, id: &HandlerId) -> bool {
4489 self.registry.contains_handler(id)
4490 }
4491
4492 pub fn handler_ids(&self) -> Vec<HandlerId> {
4494 self.registry.handler_ids()
4495 }
4496
4497 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
4499 self.registry.handler_interests(id)
4500 }
4501
4502 pub fn last_canonical_block(&self) -> Option<BlockRef> {
4512 self.coverage_head
4513 }
4514
4515 pub fn adopt_canonical_baseline(
4532 &mut self,
4533 baseline: BlockRef,
4534 ) -> Result<(), ReactiveBaselineError> {
4535 self.validate_canonical_baseline_adoption(baseline)?;
4536 if self.adopted_baseline_only().is_some() {
4537 return Ok(());
4538 }
4539
4540 self.coverage_head = Some(baseline);
4541 if self.config.journal_depth > 0 {
4542 self.journal.push_back(BlockJournal {
4543 block: baseline,
4544 inputs: Vec::new(),
4545 applied: Vec::new(),
4546 handler_ids: Vec::new(),
4547 resynced: Vec::new(),
4548 rollback_diffs: Vec::new(),
4549 });
4550 }
4551 Ok(())
4552 }
4553
4554 fn validate_canonical_baseline_adoption(
4555 &self,
4556 baseline: BlockRef,
4557 ) -> Result<(), ReactiveBaselineError> {
4558 if let Some(existing) = self.adopted_baseline_only() {
4559 return if existing == baseline {
4560 Ok(())
4561 } else {
4562 Err(ReactiveBaselineError::ConflictingBaseline {
4563 existing_number: existing.number,
4564 existing_hash: existing.hash,
4565 requested_number: baseline.number,
4566 requested_hash: baseline.hash,
4567 })
4568 };
4569 }
4570 if !self.is_pristine_for_checkpoint_restore() {
4571 return Err(ReactiveBaselineError::ActiveRuntime);
4572 }
4573 Ok(())
4574 }
4575
4576 pub const fn safe_head(&self) -> Option<&BlockRef> {
4578 self.safe_head.as_ref()
4579 }
4580
4581 pub const fn finalized_head(&self) -> Option<&BlockRef> {
4583 self.finalized_head.as_ref()
4584 }
4585
4586 pub fn has_journaled_handler_effects(&self, handler_id: &HandlerId) -> bool {
4594 self.journal
4595 .iter()
4596 .any(|entry| entry.handler_ids.contains(handler_id))
4597 }
4598
4599 pub fn journaled_handler_ids(&self) -> HashSet<HandlerId> {
4606 self.journal
4607 .iter()
4608 .flat_map(|entry| entry.handler_ids.iter().cloned())
4609 .collect()
4610 }
4611
4612 pub fn pending_resyncs(&self) -> &[ResyncRequest] {
4623 &self.pending_resyncs
4624 }
4625
4626 pub fn cancel_pending_resync(&mut self, id: &ResyncId) -> Vec<ResyncRequest> {
4635 self.cancel_pending_resyncs_by_id(std::slice::from_ref(id))
4636 }
4637
4638 pub fn cancel_pending_resyncs_by_id(&mut self, ids: &[ResyncId]) -> Vec<ResyncRequest> {
4645 if ids.is_empty() {
4646 return Vec::new();
4647 }
4648 let ids: HashSet<&ResyncId> = ids.iter().collect();
4649 let mut cancelled = Vec::new();
4650 self.pending_resyncs.retain(|request| {
4651 if ids.contains(&request.id) {
4652 cancelled.push(request.clone());
4653 false
4654 } else {
4655 true
4656 }
4657 });
4658 cancelled
4659 }
4660
4661 pub fn cancel_pending_resyncs(&mut self, address: Address) -> Vec<ResyncRequest> {
4676 let mut cancelled = Vec::new();
4677 self.pending_resyncs.retain_mut(|request| {
4678 let (matching, remaining): (Vec<_>, Vec<_>) = request
4679 .targets
4680 .drain(..)
4681 .partition(|target| resync_target_address(target) == address);
4682 request.targets = remaining;
4683 if !matching.is_empty() {
4684 cancelled.push(ResyncRequest {
4685 id: request.id.clone(),
4686 reason: request.reason.clone(),
4687 block: request.block.clone(),
4688 targets: matching,
4689 priority: request.priority,
4690 });
4691 }
4692 !request.targets.is_empty()
4693 });
4694 cancelled
4695 }
4696
4697 pub fn register_hook(&mut self, hook: Arc<dyn ReactiveHook<N>>) -> Result<(), RegisterError> {
4704 self.hooks.push(hook);
4705 Ok(())
4706 }
4707
4708 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
4710 self.registry.interests()
4711 }
4712
4713 pub fn ingest_batch(
4728 &mut self,
4729 cache: &mut EvmCache,
4730 batch: ReactiveInputBatch<N>,
4731 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4732 let preconfirmation = batch_preconfirmation(&batch)?;
4733 if let Some(flashblock) = preconfirmation.as_ref() {
4734 self.prepare_preconfirmed_branch(cache, flashblock)?;
4735 } else {
4736 self.discard_preconfirmed_branch(cache);
4737 }
4738 let cache_state = EvmCacheStateSnapshot::capture(cache);
4739 let runtime_state = self.checkpoint_state();
4740 let batch_report = match self.ingest_batch_direct(cache, batch) {
4741 Ok(report) => report,
4742 Err(error) => {
4743 cache_state.restore(cache);
4744 self.restore_transaction_state(runtime_state);
4745 return Err(error);
4746 }
4747 };
4748 if let Some(flashblock) = preconfirmation {
4749 self.restore_transaction_state(runtime_state);
4750 if let Some(branch) = self.preconfirmed_branch.as_mut() {
4751 branch.flashblock = flashblock;
4752 }
4753 }
4754 self.dispatch_reports(&batch_report.reports);
4755 let _ = &self.config;
4756 Ok(batch_report)
4757 }
4758
4759 pub fn ingest_batch_with_resync(
4776 &mut self,
4777 cache: &mut EvmCache,
4778 batch: ReactiveInputBatch<N>,
4779 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4780 let preconfirmation = batch_preconfirmation(&batch)?;
4781 if let Some(flashblock) = preconfirmation.as_ref() {
4782 self.prepare_preconfirmed_branch(cache, flashblock)?;
4783 } else {
4784 self.discard_preconfirmed_branch(cache);
4785 }
4786 let cache_state = EvmCacheStateSnapshot::capture(cache);
4787 let runtime_state = self.checkpoint_state();
4788 let batch_report = match self.ingest_batch_with_resync_direct(cache, batch) {
4789 Ok(report) => report,
4790 Err(error) => {
4791 cache_state.restore(cache);
4792 self.restore_transaction_state(runtime_state);
4793 return Err(error);
4794 }
4795 };
4796
4797 if let Some(flashblock) = preconfirmation {
4798 self.restore_transaction_state(runtime_state);
4799 if let Some(branch) = self.preconfirmed_branch.as_mut() {
4800 branch.flashblock = flashblock;
4801 }
4802 }
4803
4804 self.dispatch_reports(&batch_report.reports);
4805 let _ = &self.config;
4806 Ok(batch_report)
4807 }
4808
4809 pub fn active_preconfirmation(&self) -> Option<&FlashblockRef> {
4812 self.preconfirmed_branch
4813 .as_ref()
4814 .map(|branch| &branch.flashblock)
4815 }
4816
4817 pub fn discard_preconfirmation(&mut self, cache: &mut EvmCache) {
4820 self.discard_preconfirmed_branch(cache);
4821 }
4822
4823 fn discard_preconfirmed_branch(&mut self, cache: &mut EvmCache) {
4824 if let Some(branch) = self.preconfirmed_branch.take() {
4825 branch.canonical_cache.restore(cache);
4826 }
4827 }
4828
4829 fn prepare_preconfirmed_branch(
4830 &mut self,
4831 cache: &mut EvmCache,
4832 incoming: &FlashblockRef,
4833 ) -> Result<(), ReactiveError> {
4834 if let Some(active) = self.preconfirmed_branch.as_ref()
4835 && active.flashblock.same_payload(incoming)
4836 {
4837 if let (Some(active_index), Some(incoming_index)) =
4838 (active.flashblock.index, incoming.index)
4839 && incoming_index < active_index
4840 {
4841 return Err(ReactiveError::InvalidInputRecord {
4842 message: format!(
4843 "Flashblock index regressed from {active_index} to {incoming_index}"
4844 ),
4845 });
4846 }
4847 if active.flashblock.index.is_some()
4848 && active.flashblock.index == incoming.index
4849 && active.flashblock.content_hash != incoming.content_hash
4850 {
4851 self.discard_preconfirmed_branch(cache);
4852 return Err(ReactiveError::InvalidInputRecord {
4853 message: "same Flashblock payload/index carried conflicting cumulative content"
4854 .into(),
4855 });
4856 }
4857 install_preconfirmed_cache_context(cache, incoming);
4858 return Ok(());
4859 }
4860
4861 self.discard_preconfirmed_branch(cache);
4862 self.preconfirmed_branch = Some(PreconfirmedBranch {
4863 flashblock: incoming.clone(),
4864 canonical_cache: EvmCacheStateSnapshot::capture(cache),
4865 });
4866 install_preconfirmed_cache_context(cache, incoming);
4867 Ok(())
4868 }
4869
4870 fn ingest_batch_with_resync_direct(
4871 &mut self,
4872 cache: &mut EvmCache,
4873 batch: ReactiveInputBatch<N>,
4874 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4875 let mut batch_report = self.ingest_batch_direct(cache, batch)?;
4876 if !batch_report.resyncs.is_empty() {
4877 let resync_report = execute_resync_requests(cache, &batch_report.resyncs);
4878 let unique_requests = resync_report
4882 .requested
4883 .iter()
4884 .map(|request| &request.id)
4885 .collect::<HashSet<_>>()
4886 .len();
4887 self.metrics
4888 .resync_requests
4889 .fetch_add(unique_requests as u64, Ordering::Relaxed);
4890 self.metrics
4891 .resync_failures
4892 .fetch_add(resync_report.failed.len() as u64, Ordering::Relaxed);
4893 self.remove_pending_resyncs(batch_report.resyncs.iter().map(|request| &request.id));
4894 self.record_journal_resync(&resync_report);
4895 batch_report
4896 .reports
4897 .push(Arc::new(ReactiveReport::Resynced(resync_report)));
4898 }
4899 Ok(batch_report)
4900 }
4901
4902 fn ingest_batch_direct(
4903 &mut self,
4904 cache: &mut EvmCache,
4905 batch: ReactiveInputBatch<N>,
4906 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4907 let (records, chain_controls, batch_chain_id) = batch.into_runtime_parts();
4908 if let Some(chain_id) = batch_chain_id
4909 && chain_id != cache.chain_id()
4910 {
4911 return Err(ReactiveError::InvalidInputRecord {
4912 message: format!(
4913 "batch chain id {chain_id} does not match cache chain id {}",
4914 cache.chain_id()
4915 ),
4916 });
4917 }
4918 if !chain_controls.is_empty() && batch_chain_id.is_none() {
4919 return Err(ReactiveError::InvalidChainControl {
4920 message: "chain-control batches require an authoritative batch chain id".into(),
4921 });
4922 }
4923 for (record, _, _) in &records {
4924 record.validated_identity()?;
4925 if let Some(chain_id) = record.context.chain_id
4926 && chain_id != cache.chain_id()
4927 {
4928 return Err(ReactiveError::InvalidInputRecord {
4929 message: format!(
4930 "input chain id {chain_id} does not match cache chain id {}",
4931 cache.chain_id()
4932 ),
4933 });
4934 }
4935 }
4936 let records = sort_scoped_records(dedupe_scoped_records(records)?);
4937
4938 let mut batch_report = ReactiveBatchReport::default();
4939 let mut reports_to_dispatch = Vec::new();
4940 let control_split = validate_control_phase_order(&chain_controls)?;
4941 let (pre_record_controls, post_record_controls) = chain_controls.split_at(control_split);
4942 let pre_record_state =
4943 self.validate_ingest_sequence(pre_record_controls, post_record_controls, &records)?;
4944 self.validate_owner_catchup_against_journal(&pre_record_state, &records)?;
4945 let mut batch_dropped = BatchDroppedCanonical::default();
4946 for control in pre_record_controls {
4947 if let ChainControl::Reorg {
4948 common_ancestor,
4949 old_tip,
4950 ..
4951 } = control
4952 {
4953 batch_dropped.record_explicit(common_ancestor, old_tip);
4954 let drained = self
4955 .journal
4956 .iter()
4957 .filter(|entry| entry.block.number > common_ancestor.number)
4958 .map(|entry| entry.block)
4959 .collect::<Vec<_>>();
4960 batch_dropped.record_drained(&drained);
4961 }
4962 }
4963 let certified_progress_through = post_record_controls
4964 .iter()
4965 .filter_map(canonical_coverage_control_block)
4966 .map(|block| block.number)
4967 .max();
4968 for control in pre_record_controls.iter().cloned() {
4969 self.apply_chain_control(cache, control, &mut batch_report, &mut reports_to_dispatch);
4970 }
4971 let mut touched_addrs: HashSet<Address> = HashSet::new();
4976 let mut canonical_batch_block: Option<u64> = None;
4977
4978 for (record, audience, delivery_scope) in records {
4979 let raw_canonical_block = canonical_record_block(&record).copied();
4980 let canonical_block = raw_canonical_block.map(|block| {
4981 pre_record_state
4982 .resolved_canonical_blocks
4983 .get(&(block.number, block.hash))
4984 .copied()
4985 .unwrap_or(block)
4986 });
4987 let input_ref = record.input_ref();
4988 reports_to_dispatch.push(Arc::new(ReactiveReport::Input(InputReport {
4989 input_ref,
4990 context: record.context.clone(),
4991 provider: record.provider.clone(),
4992 _network: PhantomData,
4993 })));
4994
4995 let recovered_reorg = if delivery_scope.advances_canonical_state() {
4996 if let Some(block) = canonical_block.as_ref() {
4997 let gap_is_certified = delivery_scope == DeliveryScope::CanonicalProgress
4998 && certified_progress_through
4999 .is_some_and(|through| block.number <= through);
5000 let parentless_replacement_is_proven = raw_canonical_block.is_some_and(|raw| {
5001 raw.parent_hash.is_none()
5002 && batch_dropped.covers_implicit_number(raw.number)
5003 });
5004 self.recover_for_canonical_input(
5005 cache,
5006 block,
5007 gap_is_certified,
5008 parentless_replacement_is_proven,
5009 &mut reports_to_dispatch,
5010 )
5011 } else {
5012 None
5013 }
5014 } else {
5015 None
5016 };
5017 let recovered_reorg_for_input = recovered_reorg.is_some();
5018 if let Some(reorg_report) = recovered_reorg {
5019 self.metrics
5020 .reorgs_recovered
5021 .fetch_add(1, Ordering::Relaxed);
5022 remove_canceled_resyncs_from_batch(
5023 &mut batch_report.resyncs,
5024 &reorg_report.canceled_resyncs,
5025 );
5026 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5027 }
5028
5029 if reorg_signal_block(&record).is_some() {
5035 if delivery_scope.advances_canonical_state()
5036 && let Some(reorg_report) = self.recover_for_reorged_input(
5037 cache,
5038 &record,
5039 &mut batch_dropped,
5040 &mut reports_to_dispatch,
5041 )
5042 {
5043 self.metrics
5044 .reorgs_recovered
5045 .fetch_add(1, Ordering::Relaxed);
5046 remove_canceled_resyncs_from_batch(
5047 &mut batch_report.resyncs,
5048 &reorg_report.canceled_resyncs,
5049 );
5050 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5051 }
5052 continue;
5053 }
5054
5055 if delivery_scope == DeliveryScope::OwnerCatchup {
5062 self.validate_owner_catchup_record_against_current_journal(&record)?;
5063 }
5064
5065 if delivery_scope.advances_canonical_state()
5066 && let Some(block) = canonical_block.as_ref()
5067 {
5068 canonical_batch_block = Some(block.number);
5071 self.record_journal_input(block, input_ref);
5072 }
5073
5074 if delivery_scope.advances_canonical_state()
5080 && let Some(block) = canonical_block.as_ref()
5081 {
5082 match advance_block_for_canonical_record(cache, &record) {
5083 Some(Ok(())) => {
5084 cache.advance_compact_block(block.number, block.hash, block.timestamp, true)
5085 }
5086 Some(Err(err)) => {
5087 cache.advance_compact_block(
5088 block.number,
5089 block.hash,
5090 block.timestamp,
5091 false,
5092 );
5093 reports_to_dispatch.push(Arc::new(ReactiveReport::Error(
5094 ReactiveErrorReport {
5095 input_ref: Some(input_ref),
5096 message: err.to_string(),
5097 _network: PhantomData,
5098 },
5099 )));
5100 }
5101 None => cache.advance_compact_block(
5102 block.number,
5103 block.hash,
5104 block.timestamp,
5105 !recovered_reorg_for_input,
5106 ),
5107 }
5108 }
5109
5110 let executions = self.execute_handlers(cache, &record, input_ref, &audience)?;
5111 if executions.is_empty() {
5112 continue;
5113 }
5114
5115 reports_to_dispatch.push(Arc::new(ReactiveReport::Decoded(DecodedReport {
5116 input_ref,
5117 handler_ids: executions
5118 .iter()
5119 .map(|execution| execution.handler_id.clone())
5120 .collect(),
5121 _network: PhantomData,
5122 })));
5123
5124 detect_conflicts(input_ref, &executions)?;
5125
5126 let canonical_block_number = delivery_scope
5132 .advances_canonical_state()
5133 .then_some(canonical_block)
5134 .flatten()
5135 .map(|block| block.number);
5136
5137 for execution in executions {
5138 let diff = if execution.state_updates.is_empty() {
5139 StateDiff::default()
5140 } else {
5141 cache.apply_updates(&execution.state_updates)
5142 };
5143
5144 batch_report
5145 .resyncs
5146 .extend(execution.resyncs.iter().cloned());
5147 self.pending_resyncs
5148 .extend(execution.resyncs.iter().cloned());
5149 batch_report
5150 .speculative
5151 .extend(execution.speculative.iter().cloned());
5152
5153 let applied = AppliedReport {
5154 input_ref,
5155 handler_id: execution.handler_id,
5156 quality: execution.quality,
5157 tags: execution.tags,
5158 diff,
5159 state_updates: execution.state_updates,
5160 invalidations: execution.invalidations,
5161 resyncs: execution.resyncs,
5162 speculative: execution.speculative,
5163 hook_signals: execution.hook_signals,
5164 _network: PhantomData,
5165 };
5166 if let (Some(number), Some(registry)) =
5175 (canonical_block_number, self.freshness.as_mut())
5176 {
5177 for change in &applied.diff.slots {
5178 registry.valid_through_slot(change.address, change.slot, number);
5179 }
5180 }
5181
5182 if delivery_scope.advances_canonical_state() {
5190 collect_diff_addresses(&applied.diff, &mut touched_addrs);
5191 }
5192
5193 let report = Arc::new(ReactiveReport::Applied(applied.clone()));
5194 reports_to_dispatch.push(report);
5195 if let Some(block) = canonical_block.as_ref() {
5196 if delivery_scope.advances_canonical_state() {
5197 self.record_journal_applied(block, applied.clone());
5198 } else {
5199 self.record_journal_applied_if_present(block, applied.clone());
5200 }
5201 }
5202 batch_report.applied.push(applied);
5203 }
5204 }
5205
5206 for control in post_record_controls.iter().cloned() {
5211 if let Some(block) = canonical_coverage_control_block(&control) {
5212 canonical_batch_block = Some(
5213 canonical_batch_block.map_or(block.number, |current| current.max(block.number)),
5214 );
5215 }
5216 self.apply_chain_control(cache, control, &mut batch_report, &mut reports_to_dispatch);
5217 }
5218
5219 if self.root_gate_runnable(cache) {
5229 self.touched_since_gate
5230 .extend(touched_addrs.iter().copied());
5231 if self.root_gate_due(canonical_batch_block) {
5232 let accumulated = std::mem::take(&mut self.touched_since_gate);
5233 self.run_root_gate(
5234 cache,
5235 canonical_batch_block,
5236 &accumulated,
5237 &mut batch_report.resyncs,
5238 &mut reports_to_dispatch,
5239 );
5240 self.last_gate_block = canonical_batch_block;
5241 }
5242 } else {
5243 self.touched_since_gate.clear();
5250 }
5251
5252 batch_report.reports = reports_to_dispatch;
5253 Ok(batch_report)
5254 }
5255
5256 fn validate_owner_catchup_against_journal(
5270 &self,
5271 control_state: &ChainControlState,
5272 records: &[(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)],
5273 ) -> Result<(), ReactiveError> {
5274 for (record, _, delivery_scope) in records {
5275 if *delivery_scope != DeliveryScope::OwnerCatchup {
5276 continue;
5277 }
5278 if reorg_signal_block(record).is_some() {
5283 continue;
5284 }
5285 let context_block = canonical_record_block(record).ok_or_else(|| {
5286 ReactiveError::InvalidChainControl {
5287 message: "owner catch-up input has no canonical block identity".into(),
5288 }
5289 })?;
5290 let block = resolve_record_block_payload_metadata(record, *context_block)?;
5291 let invalidated_by_control = control_state
5292 .journal_invalidated_from
5293 .is_some_and(|from| block.number >= from);
5294 let rollbackable = !invalidated_by_control
5295 && self.journal.iter().any(|entry| {
5296 optional_block_refs_are_compatible(Some(&entry.block), Some(&block))
5297 });
5298 if !rollbackable {
5299 return Err(ReactiveError::OwnerCatchupOutsideJournal {
5300 number: block.number,
5301 hash: block.hash,
5302 });
5303 }
5304 }
5305 Ok(())
5306 }
5307
5308 fn validate_owner_catchup_record_against_current_journal(
5309 &self,
5310 record: &ReactiveInputRecord<N>,
5311 ) -> Result<(), ReactiveError> {
5312 let context_block =
5313 canonical_record_block(record).ok_or_else(|| ReactiveError::InvalidChainControl {
5314 message: "owner catch-up input has no canonical block identity".into(),
5315 })?;
5316 let block = resolve_record_block_payload_metadata(record, *context_block)?;
5317 if self
5318 .journal
5319 .iter()
5320 .any(|entry| optional_block_refs_are_compatible(Some(&entry.block), Some(&block)))
5321 {
5322 return Ok(());
5323 }
5324 Err(ReactiveError::OwnerCatchupOutsideJournal {
5325 number: block.number,
5326 hash: block.hash,
5327 })
5328 }
5329
5330 fn root_gate_runnable(&self, cache: &EvmCache) -> bool {
5335 if matches!(self.root_gate_cadence, RootGateCadence::Disabled) {
5336 return false;
5337 }
5338 let has_gated_targets = self
5339 .tracking
5340 .values()
5341 .any(|policy| !matches!(policy, TrackingPolicy::Slots { .. }));
5342 has_gated_targets && cache.account_proof_fetcher().is_some()
5343 }
5344
5345 fn root_gate_due(&self, canonical_block: Option<u64>) -> bool {
5349 let Some(block) = canonical_block else {
5350 return false;
5351 };
5352 match self.root_gate_cadence {
5353 RootGateCadence::Disabled => false,
5354 RootGateCadence::EveryNBlocks(n) => match self.last_gate_block {
5355 None => true,
5356 Some(last) => block >= last.saturating_add(n.get()),
5357 },
5358 }
5359 }
5360
5361 fn run_root_gate(
5387 &mut self,
5388 cache: &EvmCache,
5389 canonical_block: Option<u64>,
5390 touched: &HashSet<Address>,
5391 resyncs: &mut Vec<ResyncRequest>,
5392 reports: &mut Vec<Arc<ReactiveReport<N>>>,
5393 ) {
5394 if self.tracking.is_empty() {
5395 return;
5396 }
5397 let Some(block) = canonical_block else {
5398 return;
5399 };
5400 let Some(fetcher) = cache.account_proof_fetcher().cloned() else {
5401 return;
5402 };
5403
5404 let mut targets: Vec<(Address, bool)> = self
5407 .tracking
5408 .iter()
5409 .filter_map(|(address, policy)| match policy {
5410 TrackingPolicy::Slots { .. } => None,
5411 TrackingPolicy::WholeAccount => Some((*address, true)),
5412 TrackingPolicy::Scalars => Some((*address, false)),
5413 })
5414 .collect();
5415 if targets.is_empty() {
5416 return;
5417 }
5418 targets.sort_by_key(|(address, _)| *address);
5419
5420 let block_id = BlockId::number(block);
5421 let mut probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
5426 targets
5427 .iter()
5428 .map(|&(address, _)| (address, vec![]))
5429 .collect(),
5430 block_id,
5431 )
5432 .into_iter()
5433 .collect();
5434 for (address, whole_account) in targets {
5435 let Some(Ok(proof)) = probes.remove(&address) else {
5436 continue;
5439 };
5440
5441 let baseline = self.tracked_roots.get(&address).cloned();
5442 let Some(baseline) = baseline else {
5443 self.adopt_root(address, block, &proof);
5445 continue;
5446 };
5447
5448 if block <= baseline.last_block {
5453 continue;
5454 }
5455
5456 if whole_account {
5457 if proof.storage_hash == baseline.last_root {
5458 continue;
5460 }
5461 if !touched.contains(&address) {
5463 reports.push(Arc::new(ReactiveReport::CoverageGap(CoverageGapReport {
5465 address,
5466 block,
5467 _network: PhantomData,
5468 })));
5469 self.metrics.coverage_gaps.fetch_add(1, Ordering::Relaxed);
5470 resyncs.push(root_moved_account_resync(
5471 address,
5472 block,
5473 AccountFieldMask {
5474 balance: true,
5475 nonce: true,
5476 code: true,
5477 },
5478 ));
5479 }
5480 self.adopt_root(address, block, &proof);
5482 } else {
5483 let balance_moved = proof.balance != baseline.balance;
5486 let nonce_moved = proof.nonce != baseline.nonce;
5487 let code_moved = proof.code_hash != baseline.code_hash;
5488 if (balance_moved || nonce_moved || code_moved) && !touched.contains(&address) {
5489 resyncs.push(root_moved_account_resync(
5490 address,
5491 block,
5492 AccountFieldMask {
5493 balance: balance_moved,
5494 nonce: nonce_moved,
5495 code: code_moved,
5496 },
5497 ));
5498 }
5499 self.adopt_root(address, block, &proof);
5500 }
5501 }
5502 }
5503
5504 fn adopt_root(&mut self, address: Address, block: u64, proof: &AccountProof) {
5506 self.tracked_roots.insert(
5507 address,
5508 TrackedRoot {
5509 last_root: proof.storage_hash,
5510 last_block: block,
5511 balance: proof.balance,
5512 nonce: proof.nonce,
5513 code_hash: proof.code_hash,
5514 },
5515 );
5516 }
5517
5518 fn execute_handlers(
5519 &self,
5520 cache: &EvmCache,
5521 record: &ReactiveInputRecord<N>,
5522 input_ref: InputRef,
5523 audience: &DeliveryAudience,
5524 ) -> Result<Vec<HandlerExecution>, ReactiveError> {
5525 let mut executions = Vec::new();
5526 let candidates: Vec<_> = match &record.input {
5527 ReactiveInput::Log(log) => self.registry.log_handler_candidates(log),
5528 ReactiveInput::BlockHeader(_)
5529 | ReactiveInput::FullBlock(_)
5530 | ReactiveInput::PendingTxHash(_)
5531 | ReactiveInput::PendingTx(_) => self.registry.handlers().collect(),
5532 };
5533 for registered in candidates {
5534 match audience {
5535 DeliveryAudience::Owners(owners) if !owners.contains(®istered.id) => continue,
5536 DeliveryAudience::AllExcept(excluded) if excluded.contains(®istered.id) => {
5537 continue;
5538 }
5539 DeliveryAudience::All
5540 | DeliveryAudience::Owners(_)
5541 | DeliveryAudience::AllExcept(_) => {}
5542 }
5543 if !registered.matches(&record.input) {
5544 continue;
5545 }
5546
5547 let outcome = registered
5548 .handler
5549 .handle(&record.context, &record.input, cache)
5550 .map_err(|source| ReactiveError::HandlerFailed {
5551 handler_id: registered.id.clone(),
5552 source,
5553 })?;
5554
5555 if let Err(error) =
5556 validate_effects(input_ref, &record.context, ®istered.id, &outcome.effects)
5557 {
5558 if matches!(error, ReactiveError::InvalidPendingEffect { .. }) {
5559 self.metrics
5560 .pending_contamination
5561 .fetch_add(1, Ordering::Relaxed);
5562 }
5563 return Err(error);
5564 }
5565 executions.push(HandlerExecution::from_outcome(
5566 registered.id.clone(),
5567 input_ref,
5568 outcome,
5569 matches!(
5570 record.context.chain_status,
5571 ChainStatus::Preconfirmed { .. }
5572 ),
5573 ));
5574 }
5575 Ok(executions)
5576 }
5577
5578 fn dispatch_reports(&self, reports: &[Arc<ReactiveReport<N>>]) {
5579 for report in reports {
5580 for hook in &self.hooks {
5581 hook.on_report(report.clone());
5582 }
5583 }
5584 }
5585
5586 fn apply_chain_control(
5587 &mut self,
5588 cache: &mut EvmCache,
5589 control: ChainControl,
5590 batch_report: &mut ReactiveBatchReport<N>,
5591 reports: &mut Vec<Arc<ReactiveReport<N>>>,
5592 ) {
5593 match &control {
5594 ChainControl::Safe(block) => set_or_enrich_block_ref(&mut self.safe_head, block),
5595 ChainControl::Finalized(block) => {
5596 set_or_enrich_block_ref(&mut self.finalized_head, block);
5597 }
5598 ChainControl::CanonicalProgress(block)
5599 | ChainControl::Barrier {
5600 block: Some(block), ..
5601 } => {
5602 let preserve_env = self.coverage_head.as_ref().is_some_and(|current| {
5603 optional_block_refs_are_compatible(Some(current), Some(block))
5604 });
5605 cache.advance_compact_block(
5606 block.number,
5607 block.hash,
5608 block.timestamp,
5609 preserve_env,
5610 );
5611 advance_or_enrich_coverage(&mut self.coverage_head, block);
5612 let enriched = self.journal_entry_mut(block).block;
5613 advance_or_enrich_coverage(&mut self.coverage_head, &enriched);
5614 self.trim_journal();
5615 }
5616 ChainControl::Barrier { block: None, .. } => {}
5617 ChainControl::Reorg {
5618 common_ancestor,
5619 old_tip,
5620 ..
5621 } => {
5622 cache.invalidate_cached_block_hashes_from(common_ancestor.number.saturating_add(1));
5623 self.rebase_validation_state_from(common_ancestor.number.saturating_add(1));
5624 let dropped = if let Some(ancestor_index) = self.journal.iter().rposition(|entry| {
5625 entry.block.number == common_ancestor.number
5626 && entry.block.hash == common_ancestor.hash
5627 }) {
5628 self.drain_journal_after(ancestor_index)
5629 } else {
5630 if self
5635 .journal
5636 .front()
5637 .is_none_or(|entry| entry.block.number > common_ancestor.number)
5638 {
5639 reports.extend(
5640 self.warn_under_recovery(common_ancestor.number.saturating_add(1)),
5641 );
5642 }
5643 self.drain_journal_from_number(common_ancestor.number.saturating_add(1))
5644 };
5645
5646 let reorg_report = self
5647 .recover_dropped_journals(cache, dropped, ReorgReason::Explicit)
5648 .unwrap_or_else(|| ReorgReport {
5649 dropped: Some(*old_tip),
5650 dropped_blocks: Vec::new(),
5651 dropped_inputs: Vec::new(),
5652 rollback_updates: Vec::new(),
5653 rollback_diff: StateDiff::default(),
5654 purge_updates: Vec::new(),
5655 purge_diff: StateDiff::default(),
5656 canceled_resyncs: self
5657 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(old_tip)),
5658 reason: ReorgReason::Explicit,
5659 _network: PhantomData,
5660 });
5661 remove_canceled_resyncs_from_batch(
5662 &mut batch_report.resyncs,
5663 &reorg_report.canceled_resyncs,
5664 );
5665 self.metrics
5666 .reorgs_recovered
5667 .fetch_add(1, Ordering::Relaxed);
5668 reports.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5669
5670 if self.safe_head.as_ref().is_some_and(|head| {
5671 head.number > common_ancestor.number
5672 || (head.number == common_ancestor.number
5673 && head.hash != common_ancestor.hash)
5674 }) {
5675 self.safe_head = None;
5676 }
5677 if self.finalized_head.as_ref().is_some_and(|head| {
5678 head.number > common_ancestor.number
5679 || (head.number == common_ancestor.number
5680 && head.hash != common_ancestor.hash)
5681 }) {
5682 self.finalized_head = None;
5683 }
5684 let mut enriched_ancestor = *common_ancestor;
5685 if let Some(entry) = self.journal.iter().find(|entry| {
5686 entry.block.number == common_ancestor.number
5687 && entry.block.hash == common_ancestor.hash
5688 }) {
5689 enrich_block_ref(&mut enriched_ancestor, &entry.block);
5690 }
5691 if let Some(current) = self.coverage_head.as_ref()
5692 && current.number == common_ancestor.number
5693 && current.hash == common_ancestor.hash
5694 {
5695 enrich_block_ref(&mut enriched_ancestor, current);
5696 }
5697 self.coverage_head = Some(enriched_ancestor);
5698 cache.advance_compact_block(
5699 enriched_ancestor.number,
5700 enriched_ancestor.hash,
5701 enriched_ancestor.timestamp,
5702 false,
5703 );
5704 let enriched_ancestor = self.journal_entry_mut(&enriched_ancestor).block;
5705 self.coverage_head = Some(enriched_ancestor);
5706 self.trim_journal();
5707 }
5708 }
5709 reports.push(Arc::new(ReactiveReport::ChainControl(ChainControlReport {
5710 control,
5711 })));
5712 }
5713
5714 fn validate_ingest_sequence(
5715 &self,
5716 pre_record_controls: &[ChainControl],
5717 post_record_controls: &[ChainControl],
5718 records: &[(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)],
5719 ) -> Result<ChainControlState, ReactiveError> {
5720 let mut controls =
5721 Vec::with_capacity(pre_record_controls.len() + post_record_controls.len());
5722 controls.extend_from_slice(pre_record_controls);
5723 controls.extend_from_slice(post_record_controls);
5724 let state = CanonicalSequenceState::new(
5725 self.journal.iter().map(|entry| entry.block).collect(),
5726 self.coverage_head,
5727 self.safe_head,
5728 self.finalized_head,
5729 );
5730 let record_metadata = records
5731 .iter()
5732 .map(|(record, _, scope)| (record, *scope))
5733 .collect::<Vec<_>>();
5734 let validation = validate_canonical_sequence_parts(
5735 &state,
5736 &controls,
5737 &record_metadata,
5738 CanonicalSequenceValidationPolicy::ObserveIncompleteRollback,
5739 )
5740 .map_err(CanonicalSequenceError::into_reactive_error)?;
5741 let mut resolved_canonical_blocks = HashMap::new();
5742 for mutation in validation.mutations() {
5743 if let CanonicalSequenceMutation::Canonical(block) = mutation {
5744 resolved_canonical_blocks
5745 .entry((block.number, block.hash))
5746 .and_modify(|known| enrich_block_ref(known, block))
5747 .or_insert(*block);
5748 }
5749 }
5750 Ok(ChainControlState {
5751 journal_invalidated_from: pre_record_controls
5752 .iter()
5753 .filter_map(|control| match control {
5754 ChainControl::Reorg {
5755 common_ancestor, ..
5756 } => Some(common_ancestor.number.saturating_add(1)),
5757 _ => None,
5758 })
5759 .min(),
5760 resolved_canonical_blocks,
5761 })
5762 }
5763
5764 fn recover_for_canonical_input(
5765 &mut self,
5766 cache: &mut EvmCache,
5767 block: &BlockRef,
5768 gap_is_certified: bool,
5769 parentless_replacement_is_proven: bool,
5770 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
5771 ) -> Option<ReorgReport<N>> {
5772 let latest = self
5773 .coverage_head
5774 .or_else(|| self.journal.back().map(|entry| entry.block))?;
5775
5776 if latest.number == block.number && latest.hash == block.hash {
5777 return None;
5778 }
5779
5780 if self
5781 .journal
5782 .iter()
5783 .any(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
5784 {
5785 return None;
5786 }
5787
5788 if latest.number.checked_add(1) == Some(block.number)
5789 && (block.parent_hash == Some(latest.hash)
5790 || (parentless_replacement_is_proven && block.parent_hash.is_none()))
5791 {
5792 return None;
5793 }
5794
5795 if latest
5796 .number
5797 .checked_add(1)
5798 .is_some_and(|next| block.number > next)
5799 {
5800 if !gap_is_certified {
5807 self.metrics.missed_ranges.fetch_add(1, Ordering::Relaxed);
5808 health_reports.extend(self.escalate_trust(block.number));
5809 health_reports.push(Arc::new(ReactiveReport::MissedBlockRange(
5810 MissedRangeReport {
5811 from: latest.number + 1,
5812 to: block.number - 1,
5813 block: block.number,
5814 _network: PhantomData,
5815 },
5816 )));
5817 }
5818 return None;
5819 }
5820
5821 let (dropped, authenticated_anchor) = if let Some(parent_hash) = block.parent_hash {
5822 if let Some(parent_index) = self.journal.iter().rposition(|entry| {
5823 entry.block.number.checked_add(1) == Some(block.number)
5824 && entry.block.hash == parent_hash
5825 }) {
5826 let parent = self.journal[parent_index].block;
5827 cache.invalidate_cached_block_hashes_from(parent.number.saturating_add(1));
5828 (self.drain_journal_after(parent_index), Some(parent))
5829 } else {
5830 let proven_finalized_anchor = self.finalized_head.filter(|finalized| {
5836 finalized.number.checked_add(1) == Some(block.number)
5837 && parent_hash == finalized.hash
5838 });
5839 let invalidated_from = proven_finalized_anchor
5840 .map_or(0, |finalized| finalized.number.saturating_add(1));
5841 cache.invalidate_cached_block_hashes_from(invalidated_from);
5842 if block.number > 0 {
5843 cache.set_cached_block_hash(block.number.saturating_sub(1), parent_hash);
5847 }
5848 health_reports.extend(self.warn_under_recovery(block.number));
5849 let dropped = if let Some(finalized) = proven_finalized_anchor {
5850 self.drain_journal_from_number(finalized.number.saturating_add(1))
5851 } else {
5852 self.drain_journal_from_number(0)
5853 };
5854 (dropped, proven_finalized_anchor)
5855 }
5856 } else {
5857 cache.invalidate_cached_block_hashes_from(0);
5859 health_reports.extend(self.warn_under_recovery(block.number));
5860 (self.drain_journal_from_number(0), None)
5861 };
5862
5863 self.rebase_validation_state_from(
5864 authenticated_anchor.map_or(0, |anchor| anchor.number.saturating_add(1)),
5865 );
5866 let report = self
5867 .recover_dropped_journals(cache, dropped, ReorgReason::ParentMismatch)
5868 .or_else(|| {
5869 Some(ReorgReport {
5870 dropped: Some(latest),
5871 dropped_blocks: Vec::new(),
5872 dropped_inputs: Vec::new(),
5873 rollback_updates: Vec::new(),
5874 rollback_diff: StateDiff::default(),
5875 purge_updates: Vec::new(),
5876 purge_diff: StateDiff::default(),
5877 canceled_resyncs: self
5878 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&latest)),
5879 reason: ReorgReason::ParentMismatch,
5880 _network: PhantomData,
5881 })
5882 });
5883 self.coverage_head = authenticated_anchor;
5884 for head in [&mut self.safe_head, &mut self.finalized_head] {
5885 if head.is_some_and(|head| {
5886 authenticated_anchor.is_none_or(|anchor| {
5887 head.number > anchor.number
5888 || (head.number == anchor.number && head.hash != anchor.hash)
5889 })
5890 }) {
5891 *head = None;
5892 }
5893 }
5894 if let Some(anchor) = authenticated_anchor {
5895 cache.advance_compact_block(anchor.number, anchor.hash, anchor.timestamp, false);
5896 }
5897 report
5898 }
5899
5900 fn recover_for_reorged_input(
5901 &mut self,
5902 cache: &mut EvmCache,
5903 record: &ReactiveInputRecord<N>,
5904 batch_dropped: &mut BatchDroppedCanonical,
5905 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
5906 ) -> Option<ReorgReport<N>> {
5907 let (incoming_dropped_block, reason) = reorg_signal_block(record)?;
5908 if batch_dropped.contains(&incoming_dropped_block) {
5909 let canceled_resyncs = self
5914 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&incoming_dropped_block));
5915 return (!canceled_resyncs.is_empty()).then(|| ReorgReport {
5916 dropped: Some(incoming_dropped_block),
5917 dropped_blocks: vec![incoming_dropped_block],
5918 dropped_inputs: Vec::new(),
5919 rollback_updates: Vec::new(),
5920 rollback_diff: StateDiff::default(),
5921 purge_updates: Vec::new(),
5922 purge_diff: StateDiff::default(),
5923 canceled_resyncs,
5924 reason,
5925 _network: PhantomData,
5926 });
5927 }
5928 let exact_index = self.journal.iter().position(|entry| {
5929 entry.block.number == incoming_dropped_block.number
5930 && entry.block.hash == incoming_dropped_block.hash
5931 });
5932 let mut dropped_block = exact_index
5933 .map(|index| self.journal[index].block)
5934 .or_else(|| {
5935 self.coverage_head.filter(|known| {
5936 known.number == incoming_dropped_block.number
5937 && known.hash == incoming_dropped_block.hash
5938 })
5939 })
5940 .unwrap_or(incoming_dropped_block);
5941 enrich_block_ref(&mut dropped_block, &incoming_dropped_block);
5942 let replacement_is_known = exact_index.is_none()
5943 && (self.journal.iter().any(|entry| {
5944 entry.block.number == dropped_block.number && entry.block.hash != dropped_block.hash
5945 }) || self.coverage_head.is_some_and(|head| {
5946 head.number == dropped_block.number && head.hash != dropped_block.hash
5947 }));
5948
5949 if replacement_is_known {
5950 let canceled_resyncs =
5954 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
5955 return (!canceled_resyncs.is_empty()).then(|| ReorgReport {
5956 dropped: Some(dropped_block),
5957 dropped_blocks: vec![dropped_block],
5958 dropped_inputs: Vec::new(),
5959 rollback_updates: Vec::new(),
5960 rollback_diff: StateDiff::default(),
5961 purge_updates: Vec::new(),
5962 purge_diff: StateDiff::default(),
5963 canceled_resyncs,
5964 reason,
5965 _network: PhantomData,
5966 });
5967 }
5968
5969 let authenticated_anchor = exact_index.and_then(|index| {
5970 let ancestor_number = dropped_block.number.checked_sub(1)?;
5971 let retained = self
5972 .journal
5973 .iter()
5974 .take(index)
5975 .rev()
5976 .find(|entry| entry.block.number == ancestor_number)
5977 .map(|entry| entry.block);
5978 let synthetic_parent = dropped_block.parent_hash.map(|hash| BlockRef {
5979 number: ancestor_number,
5980 hash,
5981 parent_hash: None,
5982 timestamp: None,
5983 });
5984 let finalized_fallback = self
5985 .finalized_head
5986 .filter(|head| head.number == ancestor_number);
5987 let mut anchor = retained.or(synthetic_parent).or(finalized_fallback)?;
5988 for head in [self.safe_head.as_ref(), self.finalized_head.as_ref()]
5989 .into_iter()
5990 .flatten()
5991 {
5992 if head.number == anchor.number && head.hash == anchor.hash {
5993 enrich_block_ref(&mut anchor, head);
5994 }
5995 }
5996 Some(anchor)
5997 });
5998
5999 cache.invalidate_cached_block_hashes_from(dropped_block.number);
6000 let dropped = if let Some(index) = exact_index {
6001 self.drain_journal_from(index)
6002 } else {
6003 health_reports.extend(self.warn_under_recovery(dropped_block.number));
6004 self.drain_journal_from_number(dropped_block.number)
6005 };
6006 let drained_blocks = dropped.iter().map(|entry| entry.block).collect::<Vec<_>>();
6007 batch_dropped.record_drained(&drained_blocks);
6008 batch_dropped.record_identity(&dropped_block);
6009 self.rebase_validation_state_from(dropped_block.number);
6010
6011 let recovered_journal = !dropped.is_empty();
6012 let report = if !recovered_journal {
6013 let canceled_resyncs =
6014 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
6015 Some(ReorgReport {
6016 dropped: Some(dropped_block),
6017 dropped_blocks: Vec::new(),
6018 dropped_inputs: Vec::new(),
6019 rollback_updates: Vec::new(),
6020 rollback_diff: StateDiff::default(),
6021 purge_updates: Vec::new(),
6022 purge_diff: StateDiff::default(),
6023 canceled_resyncs,
6024 reason,
6025 _network: PhantomData,
6026 })
6027 } else {
6028 self.recover_dropped_journals(cache, dropped, reason)
6029 };
6030
6031 if recovered_journal {
6032 if let Some(anchor) = authenticated_anchor {
6033 self.coverage_head = Some(anchor);
6034 }
6035 let coverage = self.coverage_head;
6036 for head in [&mut self.safe_head, &mut self.finalized_head] {
6037 if head.is_some_and(|head| {
6038 coverage.is_none_or(|coverage| {
6039 head.number > coverage.number
6040 || (head.number == coverage.number && head.hash != coverage.hash)
6041 })
6042 }) {
6043 *head = None;
6044 }
6045 }
6046 }
6047
6048 if recovered_journal
6049 && report.is_some()
6050 && let Some(head) = self.coverage_head
6051 {
6052 cache.advance_compact_block(head.number, head.hash, head.timestamp, false);
6053 }
6054 report
6055 }
6056
6057 fn warn_under_recovery(&mut self, reorg_number: u64) -> Option<Arc<ReactiveReport<N>>> {
6069 let oldest_journaled = self.journal.front().map(|entry| entry.block.number);
6070 tracing::warn!(
6071 reorg_block = reorg_number,
6072 oldest_journaled = ?oldest_journaled,
6073 journal_depth = self.config.journal_depth,
6074 "reactive reorg recovery is incomplete: the reorged block is no longer \
6075 in the journal, so effects from blocks aged out of the journal are \
6076 neither rolled back nor purged (the freshness/validation loop is the \
6077 backstop). Increase ReactiveConfig::journal_depth to recover deeper \
6078 reorgs precisely."
6079 );
6080
6081 self.metrics.deep_reorgs.fetch_add(1, Ordering::Relaxed);
6082
6083 self.escalate_trust(reorg_number)
6084 }
6085
6086 fn record_journal_input(&mut self, block: &BlockRef, input_ref: InputRef) {
6087 advance_or_enrich_coverage(&mut self.coverage_head, block);
6088 let entry = self.journal_entry_mut(block);
6089 let enriched = entry.block;
6090 if !entry.inputs.contains(&input_ref) {
6091 entry.inputs.push(input_ref);
6092 }
6093 advance_or_enrich_coverage(&mut self.coverage_head, &enriched);
6094 self.trim_journal();
6095 }
6096
6097 fn record_journal_applied(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
6098 let entry = self.journal_entry_mut(block);
6099 if !entry.handler_ids.contains(&applied.handler_id) {
6100 entry.handler_ids.push(applied.handler_id.clone());
6101 }
6102 entry.rollback_diffs.push(applied.diff.clone());
6103 entry.applied.push(applied);
6104 self.trim_journal();
6105 }
6106
6107 fn record_journal_applied_if_present(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
6108 let Some(entry) = self
6109 .journal
6110 .iter_mut()
6111 .find(|entry| entry.block.number == block.number && entry.block.hash == block.hash)
6112 else {
6113 return;
6114 };
6115 if !entry.handler_ids.contains(&applied.handler_id) {
6116 entry.handler_ids.push(applied.handler_id.clone());
6117 }
6118 entry.rollback_diffs.push(applied.diff.clone());
6119 entry.applied.push(applied);
6120 }
6121
6122 fn record_journal_resync(&mut self, report: &ResyncReport) {
6123 if report.diff.is_empty() {
6124 return;
6125 }
6126 let Some(block) = single_hash_pinned_resync_block(report) else {
6127 return;
6128 };
6129 let entry = self.journal_entry_mut(&block);
6130 entry.rollback_diffs.push(report.diff.clone());
6131 entry.resynced.push(report.clone());
6132 self.trim_journal();
6133 }
6134
6135 fn journal_entry_mut(&mut self, block: &BlockRef) -> &mut BlockJournal<N> {
6136 if let Some(index) = self
6137 .journal
6138 .iter()
6139 .position(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
6140 {
6141 enrich_block_ref(&mut self.journal[index].block, block);
6142 return &mut self.journal[index];
6143 }
6144
6145 self.journal.push_back(BlockJournal {
6146 block: *block,
6147 inputs: Vec::new(),
6148 applied: Vec::new(),
6149 handler_ids: Vec::new(),
6150 resynced: Vec::new(),
6151 rollback_diffs: Vec::new(),
6152 });
6153 let index = self.journal.len() - 1;
6154 &mut self.journal[index]
6155 }
6156
6157 fn trim_journal(&mut self) {
6158 if self.config.journal_depth == 0 {
6159 self.journal.clear();
6160 return;
6161 }
6162 while self.journal.len() > self.config.journal_depth {
6163 self.journal.pop_front();
6164 }
6165 }
6166
6167 fn drain_journal_after(&mut self, index: usize) -> Vec<BlockJournal<N>> {
6168 self.journal.drain((index + 1)..).collect()
6169 }
6170
6171 fn drain_journal_from(&mut self, index: usize) -> Vec<BlockJournal<N>> {
6172 self.journal.drain(index..).collect()
6173 }
6174
6175 fn drain_journal_from_number(&mut self, number: u64) -> Vec<BlockJournal<N>> {
6176 let Some(index) = self
6177 .journal
6178 .iter()
6179 .position(|entry| entry.block.number >= number)
6180 else {
6181 return Vec::new();
6182 };
6183 self.drain_journal_from(index)
6184 }
6185
6186 fn recover_dropped_journals(
6187 &mut self,
6188 cache: &mut EvmCache,
6189 dropped: Vec<BlockJournal<N>>,
6190 reason: ReorgReason,
6191 ) -> Option<ReorgReport<N>> {
6192 if dropped.is_empty() {
6193 return None;
6194 }
6195
6196 let first_dropped_block = dropped
6197 .iter()
6198 .map(|entry| entry.block.number)
6199 .min()
6200 .expect("non-empty dropped journal set");
6201 self.rebase_validation_state_from(first_dropped_block);
6202 if self
6203 .safe_head
6204 .is_some_and(|head| head.number >= first_dropped_block)
6205 {
6206 self.safe_head = None;
6207 }
6208
6209 let dropped_blocks: Vec<_> = dropped.iter().map(|entry| entry.block).collect();
6210 let dropped_inputs: Vec<_> = dropped
6211 .iter()
6212 .flat_map(|entry| entry.inputs.iter().copied())
6213 .collect();
6214 let canceled_resyncs = self.cancel_resyncs_for_dropped_blocks(&dropped_blocks);
6215 let purge_scopes = purge_scopes_for_dropped_journals(&dropped);
6216 let rollback_updates = rollback_updates_for_dropped_journals(&dropped, &purge_scopes);
6217 let purge_updates: Vec<_> = purge_scopes
6218 .iter()
6219 .map(|(address, scope)| StateUpdate::purge(*address, scope.clone()))
6220 .collect();
6221
6222 let rollback_diff = if rollback_updates.is_empty() {
6223 StateDiff::default()
6224 } else {
6225 cache.apply_updates(&rollback_updates)
6226 };
6227 let purge_diff = if purge_updates.is_empty() {
6228 StateDiff::default()
6229 } else {
6230 cache.apply_updates(&purge_updates)
6231 };
6232 self.coverage_head = self.journal.back().map(|entry| entry.block);
6233
6234 Some(ReorgReport {
6235 dropped: dropped_blocks.first().cloned(),
6236 dropped_blocks,
6237 dropped_inputs,
6238 rollback_updates,
6239 rollback_diff,
6240 purge_updates,
6241 purge_diff,
6242 canceled_resyncs,
6243 reason,
6244 _network: PhantomData,
6245 })
6246 }
6247
6248 fn rebase_validation_state_from(&mut self, first_dropped_block: u64) {
6249 if let Some(freshness) = self.freshness.as_mut() {
6250 freshness.invalidate_valid_through_from(first_dropped_block);
6251 }
6252 self.tracked_roots
6253 .retain(|_, baseline| baseline.last_block < first_dropped_block);
6254 if self
6255 .last_gate_block
6256 .is_some_and(|block| block >= first_dropped_block)
6257 {
6258 self.last_gate_block = self
6259 .tracked_roots
6260 .values()
6261 .map(|baseline| baseline.last_block)
6262 .max();
6263 }
6264 self.touched_since_gate.clear();
6268 }
6269
6270 fn cancel_resyncs_for_dropped_blocks(
6271 &mut self,
6272 dropped_blocks: &[BlockRef],
6273 ) -> Vec<ResyncRequest> {
6274 let mut canceled = Vec::new();
6275 self.pending_resyncs.retain(|request| {
6276 let should_cancel = resync_request_targets_dropped_block(request, dropped_blocks);
6277 if should_cancel {
6278 canceled.push(request.clone());
6279 }
6280 !should_cancel
6281 });
6282 canceled
6283 }
6284
6285 fn remove_pending_resyncs<'a>(&mut self, ids: impl IntoIterator<Item = &'a ResyncId>) {
6286 let ids: HashSet<_> = ids.into_iter().cloned().collect();
6287 self.pending_resyncs
6288 .retain(|request| !ids.contains(&request.id));
6289 }
6290}
6291
6292fn install_preconfirmed_cache_context(cache: &mut EvmCache, flashblock: &FlashblockRef) {
6293 cache.set_block(BlockId::pending());
6294 cache.set_block_context(Some(flashblock.block_number), flashblock.base_fee_per_gas);
6295 cache.set_coinbase(flashblock.beneficiary);
6296 cache.set_prevrandao(flashblock.prevrandao);
6297 cache.set_block_gas_limit(flashblock.gas_limit);
6298 cache.set_timestamp(flashblock.timestamp);
6299}
6300
6301pub fn validate_canonical_sequence<N: Network>(
6333 state: &CanonicalSequenceState,
6334 batch: &ReactiveInputBatch<N>,
6335) -> Result<CanonicalSequenceValidation, ReactiveError> {
6336 validate_canonical_sequence_diagnostic(state, batch)
6337 .map_err(CanonicalSequenceError::into_reactive_error)
6338}
6339
6340pub fn validate_canonical_sequence_diagnostic<N: Network>(
6355 state: &CanonicalSequenceState,
6356 batch: &ReactiveInputBatch<N>,
6357) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6358 validate_canonical_sequence_internal(
6359 state,
6360 batch,
6361 CanonicalSequenceValidationPolicy::RequireCompleteRollback,
6362 )
6363}
6364
6365pub fn normalize_and_validate_canonical_sequence<N: Network>(
6388 state: &CanonicalSequenceState,
6389 batch: &ReactiveInputBatch<N>,
6390) -> Result<CanonicalSequenceValidation, ReactiveError> {
6391 normalize_and_validate_canonical_sequence_diagnostic(state, batch)
6392 .map_err(CanonicalSequenceError::into_reactive_error)
6393}
6394
6395pub fn normalize_and_validate_canonical_sequence_diagnostic<N: Network>(
6410 state: &CanonicalSequenceState,
6411 batch: &ReactiveInputBatch<N>,
6412) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6413 validate_canonical_sequence_internal(
6414 state,
6415 batch,
6416 CanonicalSequenceValidationPolicy::RequireCompleteRollbackNormalizeCoverage,
6417 )
6418}
6419
6420fn validate_canonical_sequence_internal<N: Network>(
6421 state: &CanonicalSequenceState,
6422 batch: &ReactiveInputBatch<N>,
6423 policy: CanonicalSequenceValidationPolicy,
6424) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6425 let records = batch
6426 .records()
6427 .iter()
6428 .enumerate()
6429 .map(|(index, record)| {
6430 (
6431 record.clone(),
6432 DeliveryAudience::All,
6433 batch
6434 .record_delivery_scope(index)
6435 .expect("enumerated record always has a delivery scope"),
6436 )
6437 })
6438 .collect::<Vec<_>>();
6439 let records = sort_scoped_records(dedupe_scoped_records(records)?);
6440 let records = records
6441 .iter()
6442 .map(|(record, _, scope)| (record, *scope))
6443 .collect::<Vec<_>>();
6444 validate_canonical_sequence_parts(state, batch.chain_controls(), &records, policy)
6445}
6446
6447#[derive(Clone, Copy)]
6448enum CanonicalSequenceValidationPolicy {
6449 RequireCompleteRollback,
6450 RequireCompleteRollbackNormalizeCoverage,
6451 ObserveIncompleteRollback,
6452}
6453
6454#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6457#[non_exhaustive]
6458pub enum CanonicalRollbackKind {
6459 Explicit,
6461 Removed,
6463 ImplicitParent,
6465 MissingReplacement,
6467}
6468
6469#[derive(Debug, thiserror::Error)]
6474#[non_exhaustive]
6475pub enum CanonicalSequenceError {
6476 #[error(transparent)]
6478 Invalid(#[from] ReactiveError),
6479 #[error(
6482 "{kind:?} rollback after block {common_ancestor} exceeds retained canonical history starting at {oldest_retained:?}"
6483 )]
6484 IncompleteRollback {
6485 common_ancestor: u64,
6487 oldest_retained: Option<u64>,
6489 kind: CanonicalRollbackKind,
6491 },
6492}
6493
6494#[derive(Clone, Copy, Debug)]
6495struct RequiredReorgAnchor {
6496 number: u64,
6497 block: Option<BlockRef>,
6498 permits_missing_child_parent: bool,
6499 must_be_consumed: bool,
6500}
6501
6502#[derive(Debug)]
6503struct SequenceRewind {
6504 common_ancestor: Option<BlockRef>,
6505 dropped: Vec<BlockRef>,
6506}
6507
6508impl RequiredReorgAnchor {
6509 const fn hash(self) -> Option<B256> {
6510 match self.block {
6511 Some(block) => Some(block.hash),
6512 None => None,
6513 }
6514 }
6515}
6516
6517impl CanonicalSequenceError {
6518 pub const fn requires_history(&self) -> bool {
6521 matches!(self, Self::IncompleteRollback { .. })
6522 }
6523
6524 pub fn into_reactive_error(self) -> ReactiveError {
6526 match self {
6527 Self::Invalid(error) => error,
6528 Self::IncompleteRollback {
6529 common_ancestor,
6530 oldest_retained,
6531 kind,
6532 } => ReactiveError::InvalidChainControl {
6533 message: format!(
6534 "{kind:?} rollback after block {common_ancestor} exceeds retained canonical history starting at {oldest_retained:?}"
6535 ),
6536 },
6537 }
6538 }
6539}
6540
6541impl CanonicalSequenceValidationPolicy {
6542 const fn requires_complete_rollback(self) -> bool {
6543 matches!(
6544 self,
6545 Self::RequireCompleteRollback | Self::RequireCompleteRollbackNormalizeCoverage
6546 )
6547 }
6548
6549 const fn normalizes_coverage(self) -> bool {
6550 matches!(self, Self::RequireCompleteRollbackNormalizeCoverage)
6551 }
6552}
6553
6554fn validate_canonical_sequence_parts<N: Network>(
6555 initial: &CanonicalSequenceState,
6556 controls: &[ChainControl],
6557 records: &[(&ReactiveInputRecord<N>, DeliveryScope)],
6558 policy: CanonicalSequenceValidationPolicy,
6559) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6560 validate_canonical_sequence_snapshot(initial)?;
6561 let control_split = validate_control_phase_order(controls)?;
6562 let (pre_record_controls, post_record_controls) = controls.split_at(control_split);
6563 let mut state = initial.clone();
6564 let mut asserted_blocks = HashMap::<u64, BlockRef>::new();
6565 let mut mutations = Vec::new();
6566 let mut normalized_chain_controls = Vec::with_capacity(controls.len());
6567 let mut batch_dropped = BatchDroppedCanonical::default();
6568 let mut removed_assertions = HashMap::<(u64, B256), BlockRef>::new();
6569 let mut removed_heights_by_hash = HashMap::<B256, u64>::new();
6570 let mut record_proof_control_identities = HashSet::<(u64, B256)>::new();
6571 let rollback_oldest = initial
6572 .retained_canonical_history
6573 .first()
6574 .map(|block| block.number);
6575
6576 for control in pre_record_controls {
6577 normalized_chain_controls.push(control.clone());
6578 validate_sequence_control(&state, control)?;
6579 assert_chain_control_identities(&mut asserted_blocks, control)?;
6580 let ChainControl::Reorg {
6581 common_ancestor,
6582 old_tip,
6583 ..
6584 } = control
6585 else {
6586 unreachable!("phase validation leaves only reorg controls before records")
6587 };
6588 let exact_ancestor = state.retained_canonical_history.iter().any(|block| {
6589 block.number == common_ancestor.number && block.hash == common_ancestor.hash
6590 });
6591 let rollback_horizon_covers_ancestor = state
6592 .retained_canonical_history
6593 .first()
6594 .is_some_and(|oldest| oldest.number <= common_ancestor.number);
6595 if policy.requires_complete_rollback()
6596 && !exact_ancestor
6597 && !rollback_horizon_covers_ancestor
6598 {
6599 return Err(CanonicalSequenceError::IncompleteRollback {
6600 common_ancestor: common_ancestor.number,
6601 oldest_retained: rollback_oldest,
6602 kind: CanonicalRollbackKind::Explicit,
6603 });
6604 }
6605 let dropped = state
6606 .retained_canonical_history
6607 .iter()
6608 .copied()
6609 .filter(|block| block.number > common_ancestor.number)
6610 .collect::<Vec<_>>();
6611 state
6612 .retained_canonical_history
6613 .retain(|block| block.number <= common_ancestor.number);
6614 upsert_sequence_history(&mut state.retained_canonical_history, common_ancestor)?;
6615 let mut enriched_ancestor = *common_ancestor;
6616 if let Some(retained) = state.retained_canonical_history.iter().find(|block| {
6617 block.number == common_ancestor.number && block.hash == common_ancestor.hash
6618 }) {
6619 enrich_block_ref(&mut enriched_ancestor, retained);
6620 }
6621 if let Some(coverage) = state.coverage_head.as_ref()
6622 && coverage.number == common_ancestor.number
6623 && coverage.hash == common_ancestor.hash
6624 {
6625 enrich_block_ref(&mut enriched_ancestor, coverage);
6626 }
6627 upsert_sequence_history(&mut state.retained_canonical_history, &enriched_ancestor)?;
6628 state.coverage_head = Some(enriched_ancestor);
6629 clear_sequence_heads_above(&mut state, &enriched_ancestor);
6630 batch_dropped.record_explicit(common_ancestor, old_tip);
6631 batch_dropped.record_drained(&dropped);
6632 mutations.push(CanonicalSequenceMutation::Rewind {
6633 common_ancestor: Some(enriched_ancestor),
6634 dropped,
6635 });
6636 }
6637 let pre_record_state = state.clone();
6638 let mut required_reorg_anchor = None::<RequiredReorgAnchor>;
6639
6640 for (record, scope) in records {
6641 if !scope.advances_canonical_state() {
6642 continue;
6643 }
6644 if let Some((incoming_dropped_block, _)) = reorg_signal_block(record) {
6645 let incoming_dropped_block =
6646 resolve_record_block_payload_metadata(record, incoming_dropped_block)?;
6647 validate_sequence_matching_metadata(&state, &incoming_dropped_block, "removed record")?;
6648 validate_sequence_adjacent_parent_identity(
6649 &state,
6650 &incoming_dropped_block,
6651 "removed record",
6652 )?;
6653 let mut dropped_block = state
6654 .retained_canonical_history
6655 .iter()
6656 .find(|known| {
6657 known.number == incoming_dropped_block.number
6658 && known.hash == incoming_dropped_block.hash
6659 })
6660 .copied()
6661 .or_else(|| {
6662 state.coverage_head.filter(|known| {
6663 known.number == incoming_dropped_block.number
6664 && known.hash == incoming_dropped_block.hash
6665 })
6666 })
6667 .unwrap_or(incoming_dropped_block);
6668 enrich_block_ref(&mut dropped_block, &incoming_dropped_block);
6669 validate_sequence_implicit_finality(&state, record, None)?;
6670 if dropped_block.number == 0 {
6671 return Err(ReactiveError::InvalidChainControl {
6672 message: "a removed/reorged genesis block has no canonical parent anchor"
6673 .into(),
6674 }
6675 .into());
6676 }
6677 let removed_identity = (dropped_block.number, dropped_block.hash);
6678 if let Some(previous_number) =
6679 removed_heights_by_hash.insert(dropped_block.hash, dropped_block.number)
6680 && previous_number != dropped_block.number
6681 {
6682 return Err(ReactiveError::InvalidChainControl {
6683 message: format!(
6684 "removed hash {:?} is reused at heights {} and {}",
6685 dropped_block.hash, previous_number, dropped_block.number
6686 ),
6687 }
6688 .into());
6689 }
6690 if let Some(previous) = removed_assertions.get_mut(&removed_identity) {
6691 if !optional_block_refs_are_compatible(Some(previous), Some(&dropped_block)) {
6692 return Err(ReactiveError::InvalidChainControl {
6693 message: format!(
6694 "duplicate removed block {}:{:?} carries conflicting metadata",
6695 dropped_block.number, dropped_block.hash
6696 ),
6697 }
6698 .into());
6699 }
6700 enrich_block_ref(previous, &dropped_block);
6701 } else {
6702 removed_assertions.insert(removed_identity, dropped_block);
6703 }
6704 if asserted_blocks
6705 .get(&dropped_block.number)
6706 .is_some_and(|asserted| asserted.hash == dropped_block.hash)
6707 {
6708 return Err(ReactiveError::InvalidChainControl {
6709 message: format!(
6710 "removed block {}:{:?} is asserted canonical by the same envelope",
6711 dropped_block.number, dropped_block.hash
6712 ),
6713 }
6714 .into());
6715 }
6716 if batch_dropped.contains(&dropped_block) {
6717 continue;
6718 }
6719 if let Some(index) = state.retained_canonical_history.iter().position(|block| {
6720 block.number == dropped_block.number && block.hash == dropped_block.hash
6721 }) {
6722 let dropped = state.retained_canonical_history.split_off(index);
6723 batch_dropped.record_drained(&dropped);
6724 let ancestor_number = dropped_block
6725 .number
6726 .checked_sub(1)
6727 .expect("genesis removal was rejected above");
6728 let retained_anchor = state
6729 .retained_canonical_history
6730 .iter()
6731 .rev()
6732 .find(|head| head.number == ancestor_number)
6733 .copied();
6734 let authenticated_anchor = retained_anchor
6735 .or_else(|| {
6736 dropped_block.parent_hash.map(|hash| BlockRef {
6737 number: ancestor_number,
6738 hash,
6739 parent_hash: None,
6740 timestamp: None,
6741 })
6742 })
6743 .or_else(|| {
6744 state
6745 .finalized_head
6746 .filter(|head| head.number == ancestor_number)
6747 });
6748 let authenticated_anchor = authenticated_anchor.map(|mut anchor| {
6749 for head in [state.safe_head.as_ref(), state.finalized_head.as_ref()]
6750 .into_iter()
6751 .flatten()
6752 {
6753 if head.number == anchor.number && head.hash == anchor.hash {
6754 enrich_block_ref(&mut anchor, head);
6755 }
6756 }
6757 anchor
6758 });
6759 required_reorg_anchor = Some(RequiredReorgAnchor {
6760 number: ancestor_number,
6761 block: authenticated_anchor,
6762 permits_missing_child_parent: retained_anchor.is_some(),
6763 must_be_consumed: authenticated_anchor.is_none()
6764 && state.retained_canonical_history.is_empty(),
6765 });
6766 state.coverage_head = authenticated_anchor
6767 .or_else(|| state.retained_canonical_history.last().copied());
6768 if let Some(head) = state.coverage_head {
6769 clear_sequence_heads_above(&mut state, &head);
6770 } else {
6771 state.safe_head = None;
6772 state.finalized_head = None;
6773 }
6774 mutations.push(CanonicalSequenceMutation::Rewind {
6775 common_ancestor: state.coverage_head,
6776 dropped,
6777 });
6778 } else {
6779 let replacement_is_known = state.retained_canonical_history.iter().any(|block| {
6780 block.number == dropped_block.number && block.hash != dropped_block.hash
6781 }) || state.coverage_head.is_some_and(|head| {
6782 head.number == dropped_block.number && head.hash != dropped_block.hash
6783 });
6784 if !replacement_is_known {
6785 if policy.requires_complete_rollback() {
6790 return Err(CanonicalSequenceError::IncompleteRollback {
6791 common_ancestor: dropped_block
6792 .number
6793 .checked_sub(1)
6794 .expect("genesis removal was rejected above"),
6795 oldest_retained: rollback_oldest,
6796 kind: CanonicalRollbackKind::Removed,
6797 });
6798 }
6799 continue;
6800 }
6801 }
6802 continue;
6803 }
6804
6805 let Some(context_block) = canonical_record_block(record) else {
6806 continue;
6807 };
6808 let incoming_block = resolve_record_block_payload_metadata(record, *context_block)?;
6809 if post_record_controls
6810 .iter()
6811 .filter_map(canonical_coverage_control_block)
6812 .any(|asserted| {
6813 asserted.number == incoming_block.number
6814 && asserted.hash == incoming_block.hash
6815 && optional_block_refs_are_compatible(Some(asserted), Some(&incoming_block))
6816 && ((incoming_block.parent_hash.is_none() && asserted.parent_hash.is_some())
6817 || (incoming_block.timestamp.is_none() && asserted.timestamp.is_some()))
6818 })
6819 {
6820 record_proof_control_identities.insert((incoming_block.number, incoming_block.hash));
6821 }
6822 let mut resolved_block = incoming_block;
6823 if let Some(asserted) = asserted_blocks
6824 .get(&incoming_block.number)
6825 .filter(|asserted| asserted.hash == incoming_block.hash)
6826 {
6827 if !optional_block_refs_are_compatible(Some(asserted), Some(&incoming_block)) {
6828 return Err(ReactiveError::InvalidChainControl {
6829 message: format!(
6830 "canonical record {}:{:?} conflicts with the same envelope's asserted metadata",
6831 incoming_block.number, incoming_block.hash
6832 ),
6833 }
6834 .into());
6835 }
6836 enrich_block_ref(&mut resolved_block, asserted);
6837 }
6838 for asserted in post_record_controls
6839 .iter()
6840 .filter_map(chain_control_canonical_assertion)
6841 .filter(|asserted| {
6842 asserted.number == incoming_block.number && asserted.hash == incoming_block.hash
6843 })
6844 {
6845 if !optional_block_refs_are_compatible(Some(&resolved_block), Some(asserted)) {
6846 return Err(ReactiveError::InvalidChainControl {
6847 message: format!(
6848 "canonical record {}:{:?} conflicts with the same envelope's asserted metadata",
6849 incoming_block.number, incoming_block.hash
6850 ),
6851 }
6852 .into());
6853 }
6854 enrich_block_ref(&mut resolved_block, asserted);
6855 }
6856 let replacement_anchor =
6857 required_reorg_anchor.filter(|required| resolved_block.number > required.number);
6858 if resolved_block.parent_hash.is_none()
6859 && replacement_anchor.is_some_and(|anchor| {
6860 anchor.permits_missing_child_parent
6861 && anchor.number.checked_add(1) == Some(resolved_block.number)
6862 })
6863 {
6864 resolved_block.parent_hash = replacement_anchor.and_then(RequiredReorgAnchor::hash);
6865 }
6866 let block = &resolved_block;
6867 if removed_assertions.contains_key(&(block.number, block.hash)) {
6868 return Err(ReactiveError::InvalidChainControl {
6869 message: format!(
6870 "canonical block {}:{:?} is also removed by the same envelope",
6871 block.number, block.hash
6872 ),
6873 }
6874 .into());
6875 }
6876 if let Some(removed_number) = removed_heights_by_hash.get(&block.hash)
6877 && *removed_number != block.number
6878 {
6879 return Err(ReactiveError::InvalidChainControl {
6880 message: format!(
6881 "canonical hash {:?} at height {} is removed at height {} by the same envelope",
6882 block.hash, block.number, removed_number
6883 ),
6884 }
6885 .into());
6886 }
6887 let replacement_proven_by_removal =
6888 validate_replacement_reorg_anchor(replacement_anchor, block, policy, rollback_oldest)?;
6889 if replacement_anchor.is_some() {
6890 required_reorg_anchor = None;
6891 }
6892 validate_sequence_matching_metadata(&state, block, "canonical record")?;
6893 validate_sequence_implicit_finality(&state, record, Some(block))?;
6894 let implicit_replacement_requires_history = if replacement_proven_by_removal {
6895 false
6896 } else {
6897 sequence_implicit_replacement_requires_history(&state, block, policy)?
6898 };
6899 if implicit_replacement_requires_history && policy.requires_complete_rollback() {
6900 return Err(CanonicalSequenceError::IncompleteRollback {
6901 common_ancestor: block.number.saturating_sub(1),
6902 oldest_retained: rollback_oldest,
6903 kind: CanonicalRollbackKind::ImplicitParent,
6904 });
6905 }
6906 assert_canonical_block_identity(&mut asserted_blocks, block, "canonical record")?;
6907 let allow_parentless_extension = replacement_anchor.is_some_and(|anchor| {
6908 anchor.permits_missing_child_parent
6909 && anchor.number.checked_add(1) == Some(block.number)
6910 });
6911 if let Some(rewind) =
6912 apply_sequence_canonical_block(&mut state, block, allow_parentless_extension)?
6913 {
6914 mutations.push(CanonicalSequenceMutation::Rewind {
6915 common_ancestor: rewind.common_ancestor,
6916 dropped: rewind.dropped,
6917 });
6918 }
6919 mutations.push(CanonicalSequenceMutation::Canonical(*block));
6920 }
6921
6922 for control in post_record_controls {
6923 if let Some(block) = chain_control_canonical_assertion(control)
6924 && removed_assertions.contains_key(&(block.number, block.hash))
6925 {
6926 return Err(ReactiveError::InvalidChainControl {
6927 message: format!(
6928 "canonical block {}:{:?} is also removed by the same envelope",
6929 block.number, block.hash
6930 ),
6931 }
6932 .into());
6933 }
6934 if let Some(block) = chain_control_canonical_assertion(control)
6935 && let Some(removed_number) = removed_heights_by_hash.get(&block.hash)
6936 && *removed_number != block.number
6937 {
6938 return Err(ReactiveError::InvalidChainControl {
6939 message: format!(
6940 "canonical hash {:?} at height {} is removed at height {} by the same envelope",
6941 block.hash, block.number, removed_number
6942 ),
6943 }
6944 .into());
6945 }
6946 let replacement_anchor = canonical_coverage_control_block(control).and_then(|block| {
6947 required_reorg_anchor.filter(|required| block.number > required.number)
6948 });
6949 if let Some(block) = canonical_coverage_control_block(control) {
6950 validate_replacement_reorg_anchor(replacement_anchor, block, policy, rollback_oldest)?;
6951 if replacement_anchor.is_some() {
6952 required_reorg_anchor = None;
6953 }
6954 }
6955 assert_chain_control_identities(&mut asserted_blocks, control)?;
6956 let preserves_record_proof =
6957 canonical_coverage_control_block(control).is_some_and(|block| {
6958 record_proof_control_identities.contains(&(block.number, block.hash))
6959 });
6960 if policy.normalizes_coverage()
6961 && !preserves_record_proof
6962 && let Some(block) = canonical_coverage_control_block(control)
6963 && state
6964 .coverage_head
6965 .is_some_and(|head| block.number <= head.number)
6966 {
6967 let is_equal_coverage = state
6968 .coverage_head
6969 .is_some_and(|head| block.number == head.number);
6970 let known = state
6971 .coverage_head
6972 .as_ref()
6973 .filter(|head| head.number == block.number && head.hash == block.hash)
6974 .or_else(|| {
6975 state
6976 .retained_canonical_history
6977 .iter()
6978 .find(|entry| entry.number == block.number && entry.hash == block.hash)
6979 });
6980 if let Some(known) = known
6981 && optional_block_refs_are_compatible(Some(known), Some(block))
6982 && (!is_equal_coverage || !sequence_block_adds_metadata(&state, block))
6983 {
6984 if let ChainControl::Barrier { id, .. } = control {
6985 normalized_chain_controls.push(ChainControl::Barrier {
6986 id: id.clone(),
6987 block: None,
6988 });
6989 }
6990 continue;
6991 }
6992 }
6993 validate_sequence_control(&state, control)?;
6994 normalized_chain_controls.push(control.clone());
6995 match control {
6996 ChainControl::Safe(block) => {
6997 set_or_enrich_block_ref(&mut state.safe_head, block);
6998 mutations.push(CanonicalSequenceMutation::Safe(
6999 state.safe_head.expect("safe head was just installed"),
7000 ));
7001 }
7002 ChainControl::Finalized(block) => {
7003 set_or_enrich_block_ref(&mut state.finalized_head, block);
7004 mutations.push(CanonicalSequenceMutation::Finalized(
7005 state
7006 .finalized_head
7007 .expect("finalized head was just installed"),
7008 ));
7009 }
7010 ChainControl::CanonicalProgress(block)
7011 | ChainControl::Barrier {
7012 block: Some(block), ..
7013 } => {
7014 let allow_parentless_extension = replacement_anchor.is_some_and(|anchor| {
7015 anchor.permits_missing_child_parent
7016 && anchor.number.checked_add(1) == Some(block.number)
7017 }) || (replacement_anchor.is_none()
7018 && block.parent_hash.is_none()
7019 && state
7020 .coverage_head
7021 .is_some_and(|head| head.number.checked_add(1) == Some(block.number)));
7022 if let Some(rewind) =
7023 apply_sequence_canonical_block(&mut state, block, allow_parentless_extension)?
7024 {
7025 mutations.push(CanonicalSequenceMutation::Rewind {
7026 common_ancestor: rewind.common_ancestor,
7027 dropped: rewind.dropped,
7028 });
7029 }
7030 mutations.push(CanonicalSequenceMutation::Canonical(*block));
7031 }
7032 ChainControl::Barrier { block: None, .. } => {}
7033 ChainControl::Reorg { .. } => {
7034 unreachable!("phase validation excludes post-record reorg controls")
7035 }
7036 }
7037 }
7038
7039 if let Some(required) = required_reorg_anchor
7040 && required.must_be_consumed
7041 && policy.requires_complete_rollback()
7042 {
7043 return Err(CanonicalSequenceError::IncompleteRollback {
7044 common_ancestor: required.number,
7045 oldest_retained: rollback_oldest,
7046 kind: CanonicalRollbackKind::MissingReplacement,
7047 });
7048 }
7049
7050 validate_canonical_sequence_snapshot(&state)?;
7051 Ok(CanonicalSequenceValidation {
7052 pre_record_state,
7053 next_state: state,
7054 mutations,
7055 normalized_chain_controls,
7056 })
7057}
7058
7059fn validate_canonical_sequence_snapshot(
7060 state: &CanonicalSequenceState,
7061) -> Result<(), ReactiveError> {
7062 let invalid = |message: String| ReactiveError::InvalidChainControl { message };
7063 let supplied_blocks = state
7064 .retained_canonical_history
7065 .iter()
7066 .chain(state.coverage_head.iter())
7067 .chain(state.safe_head.iter())
7068 .chain(state.finalized_head.iter())
7069 .collect::<Vec<_>>();
7070 validate_known_parent_hash_heights(&supplied_blocks)?;
7071 let mut prior = None::<BlockRef>;
7072 for block in &state.retained_canonical_history {
7073 if let Some(previous) = prior {
7074 if block.number < previous.number {
7075 return Err(invalid(
7076 "retained canonical history is not ordered by block number".into(),
7077 ));
7078 }
7079 if block.number == previous.number {
7080 let qualifier = if optional_block_refs_are_compatible(Some(&previous), Some(block))
7081 {
7082 "duplicate"
7083 } else {
7084 "conflicting"
7085 };
7086 return Err(invalid(format!(
7087 "retained canonical history contains {qualifier} identities at block {}",
7088 block.number
7089 )));
7090 }
7091 if previous.number.checked_add(1) == Some(block.number)
7092 && block.parent_hash.is_some()
7093 && block.parent_hash != Some(previous.hash)
7094 {
7095 return Err(invalid(format!(
7096 "adjacent retained block {}:{:?} does not descend from {}:{:?}",
7097 block.number, block.hash, previous.number, previous.hash
7098 )));
7099 }
7100 }
7101 prior = Some(*block);
7102 }
7103 if state.coverage_head.is_none() && !state.retained_canonical_history.is_empty() {
7104 return Err(invalid(
7105 "retained canonical history requires an authoritative coverage head".into(),
7106 ));
7107 }
7108 if let Some(head) = state.coverage_head.as_ref() {
7109 if let Some(retained) = state
7110 .retained_canonical_history
7111 .iter()
7112 .find(|entry| entry.number == head.number)
7113 && !optional_block_refs_are_compatible(Some(retained), Some(head))
7114 {
7115 return Err(invalid(format!(
7116 "coverage head {}:{:?} conflicts with retained identity {:?}",
7117 head.number, head.hash, retained
7118 )));
7119 }
7120 if state
7121 .retained_canonical_history
7122 .last()
7123 .is_some_and(|retained| retained.number > head.number)
7124 {
7125 return Err(invalid(
7126 "retained canonical history advances beyond the coverage head".into(),
7127 ));
7128 }
7129 if let Some(retained) = state.retained_canonical_history.last()
7130 && retained.number.checked_add(1) == Some(head.number)
7131 && head.parent_hash.is_some()
7132 && head.parent_hash != Some(retained.hash)
7133 {
7134 return Err(invalid(format!(
7135 "coverage head {}:{:?} does not descend from adjacent retained block {}:{:?}",
7136 head.number, head.hash, retained.number, retained.hash
7137 )));
7138 }
7139 }
7140 if let Some(safe) = state.safe_head.as_ref() {
7141 validate_sequence_known_identity(state, safe, "safe")?;
7142 validate_sequence_head_within_coverage(state, safe, "safe")?;
7143 validate_coverage_descends_from_adjacent_head(state.coverage_head.as_ref(), safe, "safe")?;
7144 }
7145 if let Some(finalized) = state.finalized_head.as_ref() {
7146 validate_sequence_known_identity(state, finalized, "finalized")?;
7147 validate_sequence_head_within_coverage(state, finalized, "finalized")?;
7148 validate_coverage_descends_from_adjacent_head(
7149 state.coverage_head.as_ref(),
7150 finalized,
7151 "finalized",
7152 )?;
7153 }
7154 validate_adjacent_finality(state.finalized_head.as_ref(), state.safe_head.as_ref())?;
7155 if let (Some(finalized), Some(safe)) = (state.finalized_head, state.safe_head)
7156 && (finalized.number > safe.number
7157 || (finalized.number == safe.number && finalized.hash != safe.hash))
7158 {
7159 return Err(invalid(
7160 "finalized head cannot advance beyond or conflict with safe head".into(),
7161 ));
7162 }
7163 Ok(())
7164}
7165
7166fn validate_known_parent_hash_heights(blocks: &[&BlockRef]) -> Result<(), ReactiveError> {
7167 let mut heights_by_hash = HashMap::<B256, u64>::with_capacity(blocks.len());
7168 let mut resolved_by_height = HashMap::<u64, BlockRef>::with_capacity(blocks.len());
7169 for block in blocks.iter().copied() {
7170 if let Some(previous_height) = heights_by_hash.insert(block.hash, block.number)
7171 && previous_height != block.number
7172 {
7173 return Err(ReactiveError::InvalidChainControl {
7174 message: format!(
7175 "canonical hash {:?} is reused at heights {} and {}",
7176 block.hash, previous_height, block.number
7177 ),
7178 });
7179 }
7180 if let Some(resolved) = resolved_by_height.get_mut(&block.number) {
7181 if !optional_block_refs_are_compatible(Some(resolved), Some(block)) {
7182 return Err(ReactiveError::InvalidChainControl {
7183 message: format!(
7184 "canonical aliases at height {} carry conflicting identities or metadata",
7185 block.number
7186 ),
7187 });
7188 }
7189 enrich_block_ref(resolved, block);
7190 } else {
7191 resolved_by_height.insert(block.number, *block);
7192 }
7193 }
7194 for child in resolved_by_height.values() {
7195 let Some(parent_hash) = child.parent_hash else {
7196 continue;
7197 };
7198 if let Some(parent_number) = heights_by_hash.get(&parent_hash)
7199 && parent_number.checked_add(1) != Some(child.number)
7200 {
7201 return Err(ReactiveError::InvalidChainControl {
7202 message: format!(
7203 "block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
7204 child.number, child.hash, parent_hash, parent_number
7205 ),
7206 });
7207 }
7208 if let Some(parent_number) = child.number.checked_sub(1)
7209 && let Some(parent) = resolved_by_height.get(&parent_number)
7210 && parent.hash != parent_hash
7211 {
7212 return Err(ReactiveError::InvalidChainControl {
7213 message: format!(
7214 "block {}:{:?} does not descend from supplied adjacent identity {}:{:?}",
7215 child.number, child.hash, parent.number, parent.hash
7216 ),
7217 });
7218 }
7219 }
7220 Ok(())
7221}
7222
7223fn validate_coverage_descends_from_adjacent_head(
7224 coverage: Option<&BlockRef>,
7225 head: &BlockRef,
7226 label: &str,
7227) -> Result<(), ReactiveError> {
7228 let Some(coverage) = coverage else {
7229 return Ok(());
7230 };
7231 if head.number.checked_add(1) == Some(coverage.number)
7232 && coverage
7233 .parent_hash
7234 .is_some_and(|parent| parent != head.hash)
7235 {
7236 return Err(ReactiveError::InvalidChainControl {
7237 message: format!(
7238 "canonical coverage {}:{:?} does not descend from adjacent {label} head {}:{:?}",
7239 coverage.number, coverage.hash, head.number, head.hash
7240 ),
7241 });
7242 }
7243 Ok(())
7244}
7245
7246fn validate_sequence_control(
7247 state: &CanonicalSequenceState,
7248 control: &ChainControl,
7249) -> Result<(), ReactiveError> {
7250 let invalid = |message: String| ReactiveError::InvalidChainControl { message };
7251 match control {
7252 ChainControl::Safe(block) => {
7253 validate_sequence_known_identity(state, block, "safe")?;
7254 validate_sequence_head_within_coverage(state, block, "safe")?;
7255 if let Some(current) = state.safe_head.as_ref()
7256 && (block.number < current.number
7257 || (block.number == current.number
7258 && (block.hash != current.hash
7259 || !optional_block_refs_are_compatible(Some(block), Some(current)))))
7260 {
7261 return Err(invalid(format!(
7262 "safe head {}:{:?} conflicts with current {}:{:?}",
7263 block.number, block.hash, current.number, current.hash
7264 )));
7265 }
7266 if let Some(finalized) = state.finalized_head.as_ref()
7267 && (block.number < finalized.number
7268 || (block.number == finalized.number && block.hash != finalized.hash))
7269 {
7270 return Err(invalid(
7271 "safe head cannot precede or conflict with finalized head".into(),
7272 ));
7273 }
7274 validate_adjacent_finality(state.finalized_head.as_ref(), Some(block))?;
7275 }
7276 ChainControl::Finalized(block) => {
7277 validate_sequence_known_identity(state, block, "finalized")?;
7278 validate_sequence_head_within_coverage(state, block, "finalized")?;
7279 if let Some(current) = state.finalized_head.as_ref()
7280 && (block.number < current.number
7281 || (block.number == current.number
7282 && (block.hash != current.hash
7283 || !optional_block_refs_are_compatible(Some(block), Some(current)))))
7284 {
7285 return Err(invalid(format!(
7286 "finalized head {}:{:?} conflicts with current {}:{:?}",
7287 block.number, block.hash, current.number, current.hash
7288 )));
7289 }
7290 if let Some(safe) = state.safe_head.as_ref()
7291 && (block.number > safe.number
7292 || (block.number == safe.number && block.hash != safe.hash))
7293 {
7294 return Err(invalid(
7295 "finalized head cannot advance beyond or conflict with safe head".into(),
7296 ));
7297 }
7298 validate_adjacent_finality(Some(block), state.safe_head.as_ref())?;
7299 }
7300 ChainControl::CanonicalProgress(block)
7301 | ChainControl::Barrier {
7302 block: Some(block), ..
7303 } => {
7304 validate_sequence_known_identity(state, block, "canonical coverage")?;
7305 if let Some(current) = state.coverage_head.as_ref()
7306 && (block.number < current.number
7307 || (block.number == current.number && block.hash != current.hash))
7308 {
7309 return Err(invalid(format!(
7310 "canonical coverage {}:{:?} conflicts with current {}:{:?}",
7311 block.number, block.hash, current.number, current.hash
7312 )));
7313 }
7314 if let Some(current) = state.coverage_head.as_ref()
7315 && current.number.checked_add(1) == Some(block.number)
7316 && block.parent_hash.is_some()
7317 && block.parent_hash != Some(current.hash)
7318 {
7319 return Err(invalid(format!(
7320 "canonical coverage {}:{:?} does not descend from current {}:{:?}",
7321 block.number, block.hash, current.number, current.hash
7322 )));
7323 }
7324 }
7325 ChainControl::Barrier { block: None, .. } => {}
7326 ChainControl::Reorg {
7327 common_ancestor,
7328 old_tip,
7329 new_tip,
7330 } => {
7331 validate_sequence_known_identity(state, common_ancestor, "reorg common ancestor")?;
7332 validate_reorg_ancestor_against_retained_branch(state, common_ancestor)?;
7333 validate_sequence_known_hash_height(state, old_tip, "reorg old tip")?;
7334 validate_sequence_known_hash_height(state, new_tip, "reorg new tip")?;
7335 validate_sequence_known_parent_height(state, old_tip, "reorg old tip")?;
7336 validate_sequence_known_parent_height(state, new_tip, "reorg new tip")?;
7337 validate_sequence_adjacent_parent_identity(state, old_tip, "reorg old tip")?;
7338 if let Some(current) = state.coverage_head.as_ref()
7339 && (old_tip.number != current.number
7340 || old_tip.hash != current.hash
7341 || !optional_block_refs_are_compatible(Some(old_tip), Some(current)))
7342 {
7343 return Err(invalid(format!(
7344 "reorg old tip {}:{:?} does not exactly match current metadata {}:{:?}",
7345 old_tip.number, old_tip.hash, current.number, current.hash
7346 )));
7347 }
7348 if common_ancestor.number > old_tip.number || common_ancestor.number > new_tip.number {
7349 return Err(invalid(
7350 "reorg common ancestor cannot be above either branch tip".into(),
7351 ));
7352 }
7353 if common_ancestor.number == old_tip.number || common_ancestor.number == new_tip.number
7354 {
7355 return Err(invalid(
7356 "reorg must replace non-empty old and new branches above the common ancestor"
7357 .into(),
7358 ));
7359 }
7360 if old_tip.number == new_tip.number && old_tip.hash == new_tip.hash {
7361 return Err(invalid(
7362 "reorg old and new tips cannot have the same canonical identity".into(),
7363 ));
7364 }
7365 for (label, tip) in [("old", old_tip), ("new", new_tip)] {
7366 if common_ancestor.number.checked_add(1) == Some(tip.number)
7367 && tip.parent_hash != Some(common_ancestor.hash)
7368 {
7369 return Err(invalid(format!(
7370 "reorg {label} tip does not descend from the common ancestor"
7371 )));
7372 }
7373 }
7374 if let Some(finalized) = state.finalized_head.as_ref()
7375 && (common_ancestor.number < finalized.number
7376 || (common_ancestor.number == finalized.number
7377 && common_ancestor.hash != finalized.hash))
7378 {
7379 return Err(invalid(
7380 "reorg would cross or conflict with the finalized head".into(),
7381 ));
7382 }
7383 }
7384 }
7385 Ok(())
7386}
7387
7388fn validate_sequence_known_identity(
7389 state: &CanonicalSequenceState,
7390 block: &BlockRef,
7391 label: &str,
7392) -> Result<(), ReactiveError> {
7393 validate_sequence_known_hash_height(state, block, label)?;
7394 validate_sequence_known_parent_height(state, block, label)?;
7395 let known = state
7396 .coverage_head
7397 .as_ref()
7398 .filter(|head| head.number == block.number)
7399 .or_else(|| {
7400 state
7401 .retained_canonical_history
7402 .iter()
7403 .find(|entry| entry.number == block.number)
7404 });
7405 if let Some(known) = known
7406 && !optional_block_refs_are_compatible(Some(known), Some(block))
7407 {
7408 return Err(ReactiveError::InvalidChainControl {
7409 message: format!(
7410 "{label} block {}:{:?} conflicts with known canonical block {:?}",
7411 block.number, block.hash, known
7412 ),
7413 });
7414 }
7415 Ok(())
7416}
7417
7418fn validate_sequence_known_parent_height(
7419 state: &CanonicalSequenceState,
7420 block: &BlockRef,
7421 label: &str,
7422) -> Result<(), ReactiveError> {
7423 let Some(parent_hash) = block.parent_hash else {
7424 return Ok(());
7425 };
7426 let known_parent = state
7427 .retained_canonical_history
7428 .iter()
7429 .chain(state.coverage_head.iter())
7430 .chain(state.safe_head.iter())
7431 .chain(state.finalized_head.iter())
7432 .find(|known| known.hash == parent_hash);
7433 if let Some(parent) = known_parent
7434 && parent.number.checked_add(1) != Some(block.number)
7435 {
7436 return Err(ReactiveError::InvalidChainControl {
7437 message: format!(
7438 "{label} block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
7439 block.number, block.hash, parent.hash, parent.number
7440 ),
7441 });
7442 }
7443 Ok(())
7444}
7445
7446fn validate_sequence_head_within_coverage(
7447 state: &CanonicalSequenceState,
7448 block: &BlockRef,
7449 label: &str,
7450) -> Result<(), ReactiveError> {
7451 let Some(coverage) = state.coverage_head.as_ref() else {
7452 return Err(ReactiveError::InvalidChainControl {
7453 message: format!("{label} head requires an authoritative coverage head"),
7454 });
7455 };
7456 if block.number > coverage.number
7457 || (block.number == coverage.number
7458 && !optional_block_refs_are_compatible(Some(block), Some(coverage)))
7459 {
7460 return Err(ReactiveError::InvalidChainControl {
7461 message: format!(
7462 "{label} head {}:{:?} advances beyond or conflicts with coverage {}:{:?}",
7463 block.number, block.hash, coverage.number, coverage.hash
7464 ),
7465 });
7466 }
7467 Ok(())
7468}
7469
7470fn validate_sequence_matching_metadata(
7471 state: &CanonicalSequenceState,
7472 block: &BlockRef,
7473 label: &str,
7474) -> Result<(), ReactiveError> {
7475 validate_sequence_known_hash_height(state, block, label)?;
7476 validate_sequence_known_parent_height(state, block, label)?;
7477 let known = state
7478 .coverage_head
7479 .as_ref()
7480 .filter(|head| head.number == block.number && head.hash == block.hash)
7481 .or_else(|| {
7482 state
7483 .retained_canonical_history
7484 .iter()
7485 .find(|entry| entry.number == block.number && entry.hash == block.hash)
7486 });
7487 if let Some(known) = known
7488 && !optional_block_refs_are_compatible(Some(known), Some(block))
7489 {
7490 return Err(ReactiveError::InvalidChainControl {
7491 message: format!(
7492 "{label} block {}:{:?} carries metadata conflicting with known canonical block {:?}",
7493 block.number, block.hash, known
7494 ),
7495 });
7496 }
7497 Ok(())
7498}
7499
7500fn validate_sequence_known_hash_height(
7501 state: &CanonicalSequenceState,
7502 block: &BlockRef,
7503 label: &str,
7504) -> Result<(), ReactiveError> {
7505 let known = state
7506 .retained_canonical_history
7507 .iter()
7508 .chain(state.coverage_head.iter())
7509 .chain(state.safe_head.iter())
7510 .chain(state.finalized_head.iter())
7511 .find(|known| known.hash == block.hash);
7512 if let Some(known) = known
7513 && known.number != block.number
7514 {
7515 return Err(ReactiveError::InvalidChainControl {
7516 message: format!(
7517 "{label} block {}:{:?} reuses a canonical hash already known at height {}",
7518 block.number, block.hash, known.number
7519 ),
7520 });
7521 }
7522 Ok(())
7523}
7524
7525fn validate_reorg_ancestor_against_retained_branch(
7526 state: &CanonicalSequenceState,
7527 ancestor: &BlockRef,
7528) -> Result<(), ReactiveError> {
7529 let adjacent_number = ancestor.number.checked_add(1);
7530 for retained in state
7531 .retained_canonical_history
7532 .iter()
7533 .chain(state.coverage_head.iter())
7534 .chain(state.safe_head.iter())
7535 .chain(state.finalized_head.iter())
7536 {
7537 if Some(retained.number) == adjacent_number
7538 && retained
7539 .parent_hash
7540 .is_some_and(|parent| parent != ancestor.hash)
7541 {
7542 return Err(ReactiveError::InvalidChainControl {
7543 message: format!(
7544 "reorg common ancestor {}:{:?} conflicts with retained child {}:{:?} parent {:?}",
7545 ancestor.number,
7546 ancestor.hash,
7547 retained.number,
7548 retained.hash,
7549 retained.parent_hash
7550 ),
7551 });
7552 }
7553 if retained.parent_hash == Some(ancestor.hash) && Some(retained.number) != adjacent_number {
7554 return Err(ReactiveError::InvalidChainControl {
7555 message: format!(
7556 "reorg common ancestor {}:{:?} is named as the non-adjacent parent of retained block {}:{:?}",
7557 ancestor.number, ancestor.hash, retained.number, retained.hash
7558 ),
7559 });
7560 }
7561 }
7562 Ok(())
7563}
7564
7565fn validate_sequence_adjacent_parent_identity(
7566 state: &CanonicalSequenceState,
7567 block: &BlockRef,
7568 label: &str,
7569) -> Result<(), ReactiveError> {
7570 let Some(parent_hash) = block.parent_hash else {
7571 return Ok(());
7572 };
7573 let Some(parent_number) = block.number.checked_sub(1) else {
7574 return Ok(());
7575 };
7576 let known_parent = state
7577 .retained_canonical_history
7578 .iter()
7579 .chain(state.coverage_head.iter())
7580 .chain(state.safe_head.iter())
7581 .chain(state.finalized_head.iter())
7582 .find(|known| known.number == parent_number);
7583 if let Some(known_parent) = known_parent
7584 && known_parent.hash != parent_hash
7585 {
7586 return Err(ReactiveError::InvalidChainControl {
7587 message: format!(
7588 "{label} block {}:{:?} 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 Ok(())
7594}
7595
7596fn sequence_block_adds_metadata(state: &CanonicalSequenceState, incoming: &BlockRef) -> bool {
7597 state
7598 .coverage_head
7599 .iter()
7600 .chain(state.retained_canonical_history.iter())
7601 .filter(|known| known.number == incoming.number && known.hash == incoming.hash)
7602 .any(|known| {
7603 (known.parent_hash.is_none() && incoming.parent_hash.is_some())
7604 || (known.timestamp.is_none() && incoming.timestamp.is_some())
7605 })
7606}
7607
7608fn validate_sequence_implicit_finality<N: Network>(
7609 state: &CanonicalSequenceState,
7610 record: &ReactiveInputRecord<N>,
7611 resolved_canonical_block: Option<&BlockRef>,
7612) -> Result<(), ReactiveError> {
7613 let Some(finalized) = state.finalized_head.as_ref() else {
7614 return Ok(());
7615 };
7616 if let Some((dropped, _)) = reorg_signal_block(record) {
7617 if dropped.number <= finalized.number {
7618 return Err(ReactiveError::InvalidChainControl {
7619 message: format!(
7620 "implicit reorg at {}:{:?} would cross finalized head {}:{:?}",
7621 dropped.number, dropped.hash, finalized.number, finalized.hash
7622 ),
7623 });
7624 }
7625 return Ok(());
7626 }
7627 let Some(block) = resolved_canonical_block.or_else(|| canonical_record_block(record)) else {
7628 return Ok(());
7629 };
7630 let Some(latest) = state.coverage_head.as_ref() else {
7631 return Ok(());
7632 };
7633 if (block.number == latest.number && block.hash == latest.hash)
7634 || state
7635 .retained_canonical_history
7636 .iter()
7637 .any(|entry| entry.number == block.number && entry.hash == block.hash)
7638 || (latest.number.checked_add(1) == Some(block.number)
7639 && block.parent_hash == Some(latest.hash))
7640 || latest
7641 .number
7642 .checked_add(1)
7643 .is_some_and(|next| block.number > next)
7644 {
7645 return Ok(());
7646 }
7647 let crosses_finalized = if block.number <= finalized.number {
7648 true
7649 } else if let Some(parent_hash) = block.parent_hash {
7650 if finalized.number.checked_add(1) == Some(block.number) && parent_hash == finalized.hash {
7651 false
7652 } else if let Some(parent_index) =
7653 state.retained_canonical_history.iter().rposition(|entry| {
7654 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
7655 })
7656 {
7657 state
7658 .retained_canonical_history
7659 .iter()
7660 .skip(parent_index + 1)
7661 .any(|entry| entry.number <= finalized.number)
7662 } else {
7663 true
7664 }
7665 } else {
7666 true
7667 };
7668 if crosses_finalized {
7669 return Err(ReactiveError::InvalidChainControl {
7670 message: format!(
7671 "canonical input {}:{:?} would replace finalized head {}:{:?}",
7672 block.number, block.hash, finalized.number, finalized.hash
7673 ),
7674 });
7675 }
7676 Ok(())
7677}
7678
7679fn validate_required_reorg_anchor(
7680 required: Option<RequiredReorgAnchor>,
7681 block: &BlockRef,
7682) -> Result<(), ReactiveError> {
7683 let Some(required) = required else {
7684 return Ok(());
7685 };
7686 let ancestor_hash = required.hash();
7687 let restores_ancestor =
7688 block.number == required.number && ancestor_hash.is_some_and(|hash| block.hash == hash);
7689 let replaces_removed_child = required.number.checked_add(1) == Some(block.number)
7690 && ancestor_hash.is_some()
7691 && (block.parent_hash == ancestor_hash
7692 || (block.parent_hash.is_none() && required.permits_missing_child_parent));
7693 if restores_ancestor || replaces_removed_child {
7694 return Ok(());
7695 }
7696 Err(ReactiveError::InvalidChainControl {
7697 message: format!(
7698 "canonical replacement {}:{:?} does not prove the removed tip's parent at block {}",
7699 block.number, block.hash, required.number
7700 ),
7701 })
7702}
7703
7704fn validate_replacement_reorg_anchor(
7705 required: Option<RequiredReorgAnchor>,
7706 block: &BlockRef,
7707 policy: CanonicalSequenceValidationPolicy,
7708 oldest_retained: Option<u64>,
7709) -> Result<bool, CanonicalSequenceError> {
7710 let Some(required) = required else {
7711 return Ok(false);
7712 };
7713 match validate_required_reorg_anchor(Some(required), block) {
7714 Ok(()) => Ok(true),
7715 Err(error) if required.block.is_some() => Err(error.into()),
7716 Err(_) if policy.requires_complete_rollback() => {
7717 Err(CanonicalSequenceError::IncompleteRollback {
7718 common_ancestor: required.number,
7719 oldest_retained,
7720 kind: CanonicalRollbackKind::MissingReplacement,
7721 })
7722 }
7723 Err(_) => Ok(false),
7724 }
7725}
7726
7727fn apply_sequence_canonical_block(
7728 state: &mut CanonicalSequenceState,
7729 block: &BlockRef,
7730 allow_parentless_adjacent_extension: bool,
7731) -> Result<Option<SequenceRewind>, ReactiveError> {
7732 let latest = state.coverage_head;
7733 let already_known = state
7734 .retained_canonical_history
7735 .iter()
7736 .any(|entry| entry.number == block.number && entry.hash == block.hash);
7737 let repeats_tip =
7738 latest.is_some_and(|head| head.number == block.number && head.hash == block.hash);
7739 let extends_tip = latest.is_some_and(|head| {
7740 head.number.checked_add(1) == Some(block.number)
7741 && (block.parent_hash == Some(head.hash)
7742 || (allow_parentless_adjacent_extension && block.parent_hash.is_none()))
7743 });
7744 let forward_gap = latest.is_some_and(|head| {
7745 head.number
7746 .checked_add(1)
7747 .is_some_and(|next| block.number > next)
7748 });
7749 let mut rewind = None;
7750
7751 if latest.is_some() && !already_known && !repeats_tip && !extends_tip && !forward_gap {
7752 let retained_parent = block.parent_hash.and_then(|parent_hash| {
7753 state
7754 .retained_canonical_history
7755 .iter()
7756 .rposition(|entry| {
7757 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
7758 })
7759 .map(|index| (index, state.retained_canonical_history[index]))
7760 });
7761 let finalized_parent = block.parent_hash.and_then(|parent_hash| {
7762 state.finalized_head.filter(|finalized| {
7763 finalized.number.checked_add(1) == Some(block.number)
7764 && finalized.hash == parent_hash
7765 })
7766 });
7767 let (common_ancestor, dropped) = if let Some((parent_index, parent)) = retained_parent {
7768 let dropped = state.retained_canonical_history.split_off(parent_index + 1);
7769 (Some(parent), dropped)
7770 } else if let Some(finalized) = finalized_parent {
7771 let dropped = state
7772 .retained_canonical_history
7773 .iter()
7774 .position(|entry| entry.number > finalized.number)
7775 .map_or_else(Vec::new, |index| {
7776 state.retained_canonical_history.split_off(index)
7777 });
7778 (Some(finalized), dropped)
7779 } else {
7780 (None, std::mem::take(&mut state.retained_canonical_history))
7786 };
7787 state.coverage_head = common_ancestor;
7788 if let Some(common_ancestor) = common_ancestor {
7789 clear_sequence_heads_above(state, &common_ancestor);
7790 } else {
7791 state.safe_head = None;
7792 state.finalized_head = None;
7793 }
7794 rewind = Some(SequenceRewind {
7795 common_ancestor,
7796 dropped,
7797 });
7798 }
7799 upsert_sequence_history(&mut state.retained_canonical_history, block)?;
7800 advance_or_enrich_coverage(&mut state.coverage_head, block);
7801 Ok(rewind)
7802}
7803
7804fn sequence_implicit_replacement_requires_history(
7805 state: &CanonicalSequenceState,
7806 block: &BlockRef,
7807 policy: CanonicalSequenceValidationPolicy,
7808) -> Result<bool, ReactiveError> {
7809 let Some(latest) = state.coverage_head else {
7810 return Ok(false);
7811 };
7812 let already_known = state
7813 .retained_canonical_history
7814 .iter()
7815 .any(|entry| entry.number == block.number && entry.hash == block.hash);
7816 let repeats_tip = block.number == latest.number && block.hash == latest.hash;
7817 let extends_tip = latest.number.checked_add(1) == Some(block.number)
7818 && block.parent_hash == Some(latest.hash);
7819 let forward_gap = latest
7820 .number
7821 .checked_add(1)
7822 .is_some_and(|next| block.number > next);
7823 if already_known || repeats_tip || extends_tip || forward_gap {
7824 return Ok(false);
7825 }
7826 let Some(parent_hash) = block.parent_hash else {
7827 if policy.requires_complete_rollback() {
7828 return Err(ReactiveError::InvalidChainControl {
7829 message: format!(
7830 "implicit canonical replacement {}:{:?} must identify its parent",
7831 block.number, block.hash
7832 ),
7833 });
7834 }
7835 return Ok(true);
7836 };
7837 let known_adjacent_parent = block.number.checked_sub(1).and_then(|parent_number| {
7838 state
7839 .retained_canonical_history
7840 .iter()
7841 .chain(state.coverage_head.iter())
7842 .chain(state.safe_head.iter())
7843 .chain(state.finalized_head.iter())
7844 .find(|known| known.number == parent_number)
7845 });
7846 if let Some(known_parent) = known_adjacent_parent
7847 && known_parent.hash != parent_hash
7848 && policy.requires_complete_rollback()
7849 {
7850 return Err(ReactiveError::InvalidChainControl {
7851 message: format!(
7852 "implicit canonical replacement {}:{:?} names parent {:?}, which conflicts with known adjacent block {}:{:?}",
7853 block.number, block.hash, parent_hash, known_parent.number, known_parent.hash
7854 ),
7855 });
7856 }
7857 let retained_parent = state.retained_canonical_history.iter().any(|entry| {
7858 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
7859 });
7860 let finalized_parent = state.finalized_head.is_some_and(|finalized| {
7861 finalized.number.checked_add(1) == Some(block.number) && parent_hash == finalized.hash
7862 });
7863 Ok(!retained_parent && !finalized_parent)
7864}
7865
7866fn upsert_sequence_history(
7867 history: &mut Vec<BlockRef>,
7868 block: &BlockRef,
7869) -> Result<(), ReactiveError> {
7870 if let Some(existing) = history
7871 .iter_mut()
7872 .find(|entry| entry.number == block.number)
7873 {
7874 if existing.hash != block.hash {
7875 return Err(ReactiveError::InvalidChainControl {
7876 message: format!(
7877 "canonical block {}:{:?} conflicts with retained identity {:?}",
7878 block.number, block.hash, existing
7879 ),
7880 });
7881 }
7882 if !optional_block_refs_are_compatible(Some(existing), Some(block)) {
7883 return Err(ReactiveError::InvalidChainControl {
7884 message: format!(
7885 "canonical block {}:{:?} carries conflicting retained metadata",
7886 block.number, block.hash
7887 ),
7888 });
7889 }
7890 enrich_block_ref(existing, block);
7891 } else {
7892 history.push(*block);
7893 history.sort_by_key(|entry| entry.number);
7894 }
7895 Ok(())
7896}
7897
7898fn clear_sequence_heads_above(state: &mut CanonicalSequenceState, ancestor: &BlockRef) {
7899 if state.safe_head.as_ref().is_some_and(|head| {
7900 head.number > ancestor.number
7901 || (head.number == ancestor.number && head.hash != ancestor.hash)
7902 }) {
7903 state.safe_head = None;
7904 }
7905 if state.finalized_head.as_ref().is_some_and(|head| {
7906 head.number > ancestor.number
7907 || (head.number == ancestor.number && head.hash != ancestor.hash)
7908 }) {
7909 state.finalized_head = None;
7910 }
7911}
7912
7913fn validate_control_phase_order(controls: &[ChainControl]) -> Result<usize, ReactiveError> {
7914 let split = controls
7915 .iter()
7916 .position(|control| !matches!(control, ChainControl::Reorg { .. }))
7917 .unwrap_or(controls.len());
7918 if controls[split..]
7919 .iter()
7920 .any(|control| matches!(control, ChainControl::Reorg { .. }))
7921 {
7922 return Err(ReactiveError::InvalidChainControl {
7923 message: "reorg controls must precede records and all post-record controls in a batch"
7924 .into(),
7925 });
7926 }
7927 Ok(split)
7928}
7929
7930fn canonical_coverage_control_block(control: &ChainControl) -> Option<&BlockRef> {
7931 match control {
7932 ChainControl::CanonicalProgress(block)
7933 | ChainControl::Barrier {
7934 block: Some(block), ..
7935 } => Some(block),
7936 ChainControl::Reorg { .. }
7937 | ChainControl::Safe(_)
7938 | ChainControl::Finalized(_)
7939 | ChainControl::Barrier { block: None, .. } => None,
7940 }
7941}
7942
7943fn chain_control_canonical_assertion(control: &ChainControl) -> Option<&BlockRef> {
7944 match control {
7945 ChainControl::Safe(block)
7946 | ChainControl::Finalized(block)
7947 | ChainControl::CanonicalProgress(block)
7948 | ChainControl::Barrier {
7949 block: Some(block), ..
7950 } => Some(block),
7951 ChainControl::Reorg { .. } | ChainControl::Barrier { block: None, .. } => None,
7952 }
7953}
7954
7955fn assert_chain_control_identities(
7956 asserted_blocks: &mut HashMap<u64, BlockRef>,
7957 control: &ChainControl,
7958) -> Result<(), ReactiveError> {
7959 match control {
7960 ChainControl::Safe(block)
7961 | ChainControl::Finalized(block)
7962 | ChainControl::CanonicalProgress(block)
7963 | ChainControl::Barrier {
7964 block: Some(block), ..
7965 } => assert_canonical_block_identity(asserted_blocks, block, "chain control"),
7966 ChainControl::Barrier { block: None, .. } => Ok(()),
7967 ChainControl::Reorg {
7968 common_ancestor,
7969 new_tip,
7970 ..
7971 } => {
7972 asserted_blocks.retain(|number, _| *number <= common_ancestor.number);
7973 assert_canonical_block_identity(
7974 asserted_blocks,
7975 common_ancestor,
7976 "reorg common ancestor",
7977 )?;
7978 assert_canonical_block_identity(asserted_blocks, new_tip, "reorg new tip")
7979 }
7980 }
7981}
7982
7983fn assert_canonical_block_identity(
7984 asserted_blocks: &mut HashMap<u64, BlockRef>,
7985 block: &BlockRef,
7986 label: &str,
7987) -> Result<(), ReactiveError> {
7988 for asserted in asserted_blocks.values() {
7989 if asserted.hash == block.hash && asserted.number != block.number {
7990 return Err(ReactiveError::InvalidChainControl {
7991 message: format!(
7992 "{label} hash {:?} is already asserted at height {}, not {}",
7993 block.hash, asserted.number, block.number
7994 ),
7995 });
7996 }
7997 if block
7998 .parent_hash
7999 .is_some_and(|parent| parent == asserted.hash)
8000 && asserted.number.checked_add(1) != Some(block.number)
8001 {
8002 return Err(ReactiveError::InvalidChainControl {
8003 message: format!(
8004 "{label} block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
8005 block.number, block.hash, asserted.hash, asserted.number
8006 ),
8007 });
8008 }
8009 if asserted
8010 .parent_hash
8011 .is_some_and(|parent| parent == block.hash)
8012 && block.number.checked_add(1) != Some(asserted.number)
8013 {
8014 return Err(ReactiveError::InvalidChainControl {
8015 message: format!(
8016 "block {}:{:?} asserted earlier names {label} hash {:?} from non-adjacent height {} as its parent",
8017 asserted.number, asserted.hash, block.hash, block.number
8018 ),
8019 });
8020 }
8021 }
8022 if let Some(known) = asserted_blocks.get_mut(&block.number) {
8023 if !optional_block_refs_are_compatible(Some(known), Some(block)) {
8024 return Err(ReactiveError::InvalidChainControl {
8025 message: format!(
8026 "{label} block {}:{:?} conflicts with block identity {:?} asserted earlier in the batch",
8027 block.number, block.hash, known
8028 ),
8029 });
8030 }
8031 enrich_block_ref(known, block);
8032 } else {
8033 asserted_blocks.insert(block.number, *block);
8034 }
8035 Ok(())
8036}
8037
8038fn set_or_enrich_block_ref(current: &mut Option<BlockRef>, incoming: &BlockRef) {
8039 match current {
8040 Some(current) if current.number == incoming.number && current.hash == incoming.hash => {
8041 enrich_block_ref(current, incoming);
8042 }
8043 _ => *current = Some(*incoming),
8044 }
8045}
8046
8047fn advance_or_enrich_coverage(current: &mut Option<BlockRef>, incoming: &BlockRef) {
8048 match current {
8049 Some(current) if current.number == incoming.number && current.hash == incoming.hash => {
8050 enrich_block_ref(current, incoming);
8051 }
8052 Some(current) if current.number >= incoming.number => {}
8053 _ => *current = Some(*incoming),
8054 }
8055}
8056
8057fn validate_adjacent_finality(
8058 finalized: Option<&BlockRef>,
8059 safe: Option<&BlockRef>,
8060) -> Result<(), ReactiveError> {
8061 let Some((finalized, safe)) = finalized.zip(safe) else {
8062 return Ok(());
8063 };
8064 if finalized.number.checked_add(1) == Some(safe.number)
8065 && safe.parent_hash != Some(finalized.hash)
8066 {
8067 return Err(ReactiveError::InvalidChainControl {
8068 message: "adjacent safe head does not descend from finalized head".into(),
8069 });
8070 }
8071 Ok(())
8072}
8073
8074fn collect_diff_addresses(diff: &StateDiff, into: &mut HashSet<Address>) {
8080 into.extend(diff.slots.iter().map(|change| change.address));
8081 into.extend(diff.accounts.iter().map(|change| change.address));
8082 into.extend(diff.purged.iter().map(|purge| purge.address));
8083 into.extend(diff.skipped.iter().map(|skipped| skipped.address));
8084 into.extend(diff.skipped_balances.iter().map(|skipped| skipped.address));
8085 into.extend(diff.skipped_masks.iter().map(|skipped| skipped.address));
8086 into.extend(diff.skipped_accounts.iter().map(|skipped| skipped.address));
8087}
8088
8089fn root_moved_account_resync(
8094 address: Address,
8095 block: u64,
8096 fields: AccountFieldMask,
8097) -> ResyncRequest {
8098 ResyncRequest {
8099 id: ResyncId::new(format!("root-moved:{address:#x}:{block}")),
8100 reason: ResyncReason::RootMoved,
8101 block: ResyncBlock::Number(block),
8102 targets: vec![ResyncTarget::Account { address, fields }],
8103 priority: ResyncPriority::Normal,
8104 }
8105}
8106
8107fn batch_preconfirmation<N: Network>(
8108 batch: &ReactiveInputBatch<N>,
8109) -> Result<Option<FlashblockRef>, ReactiveError> {
8110 let mut flashblock: Option<FlashblockRef> = None;
8111 let mut has_non_preconfirmed = false;
8112 for (index, record) in batch.records().iter().enumerate() {
8113 match &record.context.chain_status {
8114 ChainStatus::Preconfirmed {
8115 flashblock: current,
8116 } => {
8117 if batch.record_delivery_scope(index) != Some(DeliveryScope::Preconfirmed) {
8118 return Err(ReactiveError::InvalidInputRecord {
8119 message: "pre-confirmed input requires pre-confirmed delivery scope".into(),
8120 });
8121 }
8122 if flashblock
8123 .as_ref()
8124 .is_some_and(|known| known != current.as_ref())
8125 {
8126 return Err(ReactiveError::InvalidInputRecord {
8127 message: "one batch cannot mix distinct Flashblock snapshots".into(),
8128 });
8129 }
8130 flashblock.get_or_insert_with(|| current.as_ref().clone());
8131 }
8132 _ => has_non_preconfirmed = true,
8133 }
8134 }
8135 if flashblock.is_some() && (has_non_preconfirmed || !batch.chain_controls().is_empty()) {
8136 return Err(ReactiveError::InvalidInputRecord {
8137 message: "pre-confirmed delivery cannot mix canonical inputs or chain controls".into(),
8138 });
8139 }
8140 Ok(flashblock)
8141}
8142
8143fn canonical_record_block<N: Network>(record: &ReactiveInputRecord<N>) -> Option<&BlockRef> {
8144 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
8145 return None;
8146 }
8147 if is_canonical_status(&record.context.chain_status) {
8148 return context_block_ref(&record.context);
8149 }
8150 None
8151}
8152
8153fn resolve_record_block_payload_metadata<N: Network>(
8154 record: &ReactiveInputRecord<N>,
8155 mut block: BlockRef,
8156) -> Result<BlockRef, ReactiveError> {
8157 let ReactiveInput::Log(log) = &record.input else {
8158 return Ok(block);
8159 };
8160 if log.block_number != Some(block.number) || log.block_hash != Some(block.hash) {
8161 return Err(ReactiveError::InvalidInputRecord {
8162 message: "log payload and canonical context carry different block identities".into(),
8163 });
8164 }
8165 if let Some(timestamp) = log.block_timestamp {
8166 if block.timestamp.is_some_and(|known| known != timestamp) {
8167 return Err(ReactiveError::InvalidInputRecord {
8168 message: "log payload and canonical context carry different block timestamps"
8169 .into(),
8170 });
8171 }
8172 block.timestamp = Some(timestamp);
8173 }
8174 Ok(block)
8175}
8176
8177fn validate_input_record<N: Network>(record: &ReactiveInputRecord<N>) -> Result<(), ReactiveError> {
8178 let invalid = |message: String| ReactiveError::InvalidInputRecord { message };
8179 if let ChainStatus::Preconfirmed { flashblock } = &record.context.chain_status
8180 && record.context.block != Some(flashblock.block_ref())
8181 {
8182 return Err(invalid(
8183 "pre-confirmed status and context carry different partial block identities".into(),
8184 ));
8185 }
8186 let status_block = match &record.context.chain_status {
8187 ChainStatus::Included { block, .. }
8188 | ChainStatus::Safe { block }
8189 | ChainStatus::Finalized { block }
8190 | ChainStatus::Reorged {
8191 dropped_from: block,
8192 } => Some(block),
8193 ChainStatus::Preconfirmed { .. } => record.context.block.as_ref(),
8194 ChainStatus::Pending => None,
8195 };
8196 match (status_block, record.context.block.as_ref()) {
8197 (Some(status), Some(context)) if status == context => {}
8198 (Some(_), Some(_)) => {
8199 return Err(invalid(
8200 "chain status and context carry different block identities".into(),
8201 ));
8202 }
8203 (Some(_), None) => {
8204 return Err(invalid(
8205 "included or reorged input is missing its context block".into(),
8206 ));
8207 }
8208 (None, Some(_)) => {
8209 return Err(invalid(
8210 "pending input cannot carry a canonical context block".into(),
8211 ));
8212 }
8213 (None, None) => {}
8214 }
8215
8216 match &record.input {
8217 ReactiveInput::Log(log) => {
8218 let Some(block) = status_block else {
8219 return Err(invalid(
8220 "log input must carry an included or reorged block identity".into(),
8221 ));
8222 };
8223 if log.removed && !matches!(record.context.chain_status, ChainStatus::Reorged { .. }) {
8224 return Err(invalid(
8225 "removed log must carry reorged chain status".into(),
8226 ));
8227 }
8228 let block_number = log
8229 .block_number
8230 .ok_or_else(|| invalid("log is missing its block number".into()))?;
8231 let block_hash = log
8232 .block_hash
8233 .ok_or_else(|| invalid("log is missing its block hash".into()))?;
8234 log.transaction_hash
8235 .ok_or_else(|| invalid("log is missing its transaction hash".into()))?;
8236 let transaction_index = log
8237 .transaction_index
8238 .ok_or_else(|| invalid("log is missing its transaction index".into()))?;
8239 let log_index = log
8240 .log_index
8241 .ok_or_else(|| invalid("log is missing its log index".into()))?;
8242 if block_number != block.number
8243 || block_hash != block.hash
8244 || !optional_metadata_compatible(
8245 log.block_timestamp.as_ref(),
8246 block.timestamp.as_ref(),
8247 )
8248 {
8249 return Err(invalid(
8250 "log payload and context carry different block identities".into(),
8251 ));
8252 }
8253 if record.context.transaction_index != Some(transaction_index)
8254 || record.context.log_index != Some(log_index)
8255 {
8256 return Err(invalid(
8257 "log payload and context carry different transaction/log positions".into(),
8258 ));
8259 }
8260 }
8261 ReactiveInput::BlockHeader(header) => {
8262 if let Some(block) = status_block {
8263 if header.number() != block.number
8264 || header.hash() != block.hash
8265 || Some(header.parent_hash()) != block.parent_hash
8266 || Some(header.timestamp()) != block.timestamp
8267 {
8268 return Err(invalid(
8269 "block header payload and context carry different block identities".into(),
8270 ));
8271 }
8272 } else if !matches!(record.context.chain_status, ChainStatus::Pending) {
8273 return Err(invalid("block header has an unsupported lifecycle".into()));
8274 }
8275 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8276 return Err(invalid(
8277 "block header context cannot carry transaction/log positions".into(),
8278 ));
8279 }
8280 }
8281 ReactiveInput::FullBlock(block_response) => {
8282 let header = block_response.header();
8283 if let Some(block) = status_block {
8284 if header.number() != block.number
8285 || header.hash() != block.hash
8286 || Some(header.parent_hash()) != block.parent_hash
8287 || Some(header.timestamp()) != block.timestamp
8288 {
8289 return Err(invalid(
8290 "full-block payload and context carry different block identities".into(),
8291 ));
8292 }
8293 } else if !matches!(record.context.chain_status, ChainStatus::Pending) {
8294 return Err(invalid("full block has an unsupported lifecycle".into()));
8295 }
8296 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8297 return Err(invalid(
8298 "full-block context cannot carry transaction/log positions".into(),
8299 ));
8300 }
8301 if let Some(transactions) = block_response.transactions().as_transactions() {
8302 for (index, transaction) in transactions.iter().enumerate() {
8303 if transaction
8304 .block_hash()
8305 .is_some_and(|hash| hash != header.hash())
8306 || transaction
8307 .block_number()
8308 .is_some_and(|number| number != header.number())
8309 || transaction
8310 .transaction_index()
8311 .is_some_and(|position| position != index as u64)
8312 {
8313 return Err(invalid(format!(
8314 "full-block transaction {index} carries contradictory inclusion metadata"
8315 )));
8316 }
8317 if transaction
8318 .chain_id()
8319 .zip(record.context.chain_id)
8320 .is_some_and(|(transaction, context)| transaction != context)
8321 {
8322 return Err(invalid(format!(
8323 "full-block transaction {index} carries a chain id conflicting with its context"
8324 )));
8325 }
8326 }
8327 }
8328 }
8329 ReactiveInput::PendingTxHash(_) => {
8330 if !matches!(record.context.chain_status, ChainStatus::Pending) {
8331 return Err(invalid(
8332 "pending transaction input must carry pending chain status".into(),
8333 ));
8334 }
8335 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8336 return Err(invalid(
8337 "pending transaction context cannot carry canonical positions".into(),
8338 ));
8339 }
8340 }
8341 ReactiveInput::PendingTx(transaction) => {
8342 if !matches!(record.context.chain_status, ChainStatus::Pending) {
8343 return Err(invalid(
8344 "pending transaction input must carry pending chain status".into(),
8345 ));
8346 }
8347 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8348 return Err(invalid(
8349 "pending transaction context cannot carry canonical positions".into(),
8350 ));
8351 }
8352 if transaction.block_hash().is_some()
8353 || transaction.block_number().is_some()
8354 || transaction.transaction_index().is_some()
8355 {
8356 return Err(invalid(
8357 "hydrated pending transaction cannot carry inclusion metadata".into(),
8358 ));
8359 }
8360 if transaction
8361 .chain_id()
8362 .zip(record.context.chain_id)
8363 .is_some_and(|(transaction, context)| transaction != context)
8364 {
8365 return Err(invalid(
8366 "pending transaction carries a chain id conflicting with its context".into(),
8367 ));
8368 }
8369 }
8370 }
8371 Ok(())
8372}
8373
8374fn advance_block_for_canonical_record<N: Network>(
8383 cache: &mut EvmCache,
8384 record: &ReactiveInputRecord<N>,
8385) -> Option<Result<(), BlockContextError>> {
8386 if !is_canonical_status(&record.context.chain_status) {
8387 return None;
8388 }
8389 match &record.input {
8390 ReactiveInput::BlockHeader(header) => Some(cache.advance_block(header)),
8391 ReactiveInput::FullBlock(block) => Some(cache.advance_block(block.header())),
8392 _ => None,
8393 }
8394}
8395
8396fn context_block_ref(ctx: &ReactiveContext) -> Option<&BlockRef> {
8397 match &ctx.chain_status {
8398 ChainStatus::Included { block, .. }
8399 | ChainStatus::Safe { block }
8400 | ChainStatus::Finalized { block } => Some(block),
8401 ChainStatus::Reorged { dropped_from } => Some(dropped_from),
8402 ChainStatus::Preconfirmed { .. } => ctx.block.as_ref(),
8403 ChainStatus::Pending => ctx.block.as_ref(),
8404 }
8405}
8406
8407fn reorg_signal_block<N: Network>(
8408 record: &ReactiveInputRecord<N>,
8409) -> Option<(BlockRef, ReorgReason)> {
8410 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
8411 return block_ref_from_record(record).map(|block| (block, ReorgReason::RemovedLog));
8412 }
8413
8414 if let ChainStatus::Reorged { dropped_from } = &record.context.chain_status {
8415 return Some((*dropped_from, ReorgReason::ReorgedInput));
8416 }
8417
8418 None
8419}
8420
8421fn block_ref_from_record<N: Network>(record: &ReactiveInputRecord<N>) -> Option<BlockRef> {
8422 context_block_ref(&record.context)
8423 .cloned()
8424 .or_else(|| match &record.input {
8425 ReactiveInput::Log(log) => Some(BlockRef {
8426 number: log.block_number?,
8427 hash: log.block_hash?,
8428 parent_hash: None,
8429 timestamp: log.block_timestamp,
8430 }),
8431 ReactiveInput::BlockHeader(header) => Some(BlockRef {
8432 number: header.number(),
8433 hash: header.hash(),
8434 parent_hash: Some(header.parent_hash()),
8435 timestamp: Some(header.timestamp()),
8436 }),
8437 ReactiveInput::FullBlock(block) => {
8438 let header = block.header();
8439 Some(BlockRef {
8440 number: header.number(),
8441 hash: header.hash(),
8442 parent_hash: Some(header.parent_hash()),
8443 timestamp: Some(header.timestamp()),
8444 })
8445 }
8446 ReactiveInput::PendingTxHash(_) | ReactiveInput::PendingTx(_) => None,
8447 })
8448}
8449
8450fn remove_canceled_resyncs_from_batch(
8451 resyncs: &mut Vec<ResyncRequest>,
8452 canceled: &[ResyncRequest],
8453) {
8454 if canceled.is_empty() {
8455 return;
8456 }
8457 let canceled_ids: HashSet<_> = canceled.iter().map(|request| request.id.clone()).collect();
8458 resyncs.retain(|request| !canceled_ids.contains(&request.id));
8459}
8460
8461fn resync_target_address(target: &ResyncTarget) -> Address {
8462 match target {
8463 ResyncTarget::StorageSlot { address, .. }
8464 | ResyncTarget::StorageSlots { address, .. }
8465 | ResyncTarget::Account { address, .. } => *address,
8466 }
8467}
8468
8469fn resync_request_targets_dropped_block(
8470 request: &ResyncRequest,
8471 dropped_blocks: &[BlockRef],
8472) -> bool {
8473 let ResyncBlock::Hash { number, hash, .. } = &request.block else {
8474 return false;
8475 };
8476 dropped_blocks
8477 .iter()
8478 .any(|block| block.hash == *hash && block.number == *number)
8479}
8480
8481fn single_hash_pinned_resync_block(report: &ResyncReport) -> Option<BlockRef> {
8482 let first = report.requested.first()?.block.clone();
8483 if !report
8484 .requested
8485 .iter()
8486 .all(|request| request.block == first)
8487 {
8488 return None;
8489 }
8490
8491 let ResyncBlock::Hash { number, hash, .. } = first else {
8492 return None;
8493 };
8494
8495 Some(BlockRef {
8496 number,
8497 hash,
8498 parent_hash: None,
8499 timestamp: None,
8500 })
8501}
8502
8503fn purge_scopes_for_dropped_journals<N: Network>(
8504 dropped: &[BlockJournal<N>],
8505) -> Vec<(Address, PurgeScope)> {
8506 let mut scopes: Vec<(Address, PurgeScope)> = Vec::new();
8507 for entry in dropped.iter().rev() {
8508 for diff in entry.rollback_diffs.iter().rev() {
8509 merge_purge_scopes_for_diff(&mut scopes, diff);
8510 }
8511 }
8512 scopes
8513}
8514
8515fn rollback_updates_for_dropped_journals<N: Network>(
8516 dropped: &[BlockJournal<N>],
8517 purge_scopes: &[(Address, PurgeScope)],
8518) -> Vec<StateUpdate> {
8519 let purge_addresses: HashSet<_> = purge_scopes
8520 .iter()
8521 .map(|(address, _scope)| *address)
8522 .collect();
8523 let mut updates = Vec::new();
8524 for entry in dropped.iter().rev() {
8525 for diff in entry.rollback_diffs.iter().rev() {
8526 push_rollback_updates_for_diff(&mut updates, diff, &purge_addresses);
8527 }
8528 }
8529 updates
8530}
8531
8532fn merge_purge_scopes_for_diff(scopes: &mut Vec<(Address, PurgeScope)>, diff: &StateDiff) {
8533 for change in &diff.accounts {
8534 merge_purge_scope(scopes, change.address, PurgeScope::Account);
8535 }
8536 for record in &diff.purged {
8537 merge_purge_scope(scopes, record.address, record.scope.clone());
8538 }
8539}
8540
8541fn push_rollback_updates_for_diff(
8542 updates: &mut Vec<StateUpdate>,
8543 diff: &StateDiff,
8544 purge_addresses: &HashSet<Address>,
8545) {
8546 for change in diff.slots.iter().rev() {
8547 if purge_addresses.contains(&change.address) {
8548 continue;
8549 }
8550 updates.push(StateUpdate::slot(change.address, change.slot, change.old));
8551 }
8552}
8553
8554fn merge_purge_scope(scopes: &mut Vec<(Address, PurgeScope)>, address: Address, scope: PurgeScope) {
8555 if let Some((_existing_address, existing_scope)) = scopes
8556 .iter_mut()
8557 .find(|(existing_address, _scope)| *existing_address == address)
8558 {
8559 *existing_scope = merged_purge_scope(existing_scope.clone(), scope);
8560 } else {
8561 scopes.push((address, scope));
8562 }
8563}
8564
8565fn merged_purge_scope(left: PurgeScope, right: PurgeScope) -> PurgeScope {
8566 match (left, right) {
8567 (PurgeScope::Account, _) | (_, PurgeScope::Account) => PurgeScope::Account,
8568 (PurgeScope::AllStorage, _) | (_, PurgeScope::AllStorage) => PurgeScope::AllStorage,
8569 (PurgeScope::Slots(mut left), PurgeScope::Slots(right)) => {
8570 for slot in right {
8571 if !left.contains(&slot) {
8572 left.push(slot);
8573 }
8574 }
8575 PurgeScope::Slots(left)
8576 }
8577 }
8578}
8579
8580#[derive(Clone, Debug)]
8581struct StorageFetchSlot {
8582 address: Address,
8583 slot: U256,
8584 origins: Vec<StorageFetchOrigin>,
8585}
8586
8587#[derive(Clone, Debug)]
8588struct StorageFetchOrigin {
8589 request_id: ResyncId,
8590 target: ResyncTarget,
8591}
8592
8593#[derive(Clone, Debug)]
8594struct StorageFetchGroup {
8595 block: ResyncBlock,
8596 slots: Vec<StorageFetchSlot>,
8597 seen: HashSet<(Address, U256)>,
8598}
8599
8600#[derive(Clone, Debug)]
8603struct AccountResyncTarget {
8604 request_id: ResyncId,
8605 block: ResyncBlock,
8606 address: Address,
8607 fields: AccountFieldMask,
8608}
8609
8610fn resolve_trace_resyncs(
8611 cache: &EvmCache,
8612 storage_groups: &mut Vec<StorageFetchGroup>,
8613 account_targets: &mut Vec<AccountResyncTarget>,
8614 state_updates: &mut Vec<StateUpdate>,
8615) {
8616 let Some(fetcher) = cache.block_state_diff_fetcher().cloned() else {
8617 return;
8618 };
8619
8620 let mut blocks = Vec::new();
8621 let mut seen = HashSet::new();
8622 for block in storage_groups
8623 .iter()
8624 .map(|group| group.block.clone())
8625 .chain(account_targets.iter().map(|target| target.block.clone()))
8626 {
8627 if seen.insert(block.clone()) {
8628 blocks.push(block);
8629 }
8630 }
8631
8632 let mut traces = HashMap::new();
8633 for block in blocks {
8634 match (fetcher)(resync_block_to_block_id(&block)) {
8635 Ok(diff) => {
8636 traces.insert(block, diff);
8637 }
8638 Err(error) => {
8639 tracing::debug!(
8640 block = ?block,
8641 error = %error,
8642 "block trace resync source failed; falling back to point resync"
8643 );
8644 }
8645 }
8646 }
8647
8648 for group in storage_groups.iter_mut() {
8649 let Some(trace) = traces.get(&group.block) else {
8650 continue;
8651 };
8652 group.slots.retain(|slot| {
8653 if let Some(value) = trace_storage_value(trace, slot.address, slot.slot) {
8654 state_updates.push(StateUpdate::slot(slot.address, slot.slot, value));
8655 return false;
8656 }
8657 cache
8658 .cached_storage_value(slot.address, slot.slot)
8659 .is_none()
8660 });
8661 group.seen = group
8662 .slots
8663 .iter()
8664 .map(|slot| (slot.address, slot.slot))
8665 .collect();
8666 }
8667 storage_groups.retain(|group| !group.slots.is_empty());
8668
8669 let mut unresolved_accounts = Vec::new();
8670 for mut account in account_targets.drain(..) {
8671 let Some(trace) = traces.get(&account.block) else {
8672 unresolved_accounts.push(account);
8673 continue;
8674 };
8675 let Some(trace_account) = trace
8676 .accounts
8677 .iter()
8678 .find(|diff| diff.address == account.address)
8679 else {
8680 unresolved_accounts.push(account);
8681 continue;
8682 };
8683
8684 let mut patch = AccountPatch::default();
8685 let mut unresolved = AccountFieldMask::default();
8686 if account.fields.balance {
8687 if let Some(balance) = trace_account.balance {
8688 patch = patch.balance(balance);
8689 } else {
8690 unresolved.balance = true;
8691 }
8692 }
8693 if account.fields.nonce {
8694 if let Some(nonce) = trace_account.nonce {
8695 patch = patch.nonce(nonce);
8696 } else {
8697 unresolved.nonce = true;
8698 }
8699 }
8700 if account.fields.code {
8701 if let Some(code) = &trace_account.code {
8702 patch = patch.code(code.clone());
8703 } else {
8704 unresolved.code = true;
8705 }
8706 }
8707
8708 if patch.balance.is_some() || patch.nonce.is_some() || patch.code.is_some() {
8709 state_updates.push(StateUpdate::account_upsert(account.address, patch));
8710 }
8711 if !account_field_mask_empty(unresolved) {
8712 account.fields = unresolved;
8713 unresolved_accounts.push(account);
8714 }
8715 }
8716 *account_targets = unresolved_accounts;
8717}
8718
8719fn trace_storage_value(trace: &BlockStateDiff, address: Address, slot: U256) -> Option<U256> {
8720 trace
8721 .accounts
8722 .iter()
8723 .find(|account| account.address == address)
8724 .and_then(|account| {
8725 account
8726 .storage
8727 .iter()
8728 .find(|entry| entry.slot == slot)
8729 .map(|entry| entry.value)
8730 })
8731}
8732
8733fn account_field_mask_empty(mask: AccountFieldMask) -> bool {
8734 !mask.balance && !mask.nonce && !mask.code
8735}
8736
8737fn execute_resync_requests(cache: &mut EvmCache, requests: &[ResyncRequest]) -> ResyncReport {
8738 let mut failed = Vec::new();
8739 let mut storage_groups: Vec<StorageFetchGroup> = Vec::new();
8740 let mut account_targets: Vec<AccountResyncTarget> = Vec::new();
8741
8742 for request in requests {
8743 for target in &request.targets {
8744 match target {
8745 ResyncTarget::StorageSlot { address, slot } => {
8746 push_storage_resync_slot(
8747 &mut storage_groups,
8748 &request.id,
8749 &request.block,
8750 *address,
8751 *slot,
8752 );
8753 }
8754 ResyncTarget::StorageSlots { address, slots } => {
8755 for slot in slots {
8756 push_storage_resync_slot(
8757 &mut storage_groups,
8758 &request.id,
8759 &request.block,
8760 *address,
8761 *slot,
8762 );
8763 }
8764 }
8765 ResyncTarget::Account { address, fields } => {
8766 account_targets.push(AccountResyncTarget {
8767 request_id: request.id.clone(),
8768 block: request.block.clone(),
8769 address: *address,
8770 fields: *fields,
8771 });
8772 }
8773 }
8774 }
8775 }
8776
8777 let mut state_updates = Vec::new();
8778 resolve_trace_resyncs(
8779 cache,
8780 &mut storage_groups,
8781 &mut account_targets,
8782 &mut state_updates,
8783 );
8784
8785 if !storage_groups.is_empty() {
8786 if let Some(fetcher) = cache.storage_batch_fetcher().cloned() {
8787 for group in storage_groups {
8788 let block = group.block.clone();
8789 let fetches: Vec<(Address, U256)> = group
8790 .slots
8791 .iter()
8792 .map(|slot| (slot.address, slot.slot))
8793 .collect();
8794 let results = (fetcher)(fetches, resync_block_to_block_id(&block));
8795 let mut pending: HashMap<(Address, U256), StorageFetchSlot> = group
8796 .slots
8797 .iter()
8798 .cloned()
8799 .map(|slot| ((slot.address, slot.slot), slot))
8800 .collect();
8801
8802 for (address, slot, fetched) in results {
8803 let Some(requested_slot) = pending.remove(&(address, slot)) else {
8804 continue;
8805 };
8806 match fetched {
8807 Ok(value) => state_updates.push(StateUpdate::slot(address, slot, value)),
8808 Err(error) => {
8809 let message = error.to_string();
8810 push_resync_failures(
8811 &mut failed,
8812 &block,
8813 requested_slot.origins,
8814 ResyncFailureKind::StorageFetchFailed,
8815 message,
8816 );
8817 }
8818 }
8819 }
8820
8821 for requested_slot in group.slots {
8822 if pending
8823 .remove(&(requested_slot.address, requested_slot.slot))
8824 .is_some()
8825 {
8826 push_resync_failures(
8827 &mut failed,
8828 &block,
8829 requested_slot.origins,
8830 ResyncFailureKind::StorageFetchOmitted,
8831 "storage batch fetcher did not return a value for slot".to_string(),
8832 );
8833 }
8834 }
8835 }
8836 } else {
8837 for group in storage_groups {
8838 let block = group.block.clone();
8839 for slot in group.slots {
8840 push_resync_failures(
8841 &mut failed,
8842 &block,
8843 slot.origins,
8844 ResyncFailureKind::MissingStorageFetcher,
8845 "storage resync requires a storage batch fetcher".to_string(),
8846 );
8847 }
8848 }
8849 }
8850 }
8851
8852 if !account_targets.is_empty() {
8853 if let Some(fetcher) = cache.account_proof_fetcher().cloned() {
8854 let mut groups: Vec<(BlockId, Vec<_>)> = Vec::new();
8860 for account in account_targets {
8861 let block_id = resync_block_to_block_id(&account.block);
8862 match groups
8863 .iter_mut()
8864 .find(|(group_block, _)| *group_block == block_id)
8865 {
8866 Some((_, group)) => group.push(account),
8867 None => groups.push((block_id, vec![account])),
8868 }
8869 }
8870 for (block_id, group) in groups {
8871 let probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
8872 group
8873 .iter()
8874 .map(|account| (account.address, vec![]))
8875 .collect(),
8876 block_id,
8877 )
8878 .into_iter()
8879 .collect();
8880 for account in group {
8881 match probes.get(&account.address).cloned() {
8885 Some(Ok(proof)) => {
8886 let mut patch = AccountPatch::default();
8891 if account.fields.balance {
8892 patch = patch.balance(proof.balance);
8893 }
8894 if account.fields.nonce {
8895 patch = patch.nonce(proof.nonce);
8896 }
8897 state_updates.push(StateUpdate::account_upsert(account.address, patch));
8903 }
8904 Some(Err(error)) => {
8905 failed.push(ResyncFailure {
8906 request_id: account.request_id,
8907 block: account.block,
8908 target: ResyncTarget::Account {
8909 address: account.address,
8910 fields: account.fields,
8911 },
8912 kind: ResyncFailureKind::AccountFetchFailed,
8913 message: error.to_string(),
8914 });
8915 }
8916 None => {
8917 failed.push(ResyncFailure {
8918 request_id: account.request_id,
8919 block: account.block,
8920 target: ResyncTarget::Account {
8921 address: account.address,
8922 fields: account.fields,
8923 },
8924 kind: ResyncFailureKind::AccountFetchOmitted,
8925 message:
8926 "account proof fetcher did not return a result for address"
8927 .to_string(),
8928 });
8929 }
8930 }
8931 }
8932 }
8933 } else {
8934 for account in account_targets {
8935 failed.push(ResyncFailure {
8936 request_id: account.request_id,
8937 block: account.block,
8938 target: ResyncTarget::Account {
8939 address: account.address,
8940 fields: account.fields,
8941 },
8942 kind: ResyncFailureKind::MissingAccountFetcher,
8943 message: "account resync requires an account proof fetcher".to_string(),
8944 });
8945 }
8946 }
8947 }
8948
8949 let diff = if state_updates.is_empty() {
8950 StateDiff::default()
8951 } else {
8952 cache.apply_updates(&state_updates)
8953 };
8954
8955 ResyncReport {
8956 requested: requests.to_vec(),
8957 state_updates,
8958 diff,
8959 failed,
8960 }
8961}
8962
8963fn push_resync_failures(
8964 failed: &mut Vec<ResyncFailure>,
8965 block: &ResyncBlock,
8966 origins: Vec<StorageFetchOrigin>,
8967 kind: ResyncFailureKind,
8968 message: String,
8969) {
8970 for origin in origins {
8971 failed.push(ResyncFailure {
8972 request_id: origin.request_id,
8973 block: block.clone(),
8974 target: origin.target,
8975 kind,
8976 message: message.clone(),
8977 });
8978 }
8979}
8980
8981fn push_storage_resync_slot(
8982 groups: &mut Vec<StorageFetchGroup>,
8983 request_id: &ResyncId,
8984 block: &ResyncBlock,
8985 address: Address,
8986 slot: U256,
8987) {
8988 let group_index = if let Some(index) = groups.iter().position(|group| group.block == *block) {
8989 index
8990 } else {
8991 groups.push(StorageFetchGroup {
8992 block: block.clone(),
8993 slots: Vec::new(),
8994 seen: HashSet::new(),
8995 });
8996 groups.len() - 1
8997 };
8998
8999 let group = &mut groups[group_index];
9000 let origin = StorageFetchOrigin {
9001 request_id: request_id.clone(),
9002 target: ResyncTarget::StorageSlot { address, slot },
9003 };
9004 if group.seen.insert((address, slot)) {
9005 group.slots.push(StorageFetchSlot {
9006 address,
9007 slot,
9008 origins: vec![origin],
9009 });
9010 } else if let Some(existing) = group
9011 .slots
9012 .iter_mut()
9013 .find(|existing| existing.address == address && existing.slot == slot)
9014 {
9015 existing.origins.push(origin);
9016 }
9017}
9018
9019fn resync_block_to_block_id(block: &ResyncBlock) -> BlockId {
9020 match block {
9021 ResyncBlock::Latest => BlockId::latest(),
9022 ResyncBlock::Pending => BlockId::pending(),
9023 ResyncBlock::Safe => BlockId::safe(),
9024 ResyncBlock::Finalized => BlockId::finalized(),
9025 ResyncBlock::Number(number) => BlockId::number(*number),
9026 ResyncBlock::Hash {
9027 number: _,
9028 hash,
9029 require_canonical,
9030 } => BlockId::from((*hash, Some(*require_canonical))),
9031 }
9032}
9033
9034impl<N: Network> RegisteredHandler<N> {
9035 fn matches(&self, input: &ReactiveInput<N>) -> bool {
9036 self.interests
9037 .iter()
9038 .any(|interest| interest_matches(interest, input))
9039 }
9040
9041 fn route_log(&self, log: &Log) -> Option<ReactiveLogRoute> {
9042 self.interests.iter().find_map(|interest| match interest {
9043 ReactiveInterest::Logs(interest) if interest.matches(log) => Some(ReactiveLogRoute {
9044 handler_id: self.id.clone(),
9045 route_key: interest.route_key(log),
9046 }),
9047 ReactiveInterest::Logs(_)
9048 | ReactiveInterest::Blocks(_)
9049 | ReactiveInterest::PendingTransactions(_) => None,
9050 })
9051 }
9052}
9053
9054fn merge_log_subscription_filter(filters: &mut Vec<Filter>, next: &Filter) {
9055 let mut candidate = next.clone();
9056 let mut insertion_index = filters.len();
9057 let mut index = 0;
9058 while index < filters.len() {
9059 if filters[index].block_option != candidate.block_option {
9060 index += 1;
9061 continue;
9062 }
9063 if let Some(merged) = exact_filter_union(&candidate, &filters[index]) {
9064 candidate = merged;
9065 insertion_index = insertion_index.min(index);
9066 filters.remove(index);
9067 index = 0;
9068 } else {
9069 index += 1;
9070 }
9071 }
9072 filters.insert(insertion_index.min(filters.len()), candidate);
9073}
9074
9075fn exact_filter_union(left: &Filter, right: &Filter) -> Option<Filter> {
9076 if filter_subsumes(left, right) {
9077 return Some(left.clone());
9078 }
9079 if filter_subsumes(right, left) {
9080 return Some(right.clone());
9081 }
9082 let differing_dimensions = usize::from(left.address != right.address)
9083 + left
9084 .topics
9085 .iter()
9086 .zip(right.topics.iter())
9087 .filter(|(left, right)| left != right)
9088 .count();
9089 if differing_dimensions != 1 {
9090 return None;
9091 }
9092
9093 let mut merged = left.clone();
9094 if merged.address != right.address {
9095 merge_filter_set(&mut merged.address, &right.address);
9096 } else {
9097 for (merged_topic, right_topic) in merged.topics.iter_mut().zip(right.topics.iter()) {
9098 if merged_topic != right_topic {
9099 merge_filter_set(merged_topic, right_topic);
9100 break;
9101 }
9102 }
9103 }
9104 Some(merged)
9105}
9106
9107fn filter_subsumes(left: &Filter, right: &Filter) -> bool {
9108 filter_set_subsumes(&left.address, &right.address)
9109 && left
9110 .topics
9111 .iter()
9112 .zip(right.topics.iter())
9113 .all(|(left, right)| filter_set_subsumes(left, right))
9114}
9115
9116fn filter_set_subsumes<T: Eq + Hash>(left: &FilterSet<T>, right: &FilterSet<T>) -> bool {
9117 left.is_empty()
9118 || (!right.is_empty()
9119 && right
9120 .iter()
9121 .all(|value| left.iter().any(|known| known == value)))
9122}
9123
9124fn merge_filter_set<T: Clone + Eq + Hash>(target: &mut FilterSet<T>, source: &FilterSet<T>) {
9125 if target.is_empty() {
9126 return;
9127 }
9128 if source.is_empty() {
9129 *target = FilterSet::default();
9130 return;
9131 }
9132 for value in source.iter() {
9133 target.insert(value.clone());
9134 }
9135}
9136
9137#[derive(Clone, Debug)]
9138struct HandlerExecution {
9139 handler_id: HandlerId,
9140 quality: StateEffectQuality,
9141 tags: Vec<ReportTag>,
9142 state_updates: Vec<StateUpdate>,
9143 invalidations: Vec<InvalidationRequest>,
9144 resyncs: Vec<ResyncRequest>,
9145 speculative: Vec<SpeculativeRequest>,
9146 hook_signals: Vec<HookSignal>,
9147}
9148
9149impl HandlerExecution {
9150 fn from_outcome(
9151 handler_id: HandlerId,
9152 input_ref: InputRef,
9153 outcome: HandlerOutcome,
9154 preconfirmed: bool,
9155 ) -> Self {
9156 let mut state_updates = Vec::new();
9157 let mut invalidations = Vec::new();
9158 let mut resyncs = Vec::new();
9159 let mut speculative = Vec::new();
9160 let mut hook_signals = Vec::new();
9161
9162 for effect in outcome.effects {
9163 match effect {
9164 ReactiveEffect::StateUpdate(update) => state_updates.push(update),
9165 ReactiveEffect::Invalidate(invalidation) => {
9166 state_updates.push(StateUpdate::purge(
9167 invalidation.address,
9168 invalidation.scope.clone(),
9169 ));
9170 invalidations.push(invalidation);
9171 }
9172 ReactiveEffect::Resync(mut request) => {
9173 if preconfirmed {
9174 request.block = ResyncBlock::Pending;
9175 }
9176 resyncs.push(request);
9177 }
9178 ReactiveEffect::Hook(signal) => hook_signals.push(signal),
9179 ReactiveEffect::Speculative(mut request) => {
9180 request.input_ref = input_ref;
9181 speculative.push(request);
9182 }
9183 }
9184 }
9185
9186 Self {
9187 handler_id,
9188 quality: outcome.quality,
9189 tags: outcome.tags,
9190 state_updates,
9191 invalidations,
9192 resyncs,
9193 speculative,
9194 hook_signals,
9195 }
9196 }
9197}
9198
9199fn dedupe_records<N: Network>(
9200 records: Vec<ReactiveInputRecord<N>>,
9201) -> Result<Vec<ReactiveInputRecord<N>>, ReactiveError> {
9202 let mut positions = HashMap::<ReactiveInputIdentity, usize>::new();
9203 let mut deduped = Vec::with_capacity(records.len());
9204 for record in records {
9205 let identity = record.validated_identity()?;
9206 if !record.is_payload_deduplicable() {
9207 deduped.push(record);
9208 continue;
9209 }
9210 if let Some(index) = positions.get(&identity).copied() {
9211 let merged = deduped[index].merge_compatible_duplicate(&record)?;
9212 debug_assert!(merged, "same indexed identity is deduplicable");
9213 } else {
9214 positions.insert(identity, deduped.len());
9215 deduped.push(record);
9216 }
9217 }
9218 Ok(deduped)
9219}
9220
9221fn dedupe_scoped_records<N: Network>(
9222 records: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
9223) -> Result<Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>, ReactiveError> {
9224 let mut positions: HashMap<ReactiveInputIdentity, usize> = HashMap::new();
9225 let mut deduped: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)> =
9226 Vec::with_capacity(records.len());
9227 for (record, audience, delivery_scope) in records {
9228 let identity = record.validated_identity()?;
9229 if !record.is_payload_deduplicable() {
9230 deduped.push((record, audience, delivery_scope));
9231 continue;
9232 }
9233 if let Some(index) = positions.get(&identity).copied() {
9234 let merged = deduped[index].0.merge_compatible_duplicate(&record)?;
9235 debug_assert!(merged, "same indexed identity is deduplicable");
9236 merge_delivery_audience(&mut deduped[index].1, audience);
9237 merge_delivery_scope(&mut deduped[index].2, delivery_scope);
9238 } else {
9239 positions.insert(identity, deduped.len());
9240 deduped.push((record, audience, delivery_scope));
9241 }
9242 }
9243 Ok(deduped)
9244}
9245
9246fn merge_delivery_scope(into: &mut DeliveryScope, incoming: DeliveryScope) {
9247 *into = match (*into, incoming) {
9248 (DeliveryScope::Canonical, _) | (_, DeliveryScope::Canonical) => DeliveryScope::Canonical,
9249 (DeliveryScope::CanonicalProgress, _) | (_, DeliveryScope::CanonicalProgress) => {
9250 DeliveryScope::CanonicalProgress
9251 }
9252 (DeliveryScope::Preconfirmed, DeliveryScope::Preconfirmed)
9253 | (DeliveryScope::Preconfirmed, DeliveryScope::OwnerCatchup)
9254 | (DeliveryScope::OwnerCatchup, DeliveryScope::Preconfirmed) => DeliveryScope::Preconfirmed,
9255 (DeliveryScope::OwnerCatchup, DeliveryScope::OwnerCatchup) => DeliveryScope::OwnerCatchup,
9256 };
9257}
9258
9259fn merge_delivery_audience(into: &mut DeliveryAudience, incoming: DeliveryAudience) {
9260 match (&mut *into, incoming) {
9261 (DeliveryAudience::All, _) => {}
9262 (current, DeliveryAudience::All) => *current = DeliveryAudience::All,
9263 (DeliveryAudience::Owners(current), DeliveryAudience::Owners(incoming)) => {
9264 for owner in incoming {
9265 if !current.contains(&owner) {
9266 current.push(owner);
9267 }
9268 }
9269 }
9270 (DeliveryAudience::AllExcept(current), DeliveryAudience::AllExcept(incoming)) => {
9271 current.retain(|owner| incoming.contains(owner));
9272 }
9273 (DeliveryAudience::AllExcept(excluded), DeliveryAudience::Owners(included)) => {
9274 excluded.retain(|owner| !included.contains(owner));
9275 }
9276 (current @ DeliveryAudience::Owners(_), DeliveryAudience::AllExcept(mut excluded)) => {
9277 let DeliveryAudience::Owners(included) = current else {
9278 unreachable!("match arm restricts the audience variant")
9279 };
9280 excluded.retain(|owner| !included.contains(owner));
9281 *current = DeliveryAudience::AllExcept(excluded);
9282 }
9283 }
9284}
9285
9286fn sort_records<N: Network>(records: Vec<ReactiveInputRecord<N>>) -> Vec<ReactiveInputRecord<N>> {
9287 let mut indexed: Vec<(usize, ReactiveInputRecord<N>)> =
9288 records.into_iter().enumerate().collect();
9289 indexed.sort_by_key(|(index, record)| record_sort_key(*index, record));
9290 indexed.into_iter().map(|(_, record)| record).collect()
9291}
9292
9293fn sort_scoped_records<N: Network>(
9294 records: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
9295) -> Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)> {
9296 let mut indexed: Vec<_> = records.into_iter().enumerate().collect();
9297 indexed.sort_by_key(|(index, (record, _, _))| record_sort_key(*index, record));
9298 indexed
9299 .into_iter()
9300 .map(|(_, scoped_record)| scoped_record)
9301 .collect()
9302}
9303
9304fn record_sort_key<N: Network>(index: usize, record: &ReactiveInputRecord<N>) -> RecordSortKey {
9305 if let Some((block, _)) = reorg_signal_block(record) {
9306 return RecordSortKey {
9307 class: 0,
9308 block_number: block.number,
9309 record_class: 0,
9310 transaction_index: record.context.transaction_index.unwrap_or(u64::MAX),
9311 log_index: record.context.log_index.unwrap_or(u64::MAX),
9312 original_index: index,
9313 };
9314 }
9315 if is_canonical_status(&record.context.chain_status)
9316 && let Some(block) = record.context.block.as_ref()
9317 {
9318 let (record_class, transaction_index, log_index) = match &record.input {
9319 ReactiveInput::BlockHeader(_) | ReactiveInput::FullBlock(_) => (0, 0, 0),
9320 ReactiveInput::Log(log) if !log.removed => (
9321 1,
9322 log.transaction_index
9323 .or(record.context.transaction_index)
9324 .unwrap_or(u64::MAX),
9325 log.log_index
9326 .or(record.context.log_index)
9327 .unwrap_or(u64::MAX),
9328 ),
9329 ReactiveInput::Log(_)
9330 | ReactiveInput::PendingTxHash(_)
9331 | ReactiveInput::PendingTx(_) => (2, u64::MAX, u64::MAX),
9332 };
9333 return RecordSortKey {
9334 class: 1,
9335 block_number: block.number,
9336 record_class,
9337 transaction_index,
9338 log_index,
9339 original_index: index,
9340 };
9341 }
9342
9343 RecordSortKey {
9344 class: 2,
9345 block_number: 0,
9346 record_class: 0,
9347 transaction_index: 0,
9348 log_index: 0,
9349 original_index: index,
9350 }
9351}
9352
9353#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
9354struct RecordSortKey {
9355 class: u8,
9356 block_number: u64,
9357 record_class: u8,
9358 transaction_index: u64,
9359 log_index: u64,
9360 original_index: usize,
9361}
9362
9363fn interest_matches<N: Network>(interest: &ReactiveInterest<N>, input: &ReactiveInput<N>) -> bool {
9364 match (interest, input) {
9365 (ReactiveInterest::Logs(interest), ReactiveInput::Log(log)) => interest.matches(log),
9366 (
9367 ReactiveInterest::Blocks(BlockInterest {
9368 mode: BlockInterestMode::Header,
9369 }),
9370 ReactiveInput::BlockHeader(_),
9371 ) => true,
9372 (
9373 ReactiveInterest::Blocks(BlockInterest {
9374 mode: BlockInterestMode::FullBlock,
9375 }),
9376 ReactiveInput::FullBlock(_),
9377 ) => true,
9378 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTxHash(_)) => {
9379 interest.matches_hash_only()
9380 }
9381 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTx(tx)) => {
9382 interest.matches_tx(tx)
9383 }
9384 _ => false,
9385 }
9386}
9387
9388fn validate_effects(
9389 input_ref: InputRef,
9390 ctx: &ReactiveContext,
9391 handler_id: &HandlerId,
9392 effects: &[ReactiveEffect],
9393) -> Result<(), ReactiveError> {
9394 let pending = matches!(ctx.chain_status, ChainStatus::Pending)
9395 || matches!(input_ref, InputRef::PendingTx { .. });
9396 if !pending {
9397 return Ok(());
9398 }
9399
9400 for effect in effects {
9401 let effect_kind = match effect {
9402 ReactiveEffect::StateUpdate(_) => Some("state_update"),
9403 ReactiveEffect::Invalidate(_) => Some("invalidate"),
9404 ReactiveEffect::Resync(_) => Some("resync"),
9405 ReactiveEffect::Hook(_) | ReactiveEffect::Speculative(_) => None,
9406 };
9407 if let Some(effect_kind) = effect_kind {
9408 return Err(ReactiveError::InvalidPendingEffect {
9409 input_ref: Box::new(input_ref),
9410 handler_id: handler_id.clone(),
9411 effect_kind,
9412 });
9413 }
9414 }
9415 Ok(())
9416}
9417
9418fn detect_conflicts(
9419 input_ref: InputRef,
9420 executions: &[HandlerExecution],
9421) -> Result<(), ReactiveError> {
9422 let mut writes: HashMap<EffectTarget, (AbsoluteValue, HandlerId)> = HashMap::new();
9423 for execution in executions {
9424 for update in &execution.state_updates {
9425 for (target, value) in absolute_writes(update) {
9426 if let Some((previous_value, previous_handler)) = writes.get(&target) {
9427 if previous_value != &value {
9428 return Err(ReactiveError::ConflictingEffects {
9429 input_ref: Box::new(input_ref),
9430 target: Box::new(target),
9431 first: previous_handler.clone(),
9432 second: execution.handler_id.clone(),
9433 });
9434 }
9435 } else {
9436 writes.insert(target, (value, execution.handler_id.clone()));
9437 }
9438 }
9439 }
9440 }
9441 Ok(())
9442}
9443
9444fn absolute_writes(update: &StateUpdate) -> Vec<(EffectTarget, AbsoluteValue)> {
9445 match update {
9446 StateUpdate::Slot {
9447 address,
9448 slot,
9449 value,
9450 } => vec![(
9451 EffectTarget::StorageSlot {
9452 address: *address,
9453 slot: *slot,
9454 },
9455 AbsoluteValue::U256(*value),
9456 )],
9457 StateUpdate::SlotMasked {
9458 address,
9459 slot,
9460 mask,
9461 value,
9462 } => vec![(
9463 EffectTarget::MaskedStorageSlot {
9464 address: *address,
9465 slot: *slot,
9466 mask: *mask,
9467 },
9468 AbsoluteValue::U256(*value),
9469 )],
9470 StateUpdate::Account { address, patch } | StateUpdate::AccountUpsert { address, patch } => {
9471 account_patch_writes(*address, patch)
9472 }
9473 StateUpdate::SlotDelta { .. }
9474 | StateUpdate::BalanceDelta { .. }
9475 | StateUpdate::Purge { .. } => Vec::new(),
9476 }
9477}
9478
9479fn account_patch_writes(
9480 address: Address,
9481 patch: &AccountPatch,
9482) -> Vec<(EffectTarget, AbsoluteValue)> {
9483 let mut writes = Vec::new();
9484 if let Some(balance) = patch.balance {
9485 writes.push((
9486 EffectTarget::AccountBalance { address },
9487 AbsoluteValue::U256(balance),
9488 ));
9489 }
9490 if let Some(nonce) = patch.nonce {
9491 writes.push((
9492 EffectTarget::AccountNonce { address },
9493 AbsoluteValue::U64(nonce),
9494 ));
9495 }
9496 if let Some(code) = &patch.code {
9497 writes.push((
9498 EffectTarget::AccountCode { address },
9499 AbsoluteValue::Bytes(code.clone()),
9500 ));
9501 }
9502 writes
9503}
9504
9505fn input_ref<N: Network>(input: &ReactiveInput<N>, ctx: &ReactiveContext) -> InputRef {
9506 match input {
9507 ReactiveInput::Log(log) => InputRef::Log {
9508 chain_id: ctx.chain_id,
9509 block_hash: log
9510 .block_hash
9511 .or(ctx.block.as_ref().map(|block| block.hash))
9512 .unwrap_or_default(),
9513 transaction_hash: log.transaction_hash.unwrap_or_default(),
9514 log_index: log.log_index.or(ctx.log_index).unwrap_or_default(),
9515 },
9516 ReactiveInput::PendingTxHash(hash) => InputRef::PendingTx {
9517 chain_id: ctx.chain_id,
9518 hash: *hash,
9519 },
9520 ReactiveInput::PendingTx(tx) => InputRef::PendingTx {
9521 chain_id: ctx.chain_id,
9522 hash: tx.tx_hash(),
9523 },
9524 ReactiveInput::BlockHeader(header) => InputRef::Block {
9525 chain_id: ctx.chain_id,
9526 hash: header.hash(),
9527 number: header.number(),
9528 },
9529 ReactiveInput::FullBlock(block) => {
9530 let header = block.header();
9531 InputRef::Block {
9532 chain_id: ctx.chain_id,
9533 hash: header.hash(),
9534 number: header.number(),
9535 }
9536 }
9537 }
9538}
9539
9540fn is_canonical_status(status: &ChainStatus) -> bool {
9541 matches!(
9542 status,
9543 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. }
9544 )
9545}
9546
9547pub struct EventDecoderHandler {
9549 id: HandlerId,
9550 decoder: Arc<dyn EventDecoder>,
9551 interest: LogInterest,
9552}
9553
9554impl EventDecoderHandler {
9555 pub fn new(id: HandlerId, decoder: Arc<dyn EventDecoder>, interest: LogInterest) -> Self {
9557 Self {
9558 id,
9559 decoder,
9560 interest,
9561 }
9562 }
9563}
9564
9565impl<N: Network> ReactiveHandler<N> for EventDecoderHandler {
9566 fn id(&self) -> HandlerId {
9567 self.id.clone()
9568 }
9569
9570 fn interests(&self) -> Vec<ReactiveInterest<N>> {
9571 vec![ReactiveInterest::Logs(self.interest.clone())]
9572 }
9573
9574 fn handle(
9575 &self,
9576 _ctx: &ReactiveContext,
9577 input: &ReactiveInput<N>,
9578 state: &dyn StateView,
9579 ) -> Result<HandlerOutcome, HandlerError> {
9580 let ReactiveInput::Log(log) = input else {
9581 return Ok(HandlerOutcome::empty(StateEffectQuality::NoStateEffect));
9582 };
9583
9584 Ok(HandlerOutcome {
9585 effects: self
9586 .decoder
9587 .decode(&log.inner, state)
9588 .into_iter()
9589 .map(ReactiveEffect::StateUpdate)
9590 .collect(),
9591 quality: StateEffectQuality::ExactFromInput,
9592 tags: Vec::new(),
9593 })
9594 }
9595}
9596
9597#[derive(
9599 Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
9600)]
9601#[non_exhaustive]
9602pub enum SubscriberCapability {
9603 Logs,
9605 BlockHeaders,
9607 FullBlocks,
9609 PendingTransactionHashes,
9611 PendingTransactions,
9613 HistoricalBackfill,
9615 Live,
9617 DurableReplay,
9627 OwnerScopedDelivery,
9629 DynamicInterests,
9631 ExplicitReorgs,
9633 FinalityUpdates,
9635 Barriers,
9637 Preconfirmations,
9639}
9640
9641#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9647pub struct SubscriberCapabilities {
9648 supported: BTreeSet<SubscriberCapability>,
9649}
9650
9651impl SubscriberCapabilities {
9652 pub fn new(capabilities: impl IntoIterator<Item = SubscriberCapability>) -> Self {
9654 Self {
9655 supported: capabilities.into_iter().collect(),
9656 }
9657 }
9658
9659 pub fn supports(&self, capability: SubscriberCapability) -> bool {
9661 self.supported.contains(&capability)
9662 }
9663
9664 pub fn iter(&self) -> impl Iterator<Item = SubscriberCapability> + '_ {
9666 self.supported.iter().copied()
9667 }
9668
9669 pub fn supports_live(&self) -> bool {
9671 self.supports(SubscriberCapability::Live)
9672 }
9673
9674 pub fn supports_durable_replay(&self) -> bool {
9677 self.supports(SubscriberCapability::DurableReplay)
9678 }
9679
9680 pub fn supports_explicit_reorgs(&self) -> bool {
9682 self.supports(SubscriberCapability::ExplicitReorgs)
9683 }
9684}
9685
9686impl FromIterator<SubscriberCapability> for SubscriberCapabilities {
9687 fn from_iter<T: IntoIterator<Item = SubscriberCapability>>(iter: T) -> Self {
9688 Self::new(iter)
9689 }
9690}
9691
9692pub trait EventSubscriber<N: Network = Ethereum>: Send {
9694 fn chain_id(&self) -> Option<u64> {
9701 None
9702 }
9703
9704 fn capabilities(&self) -> SubscriberCapabilities {
9706 SubscriberCapabilities::default()
9707 }
9708
9709 fn register_interests(
9728 &mut self,
9729 interests: &[ReactiveInterest<N>],
9730 ) -> SubscriberOperation<'_, ()>;
9731
9732 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N>;
9744
9745 fn restore_position(
9768 &mut self,
9769 _position: &SubscriberResumePosition,
9770 ) -> Result<(), SubscriberError> {
9771 Ok(())
9772 }
9773
9774 fn acknowledge_delivery(
9788 &mut self,
9789 _token: SubscriberDeliveryToken,
9790 ) -> SubscriberOperation<'_, ()> {
9791 Box::pin(async { Ok(()) })
9792 }
9793}
9794
9795pub type SubscriberOperation<'a, T> =
9800 Pin<Box<dyn Future<Output = Result<T, SubscriberError>> + Send + 'a>>;
9801
9802pub type SubscriberNextBatch<'a, N> = Pin<
9804 Box<dyn Future<Output = Result<Option<ReactiveInputBatch<N>>, SubscriberError>> + Send + 'a>,
9805>;
9806
9807pub type SubscriberNextScopedBatch<'a, N> = Pin<
9809 Box<dyn Future<Output = Result<Option<SubscriberInputBatch<N>>, SubscriberError>> + Send + 'a>,
9810>;
9811
9812#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
9814pub enum SubscriberMode {
9815 #[default]
9821 Auto,
9822 PubSub,
9824 Polling,
9826}
9827
9828#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9829enum FlashblocksAdapter {
9830 NativeSubscriptions,
9831 PendingStatePolling,
9832}
9833
9834fn flashblocks_adapter(chain_id: u64) -> Option<FlashblocksAdapter> {
9835 match chain_id {
9836 8_453 | 84_532 => Some(FlashblocksAdapter::NativeSubscriptions),
9837 10 | 11_155_420 => Some(FlashblocksAdapter::PendingStatePolling),
9838 _ => None,
9839 }
9840}
9841
9842#[derive(Clone, Debug, PartialEq, Eq)]
9844pub struct SubscriberConfig {
9845 pub preconfirmations: PreconfirmationMode,
9848 pub canonical_head_poll_interval: Duration,
9851 pub flashblock_poll_interval: Duration,
9859 pub max_consecutive_flashblock_poll_failures: usize,
9865 pub max_pending_transaction_receipts_per_tick: usize,
9873 pub max_flashblock_rpc_requests_per_second: usize,
9883 pub hydrate_pending_transactions: bool,
9885 pub verify_log_block_context: bool,
9896 pub max_batch_size: usize,
9898 pub max_log_addresses_per_subscription: usize,
9902 pub max_pending_records: usize,
9907 pub max_pending_backfills: usize,
9909 pub max_backfill_log_bytes: usize,
9912 pub max_reconcile_requests_in_flight: usize,
9915 pub reconnect: SubscriberReconnectConfig,
9917}
9918
9919impl Default for SubscriberConfig {
9920 fn default() -> Self {
9921 Self {
9922 preconfirmations: PreconfirmationMode::Disabled,
9923 canonical_head_poll_interval: Duration::from_millis(500),
9924 flashblock_poll_interval: Duration::from_millis(250),
9925 max_consecutive_flashblock_poll_failures: 10,
9926 max_pending_transaction_receipts_per_tick: 32,
9927 max_flashblock_rpc_requests_per_second: 40,
9928 hydrate_pending_transactions: false,
9929 verify_log_block_context: false,
9930 max_batch_size: 1024,
9931 max_log_addresses_per_subscription: 1024,
9932 max_pending_records: 16_384,
9933 max_pending_backfills: 4_096,
9934 max_backfill_log_bytes: 64 * 1024 * 1024,
9935 max_reconcile_requests_in_flight: 8,
9936 reconnect: SubscriberReconnectConfig::default(),
9937 }
9938 }
9939}
9940
9941#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9943pub enum FlashblocksDelivery {
9944 NativeSubscriptions,
9946 PendingStatePolling,
9948}
9949
9950#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9952pub struct FlashblocksRpcMetrics {
9953 capability_requests: u64,
9954 provider_pair_chain_requests: u64,
9955 canonical_head_requests: u64,
9956 pending_block_requests: u64,
9957 pending_log_requests: u64,
9958 pending_receipt_requests: u64,
9959 pending_receipts_completed: u64,
9960 pending_receipts_unavailable: u64,
9961 failed_requests: u64,
9962 raced_samples: u64,
9963}
9964
9965impl FlashblocksRpcMetrics {
9966 pub const fn capability_requests(self) -> u64 {
9968 self.capability_requests
9969 }
9970
9971 pub const fn provider_pair_chain_requests(self) -> u64 {
9974 self.provider_pair_chain_requests
9975 }
9976
9977 pub const fn canonical_head_requests(self) -> u64 {
9979 self.canonical_head_requests
9980 }
9981
9982 pub const fn pending_block_requests(self) -> u64 {
9984 self.pending_block_requests
9985 }
9986
9987 pub const fn pending_log_requests(self) -> u64 {
9989 self.pending_log_requests
9990 }
9991
9992 pub const fn pending_receipt_requests(self) -> u64 {
9995 self.pending_receipt_requests
9996 }
9997
9998 pub const fn pending_receipts_completed(self) -> u64 {
10000 self.pending_receipts_completed
10001 }
10002
10003 pub const fn pending_receipts_unavailable(self) -> u64 {
10006 self.pending_receipts_unavailable
10007 }
10008
10009 pub const fn failed_requests(self) -> u64 {
10011 self.failed_requests
10012 }
10013
10014 pub const fn raced_samples(self) -> u64 {
10018 self.raced_samples
10019 }
10020
10021 pub const fn total_requests(self) -> u64 {
10024 self.capability_requests
10025 .saturating_add(self.provider_pair_chain_requests)
10026 .saturating_add(self.canonical_head_requests)
10027 .saturating_add(self.pending_block_requests)
10028 .saturating_add(self.pending_log_requests)
10029 .saturating_add(self.pending_receipt_requests)
10030 }
10031}
10032
10033#[derive(Clone, Debug, PartialEq, Eq)]
10041pub struct FlashblocksPreflight {
10042 chain_id: u64,
10043 provider: ProviderRef,
10044 delivery: FlashblocksDelivery,
10045 pending_log_subscriptions: usize,
10046 pending_log_filters: usize,
10047 advertised_capabilities: Option<serde_json::Value>,
10048}
10049
10050impl FlashblocksPreflight {
10051 pub const fn chain_id(&self) -> u64 {
10053 self.chain_id
10054 }
10055
10056 pub const fn provider(&self) -> &ProviderRef {
10059 &self.provider
10060 }
10061
10062 pub const fn delivery(&self) -> FlashblocksDelivery {
10064 self.delivery
10065 }
10066
10067 pub const fn pending_log_subscriptions(&self) -> usize {
10069 self.pending_log_subscriptions
10070 }
10071
10072 pub const fn pending_log_filters(&self) -> usize {
10075 self.pending_log_filters
10076 }
10077
10078 pub const fn advertised_capabilities(&self) -> Option<&serde_json::Value> {
10081 self.advertised_capabilities.as_ref()
10082 }
10083}
10084
10085#[derive(Clone, Debug, PartialEq, Eq)]
10091pub struct SubscriberReconnectConfig {
10092 pub enabled: bool,
10094 pub initial_delay: Duration,
10096 pub retry_delay: Duration,
10099 pub max_delay: Duration,
10101 pub max_attempts: Option<usize>,
10103 pub dedupe_window: usize,
10106}
10107
10108impl Default for SubscriberReconnectConfig {
10109 fn default() -> Self {
10110 Self {
10111 enabled: true,
10112 initial_delay: Duration::ZERO,
10113 retry_delay: Duration::from_millis(250),
10114 max_delay: Duration::from_secs(30),
10115 max_attempts: Some(3),
10116 dedupe_window: 4096,
10117 }
10118 }
10119}
10120
10121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10133pub struct SubscriberBackfill {
10134 from_block: u64,
10135 to_block: Option<u64>,
10136 retained_anchor: Option<BlockRef>,
10137}
10138
10139impl SubscriberBackfill {
10140 pub fn range(from_block: u64, to_block: u64) -> Self {
10142 Self {
10143 from_block,
10144 to_block: Some(to_block),
10145 retained_anchor: None,
10146 }
10147 }
10148
10149 pub fn from_block(from_block: u64) -> Self {
10151 Self {
10152 from_block,
10153 to_block: None,
10154 retained_anchor: None,
10155 }
10156 }
10157
10158 pub fn from_canonical_block(block: BlockRef) -> Self {
10165 Self {
10166 from_block: block.number,
10167 to_block: None,
10168 retained_anchor: Some(block),
10169 }
10170 }
10171
10172 pub fn from_canonical_block_through(
10180 block: BlockRef,
10181 to_block: u64,
10182 ) -> Result<Self, SubscriberError> {
10183 if to_block < block.number {
10184 return Err(SubscriberError::InvalidConfig(
10185 "inclusive backfill upper bound precedes its retained anchor",
10186 ));
10187 }
10188 Ok(Self {
10189 from_block: block.number,
10190 to_block: Some(to_block),
10191 retained_anchor: Some(block),
10192 })
10193 }
10194
10195 pub fn after_canonical_block(block: BlockRef) -> Result<Self, SubscriberError> {
10211 Self::after_canonical_block_inner(block, None)
10212 }
10213
10214 pub fn after_canonical_block_through(
10226 block: BlockRef,
10227 to_block: u64,
10228 ) -> Result<Self, SubscriberError> {
10229 if to_block < block.number {
10230 return Err(SubscriberError::InvalidConfig(
10231 "exclusive backfill upper bound precedes its retained baseline",
10232 ));
10233 }
10234 Self::after_canonical_block_inner(block, Some(to_block))
10235 }
10236
10237 fn after_canonical_block_inner(
10238 block: BlockRef,
10239 to_block: Option<u64>,
10240 ) -> Result<Self, SubscriberError> {
10241 let from_block = block
10242 .number
10243 .checked_add(1)
10244 .ok_or(SubscriberError::InvalidConfig(
10245 "cannot construct an exclusive backfill after block u64::MAX",
10246 ))?;
10247 Ok(Self {
10248 from_block,
10249 to_block,
10250 retained_anchor: Some(block),
10251 })
10252 }
10253
10254 pub fn start_block(&self) -> u64 {
10256 self.from_block
10257 }
10258
10259 pub fn end_block(&self) -> Option<u64> {
10261 self.to_block
10262 }
10263
10264 pub fn retained_anchor(&self) -> Option<&BlockRef> {
10266 self.retained_anchor.as_ref()
10267 }
10268}
10269
10270#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
10277pub struct SubscriberOwnerEpoch {
10278 owner: HandlerId,
10279 sequence: u64,
10280}
10281
10282#[derive(Clone, Debug, PartialEq, Eq)]
10289#[non_exhaustive]
10290pub enum SubscriberInputScope {
10291 Canonical {
10293 owners: Vec<SubscriberOwnerEpoch>,
10295 },
10296 CanonicalResidual {
10300 owners: Vec<SubscriberOwnerEpoch>,
10302 excluded: Vec<HandlerId>,
10304 },
10305 OwnerOnly {
10307 owners: Vec<SubscriberOwnerEpoch>,
10309 },
10310 OwnerOnlyHandlers {
10312 owners: Vec<HandlerId>,
10314 },
10315 Preconfirmed,
10318}
10319
10320impl SubscriberInputScope {
10321 pub fn owners(&self) -> &[SubscriberOwnerEpoch] {
10323 match self {
10324 Self::Canonical { owners }
10325 | Self::CanonicalResidual { owners, .. }
10326 | Self::OwnerOnly { owners } => owners,
10327 Self::OwnerOnlyHandlers { .. } | Self::Preconfirmed => &[],
10328 }
10329 }
10330
10331 pub const fn is_canonical(&self) -> bool {
10333 matches!(
10334 self,
10335 Self::Canonical { .. } | Self::CanonicalResidual { .. }
10336 )
10337 }
10338
10339 pub const fn is_preconfirmed(&self) -> bool {
10341 matches!(self, Self::Preconfirmed)
10342 }
10343}
10344
10345#[derive(Clone, Debug)]
10347pub struct SubscriberInputRecord<N: Network = Ethereum> {
10348 record: ReactiveInputRecord<N>,
10349 scope: SubscriberInputScope,
10350}
10351
10352impl<N: Network> SubscriberInputRecord<N> {
10353 pub const fn record(&self) -> &ReactiveInputRecord<N> {
10355 &self.record
10356 }
10357
10358 pub const fn scope(&self) -> &SubscriberInputScope {
10360 &self.scope
10361 }
10362
10363 pub fn into_record(self) -> ReactiveInputRecord<N> {
10365 self.record
10366 }
10367}
10368
10369impl<N: Network> std::ops::Deref for SubscriberInputRecord<N> {
10370 type Target = ReactiveInputRecord<N>;
10371
10372 fn deref(&self) -> &Self::Target {
10373 &self.record
10374 }
10375}
10376
10377#[derive(Clone, Debug)]
10379pub struct SubscriberInputBatch<N: Network = Ethereum> {
10380 records: Vec<SubscriberInputRecord<N>>,
10381 chain_id: Option<u64>,
10382 chain_controls: Vec<ChainControl>,
10383 preconfirmation_invalidated: bool,
10384}
10385
10386#[derive(Debug)]
10389#[non_exhaustive]
10390pub enum SubscriberDriverPoll<C, N: Network = Ethereum> {
10391 Control(C),
10393 Batch(Option<SubscriberInputBatch<N>>),
10395}
10396
10397impl<N: Network> SubscriberInputBatch<N> {
10398 pub fn records(&self) -> &[SubscriberInputRecord<N>] {
10400 &self.records
10401 }
10402
10403 pub fn into_records(self) -> Vec<SubscriberInputRecord<N>> {
10405 self.records
10406 }
10407
10408 pub fn chain_controls(&self) -> &[ChainControl] {
10410 &self.chain_controls
10411 }
10412
10413 pub const fn preconfirmation_invalidated(&self) -> bool {
10416 self.preconfirmation_invalidated
10417 }
10418
10419 pub fn into_reactive_batch(self) -> ReactiveInputBatch<N> {
10425 let chain_id = self.chain_id;
10426 let chain_controls = self.chain_controls;
10427 let mut batch = ReactiveInputBatch::from_scoped_records_with_delivery_scope(
10428 self.records.into_iter().map(|scoped| {
10429 let source = scoped.record.context.source;
10430 let (audience, delivery_scope) = match scoped.scope {
10431 SubscriberInputScope::Canonical { .. } => (
10432 DeliveryAudience::All,
10433 if source == InputSource::Backfill {
10434 DeliveryScope::CanonicalProgress
10435 } else {
10436 DeliveryScope::Canonical
10437 },
10438 ),
10439 SubscriberInputScope::CanonicalResidual { excluded, .. } => (
10440 DeliveryAudience::AllExcept(excluded),
10441 if source == InputSource::Backfill {
10442 DeliveryScope::CanonicalProgress
10443 } else {
10444 DeliveryScope::Canonical
10445 },
10446 ),
10447 SubscriberInputScope::OwnerOnly { owners } => {
10448 let mut handler_ids = Vec::with_capacity(owners.len());
10449 for epoch in owners {
10450 if !handler_ids.contains(epoch.owner()) {
10451 handler_ids.push(epoch.owner().clone());
10452 }
10453 }
10454 (
10455 DeliveryAudience::Owners(handler_ids),
10456 DeliveryScope::OwnerCatchup,
10457 )
10458 }
10459 SubscriberInputScope::OwnerOnlyHandlers { owners } => (
10460 DeliveryAudience::Owners(owners),
10461 DeliveryScope::OwnerCatchup,
10462 ),
10463 SubscriberInputScope::Preconfirmed => {
10464 (DeliveryAudience::All, DeliveryScope::Preconfirmed)
10465 }
10466 };
10467 (scoped.record, audience, delivery_scope)
10468 }),
10469 )
10470 .with_chain_controls(chain_controls);
10471 if let Some(chain_id) = chain_id {
10472 batch = batch.with_chain_id(chain_id);
10473 }
10474 batch
10475 }
10476}
10477
10478impl SubscriberOwnerEpoch {
10479 pub const fn owner(&self) -> &HandlerId {
10481 &self.owner
10482 }
10483
10484 pub const fn sequence(&self) -> u64 {
10486 self.sequence
10487 }
10488}
10489
10490#[derive(Clone, Debug, PartialEq, Eq)]
10492#[non_exhaustive]
10493pub enum SubscriberOwnerStart {
10494 Live,
10496 PostBlock(BlockRef),
10503}
10504
10505#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10507#[non_exhaustive]
10508pub enum SubscriberOwnerState {
10509 Staged,
10512 Active,
10514 Removing,
10516}
10517
10518#[derive(Clone, Debug, PartialEq, Eq)]
10524pub struct SubscriberOwnerProgress {
10525 owner: SubscriberOwnerEpoch,
10526 through: BlockRef,
10527}
10528
10529impl SubscriberOwnerProgress {
10530 pub const fn owner(&self) -> &SubscriberOwnerEpoch {
10532 &self.owner
10533 }
10534
10535 pub const fn through(&self) -> &BlockRef {
10537 &self.through
10538 }
10539}
10540
10541#[derive(Debug, thiserror::Error)]
10543#[non_exhaustive]
10544pub enum SubscriberOwnerError {
10545 #[error(transparent)]
10547 Subscriber(#[from] SubscriberError),
10548 #[error("subscriber interest owner `{0}` is already registered")]
10550 AlreadyRegistered(HandlerId),
10551 #[error("post-block subscriber baseline {0} has no following block")]
10553 PostBlockOverflow(u64),
10554 #[error("subscriber owner epoch sequence exhausted")]
10556 EpochExhausted,
10557 #[error("subscriber owner epoch is not staged")]
10559 NotStaged,
10560 #[error("subscriber owner was staged live-only and has no catch-up baseline")]
10562 MissingBaseline,
10563 #[error("post-block subscriber owners support log interests only")]
10565 UnsupportedPostBlockInterest,
10566 #[error("subscriber reconcile target block {0} was not found")]
10568 BlockUnavailable(u64),
10569 #[error(
10571 "subscriber reconcile target mismatch: expected block {expected_number} {expected_hash}, got block {actual_number} {actual_hash}"
10572 )]
10573 BlockMismatch {
10574 expected_number: u64,
10576 expected_hash: B256,
10578 actual_number: u64,
10580 actual_hash: B256,
10582 },
10583 #[error("subscriber reconcile target block {target} precedes current owner position {current}")]
10585 ProgressRegression {
10586 current: u64,
10588 target: u64,
10590 },
10591 #[error(
10594 "subscriber reconcile conflicts with retained block {number} {current_hash}: target chain references {target_hash}"
10595 )]
10596 ProgressConflict {
10597 number: u64,
10599 current_hash: B256,
10601 target_hash: B256,
10603 },
10604 #[error("subscriber reconcile returned an invalid catch-up log: {0}")]
10606 InvalidBackfillLog(&'static str),
10607}
10608
10609pub trait InterestOwnerSubscriber<N: Network = Ethereum>: EventSubscriber<N> {
10624 fn upsert_interest_owners(
10637 &mut self,
10638 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
10639 ) -> SubscriberOperation<'_, ()> {
10640 Box::pin(async {
10641 Err(SubscriberError::Unsupported(
10642 "subscriber does not implement atomic bulk owner upsert",
10643 ))
10644 })
10645 }
10646
10647 fn replace_interest_owners(
10660 &mut self,
10661 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
10662 ) -> SubscriberOperation<'_, ()> {
10663 Box::pin(async {
10664 Err(SubscriberError::Unsupported(
10665 "subscriber does not implement atomic exact owner replacement",
10666 ))
10667 })
10668 }
10669
10670 fn replace_interest_owners_with_global_backfill(
10694 &mut self,
10695 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
10696 _backfill: SubscriberBackfill,
10697 ) -> SubscriberOperation<'_, ()> {
10698 Box::pin(async {
10699 Err(SubscriberError::Unsupported(
10700 "subscriber does not implement atomic owner replacement with global backfill",
10701 ))
10702 })
10703 }
10704
10705 fn add_interest_owner(
10717 &mut self,
10718 owner: HandlerId,
10719 interests: &[ReactiveInterest<N>],
10720 ) -> SubscriberOperation<'_, ()>;
10721
10722 fn add_interest_owner_with_backfill(
10730 &mut self,
10731 owner: HandlerId,
10732 interests: &[ReactiveInterest<N>],
10733 backfill: SubscriberBackfill,
10734 ) -> SubscriberOperation<'_, ()>;
10735
10736 fn add_interest_owner_with_canonical_catchup(
10754 &mut self,
10755 _owner: HandlerId,
10756 _interests: &[ReactiveInterest<N>],
10757 _retained: BlockRef,
10758 ) -> SubscriberOperation<'_, ()> {
10759 Box::pin(async {
10760 Err(SubscriberError::Unsupported(
10761 "subscriber does not implement coordinated canonical owner catch-up",
10762 ))
10763 })
10764 }
10765
10766 fn remove_interest_owner(
10777 &mut self,
10778 owner: &HandlerId,
10779 ) -> SubscriberOperation<'_, Option<Vec<ReactiveInterest<N>>>>;
10780
10781 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]>;
10783}
10784
10785pub struct ReactiveEngine<S, N: Network = Ethereum> {
10826 runtime: ReactiveRuntime<N>,
10827 subscriber: S,
10828 pending_acknowledgement: Option<PendingAcknowledgement<N>>,
10829 pending_checkpoint: Option<PendingCheckpoint<N>>,
10830 last_checkpoint_block: Option<DurableCheckpointBlock>,
10831 last_checkpoint_delivery_token: Option<SubscriberDeliveryToken>,
10832 last_checkpoint_delivery_witness: Option<B256>,
10833 last_subscriber_checkpoint: Option<SubscriberCheckpoint>,
10834 checkpoint_identity: Option<DurableCheckpointIdentity>,
10835}
10836
10837struct PendingAcknowledgement<N: Network> {
10838 token: SubscriberDeliveryToken,
10839 report: ReactiveBatchReport<N>,
10840}
10841
10842struct PendingCheckpoint<N: Network> {
10843 metadata: DurableCheckpointMetadata,
10844 delivery_token: Option<SubscriberDeliveryToken>,
10845 report: ReactiveBatchReport<N>,
10846 saved_to: Option<PathBuf>,
10847 staged_generation: u64,
10848}
10849
10850struct CheckpointStage<N: Network> {
10851 incoming_block: Option<DurableCheckpointBlock>,
10852 delivery_token: Option<SubscriberDeliveryToken>,
10853 delivery_witness: Option<B256>,
10854 subscriber_checkpoint: Option<SubscriberCheckpoint>,
10855 staged_generation: u64,
10856 report: ReactiveBatchReport<N>,
10857}
10858
10859struct DurableResumePlan {
10860 runtime: DurableRuntimeRestorePlan,
10861 position: SubscriberResumePosition,
10862 delivery_witness: Option<B256>,
10863}
10864
10865enum HandlerRegistrationCatchup {
10866 LiveOnly,
10867 OwnerBackfill(SubscriberBackfill),
10868 CoordinatedCanonical(BlockRef),
10869}
10870
10871const DELIVERY_WITNESS_VERSION: u32 = 1;
10872const DELIVERY_WITNESS_DOMAIN: &[u8] = b"evm-fork-cache/reactive-delivery-witness";
10873
10874#[derive(serde::Serialize)]
10875struct DeliveryWitnessEnvelope<'a> {
10876 version: u32,
10877 chain_id: Option<u64>,
10878 records: Vec<DeliveryRecordWitness<'a>>,
10879 chain_controls: &'a [ChainControl],
10880 subscriber_checkpoint: Option<&'a [u8]>,
10881 payload_commitment: Option<B256>,
10882}
10883
10884#[derive(serde::Serialize)]
10885struct DeliveryRecordWitness<'a> {
10886 identity: ReactiveInputIdentity,
10887 context: &'a ReactiveContext,
10888 audience: &'a DeliveryAudience,
10889 scope: DeliveryScope,
10890 payload: DeliveryPayloadWitness<'a>,
10891}
10892
10893#[derive(serde::Serialize)]
10894enum DeliveryPayloadWitness<'a> {
10895 Log {
10898 address: Address,
10899 topics: &'a [B256],
10900 data: &'a Bytes,
10901 block_hash: Option<B256>,
10902 block_number: Option<u64>,
10903 block_timestamp: Option<u64>,
10904 transaction_hash: Option<B256>,
10905 transaction_index: Option<u64>,
10906 log_index: Option<u64>,
10907 removed: bool,
10908 },
10909 IdentityCommitted,
10916}
10917
10918fn durable_delivery_witness<N: Network>(
10919 batch: &ReactiveInputBatch<N>,
10920) -> Result<B256, ReactiveEngineError> {
10921 let requires_payload_commitment = batch.records.iter().any(|record| {
10922 matches!(
10923 &record.input,
10924 ReactiveInput::BlockHeader(_)
10925 | ReactiveInput::FullBlock(_)
10926 | ReactiveInput::PendingTx(_)
10927 )
10928 });
10929 if requires_payload_commitment && batch.payload_commitment.is_none() {
10930 return Err(ReactiveEngineError::MissingPayloadCommitment);
10931 }
10932 let records = batch
10933 .records
10934 .iter()
10935 .enumerate()
10936 .map(|(index, record)| {
10937 let payload = match &record.input {
10938 ReactiveInput::Log(log) => DeliveryPayloadWitness::Log {
10939 address: log.address(),
10940 topics: log.topics(),
10941 data: &log.inner.data.data,
10942 block_hash: log.block_hash,
10943 block_number: log.block_number,
10944 block_timestamp: log.block_timestamp,
10945 transaction_hash: log.transaction_hash,
10946 transaction_index: log.transaction_index,
10947 log_index: log.log_index,
10948 removed: log.removed,
10949 },
10950 ReactiveInput::BlockHeader(_)
10951 | ReactiveInput::FullBlock(_)
10952 | ReactiveInput::PendingTxHash(_)
10953 | ReactiveInput::PendingTx(_) => DeliveryPayloadWitness::IdentityCommitted,
10954 };
10955 Ok(DeliveryRecordWitness {
10956 identity: record.validated_identity()?,
10957 context: &record.context,
10958 audience: batch
10959 .record_audience(index)
10960 .expect("enumerated record always has an audience"),
10961 scope: batch
10962 .record_delivery_scope(index)
10963 .expect("enumerated record always has a delivery scope"),
10964 payload,
10965 })
10966 })
10967 .collect::<Result<Vec<_>, ReactiveError>>()?;
10968 let envelope = DeliveryWitnessEnvelope {
10969 version: DELIVERY_WITNESS_VERSION,
10970 chain_id: batch.chain_id,
10971 records,
10972 chain_controls: &batch.chain_controls,
10973 subscriber_checkpoint: batch
10974 .subscriber_checkpoint
10975 .as_ref()
10976 .map(SubscriberCheckpoint::as_bytes),
10977 payload_commitment: batch
10978 .payload_commitment
10979 .as_ref()
10980 .map(SubscriberPayloadCommitment::digest),
10981 };
10982 let encoded = bincode::DefaultOptions::new()
10983 .with_fixint_encoding()
10984 .serialize(&envelope)
10985 .map_err(|error| ReactiveEngineError::DeliveryWitness(error.to_string()))?;
10986 let mut witness = Keccak256::new();
10987 witness.update(DELIVERY_WITNESS_DOMAIN);
10988 witness.update(encoded);
10989 Ok(witness.finalize())
10990}
10991
10992impl<S, N> ReactiveEngine<S, N>
10993where
10994 N: Network,
10995 S: EventSubscriber<N>,
10996{
10997 pub fn new(runtime: ReactiveRuntime<N>, subscriber: S) -> Self {
10999 Self {
11000 runtime,
11001 subscriber,
11002 pending_acknowledgement: None,
11003 pending_checkpoint: None,
11004 last_checkpoint_block: None,
11005 last_checkpoint_delivery_token: None,
11006 last_checkpoint_delivery_witness: None,
11007 last_subscriber_checkpoint: None,
11008 checkpoint_identity: None,
11009 }
11010 }
11011
11012 pub fn into_parts(self) -> Result<(ReactiveRuntime<N>, S), Box<Self>> {
11026 if self.pending_acknowledgement.is_some() || self.pending_checkpoint.is_some() {
11027 return Err(Box::new(self));
11028 }
11029 Ok((self.runtime, self.subscriber))
11030 }
11031
11032 fn durable_resume_plan(
11033 &self,
11034 metadata: &DurableCheckpointMetadata,
11035 ) -> Result<DurableResumePlan, ReactiveCheckpointRestoreError> {
11036 if !self.subscriber.capabilities().supports_durable_replay() {
11037 return Err(ReactiveCheckpointRestoreError::SubscriberNotDurable);
11038 }
11039 self.ensure_subscriber_restore_chain(metadata.identity.chain_id)?;
11040 if !self.runtime.is_pristine_for_checkpoint_restore()
11041 || self.pending_acknowledgement.is_some()
11042 || self.pending_checkpoint.is_some()
11043 || self.last_checkpoint_block.is_some()
11044 || self.last_checkpoint_delivery_token.is_some()
11045 || self.last_checkpoint_delivery_witness.is_some()
11046 || self.last_subscriber_checkpoint.is_some()
11047 || self.checkpoint_identity.is_some()
11048 {
11049 return Err(ReactiveCheckpointRestoreError::ActiveRuntime);
11050 }
11051
11052 let block = BlockRef {
11053 number: metadata.block.number,
11054 hash: metadata.block.hash,
11055 parent_hash: metadata.block.parent_hash,
11056 timestamp: metadata.block.timestamp,
11057 };
11058 let runtime = match metadata.runtime_checkpoint.as_deref() {
11059 Some(bytes) => self
11060 .runtime
11061 .plan_durable_checkpoint_restore(bytes, &block)?,
11062 None => DurableRuntimeRestorePlan {
11063 checkpoint: None,
11064 fallback_history: (self.runtime.config.journal_depth > 0)
11065 .then_some(block)
11066 .into_iter()
11067 .collect(),
11068 },
11069 };
11070 let delivery_token = metadata
11071 .delivery_token
11072 .clone()
11073 .map(SubscriberDeliveryToken::new);
11074 let subscriber_checkpoint = metadata
11075 .subscriber_checkpoint
11076 .clone()
11077 .map(SubscriberCheckpoint::new);
11078 let position = SubscriberResumePosition::new(
11079 metadata.identity.chain_id,
11080 block,
11081 runtime.canonical_history(),
11082 delivery_token,
11083 subscriber_checkpoint,
11084 );
11085 Ok(DurableResumePlan {
11086 runtime,
11087 position,
11088 delivery_witness: metadata.delivery_witness,
11089 })
11090 }
11091
11092 pub fn preview_durable_resume_position(
11121 &self,
11122 metadata: &DurableCheckpointMetadata,
11123 ) -> Result<SubscriberResumePosition, ReactiveCheckpointRestoreError> {
11124 Ok(self.durable_resume_plan(metadata)?.position)
11125 }
11126
11127 pub fn resume_from_durable_checkpoint(
11149 &mut self,
11150 metadata: &DurableCheckpointMetadata,
11151 ) -> Result<(), ReactiveCheckpointRestoreError> {
11152 let plan = self.durable_resume_plan(metadata)?;
11153 let prior_runtime = self.runtime.checkpoint_state();
11154
11155 let DurableResumePlan {
11156 runtime,
11157 position,
11158 delivery_witness,
11159 } = plan;
11160 self.runtime.apply_durable_checkpoint_restore(runtime);
11161 self.runtime.coverage_head = Some(position.coverage_head);
11162 if let Err(error) = self.subscriber.restore_position(&position) {
11163 self.runtime.restore_state(prior_runtime);
11164 return Err(ReactiveCheckpointRestoreError::Subscriber(error));
11165 }
11166 if let Err(error) = self.ensure_subscriber_restore_chain(metadata.identity.chain_id) {
11167 self.runtime.restore_state(prior_runtime);
11168 return Err(error);
11169 }
11170 self.last_checkpoint_block = Some(metadata.block.clone());
11171 self.last_checkpoint_delivery_token = position.delivery_token;
11172 self.last_checkpoint_delivery_witness = delivery_witness;
11173 self.last_subscriber_checkpoint = position.subscriber_checkpoint;
11174 self.checkpoint_identity = Some(metadata.identity.clone());
11175 Ok(())
11176 }
11177
11178 pub fn restore_durable_checkpoint(
11194 &mut self,
11195 cache: &mut EvmCache,
11196 loaded: LoadedDurableCheckpoint,
11197 expected: &DurableCheckpointIdentity,
11198 ) -> Result<DurableCheckpointMetadata, ReactiveCheckpointRestoreError> {
11199 if !self.subscriber.capabilities().supports_durable_replay() {
11200 return Err(ReactiveCheckpointRestoreError::SubscriberNotDurable);
11201 }
11202 self.ensure_subscriber_restore_chain(expected.chain_id)?;
11203 if !self.runtime.is_pristine_for_checkpoint_restore()
11204 || self.pending_acknowledgement.is_some()
11205 || self.pending_checkpoint.is_some()
11206 || self.last_checkpoint_block.is_some()
11207 || self.last_checkpoint_delivery_token.is_some()
11208 || self.last_checkpoint_delivery_witness.is_some()
11209 || self.last_subscriber_checkpoint.is_some()
11210 || self.checkpoint_identity.is_some()
11211 {
11212 return Err(ReactiveCheckpointRestoreError::ActiveRuntime);
11213 }
11214
11215 let prior_cache = EvmCacheStateSnapshot::capture(cache);
11216 let metadata = loaded.restore_into(cache, expected)?;
11217 if let Err(error) = self.resume_from_durable_checkpoint(&metadata) {
11218 prior_cache.restore(cache);
11219 return Err(error);
11220 }
11221 Ok(metadata)
11222 }
11223
11224 pub fn runtime(&self) -> &ReactiveRuntime<N> {
11226 &self.runtime
11227 }
11228
11229 pub fn runtime_mut(&mut self) -> &mut ReactiveRuntime<N> {
11231 &mut self.runtime
11232 }
11233
11234 pub fn subscriber(&self) -> &S {
11236 &self.subscriber
11237 }
11238
11239 pub fn subscriber_mut(&mut self) -> &mut S {
11241 &mut self.subscriber
11242 }
11243
11244 pub fn adopt_canonical_baseline(
11262 &mut self,
11263 cache: &EvmCache,
11264 baseline: ReactiveCanonicalBaseline,
11265 ) -> Result<(), ReactiveEngineError> {
11266 if self.pending_acknowledgement.is_some()
11267 || self.pending_checkpoint.is_some()
11268 || self.last_checkpoint_block.is_some()
11269 || self.last_checkpoint_delivery_token.is_some()
11270 || self.last_checkpoint_delivery_witness.is_some()
11271 || self.last_subscriber_checkpoint.is_some()
11272 || self.checkpoint_identity.is_some()
11273 {
11274 return Err(ReactiveBaselineError::ActiveRuntime.into());
11275 }
11276 self.runtime
11280 .validate_canonical_baseline_adoption(baseline.block)?;
11281 if baseline.chain_id != cache.chain_id() {
11282 return Err(ReactiveBaselineError::CacheChainMismatch {
11283 baseline_chain_id: baseline.chain_id,
11284 cache_chain_id: cache.chain_id(),
11285 }
11286 .into());
11287 }
11288 self.ensure_subscriber_chain(cache)?;
11289 let exact_selector = BlockId::from((baseline.block.hash, Some(true)));
11290 let context_matches = cache.block_number() == Some(baseline.block.number)
11291 && baseline
11292 .block
11293 .timestamp
11294 .is_none_or(|timestamp| cache.timestamp() == Some(timestamp));
11295 if cache.block() != exact_selector || !context_matches {
11296 return Err(ReactiveBaselineError::CacheBlockMismatch {
11297 number: baseline.block.number,
11298 hash: baseline.block.hash,
11299 }
11300 .into());
11301 }
11302 self.runtime.adopt_canonical_baseline(baseline.block)?;
11303 Ok(())
11304 }
11305
11306 pub fn next_batch(
11319 &mut self,
11320 cache: &EvmCache,
11321 ) -> Result<SubscriberNextBatch<'_, N>, ReactiveEngineError> {
11322 if self.pending_checkpoint.is_some() {
11323 return Err(ReactiveEngineError::PendingCheckpointCommit);
11324 }
11325 if self.pending_acknowledgement.is_some() {
11326 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11327 }
11328 self.ensure_subscriber_chain(cache)?;
11329 Ok(self.subscriber.next_batch())
11330 }
11331
11332 pub fn ingest_batch(
11341 &mut self,
11342 cache: &mut EvmCache,
11343 batch: ReactiveInputBatch<N>,
11344 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
11345 self.ensure_raw_ingest_is_safe(cache, &batch)?;
11346 Ok(self.runtime.ingest_batch(cache, batch)?)
11347 }
11348
11349 pub fn ingest_batch_with_resync(
11359 &mut self,
11360 cache: &mut EvmCache,
11361 batch: ReactiveInputBatch<N>,
11362 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
11363 self.ensure_raw_ingest_is_safe(cache, &batch)?;
11364 Ok(self.runtime.ingest_batch_with_resync(cache, batch)?)
11365 }
11366
11367 fn ensure_raw_ingest_is_safe(
11368 &self,
11369 cache: &EvmCache,
11370 batch: &ReactiveInputBatch<N>,
11371 ) -> Result<(), ReactiveEngineError> {
11372 if self.pending_checkpoint.is_some() {
11373 return Err(ReactiveEngineError::PendingCheckpointCommit);
11374 }
11375 if self.pending_acknowledgement.is_some() {
11376 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11377 }
11378 if batch.delivery_token().is_some() || batch.subscriber_checkpoint().is_some() {
11379 return Err(ReactiveEngineError::UncommittedDeliveryMetadata);
11380 }
11381 self.ensure_subscriber_chain(cache)?;
11382 Ok(())
11383 }
11384
11385 fn ensure_subscriber_chain(&self, cache: &EvmCache) -> Result<(), ReactiveEngineError> {
11386 if let Some(subscriber_chain_id) = self.subscriber.chain_id()
11387 && subscriber_chain_id != cache.chain_id()
11388 {
11389 return Err(ReactiveEngineError::SubscriberChainMismatch {
11390 subscriber_chain_id,
11391 cache_chain_id: cache.chain_id(),
11392 });
11393 }
11394 Ok(())
11395 }
11396
11397 fn ensure_subscriber_restore_chain(
11398 &self,
11399 checkpoint_chain_id: u64,
11400 ) -> Result<(), ReactiveCheckpointRestoreError> {
11401 if let Some(subscriber_chain_id) = self.subscriber.chain_id()
11402 && subscriber_chain_id != checkpoint_chain_id
11403 {
11404 return Err(ReactiveCheckpointRestoreError::SubscriberChainMismatch {
11405 subscriber_chain_id,
11406 checkpoint_chain_id,
11407 });
11408 }
11409 Ok(())
11410 }
11411
11412 pub async fn next_ingest(
11422 &mut self,
11423 cache: &mut EvmCache,
11424 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
11425 self.ensure_subscriber_chain(cache)?;
11426 if self.pending_checkpoint.is_some() {
11427 return Err(ReactiveEngineError::PendingCheckpointCommit);
11428 }
11429 if self.pending_acknowledgement.is_some() {
11430 return self.commit_pending_acknowledgement().await.map(Some);
11431 }
11432 let batch = self.subscriber.next_batch().await?;
11433 self.ensure_subscriber_chain(cache)?;
11434 let Some(mut batch) = batch else {
11435 return Ok(None);
11436 };
11437 let delivery_token = batch.take_delivery_token();
11438 let report = self.runtime.ingest_batch(cache, batch)?;
11439 self.stage_or_return_acknowledgement(delivery_token, report)
11440 .await
11441 }
11442
11443 pub async fn next_ingest_with_resync(
11454 &mut self,
11455 cache: &mut EvmCache,
11456 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
11457 self.ensure_subscriber_chain(cache)?;
11458 if self.pending_checkpoint.is_some() {
11459 return Err(ReactiveEngineError::PendingCheckpointCommit);
11460 }
11461 if self.pending_acknowledgement.is_some() {
11462 return self.commit_pending_acknowledgement().await.map(Some);
11463 }
11464 let batch = self.subscriber.next_batch().await?;
11465 self.ensure_subscriber_chain(cache)?;
11466 let Some(mut batch) = batch else {
11467 return Ok(None);
11468 };
11469 let delivery_token = batch.take_delivery_token();
11470 let report = self.runtime.ingest_batch_with_resync(cache, batch)?;
11471 self.stage_or_return_acknowledgement(delivery_token, report)
11472 .await
11473 }
11474
11475 pub async fn next_ingest_checkpointed(
11506 &mut self,
11507 cache: &mut EvmCache,
11508 store: &DurableCheckpointStore,
11509 identity: &DurableCheckpointIdentity,
11510 ) -> Result<Option<CheckpointedIngest<N>>, ReactiveEngineError> {
11511 if !self.subscriber.capabilities().supports_durable_replay() {
11512 return Err(ReactiveEngineError::SubscriberNotDurable);
11513 }
11514 self.ensure_subscriber_chain(cache)?;
11515 if self.pending_acknowledgement.is_some() {
11516 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11517 }
11518 self.ensure_checkpoint_identity(cache, identity)?;
11519 if self.pending_checkpoint.is_some() {
11520 return self.commit_pending_checkpoint(cache, store).await.map(Some);
11521 }
11522
11523 let batch = self.subscriber.next_batch().await?;
11524 self.ensure_subscriber_chain(cache)?;
11525 let Some(mut batch) = batch else {
11526 return Ok(None);
11527 };
11528 if batch_preconfirmation(&batch)?.is_some() {
11529 return Err(ReactiveEngineError::PreconfirmationNotCheckpointable);
11530 }
11531 self.runtime.discard_preconfirmed_branch(cache);
11532 let delivery_witness = batch
11533 .delivery_token()
11534 .map(|_| durable_delivery_witness(&batch))
11535 .transpose()?;
11536 let delivery_token = batch.take_delivery_token();
11537 let subscriber_checkpoint = batch.take_subscriber_checkpoint();
11538 if let (Some(replay_token), Some(committed_token)) = (
11539 delivery_token.as_ref(),
11540 self.last_checkpoint_delivery_token.as_ref(),
11541 ) && replay_token == committed_token
11542 {
11543 let committed_witness = self
11544 .last_checkpoint_delivery_witness
11545 .ok_or(ReactiveEngineError::MissingReplayWitness)?;
11546 if delivery_witness != Some(committed_witness) {
11547 return Err(ReactiveEngineError::ReplayDeliveryMismatch);
11548 }
11549 self.subscriber
11550 .acknowledge_delivery(replay_token.clone())
11551 .await
11552 .map_err(ReactiveEngineError::Acknowledgement)?;
11553 return Ok(Some(CheckpointedIngest::ReplayAcknowledged));
11554 }
11555
11556 self.ensure_checkpointable_reorgs(&batch)?;
11557
11558 let incoming_block = latest_canonical_batch_block(&batch);
11559 let cache_state = EvmCacheStateSnapshot::capture(cache);
11560 let runtime_state = self.runtime.checkpoint_state();
11561 let report = match self.runtime.ingest_batch_direct(cache, batch) {
11562 Ok(report) => report,
11563 Err(error) => {
11564 cache_state.restore(cache);
11565 self.runtime.restore_transaction_state(runtime_state);
11566 return Err(error.into());
11567 }
11568 };
11569 let reports = report.reports.clone();
11570 let stage = CheckpointStage {
11571 incoming_block,
11572 delivery_token,
11573 delivery_witness,
11574 subscriber_checkpoint,
11575 staged_generation: cache.snapshot_generation(),
11576 report,
11577 };
11578 if let Err(error) = self.stage_checkpoint(identity, stage) {
11579 cache_state.restore(cache);
11580 self.runtime.restore_transaction_state(runtime_state);
11581 return Err(error);
11582 }
11583 self.runtime.dispatch_reports(&reports);
11584 self.commit_pending_checkpoint(cache, store).await.map(Some)
11585 }
11586
11587 pub async fn next_ingest_with_resync_checkpointed(
11598 &mut self,
11599 cache: &mut EvmCache,
11600 store: &DurableCheckpointStore,
11601 identity: &DurableCheckpointIdentity,
11602 ) -> Result<Option<CheckpointedIngest<N>>, ReactiveEngineError> {
11603 if !self.subscriber.capabilities().supports_durable_replay() {
11604 return Err(ReactiveEngineError::SubscriberNotDurable);
11605 }
11606 self.ensure_subscriber_chain(cache)?;
11607 if self.pending_acknowledgement.is_some() {
11608 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11609 }
11610 self.ensure_checkpoint_identity(cache, identity)?;
11611 if self.pending_checkpoint.is_some() {
11612 return self.commit_pending_checkpoint(cache, store).await.map(Some);
11613 }
11614
11615 let batch = self.subscriber.next_batch().await?;
11616 self.ensure_subscriber_chain(cache)?;
11617 let Some(mut batch) = batch else {
11618 return Ok(None);
11619 };
11620 if batch_preconfirmation(&batch)?.is_some() {
11621 return Err(ReactiveEngineError::PreconfirmationNotCheckpointable);
11622 }
11623 self.runtime.discard_preconfirmed_branch(cache);
11624 let delivery_witness = batch
11625 .delivery_token()
11626 .map(|_| durable_delivery_witness(&batch))
11627 .transpose()?;
11628 let delivery_token = batch.take_delivery_token();
11629 let subscriber_checkpoint = batch.take_subscriber_checkpoint();
11630 if let (Some(replay_token), Some(committed_token)) = (
11631 delivery_token.as_ref(),
11632 self.last_checkpoint_delivery_token.as_ref(),
11633 ) && replay_token == committed_token
11634 {
11635 let committed_witness = self
11636 .last_checkpoint_delivery_witness
11637 .ok_or(ReactiveEngineError::MissingReplayWitness)?;
11638 if delivery_witness != Some(committed_witness) {
11639 return Err(ReactiveEngineError::ReplayDeliveryMismatch);
11640 }
11641 self.subscriber
11642 .acknowledge_delivery(replay_token.clone())
11643 .await
11644 .map_err(ReactiveEngineError::Acknowledgement)?;
11645 return Ok(Some(CheckpointedIngest::ReplayAcknowledged));
11646 }
11647
11648 self.ensure_checkpointable_reorgs(&batch)?;
11649
11650 let incoming_block = latest_canonical_batch_block(&batch);
11651 let cache_state = EvmCacheStateSnapshot::capture(cache);
11652 let runtime_state = self.runtime.checkpoint_state();
11653 let report = match self.runtime.ingest_batch_with_resync_direct(cache, batch) {
11654 Ok(report) => report,
11655 Err(error) => {
11656 cache_state.restore(cache);
11657 self.runtime.restore_transaction_state(runtime_state);
11658 return Err(error.into());
11659 }
11660 };
11661 let reports = report.reports.clone();
11662 let stage = CheckpointStage {
11663 incoming_block,
11664 delivery_token,
11665 delivery_witness,
11666 subscriber_checkpoint,
11667 staged_generation: cache.snapshot_generation(),
11668 report,
11669 };
11670 if let Err(error) = self.stage_checkpoint(identity, stage) {
11671 cache_state.restore(cache);
11672 self.runtime.restore_transaction_state(runtime_state);
11673 return Err(error);
11674 }
11675 self.runtime.dispatch_reports(&reports);
11676 self.commit_pending_checkpoint(cache, store).await.map(Some)
11677 }
11678
11679 fn stage_checkpoint(
11680 &mut self,
11681 identity: &DurableCheckpointIdentity,
11682 stage: CheckpointStage<N>,
11683 ) -> Result<(), ReactiveEngineError> {
11684 let CheckpointStage {
11685 incoming_block,
11686 delivery_token,
11687 delivery_witness,
11688 subscriber_checkpoint,
11689 staged_generation,
11690 report,
11691 } = stage;
11692 if delivery_token.is_some() != delivery_witness.is_some() {
11693 return Err(ReactiveEngineError::DeliveryWitness(
11694 "delivery token and witness must be staged together".into(),
11695 ));
11696 }
11697 let runtime_checkpoint = self.runtime.durable_checkpoint_bytes()?;
11698 let block = self
11699 .runtime
11700 .last_canonical_block()
11701 .map(|block| DurableCheckpointBlock {
11702 number: block.number,
11703 hash: block.hash,
11704 parent_hash: block.parent_hash,
11705 timestamp: block.timestamp,
11706 })
11707 .or(incoming_block)
11708 .or_else(|| self.last_checkpoint_block.clone())
11709 .ok_or(ReactiveEngineError::MissingCheckpointBlock)?;
11710 let metadata = DurableCheckpointMetadata {
11711 identity: identity.clone(),
11712 block,
11713 delivery_token: delivery_token
11714 .as_ref()
11715 .or(self.last_checkpoint_delivery_token.as_ref())
11716 .map(|token| token.as_bytes().to_vec()),
11717 delivery_witness: if delivery_token.is_some() {
11718 delivery_witness
11719 } else {
11720 self.last_checkpoint_delivery_witness
11721 },
11722 subscriber_checkpoint: subscriber_checkpoint
11723 .as_ref()
11724 .or(self.last_subscriber_checkpoint.as_ref())
11725 .map(|checkpoint| checkpoint.as_bytes().to_vec()),
11726 runtime_checkpoint: Some(runtime_checkpoint),
11727 };
11728 self.pending_checkpoint = Some(PendingCheckpoint {
11729 metadata,
11730 delivery_token,
11731 report,
11732 saved_to: None,
11733 staged_generation,
11734 });
11735 Ok(())
11736 }
11737
11738 fn ensure_checkpointable_reorgs(
11739 &self,
11740 batch: &ReactiveInputBatch<N>,
11741 ) -> Result<(), ReactiveEngineError> {
11742 let state = CanonicalSequenceState::new(
11743 self.runtime
11744 .journal
11745 .iter()
11746 .map(|entry| entry.block)
11747 .collect(),
11748 self.runtime.coverage_head,
11749 self.runtime.safe_head,
11750 self.runtime.finalized_head,
11751 );
11752 match validate_canonical_sequence_internal(
11753 &state,
11754 batch,
11755 CanonicalSequenceValidationPolicy::RequireCompleteRollback,
11756 ) {
11757 Ok(_) => Ok(()),
11758 Err(CanonicalSequenceError::Invalid(error)) => Err(error.into()),
11759 Err(CanonicalSequenceError::IncompleteRollback {
11760 common_ancestor,
11761 oldest_retained,
11762 ..
11763 }) => Err(ReactiveEngineError::CheckpointReorgOutsideJournal {
11764 common_ancestor,
11765 oldest_journaled: oldest_retained,
11766 journal_depth: self.runtime.config.journal_depth,
11767 }),
11768 }
11769 }
11770
11771 async fn stage_or_return_acknowledgement(
11772 &mut self,
11773 delivery_token: Option<SubscriberDeliveryToken>,
11774 report: ReactiveBatchReport<N>,
11775 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
11776 let Some(token) = delivery_token else {
11777 return Ok(Some(report));
11778 };
11779 self.pending_acknowledgement = Some(PendingAcknowledgement { token, report });
11780 self.commit_pending_acknowledgement().await.map(Some)
11781 }
11782
11783 async fn commit_pending_acknowledgement(
11784 &mut self,
11785 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
11786 let token = self
11787 .pending_acknowledgement
11788 .as_ref()
11789 .expect("caller checked pending acknowledgement")
11790 .token
11791 .clone();
11792 self.subscriber
11793 .acknowledge_delivery(token)
11794 .await
11795 .map_err(ReactiveEngineError::Acknowledgement)?;
11796 Ok(self
11797 .pending_acknowledgement
11798 .take()
11799 .expect("pending acknowledgement remains until commit")
11800 .report)
11801 }
11802
11803 async fn commit_pending_checkpoint(
11804 &mut self,
11805 cache: &EvmCache,
11806 store: &DurableCheckpointStore,
11807 ) -> Result<CheckpointedIngest<N>, ReactiveEngineError> {
11808 let pending = self
11809 .pending_checkpoint
11810 .as_mut()
11811 .expect("caller checked pending checkpoint");
11812 let cache_generation = cache.snapshot_generation();
11813 if cache_generation != pending.staged_generation {
11814 return Err(ReactiveEngineError::PendingCheckpointCacheChanged {
11815 staged_generation: pending.staged_generation,
11816 current_generation: cache_generation,
11817 });
11818 }
11819 if pending.saved_to.as_deref() != Some(store.path()) {
11820 store
11821 .save_async(cache, pending.metadata.clone())
11822 .await
11823 .map_err(ReactiveEngineError::Checkpoint)?;
11824 pending.saved_to = Some(store.path().to_path_buf());
11825 }
11826 if let Some(token) = pending.delivery_token.clone() {
11827 self.subscriber
11828 .acknowledge_delivery(token)
11829 .await
11830 .map_err(ReactiveEngineError::Acknowledgement)?;
11831 }
11832
11833 let pending = self
11834 .pending_checkpoint
11835 .take()
11836 .expect("pending checkpoint remains until commit");
11837 self.last_checkpoint_block = Some(pending.metadata.block);
11838 self.checkpoint_identity = Some(pending.metadata.identity);
11839 self.last_checkpoint_delivery_token = pending
11840 .metadata
11841 .delivery_token
11842 .map(SubscriberDeliveryToken::new);
11843 self.last_checkpoint_delivery_witness = pending.metadata.delivery_witness;
11844 self.last_subscriber_checkpoint = pending
11845 .metadata
11846 .subscriber_checkpoint
11847 .map(SubscriberCheckpoint::new);
11848 Ok(CheckpointedIngest::Applied(pending.report))
11849 }
11850
11851 fn ensure_checkpoint_identity(
11852 &self,
11853 cache: &EvmCache,
11854 identity: &DurableCheckpointIdentity,
11855 ) -> Result<(), ReactiveEngineError> {
11856 if identity.chain_id != cache.chain_id() {
11857 return Err(ReactiveEngineError::Checkpoint(
11858 DurableCheckpointError::CacheChainMismatch {
11859 cache_chain_id: cache.chain_id(),
11860 checkpoint_chain_id: identity.chain_id,
11861 },
11862 ));
11863 }
11864 if let Some(actual) = self.checkpoint_identity.as_ref()
11865 && actual != identity
11866 {
11867 return Err(ReactiveEngineError::Checkpoint(
11868 DurableCheckpointError::IdentityMismatch {
11869 expected: identity.clone(),
11870 actual: actual.clone(),
11871 },
11872 ));
11873 }
11874 if let Some(pending) = self.pending_checkpoint.as_ref()
11875 && &pending.metadata.identity != identity
11876 {
11877 return Err(ReactiveEngineError::Checkpoint(
11878 DurableCheckpointError::IdentityMismatch {
11879 expected: identity.clone(),
11880 actual: pending.metadata.identity.clone(),
11881 },
11882 ));
11883 }
11884 Ok(())
11885 }
11886}
11887
11888fn latest_canonical_batch_block<N: Network>(
11889 batch: &ReactiveInputBatch<N>,
11890) -> Option<DurableCheckpointBlock> {
11891 let record_block = batch
11892 .records()
11893 .iter()
11894 .enumerate()
11895 .filter(|(index, _)| {
11896 batch
11897 .record_delivery_scope(*index)
11898 .is_some_and(DeliveryScope::advances_canonical_state)
11899 })
11900 .filter_map(|(_, record)| canonical_record_block(record))
11901 .max_by_key(|block| block.number)
11902 .cloned();
11903 let control_block = batch
11904 .chain_controls()
11905 .iter()
11906 .filter_map(|control| match control {
11907 ChainControl::Reorg {
11908 common_ancestor, ..
11909 } => Some(common_ancestor),
11910 ChainControl::Barrier {
11911 block: Some(block), ..
11912 }
11913 | ChainControl::CanonicalProgress(block) => Some(block),
11914 ChainControl::Safe(_)
11915 | ChainControl::Finalized(_)
11916 | ChainControl::Barrier { block: None, .. } => None,
11917 })
11918 .max_by_key(|block| block.number)
11919 .cloned();
11920
11921 record_block
11922 .into_iter()
11923 .chain(control_block)
11924 .max_by_key(|block| block.number)
11925 .map(|block| DurableCheckpointBlock {
11926 number: block.number,
11927 hash: block.hash,
11928 parent_hash: block.parent_hash,
11929 timestamp: block.timestamp,
11930 })
11931}
11932
11933impl<S, N> ReactiveEngine<S, N>
11934where
11935 N: Network,
11936 S: InterestOwnerSubscriber<N>,
11937{
11938 pub async fn register_handler(
11964 &mut self,
11965 handler: Arc<dyn ReactiveHandler<N>>,
11966 ) -> Result<(), ReactiveEngineRegisterError> {
11967 let backfill = self
11968 .runtime
11969 .last_canonical_block()
11970 .filter(|retained| {
11971 self.runtime.journal.iter().any(|entry| {
11972 optional_block_refs_are_compatible(Some(&entry.block), Some(retained))
11973 })
11974 })
11975 .map(HandlerRegistrationCatchup::CoordinatedCanonical)
11976 .unwrap_or(HandlerRegistrationCatchup::LiveOnly);
11977 self.register_handler_inner(handler, backfill).await
11978 }
11979
11980 pub async fn register_handler_with_backfill(
11999 &mut self,
12000 handler: Arc<dyn ReactiveHandler<N>>,
12001 backfill: SubscriberBackfill,
12002 ) -> Result<(), ReactiveEngineRegisterError> {
12003 self.register_handler_inner(handler, HandlerRegistrationCatchup::OwnerBackfill(backfill))
12004 .await
12005 }
12006
12007 pub async fn register_handler_live_only(
12018 &mut self,
12019 handler: Arc<dyn ReactiveHandler<N>>,
12020 ) -> Result<(), ReactiveEngineRegisterError> {
12021 self.register_handler_inner(handler, HandlerRegistrationCatchup::LiveOnly)
12022 .await
12023 }
12024
12025 async fn register_handler_inner(
12026 &mut self,
12027 handler: Arc<dyn ReactiveHandler<N>>,
12028 catchup: HandlerRegistrationCatchup,
12029 ) -> Result<(), ReactiveEngineRegisterError> {
12030 let id = handler.id();
12031 if self.runtime.contains_handler(&id) {
12032 return Err(RegisterError::DuplicateHandler(id).into());
12033 }
12034 let interests = handler.interests();
12035
12036 if let HandlerRegistrationCatchup::OwnerBackfill(backfill) = &catchup {
12037 let retained_anchor = backfill.retained_anchor().copied();
12038 let is_exact_retained_block = retained_anchor.is_some_and(|anchor| {
12039 backfill.start_block() == anchor.number
12040 && backfill.end_block() == Some(anchor.number)
12041 && self.runtime.journal.iter().any(|entry| {
12042 optional_block_refs_are_compatible(Some(&entry.block), Some(&anchor))
12043 })
12044 });
12045 if !is_exact_retained_block {
12046 return Err(ReactiveEngineRegisterError::BackfillOutsideJournal {
12047 start_block: backfill.start_block(),
12048 end_block: backfill.end_block(),
12049 retained_anchor,
12050 });
12051 }
12052 }
12053
12054 let subscribed = match catchup {
12055 HandlerRegistrationCatchup::OwnerBackfill(backfill) => {
12056 self.subscriber
12057 .add_interest_owner_with_backfill(id.clone(), &interests, backfill)
12058 .await
12059 }
12060 HandlerRegistrationCatchup::CoordinatedCanonical(retained) => {
12061 self.subscriber
12062 .add_interest_owner_with_canonical_catchup(id.clone(), &interests, retained)
12063 .await
12064 }
12065 HandlerRegistrationCatchup::LiveOnly => {
12066 self.subscriber
12067 .add_interest_owner(id.clone(), &interests)
12068 .await
12069 }
12070 };
12071 if let Err(error) = subscribed {
12072 return Err(error.into());
12073 }
12074
12075 self.runtime
12080 .registry
12081 .insert_handler_prepared(id, handler, interests);
12082 Ok(())
12083 }
12084
12085 pub async fn sync_handler_interests(&mut self) -> Result<(), SubscriberError> {
12110 let owners = self
12111 .runtime
12112 .handler_ids()
12113 .into_iter()
12114 .map(|id| {
12115 let interests = self
12116 .runtime
12117 .handler_interests(&id)
12118 .map(<[ReactiveInterest<N>]>::to_vec)
12119 .unwrap_or_default();
12120 (id, interests)
12121 })
12122 .collect();
12123 self.subscriber.replace_interest_owners(owners).await
12124 }
12125
12126 pub async fn sync_handler_interests_with_backfill(&mut self) -> Result<(), SubscriberError> {
12154 let baseline =
12155 self.runtime
12156 .last_canonical_block()
12157 .ok_or(SubscriberError::InvalidConfig(
12158 "cannot continuity-sync handlers before a canonical runtime position exists",
12159 ))?;
12160 let backfill = SubscriberBackfill::after_canonical_block(baseline)?;
12161 let owners = self
12162 .runtime
12163 .handler_ids()
12164 .into_iter()
12165 .map(|id| {
12166 let interests = self
12167 .runtime
12168 .handler_interests(&id)
12169 .map(<[ReactiveInterest<N>]>::to_vec)
12170 .unwrap_or_default();
12171 (id, interests)
12172 })
12173 .collect();
12174 self.subscriber
12175 .replace_interest_owners_with_global_backfill(owners, backfill)
12176 .await
12177 }
12178
12179 pub async fn unregister_handler(
12211 &mut self,
12212 id: &HandlerId,
12213 ) -> Result<Option<Arc<dyn ReactiveHandler<N>>>, SubscriberError> {
12214 self.subscriber.remove_interest_owner(id).await?;
12215 Ok(self.runtime.unregister_handler(id))
12216 }
12217}
12218
12219type FlashblockReconnectFuture<N> = Pin<
12220 Box<
12221 dyn Future<
12222 Output = (
12223 SubscriberStreamSource,
12224 Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>,
12225 ),
12226 > + Send,
12227 >,
12228>;
12229
12230pub struct AlloySubscriber<P, N: Network = Ethereum> {
12251 provider: P,
12252 flashblocks_state_provider: Option<P>,
12256 provider_ref: Option<ProviderRef>,
12259 log_verification_provider: Option<P>,
12263 chain_id: Option<u64>,
12265 mode: SubscriberMode,
12266 config: SubscriberConfig,
12267 base_interests: Vec<ReactiveInterest<N>>,
12268 owned_interests: Vec<OwnedSubscriberInterests<N>>,
12269 next_owner_epoch: u64,
12270 interests: Vec<ReactiveInterest<N>>,
12271 log_source_ids: HashMap<Filter, usize>,
12276 next_log_source_id: usize,
12277 pending_backfills: VecDeque<QueuedSubscriberBackfill>,
12278 pending_source_backfills: VecDeque<SubscriberStreamSource>,
12283 sources_dirty: bool,
12286 stream_revision: u64,
12289 state: AlloySubscriberState<N>,
12290 pending_records: VecDeque<SubscriberInputRecord<N>>,
12291 pending_chain_controls: VecDeque<ChainControl>,
12292 pending_reconcile_owner_records: VecDeque<BufferedSubscriberOwnerRecord<N>>,
12297 resource_error: Option<String>,
12300 last_seen_log_blocks: HashMap<usize, u64>,
12301 verified_log_blocks: HashMap<(u64, B256), BlockRef>,
12302 verified_log_block_order: VecDeque<(u64, B256)>,
12303 recent_input_refs: VecDeque<InputRef>,
12304 recent_input_ref_set: HashSet<InputRef>,
12305 recent_owner_input_refs: HashMap<SubscriberOwnerEpoch, VecDeque<InputRef>>,
12306 recent_owner_input_ref_sets: HashMap<SubscriberOwnerEpoch, HashSet<InputRef>>,
12307 recent_compat_owner_input_refs: HashMap<HandlerId, VecDeque<InputRef>>,
12308 recent_compat_owner_input_ref_sets: HashMap<HandlerId, HashSet<InputRef>>,
12309 base_flashblock_header: Option<(FixedBytes<8>, BaseFlashblockBase)>,
12310 base_flashblock_transactions: Option<(FixedBytes<8>, u64, Vec<B256>, Vec<B256>)>,
12311 unmatched_pending_logs: VecDeque<(usize, Log)>,
12312 latest_preconfirmation: Option<FlashblockRef>,
12313 preconfirmed_seen_logs: HashSet<(B256, u64)>,
12314 preconfirmed_receipted_transactions: HashSet<B256>,
12318 preconfirmed_unavailable_receipts: HashSet<B256>,
12323 last_certified_canonical_head: Option<BlockRef>,
12324 pending_preconfirmation_invalidation: bool,
12325 pending_flashblock_reconnects: FuturesUnordered<FlashblockReconnectFuture<N>>,
12326 pending_flashblock_reconnect_sources: Vec<SubscriberStreamSource>,
12327 flashblocks_rpc_metrics: FlashblocksRpcMetrics,
12328 consecutive_flashblock_poll_failures: usize,
12329 flashblock_rpc_request_times: VecDeque<Instant>,
12330 _network: PhantomData<N>,
12331}
12332
12333struct OwnedSubscriberInterests<N: Network = Ethereum> {
12334 owner: HandlerId,
12335 interests: Vec<ReactiveInterest<N>>,
12336 epoch: Option<SubscriberOwnerEpoch>,
12337 state: SubscriberOwnerState,
12338 baseline: Option<BlockRef>,
12339 progress: Option<SubscriberOwnerProgress>,
12340 progress_stream_revision: Option<u64>,
12341}
12342
12343#[derive(Clone)]
12344struct SubscriberOwnerReconcilePlan<N: Network = Ethereum> {
12345 epoch: SubscriberOwnerEpoch,
12346 interests: Vec<ReactiveInterest<N>>,
12347 retained: BlockRef,
12348 from_block: u64,
12349}
12350
12351struct SubscriberOwnerCatchup {
12352 logs: Vec<Log>,
12353 certified: BlockRef,
12354}
12355
12356#[derive(Clone, Copy)]
12357struct SubscriberOwnerCatchupOptions {
12358 target_preverified: bool,
12359 max_logs: usize,
12360 max_log_bytes: usize,
12361 max_requests_in_flight: usize,
12362}
12363
12364struct SubscriberOwnerReconcileFilter {
12365 filter: Filter,
12366 from_block: u64,
12367}
12368
12369struct BufferedSubscriberOwnerRecord<N: Network = Ethereum> {
12370 record: ReactiveInputRecord<N>,
12371 owners: Vec<SubscriberOwnerEpoch>,
12372}
12373
12374const OWNER_RECONCILE_FILTERS_PER_CHUNK: usize = 256;
12375
12376struct QueuedSubscriberBackfill {
12377 owner: Option<HandlerId>,
12380 epoch: Option<SubscriberOwnerEpoch>,
12381 filters: Vec<Filter>,
12383 backfill: SubscriberBackfill,
12384}
12385
12386#[cfg(feature = "reactive-ws")]
12392fn ensure_ring_crypto_provider() {
12393 use std::sync::Once;
12394 static INSTALL: Once = Once::new();
12395 INSTALL.call_once(|| {
12396 let _ = rustls::crypto::ring::default_provider().install_default();
12397 });
12398}
12399
12400impl<P, N: Network> AlloySubscriber<P, N> {
12401 pub fn new(provider: P, mode: SubscriberMode, config: SubscriberConfig) -> Self {
12403 #[cfg(feature = "reactive-ws")]
12404 ensure_ring_crypto_provider();
12405 Self {
12406 provider,
12407 flashblocks_state_provider: None,
12408 provider_ref: None,
12409 log_verification_provider: None,
12410 chain_id: None,
12411 mode,
12412 config,
12413 base_interests: Vec::new(),
12414 owned_interests: Vec::new(),
12415 next_owner_epoch: 0,
12416 interests: Vec::new(),
12417 log_source_ids: HashMap::new(),
12418 next_log_source_id: 0,
12419 pending_backfills: VecDeque::new(),
12420 pending_source_backfills: VecDeque::new(),
12421 sources_dirty: true,
12422 stream_revision: 0,
12423 state: AlloySubscriberState::Uninitialized,
12424 pending_records: VecDeque::new(),
12425 pending_chain_controls: VecDeque::new(),
12426 pending_reconcile_owner_records: VecDeque::new(),
12427 resource_error: None,
12428 last_seen_log_blocks: HashMap::new(),
12429 verified_log_blocks: HashMap::new(),
12430 verified_log_block_order: VecDeque::new(),
12431 recent_input_refs: VecDeque::new(),
12432 recent_input_ref_set: HashSet::new(),
12433 recent_owner_input_refs: HashMap::new(),
12434 recent_owner_input_ref_sets: HashMap::new(),
12435 recent_compat_owner_input_refs: HashMap::new(),
12436 recent_compat_owner_input_ref_sets: HashMap::new(),
12437 base_flashblock_header: None,
12438 base_flashblock_transactions: None,
12439 unmatched_pending_logs: VecDeque::new(),
12440 latest_preconfirmation: None,
12441 preconfirmed_seen_logs: HashSet::new(),
12442 preconfirmed_receipted_transactions: HashSet::new(),
12443 preconfirmed_unavailable_receipts: HashSet::new(),
12444 last_certified_canonical_head: None,
12445 pending_preconfirmation_invalidation: false,
12446 pending_flashblock_reconnects: FuturesUnordered::new(),
12447 pending_flashblock_reconnect_sources: Vec::new(),
12448 flashblocks_rpc_metrics: FlashblocksRpcMetrics::default(),
12449 consecutive_flashblock_poll_failures: 0,
12450 flashblock_rpc_request_times: VecDeque::new(),
12451 _network: PhantomData,
12452 }
12453 }
12454
12455 pub fn provider(&self) -> &P {
12457 &self.provider
12458 }
12459
12460 #[must_use]
12464 pub fn with_provider_ref(mut self, provider: ProviderRef) -> Self {
12465 self.provider_ref = Some(provider);
12466 self
12467 }
12468
12469 #[must_use]
12477 pub fn with_flashblocks_state_provider(mut self, provider: P) -> Self {
12478 self.flashblocks_state_provider = Some(provider);
12479 self
12480 }
12481
12482 #[must_use]
12489 pub fn with_log_verification_provider(mut self, provider: P) -> Self {
12490 self.log_verification_provider = Some(provider);
12491 self
12492 }
12493
12494 pub fn mode(&self) -> SubscriberMode {
12496 self.mode
12497 }
12498
12499 pub fn config(&self) -> &SubscriberConfig {
12501 &self.config
12502 }
12503
12504 pub const fn flashblocks_rpc_metrics(&self) -> FlashblocksRpcMetrics {
12507 self.flashblocks_rpc_metrics
12508 }
12509
12510 pub fn registered_interests(&self) -> &[ReactiveInterest<N>] {
12512 &self.interests
12513 }
12514
12515 pub fn stage_interest_owner(
12532 &mut self,
12533 owner: HandlerId,
12534 interests: &[ReactiveInterest<N>],
12535 start: SubscriberOwnerStart,
12536 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
12537 validate_subscriber_config(&self.config)?;
12538 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
12539 && interests
12540 .iter()
12541 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
12542 {
12543 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
12544 }
12545 if self
12546 .owned_interests
12547 .iter()
12548 .any(|entry| entry.owner == owner)
12549 {
12550 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
12551 }
12552
12553 let mut next_owned = self.clone_owned_interests();
12554 next_owned.push(OwnedSubscriberInterests {
12555 owner: owner.clone(),
12556 interests: interests.to_vec(),
12557 epoch: None,
12558 state: SubscriberOwnerState::Staged,
12559 baseline: None,
12560 progress: None,
12561 progress_stream_revision: None,
12562 });
12563 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
12564 validate_supported_interests(self.mode, &self.config, &next_registered)?;
12565
12566 let baseline = match start {
12567 SubscriberOwnerStart::Live => None,
12568 SubscriberOwnerStart::PostBlock(block) => {
12569 block
12570 .number
12571 .checked_add(1)
12572 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
12573 Some(block)
12574 }
12575 };
12576 let sequence = self
12577 .next_owner_epoch
12578 .checked_add(1)
12579 .ok_or(SubscriberOwnerError::EpochExhausted)?;
12580 let epoch = SubscriberOwnerEpoch {
12581 owner: owner.clone(),
12582 sequence,
12583 };
12584
12585 self.next_owner_epoch = sequence;
12586 let entry = next_owned
12587 .last_mut()
12588 .expect("staged owner was appended during preflight");
12589 entry.epoch = Some(epoch.clone());
12590 entry.baseline = baseline;
12591 self.owned_interests = next_owned;
12592 self.interests = next_registered;
12593 self.sources_dirty = true;
12594
12595 Ok(epoch)
12596 }
12597
12598 pub fn stage_interest_owner_replacement(
12612 &mut self,
12613 owner: HandlerId,
12614 interests: &[ReactiveInterest<N>],
12615 start: SubscriberOwnerStart,
12616 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
12617 validate_subscriber_config(&self.config)?;
12618 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
12619 && interests
12620 .iter()
12621 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
12622 {
12623 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
12624 }
12625 let active_count = self
12626 .owned_interests
12627 .iter()
12628 .filter(|entry| {
12629 entry.owner == owner
12630 && entry.state == SubscriberOwnerState::Active
12631 && entry.epoch.is_some()
12632 })
12633 .count();
12634 if active_count != 1
12635 || self
12636 .owned_interests
12637 .iter()
12638 .any(|entry| entry.owner == owner && entry.state != SubscriberOwnerState::Active)
12639 {
12640 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
12641 }
12642
12643 let mut next_owned = self.clone_owned_interests();
12644 next_owned.push(OwnedSubscriberInterests {
12645 owner: owner.clone(),
12646 interests: interests.to_vec(),
12647 epoch: None,
12648 state: SubscriberOwnerState::Staged,
12649 baseline: None,
12650 progress: None,
12651 progress_stream_revision: None,
12652 });
12653 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
12654 validate_supported_interests(self.mode, &self.config, &next_registered)?;
12655
12656 let baseline = match start {
12657 SubscriberOwnerStart::Live => None,
12658 SubscriberOwnerStart::PostBlock(block) => {
12659 block
12660 .number
12661 .checked_add(1)
12662 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
12663 Some(block)
12664 }
12665 };
12666 let sequence = self
12667 .next_owner_epoch
12668 .checked_add(1)
12669 .ok_or(SubscriberOwnerError::EpochExhausted)?;
12670 let epoch = SubscriberOwnerEpoch {
12671 owner: owner.clone(),
12672 sequence,
12673 };
12674
12675 self.next_owner_epoch = sequence;
12676 let entry = next_owned
12677 .last_mut()
12678 .expect("staged replacement owner was appended during preflight");
12679 entry.epoch = Some(epoch.clone());
12680 entry.baseline = baseline;
12681 self.owned_interests = next_owned;
12682 self.interests = next_registered;
12683 self.sources_dirty = true;
12684 Ok(epoch)
12685 }
12686
12687 pub fn interest_owner_state(
12689 &self,
12690 epoch: &SubscriberOwnerEpoch,
12691 ) -> Option<SubscriberOwnerState> {
12692 self.owned_interests
12693 .iter()
12694 .find(|entry| entry.epoch.as_ref() == Some(epoch))
12695 .map(|entry| entry.state)
12696 }
12697
12698 pub fn interest_owner_progress(
12700 &self,
12701 epoch: &SubscriberOwnerEpoch,
12702 ) -> Option<&SubscriberOwnerProgress> {
12703 self.owned_interests
12704 .iter()
12705 .find(|entry| entry.epoch.as_ref() == Some(epoch))
12706 .and_then(|entry| entry.progress.as_ref())
12707 }
12708
12709 pub fn activate_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
12713 let stream_revision = self.stream_revision;
12714 let sources_dirty = self.sources_dirty;
12715 let Some(entry) = self
12716 .owned_interests
12717 .iter_mut()
12718 .find(|entry| entry.epoch.as_ref() == Some(epoch))
12719 else {
12720 return false;
12721 };
12722 if entry.state != SubscriberOwnerState::Staged
12723 || (entry.baseline.is_some()
12724 && (entry.progress.is_none()
12725 || entry.progress_stream_revision != Some(stream_revision)
12726 || sources_dirty))
12727 {
12728 return false;
12729 }
12730 entry.state = SubscriberOwnerState::Active;
12731 true
12732 }
12733
12734 pub fn commit_interest_owner_replacement(
12736 &mut self,
12737 active: &SubscriberOwnerEpoch,
12738 replacement: &SubscriberOwnerEpoch,
12739 ) -> bool {
12740 let Some(active_index) = self
12741 .owned_interests
12742 .iter()
12743 .position(|entry| entry.epoch.as_ref() == Some(active))
12744 else {
12745 return false;
12746 };
12747 let Some(replacement_index) = self
12748 .owned_interests
12749 .iter()
12750 .position(|entry| entry.epoch.as_ref() == Some(replacement))
12751 else {
12752 return false;
12753 };
12754 if active_index == replacement_index
12755 || active.owner() != replacement.owner()
12756 || self.owned_interests[active_index].state != SubscriberOwnerState::Active
12757 || self.owned_interests[replacement_index].state != SubscriberOwnerState::Staged
12758 || (self.owned_interests[replacement_index].baseline.is_some()
12759 && (self.owned_interests[replacement_index].progress.is_none()
12760 || self.owned_interests[replacement_index].progress_stream_revision
12761 != Some(self.stream_revision)
12762 || self.sources_dirty))
12763 {
12764 return false;
12765 }
12766
12767 self.owned_interests[replacement_index].state = SubscriberOwnerState::Active;
12768 self.owned_interests.remove(active_index);
12769 self.purge_owner_epoch(active);
12770 self.rebuild_registered_interests();
12771 self.retire_unreferenced_filters();
12772 self.sources_dirty = true;
12773 true
12774 }
12775
12776 pub fn prepare_interest_owner_removal(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
12785 let Some(entry) = self
12786 .owned_interests
12787 .iter_mut()
12788 .find(|entry| entry.epoch.as_ref() == Some(epoch))
12789 else {
12790 return false;
12791 };
12792 if entry.state != SubscriberOwnerState::Active {
12793 return false;
12794 }
12795 entry.state = SubscriberOwnerState::Removing;
12796 true
12797 }
12798
12799 pub fn finalize_interest_owner_removal(
12805 &mut self,
12806 epoch: &SubscriberOwnerEpoch,
12807 ) -> Option<Vec<ReactiveInterest<N>>> {
12808 let index = self.owned_interests.iter().position(|entry| {
12809 entry.epoch.as_ref() == Some(epoch) && entry.state == SubscriberOwnerState::Removing
12810 })?;
12811 let removed = self.owned_interests.remove(index).interests;
12812 self.purge_owner_epoch(epoch);
12813 self.rebuild_registered_interests();
12814 self.retire_unreferenced_filters();
12815 self.sources_dirty = true;
12816 Some(removed)
12817 }
12818
12819 pub fn abort_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
12825 let Some(index) = self
12826 .owned_interests
12827 .iter()
12828 .position(|entry| entry.epoch.as_ref() == Some(epoch))
12829 else {
12830 return false;
12831 };
12832 match self.owned_interests[index].state {
12833 SubscriberOwnerState::Staged => {
12834 self.owned_interests.remove(index);
12835 self.purge_owner_epoch(epoch);
12836 self.rebuild_registered_interests();
12837 self.retire_unreferenced_filters();
12838 self.sources_dirty = true;
12839 true
12840 }
12841 SubscriberOwnerState::Removing => {
12842 self.owned_interests[index].state = SubscriberOwnerState::Active;
12843 true
12844 }
12845 SubscriberOwnerState::Active => false,
12846 }
12847 }
12848
12849 fn purge_owner_epoch(&mut self, epoch: &SubscriberOwnerEpoch) {
12850 self.pending_backfills
12851 .retain(|backfill| backfill.epoch.as_ref() != Some(epoch));
12852 self.pending_records
12853 .retain_mut(|pending| match &mut pending.scope {
12854 SubscriberInputScope::Canonical { owners }
12855 | SubscriberInputScope::CanonicalResidual { owners, .. } => {
12856 owners.retain(|owner| owner != epoch);
12857 true
12858 }
12859 SubscriberInputScope::OwnerOnly { owners } => {
12860 owners.retain(|owner| owner != epoch);
12861 !owners.is_empty()
12862 }
12863 SubscriberInputScope::OwnerOnlyHandlers { .. }
12864 | SubscriberInputScope::Preconfirmed => true,
12865 });
12866 self.pending_reconcile_owner_records.retain_mut(|pending| {
12867 pending.owners.retain(|owner| owner != epoch);
12868 !pending.owners.is_empty()
12869 });
12870 self.recent_owner_input_refs.remove(epoch);
12871 self.recent_owner_input_ref_sets.remove(epoch);
12872 }
12873
12874 pub fn upsert_interest_owners(
12882 &mut self,
12883 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
12884 ) -> Result<(), SubscriberError> {
12885 self.upsert_interest_owners_inner(owners, None)
12886 }
12887
12888 pub fn upsert_interest_owners_with_backfill(
12897 &mut self,
12898 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
12899 backfill: SubscriberBackfill,
12900 ) -> Result<(), SubscriberError> {
12901 self.upsert_interest_owners_inner(owners, Some(backfill))
12902 }
12903
12904 fn upsert_interest_owners_inner(
12905 &mut self,
12906 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
12907 explicit_backfill: Option<SubscriberBackfill>,
12908 ) -> Result<(), SubscriberError> {
12909 validate_subscriber_config(&self.config)?;
12910 let mut seen = HashSet::with_capacity(owners.len());
12911 let mut next_owned = self.clone_owned_interests();
12912 for (owner, interests) in &owners {
12913 if !seen.insert(owner.clone()) {
12914 return Err(SubscriberError::InvalidConfig(
12915 "bulk owner upsert contains a duplicate owner",
12916 ));
12917 }
12918 if self
12919 .owned_interests
12920 .iter()
12921 .any(|entry| &entry.owner == owner && entry.epoch.is_some())
12922 {
12923 return Err(SubscriberError::InvalidConfig(
12924 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
12925 ));
12926 }
12927 if let Some(entry) = next_owned.iter_mut().find(|entry| &entry.owner == owner) {
12928 entry.interests = interests.clone();
12929 entry.state = SubscriberOwnerState::Active;
12930 entry.baseline = None;
12931 entry.progress = None;
12932 entry.progress_stream_revision = None;
12933 } else {
12934 next_owned.push(OwnedSubscriberInterests {
12935 owner: owner.clone(),
12936 interests: interests.clone(),
12937 epoch: None,
12938 state: SubscriberOwnerState::Active,
12939 baseline: None,
12940 progress: None,
12941 progress_stream_revision: None,
12942 });
12943 }
12944 }
12945 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
12946 validate_supported_interests(self.mode, &self.config, &next_registered)?;
12947
12948 let mut replacement_backfills = Vec::new();
12953 for (owner, interests) in &owners {
12954 let previous_filters: Vec<Filter> = self
12955 .owner_interests(owner)
12956 .map(log_filters)
12957 .unwrap_or_default();
12958 let continuity_anchor = previous_filters
12959 .iter()
12960 .filter_map(|filter| self.log_anchor(filter))
12961 .min();
12962 let filters = log_filters(interests);
12963 if let Some(backfill) = explicit_backfill
12964 && !filters.is_empty()
12965 {
12966 replacement_backfills.push(QueuedSubscriberBackfill {
12967 owner: Some(owner.clone()),
12968 epoch: None,
12969 filters: filters.clone(),
12970 backfill,
12971 });
12972 }
12973 let explicit_covers = explicit_backfill.is_some_and(|explicit| {
12974 explicit.end_block().is_none()
12975 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
12976 });
12977 let continuity_filters: Vec<_> = filters
12978 .into_iter()
12979 .filter(|filter| !previous_filters.contains(filter))
12980 .collect();
12981 if let Some(anchor) = continuity_anchor
12982 && !continuity_filters.is_empty()
12983 && !explicit_covers
12984 {
12985 replacement_backfills.push(QueuedSubscriberBackfill {
12986 owner: Some(owner.clone()),
12987 epoch: None,
12988 filters: continuity_filters,
12989 backfill: SubscriberBackfill::from_block(anchor),
12990 });
12991 }
12992 }
12993
12994 let retained_backfills = self
12995 .pending_backfills
12996 .iter()
12997 .filter(|queued| {
12998 queued
12999 .owner
13000 .as_ref()
13001 .is_none_or(|owner| !seen.contains(owner))
13002 })
13003 .map(|queued| queued.filters.len())
13004 .sum::<usize>();
13005 let replacement_units = replacement_backfills
13006 .iter()
13007 .map(|queued| queued.filters.len())
13008 .sum::<usize>();
13009 if retained_backfills.saturating_add(replacement_units) > self.config.max_pending_backfills
13010 {
13011 return Err(SubscriberError::ResourceExhausted(format!(
13012 "bulk owner update would queue more than {} lazy backfills",
13013 self.config.max_pending_backfills
13014 )));
13015 }
13016
13017 self.owned_interests = next_owned;
13021 self.interests = next_registered;
13022 for owner in &seen {
13023 self.recent_compat_owner_input_refs.remove(owner);
13024 self.recent_compat_owner_input_ref_sets.remove(owner);
13025 }
13026 self.retire_unreferenced_filters();
13027 self.sources_dirty = true;
13028 self.pending_backfills.retain(|queued| {
13029 queued
13030 .owner
13031 .as_ref()
13032 .is_none_or(|owner| !seen.contains(owner))
13033 });
13034 self.pending_backfills.extend(replacement_backfills);
13035 Ok(())
13036 }
13037
13038 pub fn replace_interest_owners(
13047 &mut self,
13048 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13049 ) -> Result<(), SubscriberError> {
13050 self.replace_interest_owners_inner(owners, None)
13051 }
13052
13053 pub fn replace_interest_owners_with_global_backfill(
13066 &mut self,
13067 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13068 backfill: SubscriberBackfill,
13069 ) -> Result<(), SubscriberError> {
13070 self.replace_interest_owners_inner(owners, Some(backfill))
13071 }
13072
13073 fn replace_interest_owners_inner(
13074 &mut self,
13075 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13076 backfill: Option<SubscriberBackfill>,
13077 ) -> Result<(), SubscriberError> {
13078 validate_subscriber_config(&self.config)?;
13079 if self
13080 .owned_interests
13081 .iter()
13082 .any(|entry| entry.epoch.is_some())
13083 {
13084 return Err(SubscriberError::InvalidConfig(
13085 "cannot replace compatibility owners while an epoch-scoped lifecycle exists",
13086 ));
13087 }
13088
13089 let mut seen = HashSet::with_capacity(owners.len());
13090 let mut next_owned = Vec::with_capacity(owners.len());
13091 for (owner, interests) in owners {
13092 if !seen.insert(owner.clone()) {
13093 return Err(SubscriberError::InvalidConfig(
13094 "owner replacement contains a duplicate owner",
13095 ));
13096 }
13097 next_owned.push(OwnedSubscriberInterests {
13098 owner,
13099 interests,
13100 epoch: None,
13101 state: SubscriberOwnerState::Active,
13102 baseline: None,
13103 progress: None,
13104 progress_stream_revision: None,
13105 });
13106 }
13107 let next_registered = aggregate_interests(&[], &next_owned);
13108 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13109 let mut filters = log_filters(&next_registered);
13110 let mut unique_filters = Vec::with_capacity(filters.len());
13111 for filter in filters.drain(..) {
13112 if !unique_filters.contains(&filter) {
13113 unique_filters.push(filter);
13114 }
13115 }
13116 let replacement_backfills: VecDeque<_> = match backfill {
13117 Some(backfill) if !unique_filters.is_empty() => {
13118 VecDeque::from([QueuedSubscriberBackfill {
13119 owner: None,
13120 epoch: None,
13121 filters: unique_filters,
13122 backfill,
13123 }])
13124 }
13125 Some(_) | None => VecDeque::new(),
13126 };
13127 let replacement_units = replacement_backfills
13128 .iter()
13129 .map(|queued| queued.filters.len())
13130 .sum::<usize>();
13131 if replacement_units > self.config.max_pending_backfills {
13132 return Err(SubscriberError::ResourceExhausted(format!(
13133 "owner replacement would queue more than {} lazy backfills",
13134 self.config.max_pending_backfills
13135 )));
13136 }
13137
13138 self.base_interests.clear();
13143 self.owned_interests = next_owned;
13144 self.interests = next_registered;
13145 self.reset_delivery_state();
13146 self.pending_backfills = replacement_backfills;
13147 self.state = AlloySubscriberState::Uninitialized;
13148 Ok(())
13149 }
13150
13151 pub fn add_interest_owner(
13174 &mut self,
13175 owner: HandlerId,
13176 interests: &[ReactiveInterest<N>],
13177 ) -> Result<(), SubscriberError> {
13178 self.set_interest_owner(owner, interests, None)
13179 }
13180
13181 pub fn add_interest_owner_with_backfill(
13200 &mut self,
13201 owner: HandlerId,
13202 interests: &[ReactiveInterest<N>],
13203 backfill: SubscriberBackfill,
13204 ) -> Result<(), SubscriberError> {
13205 self.set_interest_owner(owner, interests, Some(backfill))
13206 }
13207
13208 pub fn add_interest_owner_with_canonical_catchup(
13227 &mut self,
13228 owner: HandlerId,
13229 interests: &[ReactiveInterest<N>],
13230 retained: BlockRef,
13231 ) -> Result<(), SubscriberError> {
13232 validate_subscriber_config(&self.config)?;
13233 if self
13234 .owned_interests
13235 .iter()
13236 .any(|entry| entry.owner == owner && entry.epoch.is_some())
13237 {
13238 return Err(SubscriberError::InvalidConfig(
13239 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
13240 ));
13241 }
13242
13243 let mut next_owned = self.clone_owned_interests();
13244 if let Some(entry) = next_owned.iter_mut().find(|entry| entry.owner == owner) {
13245 entry.interests = interests.to_vec();
13246 entry.state = SubscriberOwnerState::Active;
13247 entry.baseline = None;
13248 entry.progress = None;
13249 entry.progress_stream_revision = None;
13250 entry.epoch = None;
13251 } else {
13252 next_owned.push(OwnedSubscriberInterests {
13253 owner: owner.clone(),
13254 interests: interests.to_vec(),
13255 epoch: None,
13256 state: SubscriberOwnerState::Active,
13257 baseline: None,
13258 progress: None,
13259 progress_stream_revision: None,
13260 });
13261 }
13262 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
13263 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13264 if next_registered
13265 .iter()
13266 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
13267 {
13268 return Err(SubscriberError::Unsupported(
13269 "Alloy coordinated registration supports log-only interest topologies",
13270 ));
13271 }
13272
13273 let mut owner_filters = Vec::new();
13274 for filter in log_filters(interests) {
13275 if !owner_filters.contains(&filter) {
13276 owner_filters.push(filter);
13277 }
13278 }
13279 let mut global_filters = Vec::new();
13280 for filter in log_filters(&next_registered) {
13281 if !global_filters.contains(&filter) {
13282 global_filters.push(filter);
13283 }
13284 }
13285 let owner_backfill =
13286 SubscriberBackfill::from_canonical_block_through(retained, retained.number)?;
13287 let global_backfill = SubscriberBackfill::after_canonical_block(retained)?;
13288 let replacement_units = owner_filters.len().saturating_add(global_filters.len());
13289 let retained_units = self
13290 .pending_backfills
13291 .iter()
13292 .filter(|queued| queued.owner.as_ref() != Some(&owner))
13293 .map(|queued| queued.filters.len())
13294 .sum::<usize>();
13295 if retained_units.saturating_add(replacement_units) > self.config.max_pending_backfills {
13296 return Err(SubscriberError::ResourceExhausted(format!(
13297 "coordinated owner registration would queue more than {} lazy backfills",
13298 self.config.max_pending_backfills
13299 )));
13300 }
13301
13302 let mut replacement_backfills = VecDeque::new();
13303 if !owner_filters.is_empty() {
13304 replacement_backfills.push_back(QueuedSubscriberBackfill {
13305 owner: Some(owner.clone()),
13306 epoch: None,
13307 filters: owner_filters,
13308 backfill: owner_backfill,
13309 });
13310 }
13311 replacement_backfills.push_back(QueuedSubscriberBackfill {
13314 owner: None,
13315 epoch: None,
13316 filters: global_filters,
13317 backfill: global_backfill,
13318 });
13319
13320 self.owned_interests = next_owned;
13323 self.interests = next_registered;
13324 self.recent_compat_owner_input_refs.remove(&owner);
13325 self.recent_compat_owner_input_ref_sets.remove(&owner);
13326 self.pending_backfills
13327 .retain(|queued| queued.owner.as_ref() != Some(&owner));
13328 self.pending_backfills.extend(replacement_backfills);
13329 self.retire_unreferenced_filters();
13330 self.sources_dirty = true;
13331 Ok(())
13332 }
13333
13334 pub fn remove_interest_owner(&mut self, owner: &HandlerId) -> Option<Vec<ReactiveInterest<N>>> {
13343 let index = self
13344 .owned_interests
13345 .iter()
13346 .position(|entry| &entry.owner == owner && entry.epoch.is_none())?;
13347 let removed = self.owned_interests.remove(index);
13348 if let Some(epoch) = &removed.epoch {
13349 self.purge_owner_epoch(epoch);
13350 } else {
13351 self.pending_backfills
13352 .retain(|backfill| backfill.owner.as_ref() != Some(owner));
13353 self.recent_compat_owner_input_refs.remove(owner);
13354 self.recent_compat_owner_input_ref_sets.remove(owner);
13355 }
13356 self.rebuild_registered_interests();
13357 self.retire_unreferenced_filters();
13358 self.sources_dirty = true;
13359 Some(removed.interests)
13360 }
13361
13362 pub fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
13364 self.owned_interests
13365 .iter()
13366 .find(|entry| &entry.owner == owner)
13367 .map(|entry| entry.interests.as_slice())
13368 }
13369
13370 fn set_interest_owner(
13371 &mut self,
13372 owner: HandlerId,
13373 interests: &[ReactiveInterest<N>],
13374 backfill: Option<SubscriberBackfill>,
13375 ) -> Result<(), SubscriberError> {
13376 validate_subscriber_config(&self.config)?;
13377 if self
13378 .owned_interests
13379 .iter()
13380 .any(|entry| entry.owner == owner && entry.epoch.is_some())
13381 {
13382 return Err(SubscriberError::InvalidConfig(
13383 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
13384 ));
13385 }
13386
13387 let mut next_owned = self.clone_owned_interests();
13388 let replaced_epoch = match next_owned.iter_mut().find(|entry| entry.owner == owner) {
13389 Some(entry) => {
13390 entry.interests = interests.to_vec();
13391 entry.state = SubscriberOwnerState::Active;
13392 entry.baseline = None;
13393 entry.progress = None;
13394 entry.progress_stream_revision = None;
13395 entry.epoch.take()
13396 }
13397 None => {
13398 next_owned.push(OwnedSubscriberInterests {
13399 owner: owner.clone(),
13400 interests: interests.to_vec(),
13401 epoch: None,
13402 state: SubscriberOwnerState::Active,
13403 baseline: None,
13404 progress: None,
13405 progress_stream_revision: None,
13406 });
13407 None
13408 }
13409 };
13410 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
13411 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13412
13413 let previous_filters: Vec<Filter> = self
13420 .owner_interests(&owner)
13421 .map(log_filters)
13422 .unwrap_or_default();
13423 let continuity_anchor: Option<u64> = previous_filters
13424 .iter()
13425 .filter_map(|filter| self.log_anchor(filter))
13426 .min();
13427
13428 let mut replacement_backfills = Vec::new();
13432 let filters = log_filters(interests);
13433 if let Some(backfill) = backfill
13434 && !filters.is_empty()
13435 {
13436 replacement_backfills.push(QueuedSubscriberBackfill {
13437 owner: Some(owner.clone()),
13438 epoch: None,
13439 filters: filters.clone(),
13440 backfill,
13441 });
13442 }
13443 let explicit_covers = backfill.is_some_and(|explicit| {
13444 explicit.end_block().is_none()
13445 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
13446 });
13447 let continuity_filters: Vec<_> = filters
13448 .into_iter()
13449 .filter(|filter| !previous_filters.contains(filter))
13450 .collect();
13451 if let Some(anchor) = continuity_anchor
13452 && !continuity_filters.is_empty()
13453 && !explicit_covers
13454 {
13455 replacement_backfills.push(QueuedSubscriberBackfill {
13456 owner: Some(owner.clone()),
13457 epoch: None,
13458 filters: continuity_filters,
13459 backfill: SubscriberBackfill::from_block(anchor),
13460 });
13461 }
13462 let retained_backfills = self
13463 .pending_backfills
13464 .iter()
13465 .filter(|queued| queued.owner.as_ref() != Some(&owner))
13466 .map(|queued| queued.filters.len())
13467 .sum::<usize>();
13468 let replacement_units = replacement_backfills
13469 .iter()
13470 .map(|queued| queued.filters.len())
13471 .sum::<usize>();
13472 if retained_backfills.saturating_add(replacement_units) > self.config.max_pending_backfills
13473 {
13474 return Err(SubscriberError::ResourceExhausted(format!(
13475 "owner update would queue more than {} lazy backfills",
13476 self.config.max_pending_backfills
13477 )));
13478 }
13479
13480 self.owned_interests = next_owned;
13481 self.interests = next_registered;
13482 if let Some(epoch) = replaced_epoch {
13483 self.purge_owner_epoch(&epoch);
13484 } else {
13485 self.recent_compat_owner_input_refs.remove(&owner);
13486 self.recent_compat_owner_input_ref_sets.remove(&owner);
13487 }
13488 self.retire_unreferenced_filters();
13489 self.sources_dirty = true;
13490
13491 self.pending_backfills
13494 .retain(|queued| queued.owner.as_ref() != Some(&owner));
13495 self.pending_backfills.extend(replacement_backfills);
13496 Ok(())
13497 }
13498
13499 fn clone_owned_interests(&self) -> Vec<OwnedSubscriberInterests<N>> {
13500 self.owned_interests
13501 .iter()
13502 .map(|entry| OwnedSubscriberInterests {
13503 owner: entry.owner.clone(),
13504 interests: entry.interests.clone(),
13505 epoch: entry.epoch.clone(),
13506 state: entry.state,
13507 baseline: entry.baseline,
13508 progress: entry.progress.clone(),
13509 progress_stream_revision: entry.progress_stream_revision,
13510 })
13511 .collect()
13512 }
13513
13514 fn rebuild_registered_interests(&mut self) {
13515 self.interests = aggregate_interests(&self.base_interests, &self.owned_interests);
13516 }
13517
13518 fn log_anchor(&self, filter: &Filter) -> Option<u64> {
13521 if let Some(anchor) = self
13522 .log_source_ids
13523 .get(filter)
13524 .and_then(|id| self.last_seen_log_blocks.get(id))
13525 {
13526 return Some(*anchor);
13527 }
13528
13529 self.log_source_ids
13533 .values()
13534 .filter_map(|id| self.last_seen_log_blocks.get(id).copied())
13535 .min()
13536 }
13537
13538 #[allow(clippy::mutable_key_type)]
13545 fn logical_log_filters(&self) -> Vec<Filter> {
13546 let mut filters = log_filters(&self.base_interests);
13547 for entry in &self.owned_interests {
13548 filters.extend(log_filters(&entry.interests));
13549 }
13550 let mut seen = HashSet::new();
13551 filters.retain(|filter| seen.insert(filter.clone()));
13552 filters
13553 }
13554
13555 fn log_stream_filters(&self) -> Vec<Filter> {
13561 let mut merged = Vec::new();
13562 for filter in self.logical_log_filters() {
13563 merge_log_subscription_filter(&mut merged, &filter);
13564 }
13565
13566 let max_addresses = self.config.max_log_addresses_per_subscription.max(1);
13567 let mut planned = Vec::new();
13568 for filter in merged {
13569 let mut addresses: Vec<_> = filter.address.iter().copied().collect();
13570 if addresses.len() <= max_addresses {
13571 planned.push(filter);
13572 continue;
13573 }
13574 addresses.sort_unstable();
13575 for chunk in addresses.chunks(max_addresses) {
13576 let mut split = filter.clone();
13577 split.address = FilterSet::default();
13578 for address in chunk {
13579 split.address.insert(*address);
13580 }
13581 planned.push(split);
13582 }
13583 }
13584 planned
13585 }
13586
13587 #[allow(clippy::mutable_key_type)]
13594 fn retire_unreferenced_filters(&mut self) {
13595 let mut live: HashSet<Filter> = self.log_stream_filters().into_iter().collect();
13596 if let AlloySubscriberState::Active(streams) = &self.state {
13597 for entry in &streams.entries {
13598 match &entry.source {
13599 SubscriberStreamSource::PubSubLog { filter, .. }
13600 | SubscriberStreamSource::BasePendingLog { filter, .. }
13601 | SubscriberStreamSource::PollingLog { filter } => {
13602 live.insert(filter.clone());
13603 }
13604 SubscriberStreamSource::BaseFlashblocks
13605 | SubscriberStreamSource::OpPendingFlashblocks
13606 | SubscriberStreamSource::CanonicalHeadPolling
13607 | SubscriberStreamSource::PubSubPendingHashes
13608 | SubscriberStreamSource::PubSubBlockHeaders
13609 | SubscriberStreamSource::PollingPendingHashes => {}
13610 }
13611 }
13612 }
13613 self.log_source_ids
13614 .retain(|filter, _| live.contains(filter));
13615 let live_ids: HashSet<usize> = self.log_source_ids.values().copied().collect();
13616 self.last_seen_log_blocks
13617 .retain(|id, _| live_ids.contains(id));
13618 }
13619
13620 fn drain_next_scoped_batch(&mut self) -> Option<SubscriberInputBatch<N>> {
13621 if self.pending_records.is_empty()
13622 && self.pending_chain_controls.is_empty()
13623 && !self.pending_preconfirmation_invalidation
13624 {
13625 return None;
13626 }
13627
13628 let first_preconfirmation = self.pending_records.front().and_then(|record| {
13629 if record.scope != SubscriberInputScope::Preconfirmed {
13630 return None;
13631 }
13632 match &record.record.context.chain_status {
13633 ChainStatus::Preconfirmed { flashblock } => Some(flashblock.clone()),
13634 _ => None,
13635 }
13636 });
13637 let len = self
13638 .pending_records
13639 .iter()
13640 .take(self.config.max_batch_size)
13641 .take_while(|record| match &first_preconfirmation {
13642 Some(expected) => {
13643 record.scope == SubscriberInputScope::Preconfirmed
13644 && matches!(
13645 &record.record.context.chain_status,
13646 ChainStatus::Preconfirmed { flashblock } if flashblock == expected
13647 )
13648 }
13649 None => record.scope != SubscriberInputScope::Preconfirmed,
13650 })
13651 .count();
13652 let records = self.pending_records.drain(..len).collect();
13653 let chain_controls = if first_preconfirmation.is_none() && self.pending_records.is_empty() {
13654 self.pending_chain_controls.drain(..).collect()
13655 } else {
13656 Vec::new()
13657 };
13658 Some(SubscriberInputBatch {
13659 records,
13660 chain_id: self.chain_id,
13661 chain_controls,
13662 preconfirmation_invalidated: std::mem::take(
13663 &mut self.pending_preconfirmation_invalidation,
13664 ),
13665 })
13666 }
13667
13668 fn reset_delivery_state(&mut self) {
13669 self.pending_records.clear();
13670 self.pending_chain_controls.clear();
13671 self.pending_reconcile_owner_records.clear();
13672 self.resource_error = None;
13673 self.last_seen_log_blocks.clear();
13674 self.verified_log_blocks.clear();
13675 self.verified_log_block_order.clear();
13676 self.recent_input_refs.clear();
13677 self.recent_input_ref_set.clear();
13678 self.recent_owner_input_refs.clear();
13679 self.recent_owner_input_ref_sets.clear();
13680 self.recent_compat_owner_input_refs.clear();
13681 self.recent_compat_owner_input_ref_sets.clear();
13682 self.pending_backfills.clear();
13683 self.pending_source_backfills.clear();
13684 self.pending_preconfirmation_invalidation = false;
13685 self.pending_flashblock_reconnects.clear();
13686 self.pending_flashblock_reconnect_sources.clear();
13687 self.flashblocks_rpc_metrics = FlashblocksRpcMetrics::default();
13688 self.log_source_ids.clear();
13689 self.next_log_source_id = 0;
13690 self.sources_dirty = true;
13691 self.last_certified_canonical_head = None;
13692 self.reset_flashblock_tracking();
13693 }
13694
13695 fn reset_flashblock_tracking(&mut self) {
13696 self.base_flashblock_header = None;
13697 self.base_flashblock_transactions = None;
13698 self.unmatched_pending_logs.clear();
13699 self.latest_preconfirmation = None;
13700 self.preconfirmed_seen_logs.clear();
13701 self.preconfirmed_receipted_transactions.clear();
13702 self.preconfirmed_unavailable_receipts.clear();
13703 self.consecutive_flashblock_poll_failures = 0;
13704 }
13705
13706 fn invalidate_preconfirmation_snapshot(&mut self) {
13713 self.pending_records
13714 .retain(|record| record.scope != SubscriberInputScope::Preconfirmed);
13715 self.pending_preconfirmation_invalidation = true;
13716 self.latest_preconfirmation = None;
13717 self.preconfirmed_seen_logs.clear();
13718 self.preconfirmed_receipted_transactions.clear();
13719 self.preconfirmed_unavailable_receipts.clear();
13720 }
13721
13722 fn bump_stream_revision(&mut self) {
13723 self.stream_revision = self.stream_revision.saturating_add(1);
13724 }
13725}
13726
13727impl<P, N> InterestOwnerSubscriber<N> for AlloySubscriber<P, N>
13728where
13729 P: Provider<N> + Send + Sync,
13730 N: Network + 'static,
13731 N::HeaderResponse: Send + 'static,
13732{
13733 fn upsert_interest_owners(
13734 &mut self,
13735 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13736 ) -> SubscriberOperation<'_, ()> {
13737 Box::pin(async move {
13738 if !owners.is_empty() {
13739 self.ensure_chain_id().await?;
13740 }
13741 AlloySubscriber::upsert_interest_owners(self, owners)
13742 })
13743 }
13744
13745 fn replace_interest_owners(
13746 &mut self,
13747 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13748 ) -> SubscriberOperation<'_, ()> {
13749 Box::pin(async move {
13750 if owners.iter().any(|(_, interests)| !interests.is_empty()) {
13751 self.ensure_chain_id().await?;
13752 }
13753 AlloySubscriber::replace_interest_owners(self, owners)
13754 })
13755 }
13756
13757 fn replace_interest_owners_with_global_backfill(
13758 &mut self,
13759 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13760 backfill: SubscriberBackfill,
13761 ) -> SubscriberOperation<'_, ()> {
13762 Box::pin(async move {
13763 if owners.iter().any(|(_, interests)| !interests.is_empty()) {
13764 self.ensure_chain_id().await?;
13765 }
13766 AlloySubscriber::replace_interest_owners_with_global_backfill(self, owners, backfill)
13767 })
13768 }
13769
13770 fn add_interest_owner(
13771 &mut self,
13772 owner: HandlerId,
13773 interests: &[ReactiveInterest<N>],
13774 ) -> SubscriberOperation<'_, ()> {
13775 let interests = interests.to_vec();
13776 Box::pin(async move {
13777 if !interests.is_empty() {
13778 self.ensure_chain_id().await?;
13779 }
13780 AlloySubscriber::add_interest_owner(self, owner, &interests)
13781 })
13782 }
13783
13784 fn add_interest_owner_with_backfill(
13785 &mut self,
13786 owner: HandlerId,
13787 interests: &[ReactiveInterest<N>],
13788 backfill: SubscriberBackfill,
13789 ) -> SubscriberOperation<'_, ()> {
13790 let interests = interests.to_vec();
13791 Box::pin(async move {
13792 if !interests.is_empty() {
13793 self.ensure_chain_id().await?;
13794 }
13795 AlloySubscriber::add_interest_owner_with_backfill(self, owner, &interests, backfill)
13796 })
13797 }
13798
13799 fn add_interest_owner_with_canonical_catchup(
13800 &mut self,
13801 owner: HandlerId,
13802 interests: &[ReactiveInterest<N>],
13803 retained: BlockRef,
13804 ) -> SubscriberOperation<'_, ()> {
13805 let interests = interests.to_vec();
13806 Box::pin(async move {
13807 self.ensure_chain_id().await?;
13810 AlloySubscriber::add_interest_owner_with_canonical_catchup(
13811 self, owner, &interests, retained,
13812 )
13813 })
13814 }
13815
13816 fn remove_interest_owner(
13817 &mut self,
13818 owner: &HandlerId,
13819 ) -> SubscriberOperation<'_, Option<Vec<ReactiveInterest<N>>>> {
13820 let owner = owner.clone();
13821 Box::pin(async move { Ok(AlloySubscriber::remove_interest_owner(self, &owner)) })
13822 }
13823
13824 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
13825 AlloySubscriber::owner_interests(self, owner)
13826 }
13827}
13828
13829enum AlloySubscriberState<N: Network> {
13830 Uninitialized,
13831 Active(SubscriberStreams<N>),
13832 Empty,
13833}
13834
13835struct SubscriberStreams<N: Network> {
13836 entries: Vec<SubscriberStreamEntry<N>>,
13837 next_index: usize,
13838}
13839
13840struct SubscriberStreamEntry<N: Network> {
13841 source: SubscriberStreamSource,
13842 stream: BoxStream<'static, SubscriberEvent<N>>,
13843}
13844
13845impl<N: Network> SubscriberStreams<N> {
13846 fn new() -> Self {
13847 Self {
13848 entries: Vec::new(),
13849 next_index: 0,
13850 }
13851 }
13852
13853 fn is_empty(&self) -> bool {
13854 self.entries.is_empty()
13855 }
13856
13857 fn push(
13858 &mut self,
13859 source: SubscriberStreamSource,
13860 stream: BoxStream<'static, SubscriberEvent<N>>,
13861 ) {
13862 self.entries.push(SubscriberStreamEntry { source, stream });
13863 }
13864
13865 #[cfg(test)]
13866 fn len(&self) -> usize {
13867 self.entries.len()
13868 }
13869
13870 fn contains_source(&self, source: &SubscriberStreamSource) -> bool {
13871 self.entries
13872 .iter()
13873 .any(|entry| entry.source.same_key(source))
13874 }
13875
13876 fn retain_sources(&mut self, sources: &[SubscriberStreamSource]) {
13877 self.entries
13878 .retain(|entry| sources.iter().any(|source| entry.source.same_key(source)));
13879 self.normalize_next_index();
13880 }
13881
13882 fn normalize_next_index(&mut self) {
13883 if self.entries.is_empty() {
13884 self.next_index = 0;
13885 } else if self.next_index >= self.entries.len() {
13886 self.next_index %= self.entries.len();
13887 }
13888 }
13889
13890 async fn next(&mut self) -> Option<SubscriberEvent<N>> {
13891 poll_fn(|cx| {
13892 self.normalize_next_index();
13893 if self.entries.is_empty() {
13894 return std::task::Poll::Ready(None);
13895 }
13896
13897 let mut index = self.next_index;
13898 let mut checked = 0usize;
13899 while checked < self.entries.len() {
13900 if index >= self.entries.len() {
13901 index = 0;
13902 }
13903 match self.entries[index].stream.as_mut().poll_next(cx) {
13904 std::task::Poll::Ready(Some(event)) => {
13905 if matches!(event, SubscriberEvent::StreamTerminated(_)) {
13906 self.entries.remove(index);
13907 self.next_index = if self.entries.is_empty() {
13908 0
13909 } else {
13910 index % self.entries.len()
13911 };
13912 } else {
13913 self.next_index = (index + 1) % self.entries.len();
13914 }
13915 return std::task::Poll::Ready(Some(event));
13916 }
13917 std::task::Poll::Ready(None) => {
13918 self.entries.remove(index);
13919 if self.entries.is_empty() {
13920 self.next_index = 0;
13921 return std::task::Poll::Ready(None);
13922 }
13923 }
13924 std::task::Poll::Pending => {
13925 checked += 1;
13926 index += 1;
13927 }
13928 }
13929 }
13930
13931 if self.entries.is_empty() {
13932 std::task::Poll::Ready(None)
13933 } else {
13934 self.next_index = index % self.entries.len();
13935 std::task::Poll::Pending
13936 }
13937 })
13938 .await
13939 }
13940}
13941
13942#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13943#[allow(dead_code)]
13944enum SubscriberTransport {
13945 PubSub,
13946 Polling,
13947}
13948
13949#[derive(Clone, Debug)]
13950enum SubscriberStreamSource {
13951 PubSubLog { id: usize, filter: Filter },
13952 BasePendingLog { id: usize, filter: Filter },
13953 BaseFlashblocks,
13954 OpPendingFlashblocks,
13955 CanonicalHeadPolling,
13956 PubSubPendingHashes,
13957 PubSubBlockHeaders,
13958 PollingLog { filter: Filter },
13959 PollingPendingHashes,
13960}
13961
13962impl SubscriberStreamSource {
13963 fn label(&self) -> &'static str {
13964 match self {
13965 Self::PubSubLog { .. } => "pubsub log",
13966 Self::BasePendingLog { .. } => "OP Stack pendingLogs",
13967 Self::BaseFlashblocks => "OP Stack newFlashblocks",
13968 Self::OpPendingFlashblocks => "Optimism pending Flashblocks",
13969 Self::CanonicalHeadPolling => "certified canonical head",
13970 Self::PubSubPendingHashes => "pubsub pending transaction hash",
13971 Self::PubSubBlockHeaders => "pubsub block header",
13972 Self::PollingLog { .. } => "polling log",
13973 Self::PollingPendingHashes => "polling pending transaction hash",
13974 }
13975 }
13976
13977 fn is_pubsub(&self) -> bool {
13978 matches!(
13979 self,
13980 Self::PubSubLog { .. }
13981 | Self::BasePendingLog { .. }
13982 | Self::BaseFlashblocks
13983 | Self::OpPendingFlashblocks
13984 | Self::PubSubPendingHashes
13985 | Self::PubSubBlockHeaders
13986 )
13987 }
13988
13989 fn is_flashblocks(&self) -> bool {
13990 matches!(
13991 self,
13992 Self::BasePendingLog { .. } | Self::BaseFlashblocks | Self::OpPendingFlashblocks
13993 )
13994 }
13995
13996 fn same_key(&self, other: &Self) -> bool {
13997 match (self, other) {
13998 (Self::PubSubLog { filter: left, .. }, Self::PubSubLog { filter: right, .. })
13999 | (
14000 Self::BasePendingLog { filter: left, .. },
14001 Self::BasePendingLog { filter: right, .. },
14002 )
14003 | (Self::PollingLog { filter: left }, Self::PollingLog { filter: right }) => {
14004 left == right
14005 }
14006 (Self::BaseFlashblocks, Self::BaseFlashblocks)
14007 | (Self::OpPendingFlashblocks, Self::OpPendingFlashblocks)
14008 | (Self::CanonicalHeadPolling, Self::CanonicalHeadPolling)
14009 | (Self::PubSubPendingHashes, Self::PubSubPendingHashes)
14010 | (Self::PubSubBlockHeaders, Self::PubSubBlockHeaders)
14011 | (Self::PollingPendingHashes, Self::PollingPendingHashes) => true,
14012 _ => false,
14013 }
14014 }
14015}
14016
14017#[allow(dead_code)]
14018enum SubscriberEvent<N: Network> {
14019 Log {
14020 source_id: usize,
14021 log: Log,
14022 },
14023 BackfilledLogs {
14024 source_id: usize,
14025 logs: Vec<Log>,
14026 },
14027 Logs(Vec<Log>),
14028 BlockHeader(N::HeaderResponse),
14029 PendingHash(B256),
14030 PendingHashes(Vec<B256>),
14031 BasePendingLog {
14032 source_id: usize,
14033 log: Log,
14034 },
14035 BaseFlashblock(BaseFlashblockWirePayload),
14036 OpFlashblockTick,
14037 CanonicalHeadTick,
14038 PreconfirmedLogs {
14039 flashblock: FlashblockRef,
14040 logs: Vec<Log>,
14041 },
14042 FlashblockInvalidated,
14043 FlashblockObserved,
14044 StreamTerminated(SubscriberStreamSource),
14045}
14046
14047enum SubscriberReady<N: Network> {
14048 Event(Option<SubscriberEvent<N>>),
14049 FlashblockReconnect(
14050 SubscriberStreamSource,
14051 Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>,
14052 ),
14053}
14054
14055#[derive(Debug)]
14056enum PendingFlashblockPollError {
14057 Request(SubscriberError),
14058 Integrity(SubscriberError),
14059}
14060
14061impl PendingFlashblockPollError {
14062 fn into_subscriber(self) -> SubscriberError {
14063 match self {
14064 Self::Request(error) | Self::Integrity(error) => error,
14065 }
14066 }
14067}
14068
14069fn pending_flashblock_request_error(error: impl fmt::Display) -> PendingFlashblockPollError {
14070 PendingFlashblockPollError::Request(provider_error(error))
14071}
14072
14073fn normalize_op_pending_block<N: Network>(
14074 mut value: serde_json::Value,
14075) -> Result<N::BlockResponse, SubscriberError> {
14076 let object = value.as_object_mut().ok_or_else(|| {
14077 SubscriberError::Provider("OP pending block response is not an object".into())
14078 })?;
14079 let transactions = object
14080 .get_mut("transactions")
14081 .and_then(serde_json::Value::as_array_mut)
14082 .ok_or_else(|| {
14083 SubscriberError::Provider(
14084 "OP pending block response is missing its transaction array".into(),
14085 )
14086 })?;
14087 for transaction in transactions {
14088 if transaction.is_string() {
14089 continue;
14090 }
14091 let hash = transaction
14092 .as_object()
14093 .and_then(|object| object.get("hash"))
14094 .filter(|hash| hash.is_string())
14095 .cloned()
14096 .ok_or_else(|| {
14097 SubscriberError::Provider("OP pending block transaction is missing its hash".into())
14098 })?;
14099 *transaction = hash;
14100 }
14101 if object.get("hash").is_none_or(serde_json::Value::is_null) {
14102 object.insert(
14103 "hash".into(),
14104 serde_json::Value::String(B256::ZERO.to_string()),
14105 );
14106 }
14107 if object.get("nonce").is_none_or(serde_json::Value::is_null) {
14108 object.insert(
14109 "nonce".into(),
14110 serde_json::Value::String("0x0000000000000000".into()),
14111 );
14112 }
14113 if object.get("miner").is_none_or(serde_json::Value::is_null)
14114 || object
14115 .get("beneficiary")
14116 .is_none_or(serde_json::Value::is_null)
14117 {
14118 object.insert(
14119 "miner".into(),
14120 serde_json::Value::String(Address::ZERO.to_string()),
14121 );
14122 }
14123 serde_json::from_value(value).map_err(|error| {
14124 SubscriberError::Provider(format!(
14125 "failed to decode normalized OP pending block: {error}"
14126 ))
14127 })
14128}
14129
14130fn normalize_pending_transaction_receipt(
14131 expected_transaction_hash: B256,
14132 value: serde_json::Value,
14133) -> Result<Option<Vec<Log>>, SubscriberError> {
14134 if value.is_null() {
14135 return Ok(None);
14136 }
14137 let receipt = value.as_object().ok_or_else(|| {
14138 SubscriberError::Provider("pending transaction receipt response is not an object".into())
14139 })?;
14140 let transaction_hash: B256 =
14141 serde_json::from_value(receipt.get("transactionHash").cloned().ok_or_else(|| {
14142 SubscriberError::Provider(
14143 "pending transaction receipt is missing its transaction hash".into(),
14144 )
14145 })?)
14146 .map_err(|error| {
14147 SubscriberError::Provider(format!(
14148 "failed to decode pending transaction receipt hash: {error}"
14149 ))
14150 })?;
14151 if transaction_hash != expected_transaction_hash {
14152 return Err(SubscriberError::Provider(
14153 "pending transaction receipt hash disagrees with its request".into(),
14154 ));
14155 }
14156 let receipt_logs = receipt
14157 .get("logs")
14158 .and_then(serde_json::Value::as_array)
14159 .ok_or_else(|| {
14160 SubscriberError::Provider("pending transaction receipt is missing its log array".into())
14161 })?;
14162 let mut logs = Vec::new();
14163 for log in receipt_logs {
14164 let log: Log = serde_json::from_value(log.clone()).map_err(|error| {
14165 SubscriberError::Provider(format!(
14166 "failed to decode pending transaction receipt log: {error}"
14167 ))
14168 })?;
14169 if log.transaction_hash != Some(expected_transaction_hash) {
14170 return Err(SubscriberError::Provider(
14171 "pending transaction receipt log hash disagrees with its receipt".into(),
14172 ));
14173 }
14174 logs.push(log);
14175 }
14176 Ok(Some(logs))
14177}
14178
14179impl<P, N> EventSubscriber<N> for AlloySubscriber<P, N>
14180where
14181 P: Provider<N> + Send + Sync,
14182 N: Network + 'static,
14183 N::HeaderResponse: Send + 'static,
14184{
14185 fn chain_id(&self) -> Option<u64> {
14186 self.chain_id
14187 }
14188
14189 fn capabilities(&self) -> SubscriberCapabilities {
14190 let Ok(transport) = resolve_subscriber_transport(self.mode) else {
14191 return SubscriberCapabilities::default();
14192 };
14193 let mut capabilities = vec![
14194 SubscriberCapability::Logs,
14195 SubscriberCapability::PendingTransactionHashes,
14196 SubscriberCapability::HistoricalBackfill,
14197 SubscriberCapability::Live,
14198 SubscriberCapability::OwnerScopedDelivery,
14199 SubscriberCapability::DynamicInterests,
14200 ];
14201 if transport == SubscriberTransport::PubSub {
14202 capabilities.push(SubscriberCapability::BlockHeaders);
14203 }
14204 if self.config.preconfirmations != PreconfirmationMode::Disabled
14205 && self.provider_ref.is_some()
14206 && self.chain_id.and_then(flashblocks_adapter).is_some()
14207 {
14208 capabilities.push(SubscriberCapability::Preconfirmations);
14209 }
14210 SubscriberCapabilities::new(capabilities)
14211 }
14212
14213 fn register_interests(
14214 &mut self,
14215 interests: &[ReactiveInterest<N>],
14216 ) -> SubscriberOperation<'_, ()> {
14217 let interests = interests.to_vec();
14218 Box::pin(async move {
14219 validate_subscriber_config(&self.config)?;
14220 validate_supported_interests(self.mode, &self.config, &interests)?;
14221 if !interests.is_empty() {
14222 self.ensure_chain_id().await?;
14223 }
14224 self.validate_flashblocks_setup()?;
14225
14226 self.base_interests = interests;
14227 self.owned_interests.clear();
14228 self.rebuild_registered_interests();
14229 self.reset_delivery_state();
14230 self.state = AlloySubscriberState::Uninitialized;
14231 Ok(())
14232 })
14233 }
14234
14235 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N> {
14236 Box::pin(async {
14237 Ok(self
14238 .next_scoped_batch()
14239 .await?
14240 .map(SubscriberInputBatch::into_reactive_batch))
14241 })
14242 }
14243}
14244
14245impl<P, N> AlloySubscriber<P, N>
14246where
14247 P: Provider<N> + Send + Sync,
14248 N: Network + 'static,
14249 N::HeaderResponse: Send + 'static,
14250{
14251 pub async fn establish_flashblocks_preflight(
14266 &mut self,
14267 expected_chain_id: u64,
14268 ) -> Result<FlashblocksPreflight, SubscriberError> {
14269 validate_subscriber_config(&self.config)?;
14270 if self.config.preconfirmations == PreconfirmationMode::Disabled {
14271 return Err(SubscriberError::InvalidConfig(
14272 "Flashblocks preflight requires preconfirmations",
14273 ));
14274 }
14275 if !self
14276 .interests
14277 .iter()
14278 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)))
14279 {
14280 return Err(SubscriberError::InvalidConfig(
14281 "Flashblocks preflight requires at least one active log interest",
14282 ));
14283 }
14284 let chain_id = self.ensure_chain_id().await?;
14285 if chain_id != expected_chain_id {
14286 return Err(SubscriberError::ChainMismatch {
14287 expected: expected_chain_id,
14288 actual: chain_id,
14289 });
14290 }
14291 self.validate_flashblocks_setup()?;
14292 let adapter = flashblocks_adapter(chain_id).ok_or(SubscriberError::Unsupported(
14293 "Flashblocks are currently implemented for Base and OP chains",
14294 ))?;
14295 let provider = self
14296 .provider_ref
14297 .clone()
14298 .ok_or(SubscriberError::InvalidConfig(
14299 "Flashblocks preflight requires a stable provider ref",
14300 ))?;
14301 self.flashblocks_rpc_metrics.capability_requests = self
14302 .flashblocks_rpc_metrics
14303 .capability_requests
14304 .saturating_add(1);
14305 let capability_provider = if adapter == FlashblocksAdapter::PendingStatePolling {
14306 self.flashblocks_state_provider
14307 .as_ref()
14308 .unwrap_or(&self.provider)
14309 } else {
14310 &self.provider
14311 };
14312 let advertised_capabilities = capability_provider
14313 .client()
14314 .request::<_, serde_json::Value>("op_supportedCapabilities", ())
14315 .await
14316 .ok();
14317
14318 self.ensure_streams().await?;
14319 let pending_log_filters = self.log_stream_filters();
14320 if adapter == FlashblocksAdapter::PendingStatePolling
14321 && self.pending_receipt_requests_per_tick_capacity() == 0
14322 {
14323 return Err(SubscriberError::InvalidConfig(
14324 "Flashblocks RPC budget leaves no capacity for OP transaction receipts",
14325 ));
14326 }
14327 let (delivery, pending_log_subscriptions) = match adapter {
14328 FlashblocksAdapter::NativeSubscriptions => {
14329 if resolve_subscriber_transport(self.mode)? != SubscriberTransport::PubSub {
14330 return Err(SubscriberError::Unsupported(
14331 "Base Flashblocks preflight requires pubsub",
14332 ));
14333 }
14334 let pending_sources = self
14335 .pubsub_stream_sources()
14336 .into_iter()
14337 .filter(|source| {
14338 matches!(source, SubscriberStreamSource::BasePendingLog { .. })
14339 })
14340 .collect::<Vec<_>>();
14341 let AlloySubscriberState::Active(streams) = &self.state else {
14342 return Err(SubscriberError::Provider(
14343 "Flashblocks preflight subscriptions did not become active".to_owned(),
14344 ));
14345 };
14346 if !streams.contains_source(&SubscriberStreamSource::BaseFlashblocks)
14347 || pending_sources
14348 .iter()
14349 .any(|source| !streams.contains_source(source))
14350 {
14351 return Err(SubscriberError::Provider(
14352 "Base Flashblocks preflight did not retain both subscription lanes"
14353 .to_owned(),
14354 ));
14355 }
14356 (
14357 FlashblocksDelivery::NativeSubscriptions,
14358 pending_sources.len(),
14359 )
14360 }
14361 FlashblocksAdapter::PendingStatePolling => {
14362 if let Some(state_provider) = self.flashblocks_state_provider.as_ref() {
14363 self.flashblocks_rpc_metrics.provider_pair_chain_requests = self
14364 .flashblocks_rpc_metrics
14365 .provider_pair_chain_requests
14366 .saturating_add(1);
14367 let actual = state_provider
14368 .get_chain_id()
14369 .await
14370 .map_err(provider_error)?;
14371 if actual != expected_chain_id {
14372 return Err(SubscriberError::ChainMismatch {
14373 expected: expected_chain_id,
14374 actual,
14375 });
14376 }
14377 }
14378 let AlloySubscriberState::Active(streams) = &self.state else {
14379 return Err(SubscriberError::Provider(
14380 "Flashblocks preflight streams did not become active".to_owned(),
14381 ));
14382 };
14383 if !streams.contains_source(&SubscriberStreamSource::OpPendingFlashblocks) {
14384 return Err(SubscriberError::Provider(
14385 "Optimism Flashblocks preflight did not retain its pending-state sampler"
14386 .to_owned(),
14387 ));
14388 }
14389 self.probe_pending_state(&pending_log_filters).await?;
14390 (FlashblocksDelivery::PendingStatePolling, 0)
14391 }
14392 };
14393 Ok(FlashblocksPreflight {
14394 chain_id,
14395 provider,
14396 delivery,
14397 pending_log_subscriptions,
14398 pending_log_filters: pending_log_filters.len(),
14399 advertised_capabilities,
14400 })
14401 }
14402
14403 async fn probe_pending_state(&mut self, filters: &[Filter]) -> Result<(), SubscriberError> {
14404 self.flashblocks_rpc_metrics.pending_block_requests = self
14405 .flashblocks_rpc_metrics
14406 .pending_block_requests
14407 .saturating_add(1);
14408 let pending = self
14409 .fetch_op_pending_block()
14410 .await
14411 .map_err(PendingFlashblockPollError::into_subscriber)?
14412 .ok_or_else(|| {
14413 SubscriberError::Provider(
14414 "provider returned no pending block during Flashblocks preflight".into(),
14415 )
14416 })?;
14417 self.certify_op_pending_parent(&pending)
14418 .await
14419 .map_err(PendingFlashblockPollError::into_subscriber)?;
14420 for filter in filters {
14421 self.flashblocks_rpc_metrics.pending_log_requests = self
14422 .flashblocks_rpc_metrics
14423 .pending_log_requests
14424 .saturating_add(1);
14425 self.flashblocks_state_provider
14426 .as_ref()
14427 .unwrap_or(&self.provider)
14428 .get_logs(
14429 &filter
14430 .clone()
14431 .from_block(BlockNumberOrTag::Latest)
14432 .to_block(BlockNumberOrTag::Pending),
14433 )
14434 .await
14435 .map_err(provider_error)?;
14436 }
14437 self.flashblocks_rpc_metrics.pending_receipt_requests = self
14438 .flashblocks_rpc_metrics
14439 .pending_receipt_requests
14440 .saturating_add(1);
14441 let _: serde_json::Value = self
14442 .flashblocks_state_provider
14443 .as_ref()
14444 .unwrap_or(&self.provider)
14445 .raw_request(Cow::Borrowed("eth_getTransactionReceipt"), (B256::ZERO,))
14446 .await
14447 .map_err(provider_error)?;
14448 Ok(())
14449 }
14450
14451 async fn certify_op_pending_parent(
14452 &mut self,
14453 pending: &N::BlockResponse,
14454 ) -> Result<N::HeaderResponse, PendingFlashblockPollError> {
14455 let pending_header = pending.header();
14456 let pending_number = pending_header.number();
14457 let parent_hash = pending_header.parent_hash();
14458 if pending_number == 0 || parent_hash.is_zero() {
14459 return Err(PendingFlashblockPollError::Integrity(
14460 SubscriberError::Provider(
14461 "OP pending block omitted a certifiable canonical parent".into(),
14462 ),
14463 ));
14464 }
14465 self.flashblocks_rpc_metrics.canonical_head_requests = self
14466 .flashblocks_rpc_metrics
14467 .canonical_head_requests
14468 .saturating_add(1);
14469 let parent = self
14470 .flashblocks_state_provider
14471 .as_ref()
14472 .unwrap_or(&self.provider)
14473 .get_block_by_hash(parent_hash)
14474 .await
14475 .map_err(pending_flashblock_request_error)?
14476 .ok_or_else(|| {
14477 PendingFlashblockPollError::Request(SubscriberError::Provider(
14478 "Flashblocks provider returned no exact OP pending parent block".into(),
14479 ))
14480 })?;
14481 let parent_header = parent.header();
14482 if parent_header.hash() != parent_hash
14483 || parent_header.number().checked_add(1) != Some(pending_number)
14484 {
14485 return Err(PendingFlashblockPollError::Integrity(
14486 SubscriberError::Provider(
14487 "OP pending block does not extend its exact certified parent".into(),
14488 ),
14489 ));
14490 }
14491 Ok(parent_header.clone())
14492 }
14493
14494 async fn fetch_op_pending_block(
14495 &mut self,
14496 ) -> Result<Option<N::BlockResponse>, PendingFlashblockPollError> {
14497 let state_provider = self
14498 .flashblocks_state_provider
14499 .as_ref()
14500 .unwrap_or(&self.provider);
14501 let value: Option<serde_json::Value> = state_provider
14502 .raw_request(
14503 Cow::Borrowed("eth_getBlockByNumber"),
14504 (BlockNumberOrTag::Pending, true),
14505 )
14506 .await
14507 .map_err(pending_flashblock_request_error)?;
14508 value
14509 .map(normalize_op_pending_block::<N>)
14510 .transpose()
14511 .map_err(PendingFlashblockPollError::Integrity)
14512 }
14513
14514 async fn ensure_chain_id(&mut self) -> Result<u64, SubscriberError> {
14518 if let Some(chain_id) = self.chain_id {
14519 return Ok(chain_id);
14520 }
14521 let chain_id = self.provider.get_chain_id().await.map_err(provider_error)?;
14522 self.chain_id = Some(chain_id);
14523 Ok(chain_id)
14524 }
14525
14526 fn validate_flashblocks_setup(&self) -> Result<(), SubscriberError> {
14527 if self.config.preconfirmations == PreconfirmationMode::Disabled {
14528 return Ok(());
14529 }
14530 if self.provider_ref.is_none() {
14531 return Err(SubscriberError::InvalidConfig(
14532 "Flashblocks require a stable provider ref from a pinned provider lease",
14533 ));
14534 }
14535 let Some(chain_id) = self.chain_id else {
14536 return Ok(());
14537 };
14538 match flashblocks_adapter(chain_id) {
14539 Some(FlashblocksAdapter::NativeSubscriptions)
14540 if resolve_subscriber_transport(self.mode)? != SubscriberTransport::PubSub
14541 && self.config.preconfirmations == PreconfirmationMode::Required =>
14542 {
14543 return Err(SubscriberError::Unsupported(
14544 "Base Flashblocks require pubsub for newFlashblocks and pendingLogs",
14545 ));
14546 }
14547 Some(FlashblocksAdapter::NativeSubscriptions) => {}
14548 Some(_) => {}
14549 None if self.config.preconfirmations == PreconfirmationMode::Required => {
14550 return Err(SubscriberError::Unsupported(
14551 "Flashblocks are currently implemented for Base and OP chains",
14552 ));
14553 }
14554 None => {}
14555 }
14556 Ok(())
14557 }
14558
14559 pub async fn reconcile_interest_owner(
14573 &mut self,
14574 epoch: &SubscriberOwnerEpoch,
14575 through: BlockRef,
14576 ) -> Result<SubscriberOwnerProgress, SubscriberOwnerError>
14577 where
14578 P: Clone,
14579 {
14580 self.reconcile_interest_owners(std::slice::from_ref(epoch), through)
14581 .await?
14582 .pop()
14583 .ok_or(SubscriberOwnerError::NotStaged)
14584 }
14585
14586 pub async fn reconcile_interest_owners(
14610 &mut self,
14611 epochs: &[SubscriberOwnerEpoch],
14612 through: BlockRef,
14613 ) -> Result<Vec<SubscriberOwnerProgress>, SubscriberOwnerError>
14614 where
14615 P: Clone,
14616 {
14617 if epochs.is_empty() {
14618 return Ok(Vec::new());
14619 }
14620
14621 self.ensure_chain_id().await?;
14622
14623 let mut seen = HashSet::new();
14624 let mut plans = Vec::with_capacity(epochs.len());
14625 for epoch in epochs {
14626 if !seen.insert(epoch.clone()) {
14627 continue;
14628 }
14629 let entry = self
14630 .owned_interests
14631 .iter()
14632 .find(|entry| {
14633 entry.epoch.as_ref() == Some(epoch)
14634 && entry.state == SubscriberOwnerState::Staged
14635 })
14636 .ok_or(SubscriberOwnerError::NotStaged)?;
14637 let position = entry
14638 .progress
14639 .as_ref()
14640 .map(|progress| &progress.through)
14641 .or(entry.baseline.as_ref())
14642 .ok_or(SubscriberOwnerError::MissingBaseline)?;
14643 let baseline = position.number;
14644 if through.number < baseline {
14645 return Err(SubscriberOwnerError::ProgressRegression {
14646 current: baseline,
14647 target: through.number,
14648 });
14649 }
14650 let from_block = baseline
14651 .checked_add(1)
14652 .ok_or(SubscriberOwnerError::PostBlockOverflow(baseline))?;
14653 if through.number == baseline && through.hash != position.hash {
14654 return Err(SubscriberOwnerError::ProgressConflict {
14655 number: baseline,
14656 current_hash: position.hash,
14657 target_hash: through.hash,
14658 });
14659 }
14660 if through.number == from_block
14661 && through
14662 .parent_hash
14663 .is_some_and(|parent| parent != position.hash)
14664 {
14665 return Err(SubscriberOwnerError::ProgressConflict {
14666 number: baseline,
14667 current_hash: position.hash,
14668 target_hash: through.parent_hash.expect("checked as present above"),
14669 });
14670 }
14671 if entry
14672 .interests
14673 .iter()
14674 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
14675 {
14676 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
14677 }
14678 plans.push(SubscriberOwnerReconcilePlan {
14679 epoch: epoch.clone(),
14680 interests: entry.interests.clone(),
14681 retained: *position,
14682 from_block,
14683 });
14684 }
14685
14686 self.ensure_streams().await?;
14689 let provider = self.provider.clone();
14690 let filters = merged_owner_reconcile_filters(&plans, through.number);
14691 let retained = plans.iter().map(|plan| plan.retained).collect();
14692 let target_epochs: HashSet<_> = plans.iter().map(|plan| plan.epoch.clone()).collect();
14693 let fetch = fetch_owner_catchup::<P, N>(
14694 provider,
14695 filters,
14696 retained,
14697 through,
14698 SubscriberOwnerCatchupOptions {
14699 target_preverified: false,
14700 max_logs: self.config.max_pending_records,
14701 max_log_bytes: self.config.max_backfill_log_bytes,
14702 max_requests_in_flight: self.config.max_reconcile_requests_in_flight,
14703 },
14704 );
14705 let SubscriberOwnerCatchup { logs, certified } =
14706 self.drive_reconcile_fetch(fetch, &target_epochs).await?;
14707
14708 let records = logs
14709 .into_iter()
14710 .map(|log| log_input_record(log, InputSource::Backfill))
14711 .collect();
14712 let mut routed_records = Vec::new();
14713 for record in dedupe_records(sort_records(records)).map_err(|error| {
14714 SubscriberError::InvalidBackfill(format!(
14715 "conflicting duplicate owner catch-up record: {error}"
14716 ))
14717 })? {
14718 let block_number = match &record.input {
14719 ReactiveInput::Log(log) => log
14720 .block_number
14721 .expect("bulk catch-up logs were validated before commit"),
14722 _ => unreachable!("bulk owner catch-up contains log records only"),
14723 };
14724 let owners: Vec<SubscriberOwnerEpoch> = plans
14725 .iter()
14726 .filter(|plan| block_number >= plan.from_block)
14727 .filter(|plan| {
14728 plan.interests
14729 .iter()
14730 .any(|interest| interest_matches(interest, &record.input))
14731 })
14732 .map(|plan| plan.epoch.clone())
14733 .collect();
14734 if !owners.is_empty() {
14735 routed_records.push((record, owners));
14736 }
14737 }
14738 self.ensure_pending_record_capacity(
14739 routed_records.len(),
14740 "owner reconciliation historical records",
14741 )?;
14742
14743 self.pending_backfills.retain(|queued| {
14747 queued
14748 .epoch
14749 .as_ref()
14750 .is_none_or(|epoch| !target_epochs.contains(epoch))
14751 });
14752 for (record, owners) in routed_records {
14753 self.enqueue_owner_record_for_owners_unmerged(record, owners);
14754 }
14755 self.promote_reconcile_owner_records(&target_epochs);
14756 self.seed_reconciled_filter_anchors(&plans, certified.number);
14757
14758 let stream_revision = self.stream_revision;
14759 let mut progress = Vec::with_capacity(plans.len());
14760 for plan in plans {
14761 let item = SubscriberOwnerProgress {
14762 owner: plan.epoch.clone(),
14763 through: certified,
14764 };
14765 let entry = self
14766 .owned_interests
14767 .iter_mut()
14768 .find(|entry| entry.epoch.as_ref() == Some(&plan.epoch))
14769 .expect("bulk reconcile holds exclusive access after epoch preflight");
14770 entry.progress = Some(item.clone());
14771 entry.progress_stream_revision = Some(stream_revision);
14772 progress.push(item);
14773 }
14774 Ok(progress)
14775 }
14776
14777 async fn drive_reconcile_fetch<T, F>(
14778 &mut self,
14779 fetch: F,
14780 target_epochs: &HashSet<SubscriberOwnerEpoch>,
14781 ) -> Result<T, SubscriberOwnerError>
14782 where
14783 F: Future<Output = Result<T, SubscriberOwnerError>>,
14784 {
14785 if !matches!(&self.state, AlloySubscriberState::Active(_)) {
14786 return fetch.await;
14787 }
14788 let mut fetch = Box::pin(fetch);
14789 loop {
14790 let event = {
14791 let live = Box::pin(self.next_event());
14792 match select(fetch, live).await {
14793 Either::Left((result, pending_live)) => {
14794 drop(pending_live);
14795 return result;
14796 }
14797 Either::Right((event, pending_fetch)) => {
14798 fetch = pending_fetch;
14799 event
14800 }
14801 }
14802 };
14803 let event = event?.ok_or_else(|| {
14804 SubscriberError::Provider(
14805 "Alloy subscriber streams ended during owner reconcile".to_owned(),
14806 )
14807 })?;
14808 self.buffer_reconcile_event_for_owners(&event, target_epochs);
14809 self.enqueue_event_excluding_owners(event, target_epochs);
14810 self.check_resource_error()?;
14811 }
14812 }
14813
14814 pub async fn next_scoped_batch_or<C, F>(
14833 &mut self,
14834 control: Pin<&mut F>,
14835 ) -> Result<SubscriberDriverPoll<C, N>, SubscriberError>
14836 where
14837 C: Send,
14838 F: Future<Output = C> + Send,
14839 {
14840 let batch = self.next_scoped_batch();
14841 match select(control, batch).await {
14842 Either::Left((control, pending_batch)) => {
14843 drop(pending_batch);
14844 Ok(SubscriberDriverPoll::Control(control))
14845 }
14846 Either::Right((batch, _pending_control)) => batch.map(SubscriberDriverPoll::Batch),
14847 }
14848 }
14849
14850 pub fn next_scoped_batch(&mut self) -> SubscriberNextScopedBatch<'_, N> {
14861 Box::pin(async {
14862 self.check_resource_error()?;
14863 if self.chain_id.is_none()
14864 && (!self.pending_records.is_empty()
14865 || !self.pending_chain_controls.is_empty()
14866 || !self.pending_backfills.is_empty()
14867 || !self.interests.is_empty())
14868 {
14869 self.ensure_chain_id().await?;
14870 }
14871 if let Some(batch) = self.drain_next_scoped_batch() {
14872 return Ok(Some(batch));
14873 }
14874
14875 self.ensure_streams().await?;
14880 self.check_resource_error()?;
14881 if let Some(batch) = self.drain_next_scoped_batch() {
14882 return Ok(Some(batch));
14883 }
14884
14885 self.drain_pending_backfills().await?;
14886 self.check_resource_error()?;
14887 if let Some(batch) = self.drain_next_scoped_batch() {
14888 return Ok(Some(batch));
14889 }
14890
14891 if self.interests.is_empty() {
14892 return Ok(None);
14893 }
14894
14895 loop {
14896 let Some(event) = self.next_event().await? else {
14897 return Ok(None);
14898 };
14899
14900 self.enqueue_event(event);
14901 self.check_resource_error()?;
14902 if let Some(batch) = self.drain_next_scoped_batch() {
14903 return Ok(Some(batch));
14904 }
14905 }
14906 })
14907 }
14908
14909 async fn ensure_streams(&mut self) -> Result<(), SubscriberError> {
14924 if !self.sources_dirty {
14925 return Ok(());
14926 }
14927 if matches!(self.state, AlloySubscriberState::Uninitialized) && self.interests.is_empty() {
14932 self.bump_stream_revision();
14933 self.sources_dirty = false;
14934 return Ok(());
14935 }
14936
14937 let desired = self.stream_sources()?;
14938 let missing: Vec<SubscriberStreamSource> = match &self.state {
14939 AlloySubscriberState::Active(streams) => desired
14940 .iter()
14941 .filter(|source| !streams.contains_source(source))
14942 .cloned()
14943 .collect(),
14944 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => desired.clone(),
14945 };
14946
14947 for source in missing {
14948 let stream = match self.connect_source_stream(source.clone()).await {
14949 Ok(stream) => stream,
14950 Err(error)
14951 if source.is_flashblocks()
14952 && self.config.preconfirmations == PreconfirmationMode::Preferred =>
14953 {
14954 tracing::warn!(
14955 stream = source.label(),
14956 error = %error,
14957 "Flashblocks source unavailable; canonical delivery remains active"
14958 );
14959 if self.config.reconnect.enabled {
14960 self.schedule_flashblock_reconnect(
14961 source,
14962 self.config.reconnect.retry_delay,
14963 );
14964 }
14965 continue;
14966 }
14967 Err(error) => return Err(error),
14968 };
14969 self.install_source_stream(source.clone(), stream);
14973 if self.source_requires_backfill(&source) {
14974 self.queue_source_backfill(source);
14975 }
14976 }
14977
14978 while let Some(source) = self.pending_source_backfills.front().cloned() {
14979 let desired_and_live = desired.iter().any(|item| item.same_key(&source))
14980 && matches!(
14981 &self.state,
14982 AlloySubscriberState::Active(streams) if streams.contains_source(&source)
14983 );
14984 if !desired_and_live {
14985 self.pending_source_backfills.pop_front();
14986 continue;
14987 }
14988
14989 let event = self.backfill_reconnected_source(&source).await?;
14995 self.pending_source_backfills.pop_front();
14996 if let Some(event) = event {
14997 self.enqueue_event(event);
14998 }
14999 }
15000
15001 if let AlloySubscriberState::Active(streams) = &mut self.state {
15002 streams.retain_sources(&desired);
15003 if streams.is_empty() {
15004 self.state = AlloySubscriberState::Empty;
15005 }
15006 }
15007
15008 self.bump_stream_revision();
15009 self.sources_dirty = false;
15010 self.retire_unreferenced_filters();
15011 Ok(())
15012 }
15013
15014 fn install_source_stream(
15015 &mut self,
15016 source: SubscriberStreamSource,
15017 stream: BoxStream<'static, SubscriberEvent<N>>,
15018 ) {
15019 match &mut self.state {
15020 AlloySubscriberState::Active(streams) => {
15021 if streams.contains_source(&source) {
15022 return;
15023 }
15024 streams.push(source, stream);
15025 }
15026 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
15027 let mut streams = SubscriberStreams::new();
15028 streams.push(source, stream);
15029 self.state = AlloySubscriberState::Active(streams);
15030 }
15031 }
15032 self.bump_stream_revision();
15035 }
15036
15037 fn schedule_flashblock_reconnect(
15038 &mut self,
15039 source: SubscriberStreamSource,
15040 first_delay: Duration,
15041 ) {
15042 if self
15043 .pending_flashblock_reconnect_sources
15044 .iter()
15045 .any(|pending| pending.same_key(&source))
15046 {
15047 return;
15048 }
15049 self.pending_flashblock_reconnect_sources
15050 .push(source.clone());
15051 self.pending_flashblock_reconnects
15052 .push(flashblock_reconnect_future(
15053 self.provider.root().clone(),
15054 source,
15055 self.config.max_batch_size,
15056 self.config.reconnect.clone(),
15057 first_delay,
15058 self.config.flashblock_poll_interval,
15059 ));
15060 }
15061
15062 fn reschedule_preferred_flashblock(&mut self, source: SubscriberStreamSource) {
15063 if !self.config.reconnect.enabled {
15064 return;
15065 }
15066 self.schedule_flashblock_reconnect(source, self.config.reconnect.max_delay);
15067 }
15068
15069 fn source_requires_backfill(&self, source: &SubscriberStreamSource) -> bool {
15070 matches!(source, SubscriberStreamSource::PubSubLog { id, .. }
15071 if self.last_seen_log_blocks.contains_key(id))
15072 }
15073
15074 fn queue_source_backfill(&mut self, source: SubscriberStreamSource) {
15075 if !self
15076 .pending_source_backfills
15077 .iter()
15078 .any(|pending| pending.same_key(&source))
15079 {
15080 self.pending_source_backfills.push_back(source);
15081 }
15082 }
15083
15084 async fn drain_pending_backfills(&mut self) -> Result<(), SubscriberError> {
15096 while let Some(queued) = self.pending_backfills.front() {
15097 let epoch = queued.epoch.clone();
15099 let owner = queued.owner.clone();
15100 let owner_exists = match (&epoch, &owner) {
15101 (Some(epoch), _) => self.interest_owner_state(epoch).is_some(),
15102 (None, Some(owner)) => self.owner_interests(owner).is_some(),
15103 (None, None) => true,
15104 };
15105 if !owner_exists {
15106 self.pending_backfills.pop_front();
15107 continue;
15108 }
15109 let filters = queued.filters.clone();
15110 let backfill = queued.backfill;
15111
15112 let to_block = match backfill.end_block() {
15113 Some(to_block) => to_block,
15114 None => self
15115 .provider
15116 .get_block_number()
15117 .await
15118 .map_err(provider_error)?,
15119 };
15120 if to_block < backfill.start_block() {
15121 let certified = if let Some(retained) = backfill.retained_anchor() {
15126 let actual =
15127 fetch_provider_block_ref::<P, N>(&self.provider, retained.number).await?;
15128 if !block_ref_satisfies_expected(&actual, retained) {
15129 return Err(SubscriberError::InvalidBackfill(format!(
15130 "retained anchor {}:{:?} conflicts with provider block {}:{:?}",
15131 retained.number, retained.hash, actual.number, actual.hash
15132 )));
15133 }
15134 if to_block < retained.number {
15135 return Err(SubscriberError::InvalidBackfill(format!(
15136 "backfill upper bound {to_block} precedes retained anchor {}",
15137 retained.number
15138 )));
15139 }
15140 Some(actual)
15141 } else {
15142 None
15143 };
15144 self.pending_backfills.pop_front();
15145 for filter in &filters {
15146 let source_id = self.log_source_id(filter);
15147 if let Some(certified) = certified {
15148 self.last_seen_log_blocks
15149 .entry(source_id)
15150 .and_modify(|anchor| *anchor = (*anchor).max(certified.number))
15151 .or_insert(certified.number);
15152 }
15153 }
15154 if owner.is_none()
15155 && let Some(certified) = certified
15156 {
15157 self.pending_chain_controls
15158 .push_back(global_backfill_barrier(backfill, certified));
15159 }
15160 if !self.pending_chain_controls.is_empty() {
15161 break;
15162 }
15163 continue;
15164 }
15165
15166 let through = fetch_provider_block_ref::<P, N>(&self.provider, to_block).await?;
15167 let request_filters =
15168 merged_lazy_backfill_filters(&filters, backfill.start_block(), through.number);
15169 let retained = backfill.retained_anchor().copied().into_iter().collect();
15170 let SubscriberOwnerCatchup {
15171 mut logs,
15172 certified,
15173 } = fetch_owner_catchup::<&P, N>(
15174 &self.provider,
15175 request_filters,
15176 retained,
15177 through,
15178 SubscriberOwnerCatchupOptions {
15179 target_preverified: true,
15180 max_logs: self.config.max_pending_records,
15181 max_log_bytes: self.config.max_backfill_log_bytes,
15182 max_requests_in_flight: self.config.max_reconcile_requests_in_flight,
15183 },
15184 )
15185 .await
15186 .map_err(lazy_backfill_error)?;
15187 logs.sort_by_key(|log| {
15188 (
15189 log.block_number.unwrap_or_default(),
15190 log.transaction_index.unwrap_or_default(),
15191 log.log_index.unwrap_or_default(),
15192 )
15193 });
15194 logs.dedup();
15195 self.ensure_pending_record_capacity(logs.len(), "lazy subscriber backfill records")?;
15196
15197 self.pending_backfills.pop_front();
15200 if let Some(epoch) = epoch.as_ref() {
15201 self.enqueue_backfilled_logs(logs, None, Some(epoch), Some(backfill));
15202 } else if let Some(owner) = owner.as_ref() {
15203 self.enqueue_compat_owner_backfilled_logs(logs, owner, backfill);
15204 } else {
15205 self.enqueue_backfilled_logs(logs, None, None, Some(backfill));
15206 self.pending_chain_controls
15207 .push_back(global_backfill_barrier(backfill, certified));
15208 }
15209 for filter in &filters {
15210 let source_id = self.log_source_id(filter);
15211 let anchor = self
15212 .last_seen_log_blocks
15213 .entry(source_id)
15214 .or_insert(certified.number);
15215 *anchor = (*anchor).max(certified.number);
15216 }
15217
15218 if !self.pending_records.is_empty() || !self.pending_chain_controls.is_empty() {
15219 break;
15220 }
15221 }
15222 Ok(())
15223 }
15224
15225 fn stream_sources(&mut self) -> Result<Vec<SubscriberStreamSource>, SubscriberError> {
15226 match resolve_subscriber_transport(self.mode)? {
15227 SubscriberTransport::PubSub => Ok(self.pubsub_stream_sources()),
15228 SubscriberTransport::Polling => Ok(self.polling_stream_sources()),
15229 }
15230 }
15231
15232 fn pubsub_stream_sources(&mut self) -> Vec<SubscriberStreamSource> {
15233 let mut sources = Vec::new();
15234 let inherited_anchor = self.last_seen_log_blocks.values().copied().min();
15235
15236 for filter in self.log_stream_filters() {
15237 let id = self.log_source_id(&filter);
15238 if let Some(anchor) = inherited_anchor {
15239 self.last_seen_log_blocks.entry(id).or_insert(anchor);
15240 }
15241 sources.push(SubscriberStreamSource::PubSubLog { id, filter });
15242 }
15243
15244 if needs_pending_hash_stream(&self.interests) {
15245 sources.push(SubscriberStreamSource::PubSubPendingHashes);
15246 }
15247
15248 if needs_header_block_stream(&self.interests) {
15249 if self.config.preconfirmations != PreconfirmationMode::Disabled
15250 && self.chain_id.and_then(flashblocks_adapter).is_some()
15251 {
15252 sources.push(SubscriberStreamSource::CanonicalHeadPolling);
15253 } else {
15254 sources.push(SubscriberStreamSource::PubSubBlockHeaders);
15255 }
15256 }
15257
15258 if self.config.preconfirmations != PreconfirmationMode::Disabled {
15259 match self.chain_id.and_then(flashblocks_adapter) {
15260 Some(FlashblocksAdapter::NativeSubscriptions) => {
15261 sources.push(SubscriberStreamSource::BaseFlashblocks);
15262 for filter in self.log_stream_filters() {
15263 let id = self.log_source_id(&filter);
15264 sources.push(SubscriberStreamSource::BasePendingLog { id, filter });
15265 }
15266 }
15267 Some(FlashblocksAdapter::PendingStatePolling) => {
15268 sources.push(SubscriberStreamSource::OpPendingFlashblocks);
15269 }
15270 None => {}
15271 }
15272 }
15273
15274 sources
15275 }
15276
15277 fn polling_stream_sources(&self) -> Vec<SubscriberStreamSource> {
15278 let mut sources = Vec::new();
15279
15280 for filter in self.log_stream_filters() {
15281 sources.push(SubscriberStreamSource::PollingLog { filter });
15282 }
15283
15284 if needs_pending_hash_stream(&self.interests) {
15285 sources.push(SubscriberStreamSource::PollingPendingHashes);
15286 }
15287
15288 if self.config.preconfirmations != PreconfirmationMode::Disabled
15289 && self.chain_id.and_then(flashblocks_adapter)
15290 == Some(FlashblocksAdapter::PendingStatePolling)
15291 {
15292 sources.push(SubscriberStreamSource::OpPendingFlashblocks);
15293 }
15294
15295 sources
15296 }
15297
15298 fn log_source_id(&mut self, filter: &Filter) -> usize {
15299 if let Some(id) = self.log_source_ids.get(filter) {
15300 return *id;
15301 }
15302
15303 let id = self.next_log_source_id;
15304 self.next_log_source_id = self.next_log_source_id.saturating_add(1);
15305 self.log_source_ids.insert(filter.clone(), id);
15306 id
15307 }
15308
15309 async fn connect_source_stream(
15310 &mut self,
15311 source: SubscriberStreamSource,
15312 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15313 match source {
15314 SubscriberStreamSource::PubSubLog { id, filter } => {
15315 self.connect_pubsub_log_stream(id, filter).await
15316 }
15317 SubscriberStreamSource::BasePendingLog { id, filter } => {
15318 self.connect_base_pending_log_stream(id, filter).await
15319 }
15320 SubscriberStreamSource::BaseFlashblocks => self.connect_base_flashblock_stream().await,
15321 SubscriberStreamSource::OpPendingFlashblocks => {
15322 self.connect_op_flashblock_tick_stream()
15323 }
15324 SubscriberStreamSource::CanonicalHeadPolling => {
15325 self.connect_canonical_head_tick_stream()
15326 }
15327 SubscriberStreamSource::PubSubPendingHashes => {
15328 self.connect_pubsub_pending_hash_stream().await
15329 }
15330 SubscriberStreamSource::PubSubBlockHeaders => {
15331 self.connect_pubsub_block_header_stream().await
15332 }
15333 SubscriberStreamSource::PollingLog { filter } => {
15334 self.connect_polling_log_stream(filter).await
15335 }
15336 SubscriberStreamSource::PollingPendingHashes => {
15337 self.connect_polling_pending_hash_stream().await
15338 }
15339 }
15340 }
15341
15342 async fn connect_pubsub_log_stream(
15343 &mut self,
15344 id: usize,
15345 filter: Filter,
15346 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15347 #[cfg(feature = "reactive-ws")]
15348 {
15349 let source = SubscriberStreamSource::PubSubLog {
15350 id,
15351 filter: filter.clone(),
15352 };
15353 let stream = self
15354 .provider
15355 .subscribe_logs(&filter)
15356 .channel_size(self.config.max_batch_size.max(1))
15357 .await
15358 .map_err(provider_error)?
15359 .into_stream()
15360 .map(move |log| SubscriberEvent::Log { source_id: id, log });
15361 Ok(stream_with_termination(stream, source))
15362 }
15363
15364 #[cfg(not(feature = "reactive-ws"))]
15365 {
15366 let _ = (id, filter);
15367 Err(SubscriberError::Unsupported(
15368 "AlloySubscriber pubsub mode requires the reactive-ws feature",
15369 ))
15370 }
15371 }
15372
15373 async fn connect_base_pending_log_stream(
15374 &mut self,
15375 id: usize,
15376 filter: Filter,
15377 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15378 #[cfg(feature = "reactive-ws")]
15379 {
15380 let source = SubscriberStreamSource::BasePendingLog {
15381 id,
15382 filter: filter.clone(),
15383 };
15384 let params = base_pending_log_filter(&filter)?;
15385 let stream = self
15386 .provider
15387 .subscribe::<_, Log>(("pendingLogs", params))
15388 .channel_size(self.config.max_batch_size.max(1))
15389 .await
15390 .map_err(provider_error)?
15391 .into_stream()
15392 .map(move |log| SubscriberEvent::BasePendingLog { source_id: id, log });
15393 Ok(stream_with_termination(stream, source))
15394 }
15395
15396 #[cfg(not(feature = "reactive-ws"))]
15397 {
15398 let _ = (id, filter);
15399 Err(SubscriberError::Unsupported(
15400 "Base Flashblocks require the reactive-ws feature",
15401 ))
15402 }
15403 }
15404
15405 async fn connect_base_flashblock_stream(
15406 &mut self,
15407 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15408 #[cfg(feature = "reactive-ws")]
15409 {
15410 let stream = self
15411 .provider
15412 .subscribe::<_, BaseFlashblockWirePayload>(("newFlashblocks",))
15413 .channel_size(self.config.max_batch_size.max(1))
15414 .await
15415 .map_err(provider_error)?
15416 .into_stream()
15417 .map(SubscriberEvent::BaseFlashblock);
15418 Ok(stream_with_termination(
15419 stream,
15420 SubscriberStreamSource::BaseFlashblocks,
15421 ))
15422 }
15423
15424 #[cfg(not(feature = "reactive-ws"))]
15425 {
15426 Err(SubscriberError::Unsupported(
15427 "Base Flashblocks require the reactive-ws feature",
15428 ))
15429 }
15430 }
15431
15432 fn connect_canonical_head_tick_stream(
15433 &self,
15434 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15435 let mut interval = tokio::time::interval(self.config.canonical_head_poll_interval);
15436 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
15437 let stream = stream::unfold(interval, |mut interval| async move {
15438 interval.tick().await;
15439 Some((SubscriberEvent::CanonicalHeadTick, interval))
15440 });
15441 Ok(stream_with_termination(
15442 stream,
15443 SubscriberStreamSource::CanonicalHeadPolling,
15444 ))
15445 }
15446
15447 fn connect_op_flashblock_tick_stream(
15448 &self,
15449 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15450 let first_tick = tokio::time::Instant::now() + self.config.flashblock_poll_interval;
15451 let mut interval =
15452 tokio::time::interval_at(first_tick, self.config.flashblock_poll_interval);
15453 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
15454 let stream = stream::unfold(interval, |mut interval| async move {
15455 interval.tick().await;
15456 Some((SubscriberEvent::OpFlashblockTick, interval))
15457 });
15458 Ok(stream_with_termination(
15459 stream,
15460 SubscriberStreamSource::OpPendingFlashblocks,
15461 ))
15462 }
15463
15464 async fn connect_pubsub_pending_hash_stream(
15465 &mut self,
15466 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15467 #[cfg(feature = "reactive-ws")]
15468 {
15469 let stream = self
15470 .provider
15471 .subscribe_pending_transactions()
15472 .channel_size(self.config.max_batch_size.max(1))
15473 .await
15474 .map_err(provider_error)?
15475 .into_stream()
15476 .map(SubscriberEvent::PendingHash);
15477 Ok(stream_with_termination(
15478 stream,
15479 SubscriberStreamSource::PubSubPendingHashes,
15480 ))
15481 }
15482
15483 #[cfg(not(feature = "reactive-ws"))]
15484 {
15485 Err(SubscriberError::Unsupported(
15486 "AlloySubscriber pubsub mode requires the reactive-ws feature",
15487 ))
15488 }
15489 }
15490
15491 async fn connect_pubsub_block_header_stream(
15492 &mut self,
15493 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15494 #[cfg(feature = "reactive-ws")]
15495 {
15496 let stream = self
15497 .provider
15498 .subscribe_blocks()
15499 .channel_size(self.config.max_batch_size.max(1))
15500 .await
15501 .map_err(provider_error)?
15502 .into_stream()
15503 .map(SubscriberEvent::BlockHeader);
15504 Ok(stream_with_termination(
15505 stream,
15506 SubscriberStreamSource::PubSubBlockHeaders,
15507 ))
15508 }
15509
15510 #[cfg(not(feature = "reactive-ws"))]
15511 {
15512 Err(SubscriberError::Unsupported(
15513 "AlloySubscriber pubsub mode requires the reactive-ws feature",
15514 ))
15515 }
15516 }
15517
15518 async fn connect_polling_log_stream(
15519 &mut self,
15520 filter: Filter,
15521 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15522 #[cfg(feature = "reactive-polling")]
15523 {
15524 let source = SubscriberStreamSource::PollingLog {
15525 filter: filter.clone(),
15526 };
15527 let stream = self
15528 .provider
15529 .watch_logs(&filter)
15530 .await
15531 .map_err(provider_error)?
15532 .with_channel_size(self.config.max_batch_size.max(1))
15533 .into_stream()
15534 .map(SubscriberEvent::Logs);
15535 Ok(stream_with_termination(stream, source))
15536 }
15537
15538 #[cfg(not(feature = "reactive-polling"))]
15539 {
15540 let _ = filter;
15541 Err(SubscriberError::Unsupported(
15542 "AlloySubscriber polling mode requires the reactive-polling feature",
15543 ))
15544 }
15545 }
15546
15547 async fn connect_polling_pending_hash_stream(
15548 &mut self,
15549 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15550 #[cfg(feature = "reactive-polling")]
15551 {
15552 let stream = self
15553 .provider
15554 .watch_pending_transactions()
15555 .await
15556 .map_err(provider_error)?
15557 .with_channel_size(self.config.max_batch_size.max(1))
15558 .into_stream()
15559 .map(SubscriberEvent::PendingHashes);
15560 Ok(stream_with_termination(
15561 stream,
15562 SubscriberStreamSource::PollingPendingHashes,
15563 ))
15564 }
15565
15566 #[cfg(not(feature = "reactive-polling"))]
15567 {
15568 Err(SubscriberError::Unsupported(
15569 "AlloySubscriber polling mode requires the reactive-polling feature",
15570 ))
15571 }
15572 }
15573
15574 async fn next_event(&mut self) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
15575 loop {
15576 let ready = match &mut self.state {
15577 AlloySubscriberState::Active(streams)
15578 if !self.pending_flashblock_reconnects.is_empty() =>
15579 {
15580 let stream_event = Box::pin(streams.next());
15581 let reconnect = Box::pin(self.pending_flashblock_reconnects.next());
15582 match select(reconnect, stream_event).await {
15583 Either::Left((reconnect, pending_event)) => {
15584 drop(pending_event);
15585 let Some((source, result)) = reconnect else {
15586 continue;
15587 };
15588 SubscriberReady::FlashblockReconnect(source, result)
15589 }
15590 Either::Right((event, pending_reconnect)) => {
15591 drop(pending_reconnect);
15592 SubscriberReady::Event(event)
15593 }
15594 }
15595 }
15596 AlloySubscriberState::Active(streams) => {
15597 SubscriberReady::Event(streams.next().await)
15598 }
15599 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty
15600 if !self.pending_flashblock_reconnects.is_empty() =>
15601 {
15602 let Some((source, result)) = self.pending_flashblock_reconnects.next().await
15603 else {
15604 continue;
15605 };
15606 SubscriberReady::FlashblockReconnect(source, result)
15607 }
15608 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
15609 return Ok(None);
15610 }
15611 };
15612
15613 let event = match ready {
15614 SubscriberReady::Event(event) => event,
15615 SubscriberReady::FlashblockReconnect(source, result) => {
15616 self.pending_flashblock_reconnect_sources
15617 .retain(|pending| !pending.same_key(&source));
15618 match result {
15619 Ok(stream) => {
15620 self.install_source_stream(source, stream);
15621 }
15622 Err(error)
15623 if self.config.preconfirmations == PreconfirmationMode::Preferred =>
15624 {
15625 tracing::warn!(
15626 stream = source.label(),
15627 error = %error,
15628 "Flashblocks reconnect window exhausted; canonical delivery remains active"
15629 );
15630 self.reschedule_preferred_flashblock(source);
15631 }
15632 Err(error) => return Err(error),
15633 }
15634 continue;
15635 }
15636 };
15637
15638 let Some(event) = event else {
15639 return Err(SubscriberError::Provider(
15640 "Alloy subscriber streams terminated before the subscriber was stopped"
15641 .to_owned(),
15642 ));
15643 };
15644
15645 match event {
15646 SubscriberEvent::StreamTerminated(source) => {
15647 if source.is_flashblocks() {
15651 self.invalidate_flashblock_generation();
15652 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
15653 }
15654 self.sources_dirty = true;
15655 self.bump_stream_revision();
15656 if let Some(backfill_event) = self.reconnect_source_stream(source).await? {
15657 self.sources_dirty = false;
15658 if let Some(backfill_event) =
15659 self.normalize_flashblock_event(backfill_event).await?
15660 {
15661 self.verify_event_log_blocks(&backfill_event).await?;
15662 return Ok(Some(backfill_event));
15663 }
15664 }
15665 self.sources_dirty = false;
15666 }
15667 event => {
15668 let Some(event) = self.normalize_flashblock_event(event).await? else {
15669 continue;
15670 };
15671 self.verify_event_log_blocks(&event).await?;
15672 return Ok(Some(event));
15673 }
15674 }
15675 }
15676 }
15677
15678 fn invalidate_flashblock_generation(&mut self) {
15679 self.pending_records
15680 .retain(|record| record.scope != SubscriberInputScope::Preconfirmed);
15681 self.pending_preconfirmation_invalidation = true;
15682 self.reset_flashblock_tracking();
15683 if let Some(provider) = self.provider_ref.as_mut() {
15684 provider.generation = provider.generation.saturating_add(1);
15685 }
15686 if let AlloySubscriberState::Active(streams) = &mut self.state {
15687 streams
15688 .entries
15689 .retain(|entry| !entry.source.is_flashblocks());
15690 streams.normalize_next_index();
15691 }
15692 let reconnect_sources = self
15693 .stream_sources()
15694 .unwrap_or_default()
15695 .into_iter()
15696 .filter(SubscriberStreamSource::is_flashblocks)
15697 .collect::<Vec<_>>();
15698 self.pending_flashblock_reconnects.clear();
15699 self.pending_flashblock_reconnect_sources.clear();
15700 if self.config.preconfirmations == PreconfirmationMode::Required
15701 || self.config.reconnect.enabled
15702 {
15703 for source in reconnect_sources {
15704 self.schedule_flashblock_reconnect(source, self.config.reconnect.initial_delay);
15705 }
15706 }
15707 self.sources_dirty = false;
15708 self.bump_stream_revision();
15709 }
15710
15711 async fn normalize_flashblock_event(
15712 &mut self,
15713 event: SubscriberEvent<N>,
15714 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
15715 match event {
15716 SubscriberEvent::BasePendingLog { source_id, log } => {
15717 let block_number = log.block_number.ok_or_else(|| {
15718 SubscriberError::Provider(
15719 "pendingLogs item is missing its pending block number".into(),
15720 )
15721 })?;
15722 let transaction_hash = log.transaction_hash.ok_or_else(|| {
15723 SubscriberError::Provider(
15724 "pendingLogs item is missing its transaction hash".into(),
15725 )
15726 })?;
15727 let matching = self.latest_preconfirmation.as_ref().filter(|flashblock| {
15728 flashblock.block_number == block_number
15729 && flashblock.contains_transaction(&transaction_hash)
15730 });
15731 let Some(flashblock) = matching.cloned() else {
15732 if self
15733 .latest_preconfirmation
15734 .as_ref()
15735 .is_some_and(|latest| block_number < latest.block_number)
15736 {
15737 return Ok(None);
15738 }
15739 if self.unmatched_pending_logs.len() >= self.config.max_pending_records {
15740 return Err(SubscriberError::ResourceExhausted(
15741 "unmatched pendingLogs exceeded max_pending_records".into(),
15742 ));
15743 }
15744 self.unmatched_pending_logs.push_back((source_id, log));
15745 return Ok(None);
15746 };
15747 let logs = self.filter_preconfirmed_logs(&flashblock, vec![log])?;
15748 Ok(Some(if logs.is_empty() {
15749 SubscriberEvent::FlashblockObserved
15750 } else {
15751 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
15752 }))
15753 }
15754 SubscriberEvent::BaseFlashblock(payload) => {
15755 let (flashblock, recover_pending_snapshot) =
15756 self.accept_base_flashblock(payload)?;
15757 let mut logs = Vec::new();
15758 let mut retained = VecDeque::new();
15759 while let Some((source_id, log)) = self.unmatched_pending_logs.pop_front() {
15760 let transaction_hash = log.transaction_hash;
15761 if log.block_number == Some(flashblock.block_number)
15762 && transaction_hash
15763 .as_ref()
15764 .is_some_and(|hash| flashblock.contains_transaction(hash))
15765 {
15766 let _ = source_id;
15767 logs.push(log);
15768 } else if log
15769 .block_number
15770 .is_some_and(|number| number >= flashblock.block_number)
15771 {
15772 retained.push_back((source_id, log));
15773 } else {
15774 }
15777 }
15778 self.unmatched_pending_logs = retained;
15779
15780 let indexed_recovery = recover_pending_snapshot.then(|| {
15781 let payload_id = flashblock
15782 .payload_id
15783 .expect("indexed recovery carries a payload id");
15784 let index = flashblock.index.expect("indexed recovery carries an index");
15785 let last_diff = self
15786 .base_flashblock_transactions
15787 .as_ref()
15788 .filter(|(known_payload, known_index, _, _)| {
15789 *known_payload == payload_id && *known_index == index
15790 })
15791 .map(|(_, _, _, last_diff)| last_diff.clone())
15792 .unwrap_or_default();
15793 (payload_id, index, last_diff)
15794 });
15795 if recover_pending_snapshot {
15796 if let Some(event) = self
15797 .fetch_pending_flashblock(indexed_recovery)
15798 .await
15799 .map_err(PendingFlashblockPollError::into_subscriber)?
15800 {
15801 return Ok(Some(event));
15802 }
15803 self.invalidate_flashblock_generation();
15804 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
15805 }
15806 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
15807 Ok(Some(if logs.is_empty() {
15808 SubscriberEvent::FlashblockObserved
15809 } else {
15810 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
15811 }))
15812 }
15813 SubscriberEvent::OpFlashblockTick => self.poll_op_pending_flashblock().await,
15814 SubscriberEvent::CanonicalHeadTick => self.fetch_certified_canonical_head().await,
15815 SubscriberEvent::PreconfirmedLogs { flashblock, logs } => {
15816 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
15817 Ok(Some(if logs.is_empty() {
15818 SubscriberEvent::FlashblockObserved
15819 } else {
15820 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
15821 }))
15822 }
15823 SubscriberEvent::FlashblockObserved => Ok(None),
15824 event => Ok(Some(event)),
15825 }
15826 }
15827
15828 async fn fetch_certified_canonical_head(
15829 &mut self,
15830 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
15831 if self.chain_id.and_then(flashblocks_adapter)
15832 == Some(FlashblocksAdapter::PendingStatePolling)
15833 {
15834 if !self.reserve_flashblock_rpc_methods(2) {
15835 return Ok(None);
15836 }
15837 self.flashblocks_rpc_metrics.pending_block_requests = self
15838 .flashblocks_rpc_metrics
15839 .pending_block_requests
15840 .saturating_add(1);
15841 let pending = self
15842 .fetch_op_pending_block()
15843 .await
15844 .map_err(PendingFlashblockPollError::into_subscriber)?
15845 .ok_or_else(|| {
15846 SubscriberError::Provider(
15847 "provider returned no OP pending block while certifying its parent".into(),
15848 )
15849 })?;
15850 let header = self
15851 .certify_op_pending_parent(&pending)
15852 .await
15853 .map_err(PendingFlashblockPollError::into_subscriber)?;
15854 let certified = BlockRef {
15855 number: header.number(),
15856 hash: header.hash(),
15857 parent_hash: Some(header.parent_hash()),
15858 timestamp: Some(header.timestamp()),
15859 };
15860 if self.last_certified_canonical_head.as_ref() == Some(&certified) {
15861 return Ok(None);
15862 }
15863 self.last_certified_canonical_head = Some(certified);
15864 return Ok(Some(SubscriberEvent::BlockHeader(header)));
15865 }
15866 self.flashblocks_rpc_metrics.canonical_head_requests = self
15867 .flashblocks_rpc_metrics
15868 .canonical_head_requests
15869 .saturating_add(1);
15870 let block = self
15871 .provider
15872 .get_block_by_number(BlockNumberOrTag::Latest)
15873 .await
15874 .map_err(provider_error)?
15875 .ok_or_else(|| {
15876 SubscriberError::Provider(
15877 "provider returned no latest block while certifying canonical head".into(),
15878 )
15879 })?;
15880 let header = block.header();
15881 if header.hash().is_zero() {
15882 return Err(SubscriberError::Provider(
15883 "provider returned a placeholder hash for the latest canonical head".into(),
15884 ));
15885 }
15886 let certified = BlockRef {
15887 number: header.number(),
15888 hash: header.hash(),
15889 parent_hash: Some(header.parent_hash()),
15890 timestamp: Some(header.timestamp()),
15891 };
15892 if self.last_certified_canonical_head.as_ref() == Some(&certified) {
15893 return Ok(None);
15894 }
15895 self.last_certified_canonical_head = Some(certified);
15896 Ok(Some(SubscriberEvent::BlockHeader(header.clone())))
15897 }
15898
15899 fn accept_base_flashblock(
15900 &mut self,
15901 payload: BaseFlashblockWirePayload,
15902 ) -> Result<(FlashblockRef, bool), SubscriberError> {
15903 let provider = self.provider_ref.clone().ok_or({
15904 SubscriberError::InvalidConfig(
15905 "Flashblocks require a stable provider ref from a pinned provider lease",
15906 )
15907 })?;
15908
15909 let (flashblock, recover_pending_snapshot) = match payload {
15910 BaseFlashblockWirePayload::Indexed(payload) => {
15911 if payload.index == 0 {
15912 let base = payload.base.clone().ok_or_else(|| {
15913 SubscriberError::Provider(
15914 "indexed newFlashblocks item zero omitted its base header".into(),
15915 )
15916 })?;
15917 self.base_flashblock_header = Some((payload.payload_id, base));
15918 }
15919
15920 let base = self
15921 .base_flashblock_header
15922 .as_ref()
15923 .filter(|(payload_id, _)| *payload_id == payload.payload_id)
15924 .map(|(_, base)| base);
15925 let block_number = base.map(|base| base.block_number).or_else(|| {
15926 payload
15927 .metadata
15928 .as_ref()
15929 .map(|metadata| metadata.block_number)
15930 });
15931 let block_number = block_number.ok_or_else(|| {
15932 SubscriberError::Provider(
15933 "indexed newFlashblocks payload omitted both base and metadata block number"
15934 .into(),
15935 )
15936 })?;
15937 let diff_transactions = flashblock_transaction_hashes(&payload.diff.transactions)?;
15938 let transaction_hashes = match self.base_flashblock_transactions.as_mut() {
15939 Some((known_payload, known_index, transactions, last_diff))
15940 if *known_payload == payload.payload_id =>
15941 {
15942 if payload.index < *known_index {
15943 return self
15944 .latest_preconfirmation
15945 .clone()
15946 .map(|flashblock| (flashblock, false))
15947 .ok_or_else(|| {
15948 SubscriberError::Provider(
15949 "regressive indexed Flashblock arrived without an active snapshot"
15950 .into(),
15951 )
15952 });
15953 }
15954 if payload.index == *known_index {
15955 if *last_diff != diff_transactions {
15956 return Err(SubscriberError::Provider(
15957 "conflicting duplicate indexed Flashblock payload".into(),
15958 ));
15959 }
15960 } else {
15961 if diff_transactions
15962 .iter()
15963 .any(|hash| transactions.contains(hash))
15964 {
15965 return Err(SubscriberError::Provider(
15966 "indexed Flashblock repeated a transaction from an earlier diff"
15967 .into(),
15968 ));
15969 }
15970 transactions.extend(diff_transactions.iter().copied());
15971 *known_index = payload.index;
15972 *last_diff = diff_transactions;
15973 }
15974 transactions.clone()
15975 }
15976 _ => {
15977 self.base_flashblock_transactions = Some((
15978 payload.payload_id,
15979 payload.index,
15980 diff_transactions.clone(),
15981 diff_transactions.clone(),
15982 ));
15983 diff_transactions
15984 }
15985 };
15986 let partial_block_hash = non_placeholder_hash(payload.diff.block_hash);
15987 let transactions_root = payload
15988 .diff
15989 .transactions_root
15990 .and_then(non_placeholder_hash);
15991 let parent_hash = base.and_then(|base| non_placeholder_hash(base.parent_hash));
15992 let state_root = non_placeholder_hash(payload.diff.state_root);
15993 let timestamp = base.map(|base| base.timestamp);
15994 let base_fee_per_gas = base.and_then(|base| base.base_fee_per_gas);
15995 let beneficiary = base.and_then(|base| base.beneficiary);
15996 let prevrandao = base
15997 .and_then(|base| base.prevrandao)
15998 .and_then(non_placeholder_hash);
15999 let gas_limit = base.and_then(|base| base.gas_limit);
16000 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
16001 provider: &provider,
16002 payload_id: Some(payload.payload_id),
16003 index: Some(payload.index),
16004 block_number,
16005 partial_block_hash,
16006 parent_hash,
16007 state_root,
16008 transactions_root,
16009 transaction_hashes: &transaction_hashes,
16010 timestamp,
16011 base_fee_per_gas,
16012 beneficiary,
16013 prevrandao,
16014 gas_limit,
16015 });
16016 let flashblock = FlashblockRef {
16017 provider,
16018 payload_id: Some(payload.payload_id),
16019 index: Some(payload.index),
16020 block_number,
16021 content_hash,
16022 partial_block_hash,
16023 parent_hash,
16024 state_root,
16025 transactions_root,
16026 transaction_hashes,
16027 timestamp,
16028 base_fee_per_gas,
16029 beneficiary,
16030 prevrandao,
16031 gas_limit,
16032 };
16033 if let Some(previous) = self.latest_preconfirmation.as_ref()
16034 && previous.same_payload(&flashblock)
16035 && previous.index == flashblock.index
16036 && previous.content_hash != flashblock.content_hash
16037 {
16038 return Err(SubscriberError::Provider(
16039 "conflicting duplicate indexed Flashblock content".into(),
16040 ));
16041 }
16042 let recover = match self.latest_preconfirmation.as_ref() {
16043 Some(previous) if previous.same_payload(&flashblock) => {
16044 if let (Some(previous), Some(current)) = (previous.index, flashblock.index)
16045 {
16046 if current < previous {
16047 return Ok((flashblock, false));
16048 }
16049 current > previous.saturating_add(1)
16050 } else {
16051 false
16052 }
16053 }
16054 Some(_) => payload.index != 0,
16055 None => payload.index != 0,
16056 };
16057 (flashblock, recover)
16058 }
16059 BaseFlashblockWirePayload::Block(payload) => {
16060 let transaction_hashes = flashblock_transaction_hashes(&payload.transactions)?;
16061 let parent_hash = non_placeholder_hash(payload.parent_hash);
16062 let state_root = non_placeholder_hash(payload.state_root);
16063 let transactions_root = payload.transactions_root.and_then(non_placeholder_hash);
16064 let partial_block_hash = non_placeholder_hash(payload.hash);
16065 let prevrandao = payload.mix_hash.and_then(non_placeholder_hash);
16066 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
16067 provider: &provider,
16068 payload_id: None,
16069 index: None,
16070 block_number: payload.number,
16071 partial_block_hash,
16072 parent_hash,
16073 state_root,
16074 transactions_root,
16075 transaction_hashes: &transaction_hashes,
16076 timestamp: Some(payload.timestamp),
16077 base_fee_per_gas: payload.base_fee_per_gas,
16078 beneficiary: payload.miner,
16079 prevrandao,
16080 gas_limit: payload.gas_limit,
16081 });
16082 let flashblock = FlashblockRef {
16083 provider,
16084 payload_id: None,
16085 index: None,
16086 block_number: payload.number,
16087 content_hash,
16088 partial_block_hash,
16089 parent_hash,
16090 state_root,
16091 transactions_root,
16092 transaction_hashes,
16093 timestamp: Some(payload.timestamp),
16094 base_fee_per_gas: payload.base_fee_per_gas,
16095 beneficiary: payload.miner,
16096 prevrandao,
16097 gas_limit: payload.gas_limit,
16098 };
16099 if let Some(previous) = self.latest_preconfirmation.as_ref()
16100 && flashblock.same_payload(previous)
16101 && flashblock.content_hash != previous.content_hash
16102 && !flashblock.is_cumulative_successor_of(previous)
16103 {
16104 return Err(SubscriberError::Provider(
16105 "cumulative Flashblock transaction membership is non-monotonic".into(),
16106 ));
16107 }
16108 (flashblock, false)
16109 }
16110 };
16111 Ok((flashblock, recover_pending_snapshot))
16112 }
16113
16114 async fn poll_op_pending_flashblock(
16115 &mut self,
16116 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
16117 match self.fetch_pending_flashblock(None).await {
16118 Ok(event) => {
16119 self.consecutive_flashblock_poll_failures = 0;
16120 Ok(event)
16121 }
16122 Err(PendingFlashblockPollError::Request(error)) => {
16123 self.flashblocks_rpc_metrics.failed_requests = self
16124 .flashblocks_rpc_metrics
16125 .failed_requests
16126 .saturating_add(1);
16127 self.consecutive_flashblock_poll_failures =
16128 self.consecutive_flashblock_poll_failures.saturating_add(1);
16129 if self.consecutive_flashblock_poll_failures
16130 >= self.config.max_consecutive_flashblock_poll_failures
16131 {
16132 return Err(error);
16133 }
16134 tracing::warn!(
16135 consecutive_failures = self.consecutive_flashblock_poll_failures,
16136 failure_limit = self.config.max_consecutive_flashblock_poll_failures,
16137 error = %error,
16138 "Optimism pending-state Flashblocks request failed; retrying on the next tick"
16139 );
16140 Ok(None)
16141 }
16142 Err(PendingFlashblockPollError::Integrity(error)) => Err(error),
16143 }
16144 }
16145
16146 async fn fetch_pending_flashblock(
16147 &mut self,
16148 indexed_recovery: Option<(FixedBytes<8>, u64, Vec<B256>)>,
16149 ) -> Result<Option<SubscriberEvent<N>>, PendingFlashblockPollError> {
16150 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
16151 == Some(FlashblocksAdapter::PendingStatePolling);
16152 if samples_pending_range {
16153 let fixed_methods = 2_usize.saturating_add(self.log_stream_filters().len());
16154 if !self.reserve_flashblock_rpc_methods(fixed_methods) {
16155 return Ok(None);
16156 }
16157 }
16158 let state_provider = if samples_pending_range {
16159 self.flashblocks_state_provider
16160 .as_ref()
16161 .unwrap_or(&self.provider)
16162 } else {
16163 &self.provider
16164 };
16165 let latest = if samples_pending_range {
16166 None
16167 } else {
16168 self.flashblocks_rpc_metrics.canonical_head_requests = self
16169 .flashblocks_rpc_metrics
16170 .canonical_head_requests
16171 .saturating_add(1);
16172 Some(
16173 state_provider
16174 .get_block_number()
16175 .await
16176 .map_err(pending_flashblock_request_error)?,
16177 )
16178 };
16179 self.flashblocks_rpc_metrics.pending_block_requests = self
16180 .flashblocks_rpc_metrics
16181 .pending_block_requests
16182 .saturating_add(1);
16183 let pending_block = if samples_pending_range {
16184 self.fetch_op_pending_block().await?
16185 } else {
16186 self.provider
16187 .get_block_by_number(BlockNumberOrTag::Pending)
16188 .await
16189 .map_err(pending_flashblock_request_error)?
16190 };
16191 let Some(block) = pending_block else {
16192 if self.config.preconfirmations == PreconfirmationMode::Required {
16193 return Err(PendingFlashblockPollError::Request(
16194 SubscriberError::Provider(
16195 "Flashblocks provider returned no pending block".into(),
16196 ),
16197 ));
16198 }
16199 return Ok(None);
16200 };
16201 let latest = if samples_pending_range {
16202 self.certify_op_pending_parent(&block).await?.number()
16203 } else {
16204 latest.expect("non-OP pending recovery fetched a canonical height")
16205 };
16206 let header = block.header();
16207 if header.number() <= latest {
16208 return Ok(None);
16209 }
16210
16211 let provider = self.provider_ref.clone().ok_or({
16212 PendingFlashblockPollError::Integrity(SubscriberError::InvalidConfig(
16213 "Flashblocks require a stable provider ref from a pinned provider lease",
16214 ))
16215 })?;
16216 let parent_hash = Some(header.parent_hash());
16217 let transaction_hashes = if let Some(hashes) = block.transactions().as_hashes() {
16218 hashes.to_vec()
16219 } else if let Some(transactions) = block.transactions().as_transactions() {
16220 transactions
16221 .iter()
16222 .map(|transaction| transaction.tx_hash())
16223 .collect()
16224 } else {
16225 Vec::new()
16226 };
16227 let state_root = non_placeholder_hash(header.state_root());
16228 let transactions_root = non_placeholder_hash(header.transactions_root());
16229 let partial_block_hash = non_placeholder_hash(header.hash());
16230 let prevrandao = header.mix_hash().and_then(non_placeholder_hash);
16231 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
16232 provider: &provider,
16233 payload_id: None,
16234 index: None,
16235 block_number: header.number(),
16236 partial_block_hash,
16237 parent_hash,
16238 state_root,
16239 transactions_root,
16240 transaction_hashes: &transaction_hashes,
16241 timestamp: Some(header.timestamp()),
16242 base_fee_per_gas: header.base_fee_per_gas(),
16243 beneficiary: Some(header.beneficiary()),
16244 prevrandao,
16245 gas_limit: Some(header.gas_limit()),
16246 });
16247 let flashblock = FlashblockRef {
16248 provider,
16249 payload_id: None,
16250 index: None,
16251 block_number: header.number(),
16252 content_hash,
16253 partial_block_hash,
16254 parent_hash,
16255 state_root,
16256 transactions_root,
16257 transaction_hashes,
16258 timestamp: Some(header.timestamp()),
16259 base_fee_per_gas: header.base_fee_per_gas(),
16260 beneficiary: Some(header.beneficiary()),
16261 prevrandao,
16262 gas_limit: Some(header.gas_limit()),
16263 };
16264 if samples_pending_range
16265 && self
16266 .latest_preconfirmation
16267 .as_ref()
16268 .is_some_and(|previous| !previous.same_payload(&flashblock))
16269 {
16270 self.invalidate_preconfirmation_snapshot();
16273 }
16274 if let Some((payload_id, index, last_diff)) = indexed_recovery {
16275 self.base_flashblock_transactions = Some((
16276 payload_id,
16277 index,
16278 flashblock.transaction_hashes.clone(),
16279 last_diff,
16280 ));
16281 }
16282 let repeats_pending_snapshot = self
16283 .latest_preconfirmation
16284 .as_ref()
16285 .is_some_and(|previous| previous == &flashblock);
16286 if repeats_pending_snapshot && !samples_pending_range {
16287 return Ok(None);
16288 }
16289
16290 if let Some(previous) = self.latest_preconfirmation.as_ref()
16291 && flashblock.same_payload(previous)
16292 && !flashblock.is_cumulative_successor_of(previous)
16293 {
16294 if samples_pending_range {
16295 self.invalidate_preconfirmation_snapshot();
16300 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
16301 }
16302 return Err(PendingFlashblockPollError::Integrity(
16303 SubscriberError::Provider(
16304 "sampled cumulative Flashblock transaction membership is non-monotonic".into(),
16305 ),
16306 ));
16307 }
16308
16309 let mut logs = self.fetch_pending_logs(flashblock.block_number).await?;
16310 if samples_pending_range {
16311 let (mut receipt_logs, completed_receipts, unavailable_receipts) =
16312 self.fetch_pending_transaction_receipts(&flashblock).await?;
16313 logs.append(&mut receipt_logs);
16314 logs.retain(|log| log.block_number == Some(flashblock.block_number));
16315 for log in &logs {
16316 let transaction_hash = log.transaction_hash.ok_or_else(|| {
16317 PendingFlashblockPollError::Integrity(SubscriberError::Provider(
16318 "pre-confirmed log is missing its transaction hash".into(),
16319 ))
16320 })?;
16321 if !flashblock.contains_transaction(&transaction_hash) {
16322 self.flashblocks_rpc_metrics.raced_samples =
16323 self.flashblocks_rpc_metrics.raced_samples.saturating_add(1);
16324 return Ok(None);
16325 }
16326 }
16327 let logs = self
16328 .filter_preconfirmed_logs(&flashblock, logs)
16329 .map_err(PendingFlashblockPollError::Integrity)?;
16330 self.preconfirmed_unavailable_receipts
16331 .extend(unavailable_receipts);
16332 for transaction_hash in &completed_receipts {
16333 self.preconfirmed_unavailable_receipts
16334 .remove(transaction_hash);
16335 }
16336 self.preconfirmed_receipted_transactions
16337 .extend(completed_receipts);
16338 if repeats_pending_snapshot && logs.is_empty() {
16339 return Ok(None);
16340 }
16341 return Ok(Some(if logs.is_empty() {
16342 SubscriberEvent::FlashblockObserved
16343 } else {
16344 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
16345 }));
16346 }
16347 let logs = self
16348 .filter_preconfirmed_logs(&flashblock, logs)
16349 .map_err(PendingFlashblockPollError::Integrity)?;
16350 Ok(Some(if logs.is_empty() {
16351 SubscriberEvent::FlashblockObserved
16352 } else {
16353 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
16354 }))
16355 }
16356
16357 async fn fetch_pending_logs(
16358 &mut self,
16359 pending_block_number: u64,
16360 ) -> Result<Vec<Log>, PendingFlashblockPollError> {
16361 let mut logs = Vec::new();
16362 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
16363 == Some(FlashblocksAdapter::PendingStatePolling);
16364 let state_provider = if samples_pending_range {
16365 self.flashblocks_state_provider
16366 .as_ref()
16367 .unwrap_or(&self.provider)
16368 } else {
16369 &self.provider
16370 };
16371 for filter in self.log_stream_filters() {
16372 self.flashblocks_rpc_metrics.pending_log_requests = self
16373 .flashblocks_rpc_metrics
16374 .pending_log_requests
16375 .saturating_add(1);
16376 let filter = if samples_pending_range {
16377 filter
16378 .from_block(pending_block_number)
16379 .to_block(BlockNumberOrTag::Pending)
16380 } else {
16381 filter
16382 .from_block(BlockNumberOrTag::Pending)
16383 .to_block(BlockNumberOrTag::Pending)
16384 };
16385 logs.extend(
16386 state_provider
16387 .get_logs(&filter)
16388 .await
16389 .map_err(pending_flashblock_request_error)?,
16390 );
16391 }
16392 if samples_pending_range {
16393 logs.retain(|log| log.block_number == Some(pending_block_number));
16394 }
16395 Ok(logs)
16396 }
16397
16398 async fn fetch_pending_transaction_receipts(
16399 &mut self,
16400 flashblock: &FlashblockRef,
16401 ) -> Result<(Vec<Log>, Vec<B256>, Vec<B256>), PendingFlashblockPollError> {
16402 let receipt_allowance = self.pending_receipt_request_allowance();
16403 let receipt_limit = self
16404 .config
16405 .max_pending_transaction_receipts_per_tick
16406 .min(receipt_allowance);
16407 if receipt_limit == 0 {
16408 return Ok((Vec::new(), Vec::new(), Vec::new()));
16409 }
16410 let mut transaction_hashes = Vec::with_capacity(receipt_limit);
16411 for transaction_hash in &flashblock.transaction_hashes {
16412 if !self
16413 .preconfirmed_receipted_transactions
16414 .contains(transaction_hash)
16415 && !self
16416 .preconfirmed_unavailable_receipts
16417 .contains(transaction_hash)
16418 {
16419 transaction_hashes.push(*transaction_hash);
16420 if transaction_hashes.len() == receipt_limit {
16421 break;
16422 }
16423 }
16424 }
16425 if transaction_hashes.len() < receipt_limit {
16426 for transaction_hash in &flashblock.transaction_hashes {
16427 if self
16428 .preconfirmed_unavailable_receipts
16429 .contains(transaction_hash)
16430 {
16431 transaction_hashes.push(*transaction_hash);
16432 if transaction_hashes.len() == receipt_limit {
16433 break;
16434 }
16435 }
16436 }
16437 }
16438 if transaction_hashes.is_empty() {
16439 return Ok((Vec::new(), Vec::new(), Vec::new()));
16440 }
16441 let reserved = self.reserve_flashblock_rpc_methods(transaction_hashes.len());
16442 debug_assert!(reserved, "receipt allowance must remain reserved until use");
16443 if !reserved {
16444 return Ok((Vec::new(), Vec::new(), Vec::new()));
16445 }
16446 self.flashblocks_rpc_metrics.pending_receipt_requests = self
16447 .flashblocks_rpc_metrics
16448 .pending_receipt_requests
16449 .saturating_add(transaction_hashes.len() as u64);
16450 let state_provider = self
16451 .flashblocks_state_provider
16452 .as_ref()
16453 .unwrap_or(&self.provider);
16454 let client = state_provider.client();
16455 let mut batch = BatchRequest::new(client);
16456 let mut waiters = Vec::with_capacity(transaction_hashes.len());
16457 for transaction_hash in transaction_hashes {
16458 let waiter = batch
16459 .add_call::<_, serde_json::Value>("eth_getTransactionReceipt", &(transaction_hash,))
16460 .map_err(pending_flashblock_request_error)?;
16461 waiters.push((transaction_hash, waiter));
16462 }
16463 batch
16464 .send()
16465 .await
16466 .map_err(pending_flashblock_request_error)?;
16467 let mut logs = Vec::new();
16468 let mut completed = Vec::new();
16469 let mut unavailable = Vec::new();
16470 for (transaction_hash, waiter) in waiters {
16471 let value = waiter.await.map_err(pending_flashblock_request_error)?;
16472 if let Some(mut receipt_logs) =
16473 normalize_pending_transaction_receipt(transaction_hash, value)
16474 .map_err(PendingFlashblockPollError::Integrity)?
16475 {
16476 self.flashblocks_rpc_metrics.pending_receipts_completed = self
16477 .flashblocks_rpc_metrics
16478 .pending_receipts_completed
16479 .saturating_add(1);
16480 logs.append(&mut receipt_logs);
16481 completed.push(transaction_hash);
16482 } else {
16483 self.flashblocks_rpc_metrics.pending_receipts_unavailable = self
16484 .flashblocks_rpc_metrics
16485 .pending_receipts_unavailable
16486 .saturating_add(1);
16487 unavailable.push(transaction_hash);
16488 }
16489 }
16490 Ok((logs, completed, unavailable))
16491 }
16492
16493 fn pending_receipt_request_allowance(&mut self) -> usize {
16494 self.prune_flashblock_rpc_request_times();
16495 let rolling_capacity = self
16496 .config
16497 .max_flashblock_rpc_requests_per_second
16498 .saturating_sub(self.flashblock_rpc_request_times.len());
16499 rolling_capacity.min(self.pending_receipt_requests_per_tick_capacity())
16500 }
16501
16502 fn pending_receipt_requests_per_tick_capacity(&self) -> usize {
16503 let interval_nanos = self.config.flashblock_poll_interval.as_nanos().max(1);
16504 let ticks_per_second = Duration::from_secs(1).as_nanos().div_ceil(interval_nanos);
16505 let ticks_per_second = usize::try_from(ticks_per_second).unwrap_or(usize::MAX);
16506 self.pending_receipt_requests_per_second_capacity()
16507 .checked_div(ticks_per_second)
16508 .unwrap_or(0)
16509 }
16510
16511 fn pending_receipt_requests_per_second_capacity(&self) -> usize {
16512 let interval_nanos = self.config.flashblock_poll_interval.as_nanos().max(1);
16513 let ticks_per_second = Duration::from_secs(1).as_nanos().div_ceil(interval_nanos);
16514 let ticks_per_second = usize::try_from(ticks_per_second).unwrap_or(usize::MAX);
16515 let fixed_methods_per_tick = 2_usize.saturating_add(self.log_stream_filters().len());
16516 let mut reserved_methods = ticks_per_second.saturating_mul(fixed_methods_per_tick);
16517 if needs_header_block_stream(&self.interests) {
16518 let canonical_interval_nanos =
16519 self.config.canonical_head_poll_interval.as_nanos().max(1);
16520 let canonical_ticks = Duration::from_secs(1)
16521 .as_nanos()
16522 .div_ceil(canonical_interval_nanos);
16523 let canonical_ticks = usize::try_from(canonical_ticks).unwrap_or(usize::MAX);
16524 reserved_methods = reserved_methods.saturating_add(canonical_ticks.saturating_mul(2));
16525 }
16526 self.config
16527 .max_flashblock_rpc_requests_per_second
16528 .saturating_sub(reserved_methods)
16529 }
16530
16531 fn reserve_flashblock_rpc_methods(&mut self, methods: usize) -> bool {
16532 self.prune_flashblock_rpc_request_times();
16533 if self
16534 .flashblock_rpc_request_times
16535 .len()
16536 .saturating_add(methods)
16537 > self.config.max_flashblock_rpc_requests_per_second
16538 {
16539 return false;
16540 }
16541 let now = Instant::now();
16542 for _ in 0..methods {
16543 self.flashblock_rpc_request_times.push_back(now);
16544 }
16545 true
16546 }
16547
16548 fn prune_flashblock_rpc_request_times(&mut self) {
16549 let now = Instant::now();
16550 while self
16551 .flashblock_rpc_request_times
16552 .front()
16553 .is_some_and(|requested| now.duration_since(*requested) >= Duration::from_secs(1))
16554 {
16555 self.flashblock_rpc_request_times.pop_front();
16556 }
16557 }
16558
16559 fn filter_preconfirmed_logs(
16560 &mut self,
16561 flashblock: &FlashblockRef,
16562 mut logs: Vec<Log>,
16563 ) -> Result<Vec<Log>, SubscriberError> {
16564 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
16565 == Some(FlashblocksAdapter::PendingStatePolling);
16566 if self
16567 .latest_preconfirmation
16568 .as_ref()
16569 .is_some_and(|previous| !previous.same_payload(flashblock))
16570 {
16571 self.invalidate_preconfirmation_snapshot();
16576 }
16577 if self
16578 .latest_preconfirmation
16579 .as_ref()
16580 .is_none_or(|previous| !previous.same_payload(flashblock))
16581 {
16582 self.preconfirmed_seen_logs.clear();
16583 }
16584 if let Some(previous) = self.latest_preconfirmation.as_ref()
16585 && previous.same_payload(flashblock)
16586 && let (Some(previous_index), Some(current_index)) = (previous.index, flashblock.index)
16587 && current_index < previous_index
16588 {
16589 return Ok(Vec::new());
16590 }
16591 self.latest_preconfirmation = Some(flashblock.clone());
16592
16593 logs.sort_by_key(|log| (log.transaction_index.unwrap_or(u64::MAX), log.log_index));
16594 let mut filtered = Vec::new();
16595 for mut log in logs {
16596 if log.removed || log.block_number != Some(flashblock.block_number) {
16597 return Err(SubscriberError::Provider(
16598 "pre-confirmed log disagrees with its Flashblock snapshot".into(),
16599 ));
16600 }
16601 let transaction_hash = log.transaction_hash.ok_or_else(|| {
16602 SubscriberError::Provider(
16603 "pre-confirmed log is missing its transaction hash".into(),
16604 )
16605 })?;
16606 let log_index = log.log_index.ok_or_else(|| {
16607 SubscriberError::Provider("pre-confirmed log is missing its log index".into())
16608 })?;
16609 let transaction_index =
16610 flashblock
16611 .transaction_index(&transaction_hash)
16612 .ok_or_else(|| {
16613 SubscriberError::Provider(
16614 "pre-confirmed log transaction is absent from the cumulative Flashblock"
16615 .into(),
16616 )
16617 })?;
16618 if log
16619 .transaction_index
16620 .is_some_and(|reported| reported != transaction_index)
16621 {
16622 return Err(SubscriberError::Provider(
16623 "pre-confirmed log transaction index disagrees with cumulative membership"
16624 .into(),
16625 ));
16626 }
16627 let reported_hash = log.block_hash.and_then(non_placeholder_hash);
16628 if !samples_pending_range
16629 && let (Some(reported), Some(expected)) =
16630 (reported_hash, flashblock.partial_block_hash)
16631 && reported != expected
16632 {
16633 return Err(SubscriberError::Provider(
16634 "pre-confirmed log partial block hash disagrees with its Flashblock snapshot"
16635 .into(),
16636 ));
16637 }
16638 log.block_hash = Some(flashblock.content_hash);
16639 log.block_timestamp = flashblock.timestamp.or(log.block_timestamp);
16640 log.transaction_index = Some(transaction_index);
16641 if self
16642 .preconfirmed_seen_logs
16643 .insert((transaction_hash, log_index))
16644 && log_matches_any_interest(&log, &self.interests)
16645 {
16646 filtered.push(log);
16647 }
16648 }
16649 Ok(filtered)
16650 }
16651
16652 async fn verify_event_log_blocks(
16653 &mut self,
16654 event: &SubscriberEvent<N>,
16655 ) -> Result<(), SubscriberError> {
16656 if !self.config.verify_log_block_context {
16657 return Ok(());
16658 }
16659 match event {
16660 SubscriberEvent::Log { log, .. } => self.verify_log_block_context(log).await,
16661 SubscriberEvent::BackfilledLogs { logs, .. } | SubscriberEvent::Logs(logs) => {
16662 for log in logs {
16663 self.verify_log_block_context(log).await?;
16664 }
16665 Ok(())
16666 }
16667 SubscriberEvent::BlockHeader(_)
16668 | SubscriberEvent::PendingHash(_)
16669 | SubscriberEvent::PendingHashes(_)
16670 | SubscriberEvent::BasePendingLog { .. }
16671 | SubscriberEvent::BaseFlashblock(_)
16672 | SubscriberEvent::OpFlashblockTick
16673 | SubscriberEvent::CanonicalHeadTick
16674 | SubscriberEvent::PreconfirmedLogs { .. }
16675 | SubscriberEvent::FlashblockInvalidated
16676 | SubscriberEvent::FlashblockObserved
16677 | SubscriberEvent::StreamTerminated(_) => Ok(()),
16678 }
16679 }
16680
16681 async fn verify_log_block_context(&mut self, log: &Log) -> Result<(), SubscriberError> {
16682 if log.removed {
16683 return Ok(());
16684 }
16685 let number = log.block_number.ok_or_else(|| {
16686 SubscriberError::Provider(
16687 "canonical log is missing its block number during context verification".into(),
16688 )
16689 })?;
16690 let hash = log.block_hash.ok_or_else(|| {
16691 SubscriberError::Provider(
16692 "canonical log is missing its block hash during context verification".into(),
16693 )
16694 })?;
16695 let key = (number, hash);
16696 if self.verified_log_blocks.contains_key(&key) {
16697 return Ok(());
16698 }
16699 let provider = self
16700 .log_verification_provider
16701 .as_ref()
16702 .unwrap_or(&self.provider);
16703 let block = provider
16704 .get_block_by_number(BlockNumberOrTag::Number(number))
16705 .await
16706 .map_err(provider_error)?
16707 .ok_or_else(|| {
16708 SubscriberError::Provider(format!(
16709 "canonical log block {number} is unavailable during context verification"
16710 ))
16711 })?;
16712 let header = block.header();
16713 let verified = BlockRef {
16714 number: header.number(),
16715 hash: header.hash(),
16716 parent_hash: Some(header.parent_hash()),
16717 timestamp: Some(header.timestamp()),
16718 };
16719 if verified.number != number
16720 || verified.hash != hash
16721 || log
16722 .block_timestamp
16723 .is_some_and(|timestamp| verified.timestamp != Some(timestamp))
16724 {
16725 return Err(SubscriberError::Provider(format!(
16726 "canonical log block {number}:{hash:?} disagrees with the provider's current canonical identity"
16727 )));
16728 }
16729 self.verified_log_blocks.insert(key, verified);
16730 self.verified_log_block_order.push_back(key);
16731 let capacity = self.config.reconnect.dedupe_window.max(1);
16732 while self.verified_log_block_order.len() > capacity {
16733 if let Some(evicted) = self.verified_log_block_order.pop_front() {
16734 self.verified_log_blocks.remove(&evicted);
16735 }
16736 }
16737 Ok(())
16738 }
16739
16740 fn enqueue_event(&mut self, event: SubscriberEvent<N>) {
16741 self.enqueue_event_with_excluded_owners(event, None);
16742 }
16743
16744 fn buffer_reconcile_event_for_owners(
16745 &mut self,
16746 event: &SubscriberEvent<N>,
16747 target_epochs: &HashSet<SubscriberOwnerEpoch>,
16748 ) {
16749 match event {
16750 SubscriberEvent::Log { log, .. } => {
16751 self.buffer_reconcile_log_for_owners(log, InputSource::Subscription, target_epochs)
16752 }
16753 SubscriberEvent::BackfilledLogs { logs, .. } => {
16754 for log in logs {
16755 self.buffer_reconcile_log_for_owners(log, InputSource::Backfill, target_epochs);
16756 }
16757 }
16758 SubscriberEvent::Logs(logs) => {
16759 for log in logs {
16760 self.buffer_reconcile_log_for_owners(log, InputSource::Poll, target_epochs);
16761 }
16762 }
16763 SubscriberEvent::BlockHeader(_)
16764 | SubscriberEvent::PendingHash(_)
16765 | SubscriberEvent::PendingHashes(_)
16766 | SubscriberEvent::BasePendingLog { .. }
16767 | SubscriberEvent::BaseFlashblock(_)
16768 | SubscriberEvent::OpFlashblockTick
16769 | SubscriberEvent::CanonicalHeadTick
16770 | SubscriberEvent::PreconfirmedLogs { .. }
16771 | SubscriberEvent::FlashblockInvalidated
16772 | SubscriberEvent::FlashblockObserved
16773 | SubscriberEvent::StreamTerminated(_) => {}
16774 }
16775 }
16776
16777 fn buffer_reconcile_log_for_owners(
16778 &mut self,
16779 log: &Log,
16780 source: InputSource,
16781 target_epochs: &HashSet<SubscriberOwnerEpoch>,
16782 ) {
16783 let record = self.with_chain_id(log_input_record(log.clone(), source));
16784 let owners = self
16785 .staged_owners_for_record(&record)
16786 .into_iter()
16787 .filter(|owner| target_epochs.contains(owner))
16788 .collect::<Vec<_>>();
16789 if !owners.is_empty() {
16790 self.push_pending_reconcile_record(BufferedSubscriberOwnerRecord { record, owners });
16791 }
16792 }
16793
16794 fn promote_reconcile_owner_records(&mut self, target_epochs: &HashSet<SubscriberOwnerEpoch>) {
16795 let mut retained = VecDeque::new();
16796 while let Some(mut buffered) = self.pending_reconcile_owner_records.pop_front() {
16797 let mut promoted = Vec::new();
16798 buffered.owners.retain(|owner| {
16799 if target_epochs.contains(owner) {
16800 promoted.push(owner.clone());
16801 false
16802 } else {
16803 true
16804 }
16805 });
16806 if promoted.is_empty() {
16807 retained.push_back(buffered);
16808 continue;
16809 }
16810 let promoted_record = if buffered.owners.is_empty() {
16811 buffered.record
16812 } else {
16813 let record = buffered.record.clone();
16814 retained.push_back(buffered);
16815 record
16816 };
16817 self.enqueue_owner_record_for_owners_unmerged(promoted_record, promoted);
16818 }
16819 self.pending_reconcile_owner_records = retained;
16820 }
16821
16822 fn seed_reconciled_filter_anchors(
16823 &mut self,
16824 plans: &[SubscriberOwnerReconcilePlan<N>],
16825 through: u64,
16826 ) {
16827 for filter in plans.iter().flat_map(|plan| log_filters(&plan.interests)) {
16828 let Some(source_id) = self.log_source_ids.get(&filter).copied() else {
16829 continue;
16830 };
16831 let anchor = self
16832 .last_seen_log_blocks
16833 .entry(source_id)
16834 .or_insert(through);
16835 *anchor = (*anchor).max(through);
16836 }
16837 }
16838
16839 fn enqueue_event_excluding_owners(
16840 &mut self,
16841 event: SubscriberEvent<N>,
16842 excluded: &HashSet<SubscriberOwnerEpoch>,
16843 ) {
16844 self.enqueue_event_with_excluded_owners(event, Some(excluded));
16845 }
16846
16847 fn enqueue_event_with_excluded_owners(
16848 &mut self,
16849 event: SubscriberEvent<N>,
16850 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
16851 ) {
16852 match event {
16853 SubscriberEvent::Log { source_id, log } => {
16854 if log_matches_any_interest(&log, &self.interests) {
16855 let record = log_input_record(log, InputSource::Subscription);
16856 self.note_log_block(source_id, &record);
16857 self.enqueue_record_with_excluded_owners(record, excluded);
16858 }
16859 }
16860 SubscriberEvent::BackfilledLogs { source_id, logs } => {
16861 self.enqueue_backfilled_logs_with_excluded_owners(
16862 logs,
16863 Some(source_id),
16864 None,
16865 None,
16866 excluded,
16867 );
16868 }
16869 SubscriberEvent::Logs(logs) => {
16870 for log in logs {
16871 if log_matches_any_interest(&log, &self.interests) {
16872 self.enqueue_record_with_excluded_owners(
16873 log_input_record(log, InputSource::Poll),
16874 excluded,
16875 );
16876 }
16877 }
16878 }
16879 SubscriberEvent::BlockHeader(header) => {
16880 if needs_header_block_stream(&self.interests) {
16881 let record = block_header_input_record::<N>(header);
16882 self.enqueue_record_with_excluded_owners(record, excluded);
16883 }
16884 }
16885 SubscriberEvent::PendingHash(hash) => {
16886 let record = pending_hash_input_record::<N>(hash, InputSource::Subscription);
16887 self.enqueue_record_with_excluded_owners(record, excluded);
16888 }
16889 SubscriberEvent::PendingHashes(hashes) => {
16890 for hash in hashes {
16891 self.enqueue_record_with_excluded_owners(
16892 pending_hash_input_record::<N>(hash, InputSource::Poll),
16893 excluded,
16894 );
16895 }
16896 }
16897 SubscriberEvent::PreconfirmedLogs { flashblock, logs } => {
16898 for log in logs {
16899 let record = self
16900 .with_chain_id(preconfirmed_log_input_record::<N>(log, flashblock.clone()));
16901 self.push_pending_record(SubscriberInputRecord {
16902 record,
16903 scope: SubscriberInputScope::Preconfirmed,
16904 });
16905 }
16906 }
16907 SubscriberEvent::FlashblockInvalidated => {
16908 self.pending_preconfirmation_invalidation = true;
16909 }
16910 SubscriberEvent::BasePendingLog { .. }
16911 | SubscriberEvent::BaseFlashblock(_)
16912 | SubscriberEvent::OpFlashblockTick
16913 | SubscriberEvent::CanonicalHeadTick
16914 | SubscriberEvent::FlashblockObserved => {}
16915 SubscriberEvent::StreamTerminated(_) => {}
16916 }
16917 }
16918
16919 fn enqueue_backfilled_logs(
16920 &mut self,
16921 logs: Vec<Log>,
16922 source_id: Option<usize>,
16923 owner: Option<&SubscriberOwnerEpoch>,
16924 range: Option<SubscriberBackfill>,
16925 ) {
16926 self.enqueue_backfilled_logs_with_excluded_owners(logs, source_id, owner, range, None);
16927 }
16928
16929 fn enqueue_backfilled_logs_with_excluded_owners(
16930 &mut self,
16931 logs: Vec<Log>,
16932 source_id: Option<usize>,
16933 owner: Option<&SubscriberOwnerEpoch>,
16934 range: Option<SubscriberBackfill>,
16935 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
16936 ) {
16937 for log in logs {
16938 if range.as_ref().is_some_and(|range| {
16939 log.block_number.is_some_and(|block| {
16940 block < range.start_block() || range.end_block().is_some_and(|end| block > end)
16941 })
16942 }) {
16943 continue;
16944 }
16945 let matches = match owner {
16946 Some(epoch) => self
16947 .owned_interests
16948 .iter()
16949 .find(|entry| entry.epoch.as_ref() == Some(epoch))
16950 .is_some_and(|entry| log_matches_any_interest(&log, &entry.interests)),
16951 None => log_matches_any_interest(&log, &self.interests),
16952 };
16953 if matches {
16954 let record = log_input_record(log, InputSource::Backfill);
16955 if let Some(epoch) = owner {
16956 self.enqueue_owner_record(record, epoch.clone());
16957 } else {
16958 if let Some(source_id) = source_id {
16959 self.note_log_block(source_id, &record);
16960 }
16961 self.enqueue_record_with_excluded_owners(record, excluded);
16962 }
16963 }
16964 }
16965 }
16966
16967 fn enqueue_compat_owner_backfilled_logs(
16968 &mut self,
16969 logs: Vec<Log>,
16970 owner: &HandlerId,
16971 range: SubscriberBackfill,
16972 ) {
16973 let interests = self
16974 .owned_interests
16975 .iter()
16976 .find(|entry| {
16977 &entry.owner == owner
16978 && entry.epoch.is_none()
16979 && entry.state == SubscriberOwnerState::Active
16980 })
16981 .map(|entry| entry.interests.clone());
16982 let Some(interests) = interests else {
16983 return;
16984 };
16985 for log in logs {
16986 if log.block_number.is_some_and(|block| {
16987 block < range.start_block() || range.end_block().is_some_and(|end| block > end)
16988 }) || !log_matches_any_interest(&log, &interests)
16989 {
16990 continue;
16991 }
16992 let record = log_input_record(log, InputSource::Backfill);
16993 self.enqueue_compat_owner_record(record, owner.clone());
16994 }
16995 }
16996
16997 async fn reconnect_source_stream(
16998 &mut self,
16999 source: SubscriberStreamSource,
17000 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17001 if !source.is_pubsub() {
17002 return Err(stream_terminated_error(&source));
17003 }
17004
17005 if !self.config.reconnect.enabled {
17006 return Err(SubscriberError::Provider(format!(
17007 "Alloy subscriber {} stream terminated and reconnect is disabled",
17008 source.label()
17009 )));
17010 }
17011
17012 let mut attempts = 0usize;
17013 let mut delay = self.config.reconnect.initial_delay;
17014 let mut retry_delay = self.config.reconnect.retry_delay;
17015
17016 loop {
17017 attempts = attempts.saturating_add(1);
17018 if !delay.is_zero() {
17019 tokio::time::sleep(delay).await;
17020 }
17021
17022 match self.reconnect_source_once(source.clone()).await {
17023 Ok(backfill_event) => return Ok(backfill_event),
17024 Err(error) if reconnect_attempts_exhausted(attempts, &self.config.reconnect) => {
17025 return Err(SubscriberError::Provider(format!(
17026 "Alloy subscriber {} stream terminated and reconnect failed after {attempts} attempt(s): {error}",
17027 source.label()
17028 )));
17029 }
17030 Err(error) => {
17031 tracing::warn!(
17032 stream = source.label(),
17033 attempts,
17034 error = %error,
17035 "Alloy subscriber reconnect attempt failed"
17036 );
17037 delay = retry_delay;
17038 retry_delay =
17039 next_reconnect_delay(retry_delay, self.config.reconnect.max_delay);
17040 }
17041 }
17042 }
17043 }
17044
17045 async fn reconnect_source_once(
17046 &mut self,
17047 source: SubscriberStreamSource,
17048 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17049 if matches!(
17050 &self.state,
17051 AlloySubscriberState::Active(streams) if streams.contains_source(&source)
17052 ) {
17053 let backfill_event = self.backfill_reconnected_source(&source).await?;
17057 self.pending_source_backfills
17058 .retain(|pending| !pending.same_key(&source));
17059 return Ok(backfill_event);
17060 }
17061 let stream = self.connect_source_stream(source.clone()).await?;
17062 if !matches!(self.state, AlloySubscriberState::Active(_)) {
17063 return Err(SubscriberError::Provider(
17064 "Alloy subscriber state changed before reconnect completed".to_owned(),
17065 ));
17066 }
17067 self.install_source_stream(source.clone(), stream);
17068 if self.source_requires_backfill(&source) {
17069 self.queue_source_backfill(source.clone());
17070 }
17071 let backfill_event = self.backfill_reconnected_source(&source).await?;
17072 self.pending_source_backfills
17073 .retain(|pending| !pending.same_key(&source));
17074
17075 Ok(backfill_event)
17076 }
17077
17078 async fn backfill_reconnected_source(
17079 &mut self,
17080 source: &SubscriberStreamSource,
17081 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17082 if source.is_flashblocks() {
17083 return Ok(None);
17084 }
17085 let SubscriberStreamSource::PubSubLog { id, filter } = source else {
17086 return Ok(None);
17087 };
17088 let Some(from_block) = self.last_seen_log_blocks.get(id).copied() else {
17089 return Ok(None);
17090 };
17091
17092 let latest = self
17093 .provider
17094 .get_block_number()
17095 .await
17096 .map_err(provider_error)?;
17097 if latest < from_block {
17098 return Ok(None);
17099 }
17100
17101 let logs = self
17102 .provider
17103 .get_logs(&filter.clone().from_block(from_block).to_block(latest))
17104 .await
17105 .map_err(provider_error)?;
17106 Ok(Some(SubscriberEvent::BackfilledLogs {
17107 source_id: *id,
17108 logs,
17109 }))
17110 }
17111
17112 fn note_log_block(&mut self, source_id: usize, record: &ReactiveInputRecord<N>) {
17113 if let Some(block) = record.context.block.as_ref() {
17114 self.last_seen_log_blocks.insert(source_id, block.number);
17115 }
17116 }
17117
17118 fn enqueue_record_with_excluded_owners(
17119 &mut self,
17120 record: ReactiveInputRecord<N>,
17121 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
17122 ) {
17123 let record = self.with_chain_id(record);
17124 let mut owners = self.staged_owners_for_record(&record);
17125 if let Some(excluded) = excluded {
17126 owners.retain(|owner| !excluded.contains(owner));
17127 }
17128 let canonical_duplicate = self.should_skip_recent_duplicate(&record);
17129 let owners = self.filter_recent_owner_duplicates(&record, owners);
17130 let compatibility_owners = self.compatibility_owners_for_record(&record);
17131 let (already_served, newly_served): (Vec<_>, Vec<_>) = compatibility_owners
17132 .into_iter()
17133 .partition(|owner| self.compatibility_owner_has_seen(&record, owner));
17134 if canonical_duplicate {
17135 if !owners.is_empty() {
17136 self.push_pending_record(SubscriberInputRecord {
17137 record: record.clone(),
17138 scope: SubscriberInputScope::OwnerOnly { owners },
17139 });
17140 }
17141 if !newly_served.is_empty() {
17142 for owner in &newly_served {
17143 self.remember_compatibility_owner_record(&record, owner);
17144 }
17145 self.push_pending_record(SubscriberInputRecord {
17146 record,
17147 scope: SubscriberInputScope::OwnerOnlyHandlers {
17148 owners: newly_served,
17149 },
17150 });
17151 }
17152 return;
17153 }
17154 self.remember_record(&record);
17155 for owner in already_served.iter().chain(&newly_served) {
17156 self.remember_compatibility_owner_record(&record, owner);
17157 }
17158 self.push_pending_record(SubscriberInputRecord {
17159 record,
17160 scope: if already_served.is_empty() {
17161 SubscriberInputScope::Canonical { owners }
17162 } else {
17163 SubscriberInputScope::CanonicalResidual {
17164 owners,
17165 excluded: already_served,
17166 }
17167 },
17168 });
17169 }
17170
17171 fn enqueue_compat_owner_record(&mut self, record: ReactiveInputRecord<N>, owner: HandlerId) {
17172 let record = self.with_chain_id(record);
17173 if self.compatibility_owner_has_seen(&record, &owner) {
17174 return;
17175 }
17176 self.remember_compatibility_owner_record(&record, &owner);
17177 self.push_pending_record(SubscriberInputRecord {
17178 record,
17179 scope: SubscriberInputScope::OwnerOnlyHandlers {
17180 owners: vec![owner],
17181 },
17182 });
17183 }
17184
17185 fn compatibility_owners_for_record(&self, record: &ReactiveInputRecord<N>) -> Vec<HandlerId> {
17186 self.owned_interests
17187 .iter()
17188 .filter(|entry| entry.epoch.is_none() && entry.state == SubscriberOwnerState::Active)
17189 .filter(|entry| {
17190 entry
17191 .interests
17192 .iter()
17193 .any(|interest| interest_matches(interest, &record.input))
17194 })
17195 .map(|entry| entry.owner.clone())
17196 .collect()
17197 }
17198
17199 fn compatibility_owner_has_seen(
17200 &self,
17201 record: &ReactiveInputRecord<N>,
17202 owner: &HandlerId,
17203 ) -> bool {
17204 should_dedupe_record(record)
17205 && self
17206 .recent_compat_owner_input_ref_sets
17207 .get(owner)
17208 .is_some_and(|seen| seen.contains(&record.input_ref()))
17209 }
17210
17211 fn remember_compatibility_owner_record(
17212 &mut self,
17213 record: &ReactiveInputRecord<N>,
17214 owner: &HandlerId,
17215 ) {
17216 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
17217 return;
17218 }
17219 let input_ref = record.input_ref();
17220 let seen = self
17221 .recent_compat_owner_input_ref_sets
17222 .entry(owner.clone())
17223 .or_default();
17224 if !seen.insert(input_ref) {
17225 return;
17226 }
17227 let recent = self
17228 .recent_compat_owner_input_refs
17229 .entry(owner.clone())
17230 .or_default();
17231 recent.push_back(input_ref);
17232 while recent.len() > self.config.reconnect.dedupe_window {
17233 if let Some(evicted) = recent.pop_front() {
17234 seen.remove(&evicted);
17235 }
17236 }
17237 }
17238
17239 fn enqueue_owner_record(
17240 &mut self,
17241 record: ReactiveInputRecord<N>,
17242 owner: SubscriberOwnerEpoch,
17243 ) {
17244 self.enqueue_owner_record_for_owners(record, vec![owner]);
17245 }
17246
17247 fn enqueue_owner_record_for_owners(
17248 &mut self,
17249 record: ReactiveInputRecord<N>,
17250 owners: Vec<SubscriberOwnerEpoch>,
17251 ) {
17252 self.enqueue_owner_record_for_owners_inner(record, owners, true);
17253 }
17254
17255 fn enqueue_owner_record_for_owners_unmerged(
17256 &mut self,
17257 record: ReactiveInputRecord<N>,
17258 owners: Vec<SubscriberOwnerEpoch>,
17259 ) {
17260 self.enqueue_owner_record_for_owners_inner(record, owners, false);
17261 }
17262
17263 fn enqueue_owner_record_for_owners_inner(
17264 &mut self,
17265 record: ReactiveInputRecord<N>,
17266 owners: Vec<SubscriberOwnerEpoch>,
17267 merge_pending: bool,
17268 ) {
17269 let record = self.with_chain_id(record);
17270 let owners = self.filter_recent_owner_duplicates(&record, owners);
17271 if owners.is_empty() {
17272 return;
17273 }
17274 if merge_pending
17275 && should_dedupe_record(&record)
17276 && self.config.reconnect.dedupe_window != 0
17277 {
17278 let input_ref = record.input_ref();
17279 if let Some(pending) = self
17280 .pending_records
17281 .iter_mut()
17282 .rev()
17283 .find(|pending| pending.record.input_ref() == input_ref)
17284 {
17285 let pending_owners = match &mut pending.scope {
17286 SubscriberInputScope::Canonical { owners }
17287 | SubscriberInputScope::CanonicalResidual { owners, .. }
17288 | SubscriberInputScope::OwnerOnly { owners } => Some(owners),
17289 SubscriberInputScope::OwnerOnlyHandlers { .. }
17290 | SubscriberInputScope::Preconfirmed => None,
17291 };
17292 if let Some(pending_owners) = pending_owners {
17293 for owner in owners {
17294 if !pending_owners.contains(&owner) {
17295 pending_owners.push(owner);
17296 }
17297 }
17298 return;
17299 }
17300 }
17301 }
17302 self.push_pending_record(SubscriberInputRecord {
17303 record,
17304 scope: SubscriberInputScope::OwnerOnly { owners },
17305 });
17306 }
17307
17308 fn push_pending_record(&mut self, record: SubscriberInputRecord<N>) {
17309 if self.pending_record_count() >= self.config.max_pending_records {
17310 self.note_resource_error(format!(
17311 "pending record queues reached the configured limit of {}",
17312 self.config.max_pending_records
17313 ));
17314 return;
17315 }
17316 self.pending_records.push_back(record);
17317 }
17318
17319 fn ensure_pending_record_capacity(
17320 &mut self,
17321 additional: usize,
17322 operation: &str,
17323 ) -> Result<(), SubscriberError> {
17324 let required = self.pending_record_count().saturating_add(additional);
17325 if required > self.config.max_pending_records {
17326 self.note_resource_error(format!(
17327 "{operation} require {required} pending records, above the configured limit of {}",
17328 self.config.max_pending_records
17329 ));
17330 return self.check_resource_error();
17331 }
17332 Ok(())
17333 }
17334
17335 fn push_pending_reconcile_record(&mut self, record: BufferedSubscriberOwnerRecord<N>) {
17336 if self.pending_record_count() >= self.config.max_pending_records {
17337 self.note_resource_error(format!(
17338 "pending record queues reached the configured limit of {}",
17339 self.config.max_pending_records
17340 ));
17341 return;
17342 }
17343 self.pending_reconcile_owner_records.push_back(record);
17344 }
17345
17346 fn pending_record_count(&self) -> usize {
17347 self.pending_records
17348 .len()
17349 .saturating_add(self.pending_reconcile_owner_records.len())
17350 }
17351
17352 fn note_resource_error(&mut self, message: String) {
17353 if self.resource_error.is_none() {
17354 self.resource_error = Some(message);
17355 }
17356 }
17357
17358 fn check_resource_error(&self) -> Result<(), SubscriberError> {
17359 match &self.resource_error {
17360 Some(message) => Err(SubscriberError::ResourceExhausted(message.clone())),
17361 None => Ok(()),
17362 }
17363 }
17364
17365 fn with_chain_id(&self, mut record: ReactiveInputRecord<N>) -> ReactiveInputRecord<N> {
17366 record.context.chain_id = self.chain_id;
17367 if self.config.verify_log_block_context
17368 && let ReactiveInput::Log(log) = &record.input
17369 && !log.removed
17370 && let (Some(number), Some(hash)) = (log.block_number, log.block_hash)
17371 && let Some(verified) = self.verified_log_blocks.get(&(number, hash)).copied()
17372 {
17373 record.context.block = Some(verified);
17374 record.context.chain_status = ChainStatus::Included {
17375 block: verified,
17376 confirmations: 0,
17377 };
17378 }
17379 record
17380 }
17381
17382 fn staged_owners_for_record(
17383 &self,
17384 record: &ReactiveInputRecord<N>,
17385 ) -> Vec<SubscriberOwnerEpoch> {
17386 self.owned_interests
17387 .iter()
17388 .filter(|entry| entry.state == SubscriberOwnerState::Staged)
17389 .filter(|entry| {
17390 entry
17391 .interests
17392 .iter()
17393 .any(|interest| interest_matches(interest, &record.input))
17394 })
17395 .filter_map(|entry| entry.epoch.clone())
17396 .collect()
17397 }
17398
17399 fn filter_recent_owner_duplicates(
17400 &mut self,
17401 record: &ReactiveInputRecord<N>,
17402 owners: Vec<SubscriberOwnerEpoch>,
17403 ) -> Vec<SubscriberOwnerEpoch> {
17404 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
17405 return owners;
17406 }
17407 let input_ref = record.input_ref();
17408 let window = self.config.reconnect.dedupe_window;
17409 owners
17410 .into_iter()
17411 .filter(|owner| {
17412 let seen = self
17413 .recent_owner_input_ref_sets
17414 .entry(owner.clone())
17415 .or_default();
17416 if !seen.insert(input_ref) {
17417 return false;
17418 }
17419 let recent = self
17420 .recent_owner_input_refs
17421 .entry(owner.clone())
17422 .or_default();
17423 recent.push_back(input_ref);
17424 while recent.len() > window {
17425 if let Some(evicted) = recent.pop_front() {
17426 seen.remove(&evicted);
17427 }
17428 }
17429 true
17430 })
17431 .collect()
17432 }
17433
17434 fn should_skip_recent_duplicate(&self, record: &ReactiveInputRecord<N>) -> bool {
17435 if !should_dedupe_record(record) {
17436 return false;
17437 }
17438 self.recent_input_ref_set.contains(&record.input_ref())
17439 }
17440
17441 fn remember_record(&mut self, record: &ReactiveInputRecord<N>) {
17442 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
17443 return;
17444 }
17445
17446 let input_ref = record.input_ref();
17447 if !self.recent_input_ref_set.insert(input_ref) {
17448 return;
17449 }
17450 self.recent_input_refs.push_back(input_ref);
17451
17452 while self.recent_input_refs.len() > self.config.reconnect.dedupe_window {
17453 if let Some(evicted) = self.recent_input_refs.pop_front() {
17454 self.recent_input_ref_set.remove(&evicted);
17455 }
17456 }
17457 }
17458}
17459
17460fn stream_with_termination<N, S>(
17461 stream: S,
17462 source: SubscriberStreamSource,
17463) -> BoxStream<'static, SubscriberEvent<N>>
17464where
17465 N: Network + 'static,
17466 S: futures::Stream<Item = SubscriberEvent<N>> + Send + 'static,
17467{
17468 stream
17469 .chain(stream::once(async move {
17470 SubscriberEvent::StreamTerminated(source)
17471 }))
17472 .boxed()
17473}
17474
17475fn flashblock_reconnect_future<N>(
17476 provider: RootProvider<N>,
17477 source: SubscriberStreamSource,
17478 channel_size: usize,
17479 reconnect: SubscriberReconnectConfig,
17480 first_delay: Duration,
17481 flashblock_poll_interval: Duration,
17482) -> FlashblockReconnectFuture<N>
17483where
17484 N: Network + 'static,
17485{
17486 Box::pin(async move {
17487 if !reconnect.enabled {
17488 let error = SubscriberError::Provider(format!(
17489 "Alloy subscriber {} stream terminated and reconnect is disabled",
17490 source.label()
17491 ));
17492 return (source, Err(error));
17493 }
17494
17495 let mut attempts = 0_usize;
17496 let mut delay = first_delay;
17497 let mut retry_delay = reconnect.retry_delay;
17498 loop {
17499 attempts = attempts.saturating_add(1);
17500 if !delay.is_zero() {
17501 tokio::time::sleep(delay).await;
17502 }
17503 match connect_flashblock_source_once(
17504 &provider,
17505 source.clone(),
17506 channel_size,
17507 flashblock_poll_interval,
17508 )
17509 .await
17510 {
17511 Ok(stream) => return (source, Ok(stream)),
17512 Err(error) if reconnect_attempts_exhausted(attempts, &reconnect) => {
17513 return (
17514 source.clone(),
17515 Err(SubscriberError::Provider(format!(
17516 "Alloy subscriber {} stream reconnect failed after {attempts} attempt(s): {error}",
17517 source.label()
17518 ))),
17519 );
17520 }
17521 Err(error) => {
17522 tracing::warn!(
17523 stream = source.label(),
17524 attempts,
17525 error = %error,
17526 "Flashblocks reconnect attempt failed"
17527 );
17528 delay = retry_delay;
17529 retry_delay = next_reconnect_delay(retry_delay, reconnect.max_delay);
17530 }
17531 }
17532 }
17533 })
17534}
17535
17536async fn connect_flashblock_source_once<N>(
17537 provider: &RootProvider<N>,
17538 source: SubscriberStreamSource,
17539 channel_size: usize,
17540 flashblock_poll_interval: Duration,
17541) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>
17542where
17543 N: Network + 'static,
17544{
17545 #[cfg(not(feature = "reactive-ws"))]
17546 let _ = provider;
17547
17548 match source {
17549 SubscriberStreamSource::BasePendingLog { id, filter } => {
17550 #[cfg(feature = "reactive-ws")]
17551 {
17552 let source = SubscriberStreamSource::BasePendingLog {
17553 id,
17554 filter: filter.clone(),
17555 };
17556 let params = base_pending_log_filter(&filter)?;
17557 let stream = provider
17558 .subscribe::<_, Log>(("pendingLogs", params))
17559 .channel_size(channel_size.max(1))
17560 .await
17561 .map_err(provider_error)?
17562 .into_stream()
17563 .map(move |log| SubscriberEvent::BasePendingLog { source_id: id, log });
17564 Ok(stream_with_termination(stream, source))
17565 }
17566 #[cfg(not(feature = "reactive-ws"))]
17567 {
17568 let _ = (id, filter, channel_size);
17569 Err(SubscriberError::Unsupported(
17570 "Base Flashblocks require the reactive-ws feature",
17571 ))
17572 }
17573 }
17574 SubscriberStreamSource::BaseFlashblocks => {
17575 #[cfg(feature = "reactive-ws")]
17576 {
17577 let stream = provider
17578 .subscribe::<_, BaseFlashblockWirePayload>(("newFlashblocks",))
17579 .channel_size(channel_size.max(1))
17580 .await
17581 .map_err(provider_error)?
17582 .into_stream()
17583 .map(SubscriberEvent::BaseFlashblock);
17584 Ok(stream_with_termination(
17585 stream,
17586 SubscriberStreamSource::BaseFlashblocks,
17587 ))
17588 }
17589 #[cfg(not(feature = "reactive-ws"))]
17590 {
17591 let _ = channel_size;
17592 Err(SubscriberError::Unsupported(
17593 "Base Flashblocks require the reactive-ws feature",
17594 ))
17595 }
17596 }
17597 SubscriberStreamSource::OpPendingFlashblocks => {
17598 let first_tick = tokio::time::Instant::now();
17599 let mut interval = tokio::time::interval_at(first_tick, flashblock_poll_interval);
17600 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
17601 let stream = stream::unfold(interval, |mut interval| async move {
17602 interval.tick().await;
17603 Some((SubscriberEvent::OpFlashblockTick, interval))
17604 });
17605 Ok(stream_with_termination(
17606 stream,
17607 SubscriberStreamSource::OpPendingFlashblocks,
17608 ))
17609 }
17610 source => Err(SubscriberError::InvalidConfig(match source {
17611 SubscriberStreamSource::PubSubLog { .. }
17612 | SubscriberStreamSource::CanonicalHeadPolling
17613 | SubscriberStreamSource::PubSubPendingHashes
17614 | SubscriberStreamSource::PubSubBlockHeaders
17615 | SubscriberStreamSource::PollingLog { .. }
17616 | SubscriberStreamSource::PollingPendingHashes => {
17617 "Flashblocks reconnect received a canonical source"
17618 }
17619 SubscriberStreamSource::BasePendingLog { .. }
17620 | SubscriberStreamSource::BaseFlashblocks
17621 | SubscriberStreamSource::OpPendingFlashblocks => unreachable!(),
17622 })),
17623 }
17624}
17625
17626fn aggregate_interests<N: Network>(
17627 base: &[ReactiveInterest<N>],
17628 owned: &[OwnedSubscriberInterests<N>],
17629) -> Vec<ReactiveInterest<N>> {
17630 base.iter()
17631 .cloned()
17632 .chain(
17633 owned
17634 .iter()
17635 .flat_map(|entry| entry.interests.iter().cloned()),
17636 )
17637 .collect()
17638}
17639
17640fn stream_terminated_error(source: &SubscriberStreamSource) -> SubscriberError {
17641 SubscriberError::Provider(format!(
17642 "Alloy subscriber {} stream terminated before the subscriber was stopped",
17643 source.label()
17644 ))
17645}
17646
17647fn reconnect_attempts_exhausted(attempts: usize, config: &SubscriberReconnectConfig) -> bool {
17648 config
17649 .max_attempts
17650 .is_some_and(|max_attempts| attempts >= max_attempts)
17651}
17652
17653fn next_reconnect_delay(current: Duration, max: Duration) -> Duration {
17654 if current.is_zero() {
17655 return current;
17656 }
17657 current.checked_mul(2).unwrap_or(max).min(max)
17658}
17659
17660fn should_dedupe_record<N: Network>(record: &ReactiveInputRecord<N>) -> bool {
17661 match &record.input {
17662 ReactiveInput::Log(log) => {
17663 is_canonical_status(&record.context.chain_status) && !log.removed
17664 }
17665 ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_) => true,
17666 ReactiveInput::FullBlock(_) | ReactiveInput::PendingTx(_) => false,
17667 }
17668}
17669
17670#[cfg(test)]
17671mod subscriber_helper_tests {
17672 use super::*;
17673 use alloy_provider::ProviderBuilder;
17674 use alloy_transport::mock::Asserter;
17675
17676 fn indexed_flashblock(transaction_hash: B256, state_root: B256) -> BaseFlashblockWirePayload {
17677 BaseFlashblockWirePayload::Indexed(BaseFlashblockPayload {
17678 payload_id: FixedBytes::repeat_byte(0x11),
17679 index: 0,
17680 base: Some(BaseFlashblockBase {
17681 parent_hash: B256::repeat_byte(100),
17682 block_number: 101,
17683 timestamp: 1_700_000_101,
17684 gas_limit: Some(30_000_000),
17685 base_fee_per_gas: Some(7),
17686 beneficiary: Some(Address::repeat_byte(0xcb)),
17687 prevrandao: Some(B256::repeat_byte(0x77)),
17688 }),
17689 diff: BaseFlashblockDiff {
17690 state_root,
17691 block_hash: B256::ZERO,
17692 transactions: vec![serde_json::Value::String(format!("{transaction_hash:#x}"))],
17693 transactions_root: None,
17694 },
17695 metadata: None,
17696 })
17697 }
17698
17699 #[test]
17700 fn duplicate_flashblock_transaction_membership_is_rejected() {
17701 let transaction = format!("{:#x}", B256::repeat_byte(0x41));
17702 let transactions = vec![
17703 serde_json::Value::String(transaction.clone()),
17704 serde_json::Value::String(transaction),
17705 ];
17706 assert!(matches!(
17707 flashblock_transaction_hashes(&transactions),
17708 Err(SubscriberError::Provider(ref message)) if message.contains("duplicate")
17709 ));
17710 }
17711
17712 #[test]
17713 fn conflicting_duplicate_indexed_flashblock_is_rejected() {
17714 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17715 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17716 provider,
17717 SubscriberMode::PubSub,
17718 SubscriberConfig::default(),
17719 )
17720 .with_provider_ref(ProviderRef::new("base-paid", 7));
17721 subscriber.chain_id = Some(8_453);
17722
17723 subscriber
17724 .accept_base_flashblock(indexed_flashblock(
17725 B256::repeat_byte(0x41),
17726 B256::repeat_byte(0xa1),
17727 ))
17728 .expect("first indexed preview");
17729 assert!(matches!(
17730 subscriber.accept_base_flashblock(indexed_flashblock(
17731 B256::repeat_byte(0x42),
17732 B256::repeat_byte(0xa2),
17733 )),
17734 Err(SubscriberError::Provider(ref message))
17735 if message.contains("conflicting duplicate")
17736 ));
17737 }
17738
17739 #[tokio::test]
17740 async fn duplicate_index_with_changed_commitment_is_rejected() {
17741 let transaction = B256::repeat_byte(0x41);
17742 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17743 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17744 provider,
17745 SubscriberMode::PubSub,
17746 SubscriberConfig::default(),
17747 )
17748 .with_provider_ref(ProviderRef::new("base-paid", 7));
17749 subscriber.chain_id = Some(8_453);
17750
17751 subscriber
17752 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(indexed_flashblock(
17753 transaction,
17754 B256::repeat_byte(0xa1),
17755 )))
17756 .await
17757 .expect("first indexed preview");
17758
17759 let BaseFlashblockWirePayload::Indexed(mut conflicting) =
17760 indexed_flashblock(transaction, B256::repeat_byte(0xa1))
17761 else {
17762 unreachable!()
17763 };
17764 conflicting.diff.state_root = B256::repeat_byte(0xbb);
17765 assert!(matches!(
17766 subscriber
17767 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(
17768 BaseFlashblockWirePayload::Indexed(conflicting),
17769 ))
17770 .await,
17771 Err(SubscriberError::Provider(ref message))
17772 if message.contains("conflicting duplicate indexed Flashblock content")
17773 ));
17774 }
17775
17776 #[tokio::test]
17777 async fn indexed_gap_recovery_seeds_later_cumulative_membership() {
17778 let transaction_a = B256::repeat_byte(0x41);
17779 let transaction_b = B256::repeat_byte(0x42);
17780 let transaction_c = B256::repeat_byte(0x43);
17781 let transaction_d = B256::repeat_byte(0x44);
17782 let asserter = Asserter::new();
17783 asserter.push_success(&100_u64);
17784 let pending = rpc_block(101, B256::ZERO).with_transactions(
17785 alloy_network::primitives::BlockTransactions::Hashes(vec![
17786 transaction_a,
17787 transaction_b,
17788 transaction_c,
17789 ]),
17790 );
17791 asserter.push_success(&Some(pending));
17792 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
17793 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17794 provider,
17795 SubscriberMode::PubSub,
17796 SubscriberConfig::default(),
17797 )
17798 .with_provider_ref(ProviderRef::new("base-paid", 7));
17799 subscriber.chain_id = Some(8_453);
17800
17801 subscriber
17802 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(indexed_flashblock(
17803 transaction_a,
17804 B256::repeat_byte(0xa1),
17805 )))
17806 .await
17807 .expect("index zero preview");
17808 let BaseFlashblockWirePayload::Indexed(mut gap) =
17809 indexed_flashblock(transaction_c, B256::repeat_byte(0xa3))
17810 else {
17811 unreachable!()
17812 };
17813 gap.index = 2;
17814 gap.base = None;
17815 gap.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
17816 subscriber
17817 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(
17818 BaseFlashblockWirePayload::Indexed(gap),
17819 ))
17820 .await
17821 .expect("the missing index is recovered from pending state");
17822
17823 let BaseFlashblockWirePayload::Indexed(mut next) =
17824 indexed_flashblock(transaction_d, B256::repeat_byte(0xa4))
17825 else {
17826 unreachable!()
17827 };
17828 next.index = 3;
17829 next.base = None;
17830 next.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
17831 let (next, recover) = subscriber
17832 .accept_base_flashblock(BaseFlashblockWirePayload::Indexed(next))
17833 .expect("the next diff extends the recovered cumulative set");
17834 assert!(!recover);
17835 assert_eq!(
17836 next.transaction_hashes,
17837 vec![transaction_a, transaction_b, transaction_c, transaction_d]
17838 );
17839 }
17840
17841 #[tokio::test]
17842 async fn unrecoverable_indexed_gap_revokes_the_generation() {
17843 let asserter = Asserter::new();
17844 asserter.push_success(&100_u64);
17845 asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(0x64))));
17846 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
17847 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17848 provider,
17849 SubscriberMode::PubSub,
17850 SubscriberConfig {
17851 preconfirmations: PreconfirmationMode::Preferred,
17852 ..SubscriberConfig::default()
17853 },
17854 )
17855 .with_provider_ref(ProviderRef::new("base-paid", 7));
17856 subscriber.chain_id = Some(8_453);
17857
17858 subscriber
17859 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(indexed_flashblock(
17860 B256::repeat_byte(0x41),
17861 B256::repeat_byte(0xa1),
17862 )))
17863 .await
17864 .expect("index zero preview");
17865 let BaseFlashblockWirePayload::Indexed(mut gap) =
17866 indexed_flashblock(B256::repeat_byte(0x43), B256::repeat_byte(0xa3))
17867 else {
17868 unreachable!()
17869 };
17870 gap.index = 2;
17871 gap.base = None;
17872 gap.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
17873 let event = subscriber
17874 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(
17875 BaseFlashblockWirePayload::Indexed(gap),
17876 ))
17877 .await
17878 .expect("preferred mode fails closed without pending recovery")
17879 .expect("generation invalidation is observable");
17880 assert!(matches!(event, SubscriberEvent::FlashblockInvalidated));
17881 assert!(subscriber.latest_preconfirmation.is_none());
17882 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 8);
17883 }
17884
17885 #[test]
17886 fn base_flashblock_wire_decodes_cumulative_block_shape() {
17887 let payload: BaseFlashblockWirePayload = serde_json::from_str(
17888 r#"{
17889 "hash":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
17890 "number":"0x2ef403b",
17891 "parentHash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
17892 "stateRoot":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
17893 "timestamp":"0x6a68dd59",
17894 "transactions":[]
17895 }"#,
17896 )
17897 .expect("decode current Base newFlashblocks shape");
17898 let BaseFlashblockWirePayload::Block(payload) = payload else {
17899 panic!("expected cumulative block-shaped payload")
17900 };
17901 assert_eq!(payload.number, 49_233_979);
17902 assert_eq!(payload.timestamp, 1_785_257_305);
17903 assert_eq!(payload.hash, B256::repeat_byte(0xaa));
17904 assert_eq!(payload.parent_hash, B256::repeat_byte(0xbb));
17905 assert_eq!(payload.state_root, B256::repeat_byte(0xcc));
17906 }
17907
17908 #[tokio::test]
17909 async fn zero_hash_pending_log_waits_for_the_preview_containing_its_transaction() {
17910 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17911 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17912 provider,
17913 SubscriberMode::PubSub,
17914 SubscriberConfig::default(),
17915 )
17916 .with_provider_ref(ProviderRef::new("base-paid", 7));
17917 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
17918 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
17919 local_matcher: None,
17920 route_key: None,
17921 })];
17922 subscriber.interests = subscriber.base_interests.clone();
17923
17924 let first: BaseFlashblockWirePayload = serde_json::from_str(
17925 r#"{
17926 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
17927 "number":"0x65",
17928 "parentHash":"0x6464646464646464646464646464646464646464646464646464646464646464",
17929 "stateRoot":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
17930 "transactionsRoot":"0x1111111111111111111111111111111111111111111111111111111111111111",
17931 "timestamp":"0x6553f165",
17932 "transactions":["0x4141414141414141414141414141414141414141414141414141414141414141"]
17933 }"#,
17934 )
17935 .expect("decode first cumulative preview");
17936 subscriber
17937 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(first))
17938 .await
17939 .expect("first preview is accepted");
17940
17941 let mut second_log = rpc_log(false);
17942 second_log.block_hash = Some(B256::ZERO);
17943 second_log.block_number = Some(102);
17944 second_log.block_timestamp = Some(1_700_000_102);
17945 second_log.transaction_hash = Some(B256::repeat_byte(0x42));
17946 second_log.transaction_index = Some(0);
17947 second_log.log_index = Some(0);
17948
17949 let before_preview = subscriber
17950 .normalize_flashblock_event(SubscriberEvent::BasePendingLog {
17951 source_id: 0,
17952 log: second_log,
17953 })
17954 .await
17955 .expect("a zero-hash log for the next block must be buffered");
17956 assert!(before_preview.is_none());
17957
17958 let second: BaseFlashblockWirePayload = serde_json::from_str(
17959 r#"{
17960 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
17961 "number":"0x66",
17962 "parentHash":"0x6565656565656565656565656565656565656565656565656565656565656565",
17963 "stateRoot":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
17964 "transactionsRoot":"0x2222222222222222222222222222222222222222222222222222222222222222",
17965 "timestamp":"0x6553f166",
17966 "transactions":["0x4242424242424242424242424242424242424242424242424242424242424242"]
17967 }"#,
17968 )
17969 .expect("decode second cumulative preview");
17970 let event = subscriber
17971 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(second))
17972 .await
17973 .expect("second preview is accepted")
17974 .expect("the matching buffered log is released");
17975 let SubscriberEvent::PreconfirmedLogs { flashblock, logs } = event else {
17976 panic!("expected a preconfirmed log batch")
17977 };
17978 assert_eq!(flashblock.block_number, 102);
17979 assert_ne!(flashblock.content_hash, B256::ZERO);
17980 assert_eq!(flashblock.partial_block_hash, None);
17981 assert_eq!(logs.len(), 1);
17982 assert_eq!(logs[0].transaction_hash, Some(B256::repeat_byte(0x42)));
17983 assert_eq!(logs[0].block_hash, Some(flashblock.content_hash));
17984 }
17985
17986 #[test]
17987 fn flashblock_endpoints_certify_canonical_heads_instead_of_trusting_newheads() {
17988 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
17989 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
17990 provider,
17991 SubscriberMode::PubSub,
17992 SubscriberConfig {
17993 preconfirmations: PreconfirmationMode::Required,
17994 ..SubscriberConfig::default()
17995 },
17996 )
17997 .with_provider_ref(ProviderRef::new("base-paid", 7));
17998 subscriber.chain_id = Some(8_453);
17999 subscriber.interests = vec![ReactiveInterest::Blocks(BlockInterest::default())];
18000
18001 let sources = subscriber.pubsub_stream_sources();
18002 assert!(
18003 sources
18004 .iter()
18005 .any(|source| matches!(source, SubscriberStreamSource::CanonicalHeadPolling))
18006 );
18007 assert!(
18008 !sources
18009 .iter()
18010 .any(|source| matches!(source, SubscriberStreamSource::PubSubBlockHeaders))
18011 );
18012 }
18013
18014 #[tokio::test]
18015 async fn certified_canonical_heads_are_deduplicated_and_reject_placeholder_hashes() {
18016 let asserter = Asserter::new();
18017 let certified = rpc_block(101, B256::repeat_byte(0x65));
18018 asserter.push_success(&Some(certified.clone()));
18019 asserter.push_success(&Some(certified));
18020 asserter.push_success(&Some(rpc_block(102, B256::ZERO)));
18021 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
18022 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18023 provider,
18024 SubscriberMode::PubSub,
18025 SubscriberConfig::default(),
18026 );
18027
18028 assert!(matches!(
18029 subscriber
18030 .fetch_certified_canonical_head()
18031 .await
18032 .expect("first certified head"),
18033 Some(SubscriberEvent::BlockHeader(_))
18034 ));
18035 assert!(
18036 subscriber
18037 .fetch_certified_canonical_head()
18038 .await
18039 .expect("duplicate certified head")
18040 .is_none()
18041 );
18042 assert!(matches!(
18043 subscriber.fetch_certified_canonical_head().await,
18044 Err(SubscriberError::Provider(ref message))
18045 if message.contains("placeholder hash")
18046 ));
18047 }
18048
18049 #[tokio::test]
18050 async fn optimism_canonical_head_is_the_exact_parent_of_pending() {
18051 let asserter = Asserter::new();
18052 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
18053 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18054 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18055 provider,
18056 SubscriberMode::PubSub,
18057 SubscriberConfig {
18058 preconfirmations: PreconfirmationMode::Required,
18059 ..SubscriberConfig::default()
18060 },
18061 );
18062 subscriber.chain_id = Some(10);
18063 subscriber.interests = vec![ReactiveInterest::Blocks(BlockInterest::default())];
18064
18065 let event = subscriber
18066 .fetch_certified_canonical_head()
18067 .await
18068 .expect("OP pending parent can be certified")
18069 .expect("the first certified parent is emitted");
18070 let SubscriberEvent::BlockHeader(header) = event else {
18071 panic!("expected a certified canonical block header")
18072 };
18073 assert_eq!(header.number(), 100);
18074 assert_eq!(header.hash, B256::repeat_byte(0x64));
18075 assert_eq!(
18076 subscriber
18077 .flashblocks_rpc_metrics()
18078 .pending_block_requests(),
18079 1
18080 );
18081 assert_eq!(
18082 subscriber
18083 .flashblocks_rpc_metrics()
18084 .canonical_head_requests(),
18085 1
18086 );
18087 assert!(asserter.read_q().is_empty());
18088 }
18089
18090 #[test]
18091 fn optimism_uses_one_bounded_pending_state_stream() {
18092 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18093 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18094 provider,
18095 SubscriberMode::PubSub,
18096 SubscriberConfig {
18097 preconfirmations: PreconfirmationMode::Required,
18098 ..SubscriberConfig::default()
18099 },
18100 )
18101 .with_provider_ref(ProviderRef::new("op-paid", 11));
18102 subscriber.chain_id = Some(10);
18103 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
18104 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
18105 local_matcher: None,
18106 route_key: None,
18107 })];
18108 subscriber.interests = subscriber.base_interests.clone();
18109
18110 let sources = subscriber.pubsub_stream_sources();
18111 assert_eq!(
18112 sources
18113 .iter()
18114 .filter(|source| matches!(source, SubscriberStreamSource::OpPendingFlashblocks))
18115 .count(),
18116 1
18117 );
18118 assert!(sources.iter().all(|source| !matches!(
18119 source,
18120 SubscriberStreamSource::BaseFlashblocks | SubscriberStreamSource::BasePendingLog { .. }
18121 )));
18122 }
18123
18124 #[test]
18125 fn optimism_default_receipt_budget_reserves_every_fixed_sampler_method() {
18126 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18127 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18128 provider,
18129 SubscriberMode::PubSub,
18130 SubscriberConfig {
18131 preconfirmations: PreconfirmationMode::Required,
18132 ..SubscriberConfig::default()
18133 },
18134 );
18135 subscriber.chain_id = Some(10);
18136 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18137 subscriber.interests = subscriber.base_interests.clone();
18138
18139 assert_eq!(
18143 subscriber.pending_receipt_requests_per_second_capacity(),
18144 28
18145 );
18146 assert_eq!(subscriber.pending_receipt_requests_per_tick_capacity(), 7);
18147
18148 subscriber
18149 .interests
18150 .push(ReactiveInterest::Blocks(BlockInterest::default()));
18151 assert_eq!(
18152 subscriber.pending_receipt_requests_per_second_capacity(),
18153 24
18154 );
18155 assert_eq!(subscriber.pending_receipt_requests_per_tick_capacity(), 6);
18156 subscriber.interests.pop();
18157
18158 for _ in 0..4 {
18159 assert!(subscriber.reserve_flashblock_rpc_methods(3));
18160 assert_eq!(subscriber.pending_receipt_request_allowance(), 7);
18161 assert!(subscriber.reserve_flashblock_rpc_methods(7));
18162 }
18163 assert!(!subscriber.reserve_flashblock_rpc_methods(1));
18164 subscriber.reset_flashblock_tracking();
18165 assert!(
18166 !subscriber.reserve_flashblock_rpc_methods(1),
18167 "a reconnect must not reset an endpoint's rolling quota window"
18168 );
18169 }
18170
18171 #[test]
18172 fn flashblocks_config_rejects_a_zero_rpc_budget() {
18173 let config = SubscriberConfig {
18174 preconfirmations: PreconfirmationMode::Required,
18175 max_flashblock_rpc_requests_per_second: 0,
18176 ..SubscriberConfig::default()
18177 };
18178
18179 assert!(matches!(
18180 validate_subscriber_config(&config),
18181 Err(SubscriberError::InvalidConfig(
18182 "SubscriberConfig::max_flashblock_rpc_requests_per_second must be greater than zero"
18183 ))
18184 ));
18185 }
18186
18187 #[tokio::test]
18188 async fn optimism_preflight_rejects_a_budget_without_receipt_capacity() {
18189 let asserter = Asserter::new();
18190 asserter.push_success(&serde_json::json!(["flashblocksv1"]));
18191 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18192 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18193 provider,
18194 SubscriberMode::PubSub,
18195 SubscriberConfig {
18196 preconfirmations: PreconfirmationMode::Required,
18197 max_flashblock_rpc_requests_per_second: 15,
18200 ..SubscriberConfig::default()
18201 },
18202 )
18203 .with_provider_ref(ProviderRef::new("op-paid", 12));
18204 subscriber.chain_id = Some(10);
18205 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18206 subscriber.interests = subscriber.base_interests.clone();
18207 let desired = subscriber.pubsub_stream_sources();
18208 let mut streams = SubscriberStreams::new();
18209 for source in desired {
18210 streams.push(source, stream::pending().boxed());
18211 }
18212 subscriber.state = AlloySubscriberState::Active(streams);
18213 subscriber.sources_dirty = false;
18214 assert!(matches!(
18215 subscriber.establish_flashblocks_preflight(10).await,
18216 Err(SubscriberError::InvalidConfig(message))
18217 if message.contains("leaves no capacity for OP transaction receipts")
18218 ));
18219 assert!(asserter.read_q().is_empty());
18220 }
18221
18222 #[cfg(feature = "reactive-ws")]
18223 #[tokio::test]
18224 async fn flashblocks_preflight_proves_chain_and_both_subscription_lanes() {
18225 let asserter = Asserter::new();
18226 asserter.push_success(&serde_json::json!({"flashblocks": true}));
18227 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
18228 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18229 provider,
18230 SubscriberMode::PubSub,
18231 SubscriberConfig {
18232 preconfirmations: PreconfirmationMode::Required,
18233 ..SubscriberConfig::default()
18234 },
18235 )
18236 .with_provider_ref(ProviderRef::new("base-paid", 7));
18237 subscriber.chain_id = Some(8_453);
18238 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18239 subscriber.interests = subscriber.base_interests.clone();
18240 let desired = subscriber.pubsub_stream_sources();
18241 let mut streams = SubscriberStreams::new();
18242 for source in desired {
18243 streams.push(source, stream::pending().boxed());
18244 }
18245 subscriber.state = AlloySubscriberState::Active(streams);
18246 subscriber.sources_dirty = false;
18247
18248 let preflight = subscriber
18249 .establish_flashblocks_preflight(8_453)
18250 .await
18251 .expect("preflight succeeds");
18252
18253 assert_eq!(preflight.chain_id(), 8_453);
18254 assert_eq!(preflight.provider(), &ProviderRef::new("base-paid", 7));
18255 assert_eq!(
18256 preflight.delivery(),
18257 FlashblocksDelivery::NativeSubscriptions
18258 );
18259 assert_eq!(preflight.pending_log_subscriptions(), 1);
18260 assert_eq!(preflight.pending_log_filters(), 1);
18261 assert_eq!(
18262 preflight.advertised_capabilities(),
18263 Some(&serde_json::json!({"flashblocks": true}))
18264 );
18265 }
18266
18267 #[tokio::test]
18268 async fn optimism_preflight_probes_pending_state_without_native_subscriptions() {
18269 let asserter = Asserter::new();
18270 asserter.push_success(&serde_json::json!(["flashblocksv1"]));
18271 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
18272 asserter.push_success(&Vec::<Log>::new());
18273 asserter.push_success(&serde_json::json!([]));
18274 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18275 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18276 provider,
18277 SubscriberMode::PubSub,
18278 SubscriberConfig {
18279 preconfirmations: PreconfirmationMode::Required,
18280 ..SubscriberConfig::default()
18281 },
18282 )
18283 .with_provider_ref(ProviderRef::new("op-paid", 12));
18284 subscriber.chain_id = Some(10);
18285 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18286 subscriber.interests = subscriber.base_interests.clone();
18287 let desired = subscriber.pubsub_stream_sources();
18288 let mut streams = SubscriberStreams::new();
18289 for source in desired {
18290 streams.push(source, stream::pending().boxed());
18291 }
18292 subscriber.state = AlloySubscriberState::Active(streams);
18293 subscriber.sources_dirty = false;
18294
18295 let preflight = subscriber
18296 .establish_flashblocks_preflight(10)
18297 .await
18298 .expect("Optimism pending-state preflight succeeds");
18299
18300 assert_eq!(preflight.chain_id(), 10);
18301 assert_eq!(preflight.provider(), &ProviderRef::new("op-paid", 12));
18302 assert_eq!(
18303 preflight.delivery(),
18304 FlashblocksDelivery::PendingStatePolling
18305 );
18306 assert_eq!(preflight.pending_log_subscriptions(), 0);
18307 assert_eq!(preflight.pending_log_filters(), 1);
18308 assert_eq!(
18309 preflight.advertised_capabilities(),
18310 Some(&serde_json::json!(["flashblocksv1"]))
18311 );
18312 assert!(asserter.read_q().is_empty());
18313 }
18314
18315 #[test]
18316 fn optimism_full_pending_block_normalizes_op_transaction_types_to_hashes() {
18317 let transaction_hash = B256::repeat_byte(0x7e);
18318 let mut value = serde_json::to_value(rpc_block(101, B256::ZERO))
18319 .expect("serialize pending block fixture");
18320 value["transactions"] = serde_json::json!([{
18321 "type": "0x7e",
18322 "hash": transaction_hash,
18323 "sourceHash": B256::repeat_byte(0x11),
18324 "from": Address::repeat_byte(0x22),
18325 "to": Address::repeat_byte(0x33)
18326 }]);
18327
18328 let block = normalize_op_pending_block::<Ethereum>(value)
18329 .expect("OP-specific transaction bodies are reduced to hashes");
18330
18331 assert_eq!(
18332 block.transactions().as_hashes(),
18333 Some(&[transaction_hash][..])
18334 );
18335 }
18336
18337 #[tokio::test]
18338 async fn optimism_sampler_does_not_retry_malformed_pending_content() {
18339 let asserter = Asserter::new();
18340 let mut pending = serde_json::to_value(rpc_block(101, B256::ZERO))
18341 .expect("serialize pending block fixture");
18342 pending["transactions"] = serde_json::json!([{"type": "0x7e"}]);
18343 asserter.push_success(&Some(pending));
18344 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18345 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18346 provider,
18347 SubscriberMode::PubSub,
18348 SubscriberConfig {
18349 preconfirmations: PreconfirmationMode::Required,
18350 ..SubscriberConfig::default()
18351 },
18352 )
18353 .with_provider_ref(ProviderRef::new("op-paid", 12));
18354 subscriber.chain_id = Some(10);
18355
18356 let error = match subscriber
18357 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18358 .await
18359 {
18360 Err(error) => error,
18361 Ok(_) => panic!("malformed provider content must fail immediately"),
18362 };
18363 assert!(
18364 error
18365 .to_string()
18366 .contains("transaction is missing its hash")
18367 );
18368 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
18369 assert!(asserter.read_q().is_empty());
18370 }
18371
18372 #[tokio::test]
18373 async fn optimism_sampler_certifies_the_pending_block_by_exact_parent_hash() {
18374 let asserter = Asserter::new();
18375 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
18376 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
18377 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18378 provider,
18379 SubscriberMode::PubSub,
18380 SubscriberConfig {
18381 preconfirmations: PreconfirmationMode::Required,
18382 ..SubscriberConfig::default()
18383 },
18384 )
18385 .with_provider_ref(ProviderRef::new("op-paid", 12));
18386 subscriber.chain_id = Some(10);
18387
18388 assert!(
18389 subscriber
18390 .fetch_pending_flashblock(None)
18391 .await
18392 .expect("the exact parent certifies the pending payload")
18393 .is_some()
18394 );
18395 }
18396
18397 #[tokio::test]
18398 async fn optimism_sampler_rejects_a_nonconsecutive_pending_parent() {
18399 let asserter = Asserter::new();
18400 asserter.push_success(&Some(rpc_block(101, B256::ZERO)));
18401 asserter.push_success(&Some(rpc_block(99, B256::repeat_byte(0x64))));
18402 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18403 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18404 provider,
18405 SubscriberMode::PubSub,
18406 SubscriberConfig {
18407 preconfirmations: PreconfirmationMode::Required,
18408 ..SubscriberConfig::default()
18409 },
18410 )
18411 .with_provider_ref(ProviderRef::new("op-paid", 12));
18412 subscriber.chain_id = Some(10);
18413
18414 assert!(matches!(
18415 subscriber.fetch_pending_flashblock(None).await,
18416 Err(PendingFlashblockPollError::Integrity(SubscriberError::Provider(
18417 ref message
18418 ))) if message.contains("does not extend its exact certified parent")
18419 ));
18420 assert!(asserter.read_q().is_empty());
18421 }
18422
18423 #[tokio::test]
18424 async fn optimism_sampler_rechecks_unchanged_content_without_republishing_logs() {
18425 let asserter = Asserter::new();
18426 let pending = rpc_block(101, B256::ZERO);
18427 queue_op_pending(&asserter, pending.clone());
18428 asserter.push_success(&Vec::<Log>::new());
18429 queue_op_pending(&asserter, pending);
18430 asserter.push_success(&Vec::<Log>::new());
18431 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18432 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18433 provider,
18434 SubscriberMode::PubSub,
18435 SubscriberConfig {
18436 preconfirmations: PreconfirmationMode::Required,
18437 ..SubscriberConfig::default()
18438 },
18439 )
18440 .with_provider_ref(ProviderRef::new("op-paid", 12));
18441 subscriber.chain_id = Some(10);
18442 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18443 subscriber.interests = subscriber.base_interests.clone();
18444
18445 assert!(
18446 subscriber
18447 .fetch_pending_flashblock(None)
18448 .await
18449 .expect("first cumulative pending view")
18450 .is_some()
18451 );
18452 assert!(
18453 subscriber
18454 .fetch_pending_flashblock(None)
18455 .await
18456 .expect("duplicate cumulative pending view")
18457 .is_none()
18458 );
18459
18460 assert_eq!(
18461 subscriber.flashblocks_rpc_metrics(),
18462 FlashblocksRpcMetrics {
18463 capability_requests: 0,
18464 provider_pair_chain_requests: 0,
18465 canonical_head_requests: 2,
18466 pending_block_requests: 2,
18467 pending_log_requests: 2,
18468 pending_receipt_requests: 0,
18469 pending_receipts_completed: 0,
18470 pending_receipts_unavailable: 0,
18471 failed_requests: 0,
18472 raced_samples: 0,
18473 }
18474 );
18475 assert!(asserter.read_q().is_empty());
18476 }
18477
18478 #[tokio::test]
18479 async fn optimism_sampler_rechecks_logs_for_an_unchanged_pending_view() {
18480 let asserter = Asserter::new();
18481 let transaction = B256::repeat_byte(0x42);
18482 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
18483 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
18484 );
18485 let mut log = rpc_log(false);
18486 log.block_number = Some(101);
18487 log.block_hash = Some(B256::repeat_byte(0xa2));
18488 log.transaction_hash = Some(transaction);
18489 log.transaction_index = Some(0);
18490 log.log_index = Some(0);
18491 queue_op_pending(&asserter, pending.clone());
18492 asserter.push_success(&Vec::<Log>::new());
18493 asserter.push_success(&serde_json::Value::Null);
18494 queue_op_pending(&asserter, pending);
18495 asserter.push_success(&vec![log]);
18496 asserter.push_success(&serde_json::Value::Null);
18497 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18498 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18499 provider,
18500 SubscriberMode::PubSub,
18501 SubscriberConfig {
18502 preconfirmations: PreconfirmationMode::Required,
18503 ..SubscriberConfig::default()
18504 },
18505 )
18506 .with_provider_ref(ProviderRef::new("op-paid", 12));
18507 subscriber.chain_id = Some(10);
18508 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18509 subscriber.interests = subscriber.base_interests.clone();
18510
18511 assert!(matches!(
18512 subscriber
18513 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18514 .await
18515 .expect("first pending view is coherent"),
18516 Some(SubscriberEvent::FlashblockObserved)
18517 ));
18518 assert!(matches!(
18519 subscriber
18520 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18521 .await
18522 .expect("the unchanged view is checked again for lagging logs"),
18523 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
18524 ));
18525 assert!(asserter.read_q().is_empty());
18526 }
18527
18528 #[tokio::test]
18529 async fn optimism_sampler_hydrates_exact_receipts_when_filtered_logs_are_empty() {
18530 let asserter = Asserter::new();
18531 let transaction = B256::repeat_byte(0x42);
18532 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
18533 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
18534 );
18535 let mut log = rpc_log(false);
18536 log.block_number = Some(101);
18537 log.block_hash = Some(B256::repeat_byte(0xa2));
18538 log.transaction_hash = Some(transaction);
18539 log.transaction_index = Some(0);
18540 log.log_index = Some(0);
18541 queue_op_pending(&asserter, pending);
18542 asserter.push_success(&Vec::<Log>::new());
18543 asserter.push_success(&serde_json::json!({
18544 "transactionHash": transaction,
18545 "logs": [log]
18546 }));
18547 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18548 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18549 provider,
18550 SubscriberMode::PubSub,
18551 SubscriberConfig {
18552 preconfirmations: PreconfirmationMode::Required,
18553 ..SubscriberConfig::default()
18554 },
18555 )
18556 .with_provider_ref(ProviderRef::new("op-paid", 12));
18557 subscriber.chain_id = Some(10);
18558 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18559 subscriber.interests = subscriber.base_interests.clone();
18560
18561 let event = subscriber
18562 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18563 .await
18564 .expect("pending receipt fallback succeeds");
18565 assert!(matches!(
18566 event,
18567 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
18568 ));
18569 assert_eq!(
18570 subscriber
18571 .flashblocks_rpc_metrics()
18572 .pending_receipt_requests(),
18573 1
18574 );
18575 assert!(asserter.read_q().is_empty());
18576 }
18577
18578 #[tokio::test]
18579 async fn optimism_receipt_hydration_is_bounded_and_resumes_on_the_next_tick() {
18580 let asserter = Asserter::new();
18581 let transaction_a = B256::repeat_byte(0x41);
18582 let transaction_b = B256::repeat_byte(0x42);
18583 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
18584 alloy_network::primitives::BlockTransactions::Hashes(vec![
18585 transaction_a,
18586 transaction_b,
18587 ]),
18588 );
18589 let mut log = rpc_log(false);
18590 log.block_number = Some(101);
18591 log.transaction_hash = Some(transaction_b);
18592 log.transaction_index = Some(1);
18593 queue_op_pending(&asserter, pending.clone());
18594 asserter.push_success(&Vec::<Log>::new());
18595 asserter.push_success(&serde_json::json!({
18596 "transactionHash": transaction_a,
18597 "logs": []
18598 }));
18599 queue_op_pending(&asserter, pending);
18600 asserter.push_success(&Vec::<Log>::new());
18601 asserter.push_success(&serde_json::json!({
18602 "transactionHash": transaction_b,
18603 "logs": [log]
18604 }));
18605 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18606 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18607 provider,
18608 SubscriberMode::PubSub,
18609 SubscriberConfig {
18610 preconfirmations: PreconfirmationMode::Required,
18611 max_pending_transaction_receipts_per_tick: 1,
18612 ..SubscriberConfig::default()
18613 },
18614 )
18615 .with_provider_ref(ProviderRef::new("op-paid", 12));
18616 subscriber.chain_id = Some(10);
18617 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18618 subscriber.interests = subscriber.base_interests.clone();
18619
18620 assert!(matches!(
18621 subscriber
18622 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18623 .await
18624 .expect("the first bounded receipt is hydrated"),
18625 Some(SubscriberEvent::FlashblockObserved)
18626 ));
18627 assert!(matches!(
18628 subscriber
18629 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18630 .await
18631 .expect("the remaining receipt is hydrated on the next tick"),
18632 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
18633 ));
18634 assert_eq!(
18635 subscriber
18636 .flashblocks_rpc_metrics()
18637 .pending_receipt_requests(),
18638 2
18639 );
18640 assert_eq!(subscriber.preconfirmed_receipted_transactions.len(), 2);
18641 assert!(asserter.read_q().is_empty());
18642 }
18643
18644 #[tokio::test]
18645 async fn optimism_receipt_hydration_prioritizes_unattempted_hashes_over_null_retries() {
18646 let asserter = Asserter::new();
18647 let transaction_a = B256::repeat_byte(0x41);
18648 let transaction_b = B256::repeat_byte(0x42);
18649 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
18650 alloy_network::primitives::BlockTransactions::Hashes(vec![
18651 transaction_a,
18652 transaction_b,
18653 ]),
18654 );
18655 let mut log = rpc_log(false);
18656 log.block_number = Some(101);
18657 log.transaction_hash = Some(transaction_b);
18658 log.transaction_index = Some(1);
18659 queue_op_pending(&asserter, pending.clone());
18660 asserter.push_success(&Vec::<Log>::new());
18661 asserter.push_success(&serde_json::Value::Null);
18662 queue_op_pending(&asserter, pending);
18663 asserter.push_success(&Vec::<Log>::new());
18664 asserter.push_success(&serde_json::json!({
18665 "transactionHash": transaction_b,
18666 "logs": [log]
18667 }));
18668 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18669 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18670 provider,
18671 SubscriberMode::PubSub,
18672 SubscriberConfig {
18673 preconfirmations: PreconfirmationMode::Required,
18674 max_pending_transaction_receipts_per_tick: 1,
18675 ..SubscriberConfig::default()
18676 },
18677 )
18678 .with_provider_ref(ProviderRef::new("op-paid", 12));
18679 subscriber.chain_id = Some(10);
18680 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18681 subscriber.interests = subscriber.base_interests.clone();
18682
18683 assert!(matches!(
18684 subscriber
18685 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18686 .await
18687 .expect("the first null receipt remains retryable"),
18688 Some(SubscriberEvent::FlashblockObserved)
18689 ));
18690 assert!(matches!(
18691 subscriber
18692 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18693 .await
18694 .expect("the next unattempted receipt is not starved"),
18695 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
18696 ));
18697 assert!(
18698 subscriber
18699 .preconfirmed_unavailable_receipts
18700 .contains(&transaction_a)
18701 );
18702 assert!(
18703 subscriber
18704 .preconfirmed_receipted_transactions
18705 .contains(&transaction_b)
18706 );
18707 assert!(asserter.read_q().is_empty());
18708 }
18709
18710 #[tokio::test]
18711 async fn optimism_receipt_batch_commits_dedupe_only_after_every_response_succeeds() {
18712 let asserter = Asserter::new();
18713 let transaction_a = B256::repeat_byte(0x41);
18714 let transaction_b = B256::repeat_byte(0x42);
18715 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
18716 alloy_network::primitives::BlockTransactions::Hashes(vec![
18717 transaction_a,
18718 transaction_b,
18719 ]),
18720 );
18721 let mut log = rpc_log(false);
18722 log.block_number = Some(101);
18723 log.transaction_hash = Some(transaction_b);
18724 log.transaction_index = Some(1);
18725 queue_op_pending(&asserter, pending.clone());
18726 asserter.push_success(&Vec::<Log>::new());
18727 asserter.push_success(&serde_json::json!({
18728 "transactionHash": transaction_a,
18729 "logs": []
18730 }));
18731 asserter.push_failure_msg("receipt temporarily unavailable");
18732 queue_op_pending(&asserter, pending);
18733 asserter.push_success(&Vec::<Log>::new());
18734 asserter.push_success(&serde_json::json!({
18735 "transactionHash": transaction_a,
18736 "logs": []
18737 }));
18738 asserter.push_success(&serde_json::json!({
18739 "transactionHash": transaction_b,
18740 "logs": [log]
18741 }));
18742 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18743 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18744 provider,
18745 SubscriberMode::PubSub,
18746 SubscriberConfig {
18747 preconfirmations: PreconfirmationMode::Required,
18748 max_pending_transaction_receipts_per_tick: 2,
18749 max_consecutive_flashblock_poll_failures: 2,
18750 ..SubscriberConfig::default()
18751 },
18752 )
18753 .with_provider_ref(ProviderRef::new("op-paid", 12));
18754 subscriber.chain_id = Some(10);
18755 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18756 subscriber.interests = subscriber.base_interests.clone();
18757
18758 assert!(
18759 subscriber
18760 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18761 .await
18762 .expect("one failed receipt response remains retryable")
18763 .is_none()
18764 );
18765 assert!(subscriber.preconfirmed_receipted_transactions.is_empty());
18766 assert!(matches!(
18767 subscriber
18768 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18769 .await
18770 .expect("the complete batch is retried transactionally"),
18771 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
18772 ));
18773 assert_eq!(subscriber.preconfirmed_receipted_transactions.len(), 2);
18774 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 1);
18775 assert_eq!(
18776 subscriber
18777 .flashblocks_rpc_metrics()
18778 .pending_receipt_requests(),
18779 4
18780 );
18781 assert!(asserter.read_q().is_empty());
18782 }
18783
18784 #[tokio::test]
18785 async fn optimism_sampler_rejects_a_receipt_for_a_different_transaction() {
18786 let asserter = Asserter::new();
18787 let sampled_transaction = B256::repeat_byte(0x41);
18788 let advanced_transaction = B256::repeat_byte(0x42);
18789 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
18790 alloy_network::primitives::BlockTransactions::Hashes(vec![sampled_transaction]),
18791 );
18792 let mut log = rpc_log(false);
18793 log.block_number = Some(101);
18794 log.transaction_hash = Some(advanced_transaction);
18795 queue_op_pending(&asserter, pending);
18796 asserter.push_success(&Vec::<Log>::new());
18797 asserter.push_success(&serde_json::json!({
18798 "transactionHash": advanced_transaction,
18799 "logs": [log]
18800 }));
18801 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18802 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18803 provider,
18804 SubscriberMode::PubSub,
18805 SubscriberConfig {
18806 preconfirmations: PreconfirmationMode::Required,
18807 ..SubscriberConfig::default()
18808 },
18809 )
18810 .with_provider_ref(ProviderRef::new("op-paid", 12));
18811 subscriber.chain_id = Some(10);
18812 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18813 subscriber.interests = subscriber.base_interests.clone();
18814
18815 let error = match subscriber
18816 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18817 .await
18818 {
18819 Err(error) => error,
18820 Ok(_) => panic!("a receipt for another transaction must fail closed"),
18821 };
18822 assert!(
18823 error
18824 .to_string()
18825 .contains("hash disagrees with its request")
18826 );
18827 assert!(asserter.read_q().is_empty());
18828 }
18829
18830 #[tokio::test]
18831 async fn optimism_sampler_revokes_then_recovers_from_a_regressive_pending_view() {
18832 let asserter = Asserter::new();
18833 let transaction_a = B256::repeat_byte(0x41);
18834 let transaction_b = B256::repeat_byte(0x42);
18835 let first = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
18836 alloy_network::primitives::BlockTransactions::Hashes(vec![
18837 transaction_a,
18838 transaction_b,
18839 ]),
18840 );
18841 let regressive = rpc_block(101, B256::repeat_byte(0xa2)).with_transactions(
18842 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction_a]),
18843 );
18844 queue_op_pending(&asserter, first);
18845 asserter.push_success(&Vec::<Log>::new());
18846 asserter.push_success(&serde_json::Value::Null);
18847 asserter.push_success(&serde_json::Value::Null);
18848 queue_op_pending(&asserter, regressive.clone());
18849 queue_op_pending(&asserter, regressive);
18850 asserter.push_success(&Vec::<Log>::new());
18851 asserter.push_success(&serde_json::Value::Null);
18852 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18853 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18854 provider,
18855 SubscriberMode::PubSub,
18856 SubscriberConfig {
18857 preconfirmations: PreconfirmationMode::Required,
18858 ..SubscriberConfig::default()
18859 },
18860 )
18861 .with_provider_ref(ProviderRef::new("op-paid", 12));
18862 subscriber.chain_id = Some(10);
18863 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18864 subscriber.interests = subscriber.base_interests.clone();
18865
18866 assert!(
18867 subscriber
18868 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18869 .await
18870 .expect("first pending view is coherent")
18871 .is_some()
18872 );
18873 assert!(matches!(
18874 subscriber
18875 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18876 .await
18877 .expect("regression revokes instead of terminating the stream"),
18878 Some(SubscriberEvent::FlashblockInvalidated)
18879 ));
18880 assert!(subscriber.latest_preconfirmation.is_none());
18881 assert!(subscriber.pending_preconfirmation_invalidation);
18882 assert!(
18883 subscriber
18884 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18885 .await
18886 .expect("a later coherent view establishes a fresh snapshot")
18887 .is_some()
18888 );
18889 assert!(subscriber.latest_preconfirmation.is_some());
18890 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 12);
18891 assert!(asserter.read_q().is_empty());
18892 }
18893
18894 #[tokio::test]
18895 async fn optimism_new_quiet_payload_revokes_the_previous_snapshot() {
18896 let asserter = Asserter::new();
18897 let first = rpc_block(101, B256::repeat_byte(0xa1));
18898 let second = rpc_block(102, B256::repeat_byte(0xa2));
18899 queue_op_pending(&asserter, first);
18900 asserter.push_success(&Vec::<Log>::new());
18901 queue_op_pending(&asserter, second);
18902 asserter.push_success(&Vec::<Log>::new());
18903 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18904 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18905 provider,
18906 SubscriberMode::PubSub,
18907 SubscriberConfig {
18908 preconfirmations: PreconfirmationMode::Required,
18909 ..SubscriberConfig::default()
18910 },
18911 )
18912 .with_provider_ref(ProviderRef::new("op-paid", 12));
18913 subscriber.chain_id = Some(10);
18914 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18915 subscriber.interests = subscriber.base_interests.clone();
18916
18917 subscriber
18918 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18919 .await
18920 .expect("first quiet payload is observed");
18921 assert!(!subscriber.pending_preconfirmation_invalidation);
18922 subscriber
18923 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18924 .await
18925 .expect("replacement quiet payload is observed");
18926 assert!(subscriber.pending_preconfirmation_invalidation);
18927 assert_eq!(
18928 subscriber
18929 .latest_preconfirmation
18930 .as_ref()
18931 .map(|flashblock| flashblock.block_number),
18932 Some(102)
18933 );
18934 assert!(asserter.read_q().is_empty());
18935 }
18936
18937 #[tokio::test]
18938 async fn optimism_sampler_rejects_malformed_pending_receipts() {
18939 let asserter = Asserter::new();
18940 let transaction = B256::repeat_byte(0x42);
18941 let pending = rpc_block(101, B256::ZERO).with_transactions(
18942 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
18943 );
18944 queue_op_pending(&asserter, pending);
18945 asserter.push_success(&Vec::<Log>::new());
18946 asserter.push_success(&serde_json::json!({"transactionHash": transaction}));
18947 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18948 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18949 provider,
18950 SubscriberMode::PubSub,
18951 SubscriberConfig {
18952 preconfirmations: PreconfirmationMode::Required,
18953 ..SubscriberConfig::default()
18954 },
18955 )
18956 .with_provider_ref(ProviderRef::new("op-paid", 12));
18957 subscriber.chain_id = Some(10);
18958 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18959 subscriber.interests = subscriber.base_interests.clone();
18960
18961 let error = match subscriber
18962 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
18963 .await
18964 {
18965 Err(error) => error,
18966 Ok(_) => panic!("malformed receipt content must fail closed"),
18967 };
18968 assert!(error.to_string().contains("missing its log array"));
18969 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
18970 assert!(asserter.read_q().is_empty());
18971 }
18972
18973 #[tokio::test]
18974 async fn optimism_sampler_retries_when_logs_advance_past_the_sampled_block() {
18975 let asserter = Asserter::new();
18976 let transaction_a = B256::repeat_byte(0x41);
18977 let transaction_b = B256::repeat_byte(0x42);
18978 let first = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
18979 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction_a]),
18980 );
18981 let second = rpc_block(101, B256::repeat_byte(0xa2)).with_transactions(
18982 alloy_network::primitives::BlockTransactions::Hashes(vec![
18983 transaction_a,
18984 transaction_b,
18985 ]),
18986 );
18987 let mut log = rpc_log(false);
18988 log.block_number = Some(101);
18989 log.block_hash = Some(B256::repeat_byte(0xa2));
18990 log.transaction_hash = Some(transaction_b);
18991 log.transaction_index = Some(1);
18992 log.log_index = Some(0);
18993 queue_op_pending(&asserter, first);
18994 asserter.push_success(&vec![log.clone()]);
18995 asserter.push_success(&serde_json::Value::Null);
18996 queue_op_pending(&asserter, second);
18997 asserter.push_success(&vec![log.clone()]);
18998 asserter.push_success(&serde_json::Value::Null);
18999 asserter.push_success(&serde_json::json!({
19000 "transactionHash": transaction_b,
19001 "logs": [log]
19002 }));
19003 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19004 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19005 provider,
19006 SubscriberMode::PubSub,
19007 SubscriberConfig {
19008 preconfirmations: PreconfirmationMode::Required,
19009 ..SubscriberConfig::default()
19010 },
19011 )
19012 .with_provider_ref(ProviderRef::new("op-paid", 12));
19013 subscriber.chain_id = Some(10);
19014 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19015 subscriber.interests = subscriber.base_interests.clone();
19016
19017 assert!(
19018 subscriber
19019 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19020 .await
19021 .expect("a cross-request race remains retryable")
19022 .is_none()
19023 );
19024 assert!(
19025 subscriber
19026 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19027 .await
19028 .expect("the next coherent cumulative view is delivered")
19029 .is_some()
19030 );
19031 assert_eq!(subscriber.flashblocks_rpc_metrics().raced_samples(), 1);
19032 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
19033 assert!(asserter.read_q().is_empty());
19034 }
19035
19036 #[tokio::test]
19037 async fn optimism_sampler_uses_the_paired_pending_state_provider() {
19038 let stream_asserter = Asserter::new();
19039 let stream_provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
19040 let state_asserter = Asserter::new();
19041 queue_op_pending(&state_asserter, rpc_block(101, B256::ZERO));
19042 state_asserter.push_success(&Vec::<Log>::new());
19043 let state_provider = ProviderBuilder::new().connect_mocked_client(state_asserter.clone());
19044 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19045 stream_provider,
19046 SubscriberMode::PubSub,
19047 SubscriberConfig {
19048 preconfirmations: PreconfirmationMode::Required,
19049 ..SubscriberConfig::default()
19050 },
19051 )
19052 .with_provider_ref(ProviderRef::new("op-paid", 12))
19053 .with_flashblocks_state_provider(state_provider);
19054 subscriber.chain_id = Some(10);
19055 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19056 subscriber.interests = subscriber.base_interests.clone();
19057
19058 assert!(
19059 subscriber
19060 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19061 .await
19062 .expect("paired pending-state reads succeed")
19063 .is_some()
19064 );
19065 assert!(state_asserter.read_q().is_empty());
19066 assert!(stream_asserter.read_q().is_empty());
19067 }
19068
19069 #[tokio::test]
19070 async fn optimism_sampler_retries_an_isolated_provider_request_failure() {
19071 let asserter = Asserter::new();
19072 let pending = rpc_block(101, B256::ZERO);
19073 queue_op_pending(&asserter, pending.clone());
19074 asserter.push_failure_msg("temporarily unavailable");
19075 queue_op_pending(&asserter, pending);
19076 asserter.push_success(&Vec::<Log>::new());
19077 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19078 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19079 provider,
19080 SubscriberMode::PubSub,
19081 SubscriberConfig {
19082 preconfirmations: PreconfirmationMode::Required,
19083 max_consecutive_flashblock_poll_failures: 2,
19084 ..SubscriberConfig::default()
19085 },
19086 )
19087 .with_provider_ref(ProviderRef::new("op-paid", 12));
19088 subscriber.chain_id = Some(10);
19089 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19090 subscriber.interests = subscriber.base_interests.clone();
19091
19092 assert!(
19093 subscriber
19094 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19095 .await
19096 .expect("one request failure stays retryable")
19097 .is_none()
19098 );
19099 assert!(
19100 subscriber
19101 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19102 .await
19103 .expect("the next cumulative view retries the missing logs")
19104 .is_some()
19105 );
19106 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 1);
19107 assert!(asserter.read_q().is_empty());
19108 }
19109
19110 #[tokio::test]
19111 async fn optimism_sampler_surfaces_sustained_provider_request_failures() {
19112 let asserter = Asserter::new();
19113 asserter.push_failure_msg("temporarily unavailable");
19114 asserter.push_failure_msg("still unavailable");
19115 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19116 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19117 provider,
19118 SubscriberMode::PubSub,
19119 SubscriberConfig {
19120 preconfirmations: PreconfirmationMode::Required,
19121 max_consecutive_flashblock_poll_failures: 2,
19122 ..SubscriberConfig::default()
19123 },
19124 )
19125 .with_provider_ref(ProviderRef::new("op-paid", 12));
19126 subscriber.chain_id = Some(10);
19127
19128 assert!(
19129 subscriber
19130 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19131 .await
19132 .expect("the first request failure stays retryable")
19133 .is_none()
19134 );
19135 let error = match subscriber
19136 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19137 .await
19138 {
19139 Err(error) => error,
19140 Ok(_) => panic!("the configured consecutive-failure limit must fail closed"),
19141 };
19142 assert!(error.to_string().contains("still unavailable"));
19143 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 2);
19144 assert!(asserter.read_q().is_empty());
19145 }
19146
19147 #[tokio::test]
19148 async fn flashblocks_preflight_rejects_a_mismatched_chain_before_subscribing() {
19149 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19150 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19151 provider,
19152 SubscriberMode::PubSub,
19153 SubscriberConfig {
19154 preconfirmations: PreconfirmationMode::Required,
19155 ..SubscriberConfig::default()
19156 },
19157 )
19158 .with_provider_ref(ProviderRef::new("wrong-chain", 1));
19159 subscriber.chain_id = Some(10);
19160 subscriber.interests = vec![log_interest_matching_rpc_log()];
19161
19162 assert!(matches!(
19163 subscriber.establish_flashblocks_preflight(8_453).await,
19164 Err(SubscriberError::ChainMismatch {
19165 expected: 8_453,
19166 actual: 10
19167 })
19168 ));
19169 }
19170
19171 #[tokio::test]
19172 async fn optimism_preflight_rejects_a_mismatched_paired_provider() {
19173 let stream_asserter = Asserter::new();
19174 let stream_provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
19175 let state_asserter = Asserter::new();
19176 state_asserter.push_success(&serde_json::json!(["flashblocksv1"]));
19177 state_asserter.push_success(&8_453_u64);
19178 let state_provider = ProviderBuilder::new().connect_mocked_client(state_asserter.clone());
19179 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19180 stream_provider,
19181 SubscriberMode::PubSub,
19182 SubscriberConfig {
19183 preconfirmations: PreconfirmationMode::Required,
19184 ..SubscriberConfig::default()
19185 },
19186 )
19187 .with_provider_ref(ProviderRef::new("op-paid", 12))
19188 .with_flashblocks_state_provider(state_provider);
19189 subscriber.chain_id = Some(10);
19190 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19191 subscriber.interests = subscriber.base_interests.clone();
19192 let desired = subscriber.pubsub_stream_sources();
19193 let mut streams = SubscriberStreams::new();
19194 for source in desired {
19195 streams.push(source, stream::pending().boxed());
19196 }
19197 subscriber.state = AlloySubscriberState::Active(streams);
19198 subscriber.sources_dirty = false;
19199 assert!(matches!(
19200 subscriber.establish_flashblocks_preflight(10).await,
19201 Err(SubscriberError::ChainMismatch {
19202 expected: 10,
19203 actual: 8_453
19204 })
19205 ));
19206 assert!(state_asserter.read_q().is_empty());
19207 assert!(stream_asserter.read_q().is_empty());
19208 }
19209
19210 #[test]
19211 fn unproven_parent_replacement_rewind_discards_every_unauthenticated_identity() {
19212 let parent = BlockRef {
19213 number: 79,
19214 hash: B256::repeat_byte(0x79),
19215 parent_hash: Some(B256::repeat_byte(0x78)),
19216 timestamp: Some(1_700_000_079),
19217 };
19218 let old_tip = BlockRef {
19219 number: 80,
19220 hash: B256::repeat_byte(0x80),
19221 parent_hash: Some(parent.hash),
19222 timestamp: Some(1_700_000_080),
19223 };
19224 let replacement = BlockRef {
19225 hash: B256::repeat_byte(0xe0),
19226 parent_hash: Some(B256::repeat_byte(0xdf)),
19227 ..old_tip
19228 };
19229 let mut state =
19230 CanonicalSequenceState::new(vec![parent, old_tip], Some(old_tip), Some(parent), None);
19231
19232 let rewind = apply_sequence_canonical_block(&mut state, &replacement, false)
19233 .expect("replacement metadata is structurally valid")
19234 .expect("unknown parent is an observable rewind");
19235
19236 assert_eq!(rewind.common_ancestor, None);
19237 assert_eq!(rewind.dropped, vec![parent, old_tip]);
19238 assert_eq!(state.retained_canonical_history(), &[replacement]);
19239 assert_eq!(state.coverage_head(), Some(&replacement));
19240 assert_eq!(state.safe_head(), None);
19241 assert_eq!(state.finalized_head(), None);
19242 }
19243
19244 #[test]
19245 fn handler_ids_are_non_empty_across_construction_and_deserialization() {
19246 assert_eq!(HandlerId::try_new("").unwrap_err(), HandlerIdError);
19247 let valid = HandlerId::try_new("owner-1").expect("non-empty id");
19248 let encoded = serde_json::to_string(&valid).expect("serialize id");
19249 assert_eq!(
19250 serde_json::from_str::<HandlerId>(&encoded).expect("deserialize valid id"),
19251 valid
19252 );
19253 assert!(serde_json::from_str::<HandlerId>(r#"""#).is_err());
19254 }
19255
19256 fn rpc_log(removed: bool) -> Log {
19257 Log {
19258 inner: alloy_primitives::Log::new_unchecked(
19259 Address::repeat_byte(0x42),
19260 vec![B256::repeat_byte(0x01)],
19261 Bytes::new(),
19262 ),
19263 block_hash: Some(B256::repeat_byte(0x02)),
19264 block_number: Some(7),
19265 block_timestamp: Some(1_700_000_000),
19266 transaction_hash: Some(B256::repeat_byte(0x03)),
19267 transaction_index: Some(4),
19268 log_index: Some(5),
19269 removed,
19270 }
19271 }
19272
19273 fn rpc_transaction(chain_id: Option<u64>) -> alloy_rpc_types_eth::Transaction {
19274 use alloy_consensus::SignableTransaction as _;
19275
19276 let envelope: alloy_consensus::TxEnvelope = alloy_consensus::TxLegacy {
19277 chain_id,
19278 ..Default::default()
19279 }
19280 .into_signed(alloy_primitives::Signature::test_signature())
19281 .into();
19282 alloy_rpc_types_eth::Transaction {
19283 inner: alloy_consensus::transaction::Recovered::new_unchecked(envelope, Address::ZERO),
19284 block_hash: None,
19285 block_number: None,
19286 transaction_index: None,
19287 effective_gas_price: None,
19288 }
19289 }
19290
19291 #[cfg(feature = "reactive-ws")]
19292 fn rpc_log_at(block_number: u64, transaction_index: u64, log_index: u64) -> Log {
19293 Log {
19294 inner: alloy_primitives::Log::new_unchecked(
19295 Address::repeat_byte(0x42),
19296 vec![B256::repeat_byte(0x01)],
19297 Bytes::new(),
19298 ),
19299 block_hash: Some(B256::repeat_byte(block_number as u8)),
19300 block_number: Some(block_number),
19301 block_timestamp: Some(1_700_000_000 + block_number),
19302 transaction_hash: Some(B256::repeat_byte(0x20 + transaction_index as u8)),
19303 transaction_index: Some(transaction_index),
19304 log_index: Some(log_index),
19305 removed: false,
19306 }
19307 }
19308
19309 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
19310 fn rpc_block(number: u64, hash: B256) -> alloy_rpc_types_eth::Block {
19311 alloy_rpc_types_eth::Block::empty(alloy_rpc_types_eth::Header {
19312 hash,
19313 inner: alloy_consensus::Header {
19314 number,
19315 parent_hash: B256::repeat_byte(number.saturating_sub(1) as u8),
19316 timestamp: 1_700_000_000 + number,
19317 ..Default::default()
19318 },
19319 total_difficulty: None,
19320 size: None,
19321 })
19322 }
19323
19324 fn queue_op_pending(asserter: &Asserter, pending: alloy_rpc_types_eth::Block) {
19325 let parent = rpc_block(
19326 pending.header().number().saturating_sub(1),
19327 pending.header().parent_hash(),
19328 );
19329 asserter.push_success(&Some(pending));
19330 asserter.push_success(&Some(parent));
19331 }
19332
19333 #[tokio::test(flavor = "multi_thread")]
19334 #[cfg(feature = "reactive-ws")]
19335 async fn verified_log_context_fetches_and_caches_exact_parent_identity() {
19336 let stream_asserter = Asserter::new();
19337 let provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
19338 let verification_asserter = Asserter::new();
19339 verification_asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(7))));
19340 let verification_provider =
19341 ProviderBuilder::new().connect_mocked_client(verification_asserter.clone());
19342 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19343 provider,
19344 SubscriberMode::PubSub,
19345 SubscriberConfig {
19346 verify_log_block_context: true,
19347 ..SubscriberConfig::default()
19348 },
19349 )
19350 .with_log_verification_provider(verification_provider);
19351 let log = rpc_log_at(7, 0, 0);
19352
19353 subscriber
19354 .verify_log_block_context(&log)
19355 .await
19356 .expect("verify live log block");
19357 subscriber
19358 .verify_log_block_context(&log)
19359 .await
19360 .expect("reuse verified block cache");
19361 let record = subscriber.with_chain_id(log_input_record(log, InputSource::Subscription));
19362
19363 assert_eq!(
19364 record.context.block.expect("verified block").parent_hash,
19365 Some(B256::repeat_byte(6))
19366 );
19367 assert!(
19368 verification_asserter.read_q().is_empty(),
19369 "one provider lookup should verify every log in the same block"
19370 );
19371 assert!(
19372 stream_asserter.read_q().is_empty(),
19373 "verification must not use the high-volume stream provider"
19374 );
19375 }
19376
19377 #[tokio::test(flavor = "multi_thread")]
19378 async fn stream_with_termination_yields_terminal_source_marker() {
19379 let mut stream = stream_with_termination::<Ethereum, _>(
19380 stream::iter([SubscriberEvent::<Ethereum>::PendingHash(B256::repeat_byte(
19381 0xaa,
19382 ))]),
19383 SubscriberStreamSource::PubSubPendingHashes,
19384 );
19385
19386 assert!(matches!(
19387 stream.next().await,
19388 Some(SubscriberEvent::PendingHash(hash)) if hash == B256::repeat_byte(0xaa)
19389 ));
19390 assert!(matches!(
19391 stream.next().await,
19392 Some(SubscriberEvent::StreamTerminated(source)) if source.is_pubsub()
19393 ));
19394 assert!(stream.next().await.is_none());
19395 }
19396
19397 #[test]
19398 fn reconnect_delay_doubles_until_capped() {
19399 assert_eq!(
19400 next_reconnect_delay(Duration::from_millis(250), Duration::from_secs(1)),
19401 Duration::from_millis(500)
19402 );
19403 assert_eq!(
19404 next_reconnect_delay(Duration::from_millis(750), Duration::from_secs(1)),
19405 Duration::from_secs(1)
19406 );
19407 assert_eq!(
19408 next_reconnect_delay(Duration::ZERO, Duration::from_secs(1)),
19409 Duration::ZERO
19410 );
19411 }
19412
19413 #[test]
19414 fn canonical_logs_are_deduped_but_removed_logs_are_not() {
19415 let included = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
19416 let removed = log_input_record::<Ethereum>(rpc_log(true), InputSource::Subscription);
19417
19418 assert!(should_dedupe_record(&included));
19419 assert!(!should_dedupe_record(&removed));
19420 }
19421
19422 #[test]
19423 fn owner_reconcile_dedupe_rejects_conflicts_and_preserves_compatible_enrichment() {
19424 let set_context_timestamp = |record: &mut ReactiveInputRecord<Ethereum>,
19425 timestamp: Option<u64>| {
19426 record.context.block.as_mut().expect("block").timestamp = timestamp;
19427 match &mut record.context.chain_status {
19428 ChainStatus::Included { block, .. }
19429 | ChainStatus::Safe { block }
19430 | ChainStatus::Finalized { block }
19431 | ChainStatus::Reorged {
19432 dropped_from: block,
19433 } => block.timestamp = timestamp,
19434 ChainStatus::Pending | ChainStatus::Preconfirmed { .. } => {
19435 panic!("log record is canonical")
19436 }
19437 }
19438 };
19439
19440 let mut payload_only = log_input_record::<Ethereum>(rpc_log(false), InputSource::Backfill);
19441 let payload_timestamp = match &payload_only.input {
19442 ReactiveInput::Log(log) => log.block_timestamp.expect("timestamp"),
19443 _ => unreachable!(),
19444 };
19445 set_context_timestamp(&mut payload_only, None);
19446 let mut context_only = payload_only.clone();
19447 if let ReactiveInput::Log(log) = &mut context_only.input {
19448 log.block_timestamp = None;
19449 }
19450 set_context_timestamp(&mut context_only, Some(payload_timestamp + 1));
19451 assert!(matches!(
19452 dedupe_records(vec![payload_only, context_only]),
19453 Err(ReactiveError::InvalidInputRecord { .. })
19454 ));
19455
19456 let mut partial = log_input_record::<Ethereum>(rpc_log(false), InputSource::Backfill);
19457 if let ReactiveInput::Log(log) = &mut partial.input {
19458 log.block_timestamp = None;
19459 }
19460 set_context_timestamp(&mut partial, None);
19461 let complete = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
19462 let deduped =
19463 dedupe_records(vec![partial, complete]).expect("compatible metadata enriches");
19464 assert_eq!(deduped.len(), 1);
19465 deduped[0]
19466 .validated_identity()
19467 .expect("merged record remains coherent");
19468 let resolved = resolve_record_block_payload_metadata(
19469 &deduped[0],
19470 *canonical_record_block(&deduped[0]).expect("canonical"),
19471 )
19472 .expect("effective block");
19473 assert_eq!(resolved.timestamp, Some(payload_timestamp));
19474 }
19475
19476 #[test]
19477 fn full_block_bodies_are_never_suppressed_from_header_hash_alone() {
19478 use alloy_rpc_types_eth::{Block, Header};
19479
19480 let block_ref = BlockRef {
19481 number: 7,
19482 hash: B256::repeat_byte(0x77),
19483 parent_hash: Some(B256::repeat_byte(0x66)),
19484 timestamp: Some(1_700_000_007),
19485 };
19486 let block = Block::empty(Header {
19487 hash: block_ref.hash,
19488 inner: alloy_consensus::Header {
19489 number: block_ref.number,
19490 parent_hash: block_ref.parent_hash.expect("parent"),
19491 timestamp: block_ref.timestamp.expect("timestamp"),
19492 ..Default::default()
19493 },
19494 total_difficulty: None,
19495 size: None,
19496 });
19497 let record = ReactiveInputRecord::<Ethereum>::new(
19498 ReactiveInput::FullBlock(block),
19499 ReactiveContext {
19500 chain_id: Some(1),
19501 source: InputSource::Subscription,
19502 chain_status: ChainStatus::Included {
19503 block: block_ref,
19504 confirmations: 0,
19505 },
19506 block: Some(block_ref),
19507 transaction_index: None,
19508 log_index: None,
19509 },
19510 );
19511
19512 assert!(!record.is_payload_deduplicable());
19513 assert!(!record.same_deduplicable_payload(&record));
19514 let retained = dedupe_scoped_records(vec![
19515 (
19516 record.clone(),
19517 DeliveryAudience::All,
19518 DeliveryScope::Canonical,
19519 ),
19520 (record, DeliveryAudience::All, DeliveryScope::Canonical),
19521 ])
19522 .expect("non-deduplicable bodies are preserved, not treated as conflicts");
19523 assert_eq!(retained.len(), 2);
19524 }
19525
19526 #[test]
19527 fn hydrated_transaction_wrappers_reject_inclusion_and_chain_identity_conflicts() {
19528 let pending_context = ReactiveContext {
19529 chain_id: Some(1),
19530 source: InputSource::Batch,
19531 chain_status: ChainStatus::Pending,
19532 block: None,
19533 transaction_index: None,
19534 log_index: None,
19535 };
19536 let mut included_pending = rpc_transaction(Some(1));
19537 included_pending.block_hash = Some(B256::repeat_byte(0xaa));
19538 assert!(matches!(
19539 ReactiveInputRecord::<Ethereum>::new(
19540 ReactiveInput::PendingTx(included_pending),
19541 pending_context.clone(),
19542 )
19543 .validated_identity(),
19544 Err(ReactiveError::InvalidInputRecord { .. })
19545 ));
19546 assert!(matches!(
19547 ReactiveInputRecord::<Ethereum>::new(
19548 ReactiveInput::PendingTx(rpc_transaction(Some(2))),
19549 pending_context,
19550 )
19551 .validated_identity(),
19552 Err(ReactiveError::InvalidInputRecord { .. })
19553 ));
19554
19555 let block_ref = BlockRef {
19556 number: 8,
19557 hash: B256::repeat_byte(0x88),
19558 parent_hash: Some(B256::repeat_byte(0x77)),
19559 timestamp: Some(1_700_000_008),
19560 };
19561 let header = alloy_rpc_types_eth::Header {
19562 hash: block_ref.hash,
19563 inner: alloy_consensus::Header {
19564 number: block_ref.number,
19565 parent_hash: block_ref.parent_hash.expect("parent"),
19566 timestamp: block_ref.timestamp.expect("timestamp"),
19567 ..Default::default()
19568 },
19569 total_difficulty: None,
19570 size: None,
19571 };
19572 let context = ReactiveContext {
19573 chain_id: Some(1),
19574 source: InputSource::Batch,
19575 chain_status: ChainStatus::Included {
19576 block: block_ref,
19577 confirmations: 0,
19578 },
19579 block: Some(block_ref),
19580 transaction_index: None,
19581 log_index: None,
19582 };
19583 for transaction in [
19584 alloy_rpc_types_eth::Transaction {
19585 block_hash: Some(B256::repeat_byte(0xff)),
19586 ..rpc_transaction(Some(1))
19587 },
19588 alloy_rpc_types_eth::Transaction {
19589 block_hash: Some(block_ref.hash),
19590 block_number: Some(block_ref.number),
19591 transaction_index: Some(1),
19592 ..rpc_transaction(Some(1))
19593 },
19594 rpc_transaction(Some(2)),
19595 ] {
19596 let block = alloy_rpc_types_eth::Block::new(
19597 header.clone(),
19598 alloy_network::primitives::BlockTransactions::Full(vec![transaction]),
19599 );
19600 assert!(matches!(
19601 ReactiveInputRecord::<Ethereum>::new(
19602 ReactiveInput::FullBlock(block),
19603 context.clone(),
19604 )
19605 .validated_identity(),
19606 Err(ReactiveError::InvalidInputRecord { .. })
19607 ));
19608 }
19609 }
19610
19611 #[test]
19612 fn compatibility_owner_backfill_and_live_overlap_split_exact_audiences() {
19613 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19614 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19615 provider,
19616 SubscriberMode::Auto,
19617 SubscriberConfig::default(),
19618 );
19619 let owner = HandlerId::new("compat-owner");
19620 subscriber
19621 .add_interest_owner(
19622 owner.clone(),
19623 &[ReactiveInterest::Logs(LogInterest {
19624 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
19625 local_matcher: None,
19626 route_key: None,
19627 })],
19628 )
19629 .unwrap();
19630 let log = rpc_log(false);
19631
19632 subscriber.enqueue_compat_owner_record(
19633 log_input_record(log.clone(), InputSource::Backfill),
19634 owner.clone(),
19635 );
19636 subscriber.enqueue_event(SubscriberEvent::Log { source_id: 0, log });
19637
19638 let batch = subscriber
19639 .drain_next_scoped_batch()
19640 .expect("owner catch-up and residual live copies");
19641 assert_eq!(batch.records.len(), 2);
19642 assert_eq!(
19643 batch.records[0].scope,
19644 SubscriberInputScope::OwnerOnlyHandlers {
19645 owners: vec![owner.clone()]
19646 }
19647 );
19648 assert_eq!(
19649 batch.records[1].scope,
19650 SubscriberInputScope::CanonicalResidual {
19651 owners: Vec::new(),
19652 excluded: vec![owner.clone()]
19653 }
19654 );
19655
19656 let reactive = batch.into_reactive_batch();
19657 assert_eq!(
19658 reactive.record_audience(0),
19659 Some(&DeliveryAudience::Owners(vec![owner.clone()]))
19660 );
19661 assert_eq!(
19662 reactive.record_delivery_scope(0),
19663 Some(DeliveryScope::OwnerCatchup)
19664 );
19665 assert_eq!(
19666 reactive.record_audience(1),
19667 Some(&DeliveryAudience::AllExcept(vec![owner]))
19668 );
19669 assert_eq!(
19670 reactive.record_delivery_scope(1),
19671 Some(DeliveryScope::Canonical)
19672 );
19673 }
19674
19675 #[test]
19676 fn active_owner_replacement_commits_atomically_to_one_new_epoch() {
19677 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19678 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19679 provider,
19680 SubscriberMode::Auto,
19681 SubscriberConfig::default(),
19682 );
19683 let owner = HandlerId::new("replace-owner");
19684 let original = ReactiveInterest::Logs(LogInterest {
19685 provider_filter: Filter::new().address(Address::repeat_byte(0x41)),
19686 local_matcher: None,
19687 route_key: None,
19688 });
19689 let replacement_interest = ReactiveInterest::Logs(LogInterest {
19690 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
19691 local_matcher: None,
19692 route_key: None,
19693 });
19694 let active = subscriber
19695 .stage_interest_owner(owner.clone(), &[original], SubscriberOwnerStart::Live)
19696 .unwrap();
19697 assert!(subscriber.activate_interest_owner(&active));
19698 let replacement = subscriber
19699 .stage_interest_owner_replacement(
19700 owner,
19701 &[replacement_interest],
19702 SubscriberOwnerStart::Live,
19703 )
19704 .unwrap();
19705
19706 assert!(subscriber.commit_interest_owner_replacement(&active, &replacement));
19707 assert_eq!(subscriber.interest_owner_state(&active), None);
19708 assert_eq!(
19709 subscriber.interest_owner_state(&replacement),
19710 Some(SubscriberOwnerState::Active)
19711 );
19712 assert_eq!(subscriber.registered_interests().len(), 1);
19713 }
19714
19715 #[test]
19716 fn compatibility_and_epoch_owner_lifecycles_cannot_mix() {
19717 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19718 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19719 provider,
19720 SubscriberMode::Auto,
19721 SubscriberConfig::default(),
19722 );
19723 let owner = HandlerId::new("one-lifecycle");
19724 let interest = ReactiveInterest::Logs(LogInterest {
19725 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
19726 local_matcher: None,
19727 route_key: None,
19728 });
19729 let epoch = subscriber
19730 .stage_interest_owner(
19731 owner.clone(),
19732 std::slice::from_ref(&interest),
19733 SubscriberOwnerStart::Live,
19734 )
19735 .expect("stage epoch owner");
19736
19737 assert!(matches!(
19738 subscriber.add_interest_owner(owner.clone(), std::slice::from_ref(&interest)),
19739 Err(SubscriberError::InvalidConfig(_))
19740 ));
19741 assert_eq!(
19742 subscriber.interest_owner_state(&epoch),
19743 Some(SubscriberOwnerState::Staged)
19744 );
19745 assert!(subscriber.abort_interest_owner(&epoch));
19746 subscriber
19747 .add_interest_owner(owner.clone(), std::slice::from_ref(&interest))
19748 .expect("compatibility owner after epoch abort");
19749 assert!(matches!(
19750 subscriber.stage_interest_owner_replacement(
19751 owner,
19752 std::slice::from_ref(&interest),
19753 SubscriberOwnerStart::Live,
19754 ),
19755 Err(SubscriberOwnerError::AlreadyRegistered(_))
19756 ));
19757 }
19758
19759 #[test]
19760 fn pending_record_overflow_is_sticky_and_fail_closed() {
19761 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19762 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19763 provider,
19764 SubscriberMode::Polling,
19765 SubscriberConfig {
19766 max_pending_records: 1,
19767 ..SubscriberConfig::default()
19768 },
19769 );
19770 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
19771 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
19772 local_matcher: None,
19773 route_key: None,
19774 })];
19775 subscriber.enqueue_event(SubscriberEvent::Log {
19776 source_id: 0,
19777 log: rpc_log(false),
19778 });
19779 let mut second = rpc_log(false);
19780 second.log_index = Some(6);
19781 second.transaction_hash = Some(B256::repeat_byte(0x04));
19782 subscriber.enqueue_event(SubscriberEvent::Log {
19783 source_id: 0,
19784 log: second,
19785 });
19786
19787 assert_eq!(subscriber.pending_records.len(), 1);
19788 assert!(matches!(
19789 subscriber.check_resource_error(),
19790 Err(SubscriberError::ResourceExhausted(_))
19791 ));
19792 subscriber.reset_delivery_state();
19793 assert!(subscriber.check_resource_error().is_ok());
19794 }
19795
19796 #[test]
19797 fn historical_log_payload_bytes_are_bounded_independently_of_log_count() {
19798 let baseline = rpc_log(false);
19799 let fixed_bytes =
19800 validate_backfill_resource_limits(std::slice::from_ref(&baseline), 1, usize::MAX)
19801 .expect("measure fixed log accounting");
19802 let mut large = baseline;
19803 large.inner = alloy_primitives::Log::new_unchecked(
19804 Address::repeat_byte(0x42),
19805 vec![B256::repeat_byte(0x01)],
19806 Bytes::from(vec![0u8; 256]),
19807 );
19808
19809 assert!(matches!(
19810 validate_backfill_resource_limits(&[large], 1, fixed_bytes + 255),
19811 Err(SubscriberError::ResourceExhausted(_))
19812 ));
19813 }
19814
19815 #[tokio::test(flavor = "multi_thread")]
19816 #[cfg(feature = "reactive-polling")]
19817 async fn reconcile_capacity_failure_does_not_publish_progress_or_partial_history() {
19818 use alloy_rpc_types_eth::{Block, Header};
19819
19820 let asserter = Asserter::new();
19821 let baseline = BlockRef {
19822 number: 6,
19823 hash: B256::repeat_byte(6),
19824 parent_hash: Some(B256::repeat_byte(5)),
19825 timestamp: Some(1_700_000_006),
19826 };
19827 let through = BlockRef {
19828 number: 7,
19829 hash: B256::repeat_byte(7),
19830 parent_hash: Some(baseline.hash),
19831 timestamp: Some(1_700_000_007),
19832 };
19833 let rpc_block = || -> Block {
19834 Block::empty(Header {
19835 hash: through.hash,
19836 inner: alloy_consensus::Header {
19837 number: through.number,
19838 parent_hash: through.parent_hash.expect("parent"),
19839 timestamp: through.timestamp.expect("timestamp"),
19840 ..Default::default()
19841 },
19842 total_difficulty: None,
19843 size: None,
19844 })
19845 };
19846 let mut historical = rpc_log(false);
19847 historical.block_hash = Some(through.hash);
19848 historical.block_timestamp = through.timestamp;
19849 asserter.push_success(&Some(rpc_block()));
19850 asserter.push_success(&vec![historical]);
19851 asserter.push_success(&Some(rpc_block()));
19852 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
19853 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19854 provider,
19855 SubscriberMode::Polling,
19856 SubscriberConfig {
19857 max_pending_records: 1,
19858 ..SubscriberConfig::default()
19859 },
19860 );
19861 subscriber.chain_id = Some(1);
19862 let interest = ReactiveInterest::Logs(LogInterest {
19863 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
19864 local_matcher: None,
19865 route_key: None,
19866 });
19867 let epoch = subscriber
19868 .stage_interest_owner(
19869 HandlerId::new("capacity-owner"),
19870 std::slice::from_ref(&interest),
19871 SubscriberOwnerStart::PostBlock(baseline),
19872 )
19873 .expect("stage owner");
19874 subscriber.sources_dirty = false;
19877 subscriber.state = AlloySubscriberState::Empty;
19878 subscriber.push_pending_record(SubscriberInputRecord {
19879 record: log_input_record(rpc_log(false), InputSource::Poll),
19880 scope: SubscriberInputScope::Canonical { owners: Vec::new() },
19881 });
19882
19883 let error = subscriber
19884 .reconcile_interest_owner(&epoch, through)
19885 .await
19886 .expect_err("historical delivery cannot displace the queued live record");
19887 assert!(matches!(
19888 error,
19889 SubscriberOwnerError::Subscriber(SubscriberError::ResourceExhausted(_))
19890 ));
19891 assert!(subscriber.interest_owner_progress(&epoch).is_none());
19892 assert_eq!(subscriber.pending_records.len(), 1);
19893 assert!(matches!(
19894 subscriber.pending_records[0].scope,
19895 SubscriberInputScope::Canonical { .. }
19896 ));
19897 }
19898
19899 #[test]
19900 fn lazy_backfill_queue_capacity_failure_is_atomic() {
19901 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19902 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19903 provider,
19904 SubscriberMode::Auto,
19905 SubscriberConfig {
19906 max_pending_backfills: 1,
19907 ..SubscriberConfig::default()
19908 },
19909 );
19910 let interest = |address| {
19911 ReactiveInterest::Logs(LogInterest {
19912 provider_filter: Filter::new().address(address),
19913 local_matcher: None,
19914 route_key: None,
19915 })
19916 };
19917 subscriber
19918 .add_interest_owner_with_backfill(
19919 HandlerId::new("owner-a"),
19920 &[interest(Address::repeat_byte(0x41))],
19921 SubscriberBackfill::from_block(10),
19922 )
19923 .expect("first queued backfill");
19924
19925 let error = subscriber
19926 .add_interest_owner_with_backfill(
19927 HandlerId::new("owner-b"),
19928 &[interest(Address::repeat_byte(0x42))],
19929 SubscriberBackfill::from_block(10),
19930 )
19931 .expect_err("second backfill must exceed capacity");
19932
19933 assert!(matches!(error, SubscriberError::ResourceExhausted(_)));
19934 assert!(
19935 subscriber
19936 .owner_interests(&HandlerId::new("owner-b"))
19937 .is_none()
19938 );
19939 assert_eq!(subscriber.pending_backfills.len(), 1);
19940 }
19941
19942 #[test]
19943 fn exact_owner_replacement_is_atomic_and_removes_crash_stale_owners() {
19944 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19945 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19946 provider,
19947 SubscriberMode::Auto,
19948 SubscriberConfig {
19949 max_pending_backfills: 1,
19950 ..SubscriberConfig::default()
19951 },
19952 );
19953 let interest = |address| {
19954 ReactiveInterest::Logs(LogInterest {
19955 provider_filter: Filter::new().address(address),
19956 local_matcher: None,
19957 route_key: None,
19958 })
19959 };
19960 subscriber
19961 .add_interest_owner(
19962 HandlerId::new("crash-stale"),
19963 &[interest(Address::repeat_byte(0xee))],
19964 )
19965 .expect("seed stale owner");
19966 subscriber.base_interests = vec![interest(Address::repeat_byte(0xdd))];
19967 subscriber.rebuild_registered_interests();
19968 subscriber.push_pending_record(SubscriberInputRecord {
19969 record: log_input_record(rpc_log(false), InputSource::Poll),
19970 scope: SubscriberInputScope::Canonical { owners: Vec::new() },
19971 });
19972 let baseline = BlockRef {
19973 number: 100,
19974 hash: B256::repeat_byte(100),
19975 parent_hash: Some(B256::repeat_byte(99)),
19976 timestamp: Some(1_700_000_100),
19977 };
19978 let backfill = SubscriberBackfill::after_canonical_block(baseline).expect("C + 1");
19979
19980 let error = subscriber
19981 .replace_interest_owners_with_global_backfill(
19982 vec![
19983 (
19984 HandlerId::new("pool-a"),
19985 vec![interest(Address::repeat_byte(0xa1))],
19986 ),
19987 (
19988 HandlerId::new("pool-b"),
19989 vec![ReactiveInterest::Logs(LogInterest {
19990 provider_filter: Filter::new()
19993 .address(Address::repeat_byte(0xb2))
19994 .from_block(7),
19995 local_matcher: None,
19996 route_key: None,
19997 })],
19998 ),
19999 ],
20000 backfill,
20001 )
20002 .expect_err("two backfills exceed atomic capacity");
20003 assert!(matches!(error, SubscriberError::ResourceExhausted(_)));
20004 assert!(
20005 subscriber
20006 .owner_interests(&HandlerId::new("crash-stale"))
20007 .is_some(),
20008 "failed replacement must preserve the prior topology"
20009 );
20010 assert!(
20011 subscriber
20012 .owner_interests(&HandlerId::new("pool-a"))
20013 .is_none()
20014 );
20015 assert_eq!(subscriber.base_interests.len(), 1);
20016 assert_eq!(subscriber.pending_records.len(), 1);
20017
20018 subscriber
20019 .replace_interest_owners_with_global_backfill(
20020 vec![(
20021 HandlerId::new("pool-a"),
20022 vec![interest(Address::repeat_byte(0xa1))],
20023 )],
20024 backfill,
20025 )
20026 .expect("replacement within capacity");
20027 assert!(
20028 subscriber
20029 .owner_interests(&HandlerId::new("crash-stale"))
20030 .is_none(),
20031 "successful exact replacement removes stale owners"
20032 );
20033 assert!(
20034 subscriber.base_interests.is_empty(),
20035 "successful exact replacement removes stale unowned interests"
20036 );
20037 assert!(
20038 subscriber.drain_next_scoped_batch().is_none(),
20039 "stale canonical delivery must not escape before C + 1 recovery"
20040 );
20041 assert!(
20042 subscriber
20043 .owner_interests(&HandlerId::new("pool-a"))
20044 .is_some()
20045 );
20046 assert_eq!(subscriber.pending_backfills.len(), 1);
20047 assert_eq!(subscriber.pending_backfills[0].backfill, backfill);
20048 assert!(
20049 subscriber.pending_backfills[0].owner.is_none(),
20050 "startup history must be global canonical catch-up, not owner-only"
20051 );
20052 }
20053
20054 #[test]
20055 fn exclusive_canonical_backfill_rejects_block_number_overflow() {
20056 let baseline = BlockRef {
20057 number: u64::MAX,
20058 hash: B256::repeat_byte(0xff),
20059 parent_hash: None,
20060 timestamp: None,
20061 };
20062 assert!(matches!(
20063 SubscriberBackfill::after_canonical_block(baseline),
20064 Err(SubscriberError::InvalidConfig(_))
20065 ));
20066 }
20067
20068 #[tokio::test(flavor = "multi_thread")]
20069 async fn exclusive_canonical_backfill_validates_the_retained_baseline_hash() {
20070 let asserter = Asserter::new();
20071 asserter.push_success(&101u64);
20072 asserter.push_success(&Some(rpc_block(101, B256::repeat_byte(101))));
20073 asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(0xee))));
20074 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
20075 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20076 provider,
20077 SubscriberMode::Auto,
20078 SubscriberConfig::default(),
20079 );
20080 let baseline = BlockRef {
20081 number: 100,
20082 hash: B256::repeat_byte(0xaa),
20083 parent_hash: None,
20084 timestamp: None,
20085 };
20086 let backfill = SubscriberBackfill::after_canonical_block(baseline).expect("C + 1");
20087 subscriber
20088 .add_interest_owner_with_backfill(
20089 HandlerId::new("pool"),
20090 &[ReactiveInterest::Logs(LogInterest {
20091 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
20092 local_matcher: None,
20093 route_key: None,
20094 })],
20095 backfill,
20096 )
20097 .expect("queue post-baseline backfill");
20098
20099 let error = subscriber
20100 .drain_pending_backfills()
20101 .await
20102 .expect_err("provider branch differs at retained baseline");
20103 assert!(matches!(error, SubscriberError::InvalidBackfill(_)));
20104 assert_eq!(subscriber.pending_backfills.len(), 1);
20105 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 101);
20106 assert!(subscriber.pending_records.is_empty());
20107 }
20108
20109 #[tokio::test(flavor = "multi_thread")]
20110 #[cfg(feature = "reactive-ws")]
20111 async fn coordinated_multifilter_windows_are_globally_sorted_for_owner_and_canonical_delivery()
20112 {
20113 let asserter = Asserter::new();
20114 let retained = BlockRef {
20115 number: 10,
20116 hash: B256::repeat_byte(10),
20117 parent_hash: Some(B256::repeat_byte(9)),
20118 timestamp: Some(1_700_000_010),
20119 };
20120 let activation = BlockRef {
20121 number: 12,
20122 hash: B256::repeat_byte(12),
20123 parent_hash: Some(B256::repeat_byte(11)),
20124 timestamp: Some(1_700_000_012),
20125 };
20126
20127 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
20131 asserter.push_success(&vec![rpc_log_at(10, 2, 2)]);
20132 asserter.push_success(&vec![rpc_log_at(10, 1, 1)]);
20133 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
20134 asserter.push_success(&activation.number);
20135 asserter.push_success(&Some(rpc_block(activation.number, activation.hash)));
20136 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
20137 asserter.push_success(&vec![rpc_log_at(12, 2, 2)]);
20138 asserter.push_success(&vec![rpc_log_at(11, 1, 1)]);
20139 asserter.push_success(&Some(rpc_block(activation.number, activation.hash)));
20140 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
20141 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20142 provider,
20143 SubscriberMode::Auto,
20144 SubscriberConfig::default(),
20145 );
20146 let interests = (0..257)
20147 .map(|start| {
20148 ReactiveInterest::Logs(LogInterest {
20149 provider_filter: Filter::new()
20150 .address(Address::repeat_byte(0x42))
20151 .event_signature(B256::repeat_byte(0x01))
20152 .from_block(start),
20153 local_matcher: None,
20154 route_key: None,
20155 })
20156 })
20157 .collect::<Vec<_>>();
20158 subscriber
20159 .add_interest_owner_with_canonical_catchup(
20160 HandlerId::new("many-filters"),
20161 &interests,
20162 retained,
20163 )
20164 .expect("queue coordinated windows");
20165 assert_eq!(subscriber.pending_backfills.len(), 2);
20166 assert_eq!(subscriber.pending_backfills[0].filters.len(), 257);
20167 assert_eq!(subscriber.pending_backfills[1].filters.len(), 257);
20168
20169 subscriber
20170 .drain_pending_backfills()
20171 .await
20172 .expect("owner filter group");
20173 let owner = subscriber
20174 .drain_next_scoped_batch()
20175 .expect("owner ordered batch");
20176 assert_eq!(owner.records.len(), 2);
20177 assert_eq!(owner.records[0].record.context.transaction_index, Some(1));
20178 assert_eq!(owner.records[1].record.context.transaction_index, Some(2));
20179 assert!(
20180 owner.records.iter().all(|record| matches!(
20181 record.scope,
20182 SubscriberInputScope::OwnerOnlyHandlers { .. }
20183 ))
20184 );
20185
20186 subscriber
20187 .drain_pending_backfills()
20188 .await
20189 .expect("global filter group");
20190 let global = subscriber
20191 .drain_next_scoped_batch()
20192 .expect("global ordered batch");
20193 assert_eq!(global.records.len(), 2);
20194 assert_eq!(
20195 global.records[0].record.context.block.map(|b| b.number),
20196 Some(11)
20197 );
20198 assert_eq!(
20199 global.records[1].record.context.block.map(|b| b.number),
20200 Some(12)
20201 );
20202 assert!(
20203 global
20204 .records
20205 .iter()
20206 .all(|record| record.scope.is_canonical())
20207 );
20208 assert!(matches!(
20209 global.chain_controls.as_slice(),
20210 [ChainControl::Barrier {
20211 block: Some(block),
20212 ..
20213 }] if block == &activation
20214 ));
20215 }
20216
20217 #[tokio::test(flavor = "multi_thread")]
20218 #[cfg(feature = "reactive-ws")]
20219 async fn aborting_staged_epoch_purges_only_its_buffered_delivery() {
20220 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20221 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20222 provider,
20223 SubscriberMode::PubSub,
20224 SubscriberConfig::default(),
20225 );
20226 subscriber.chain_id = Some(1);
20227 let interest = ReactiveInterest::Logs(LogInterest {
20228 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
20229 local_matcher: None,
20230 route_key: None,
20231 });
20232 let owner_a = subscriber
20233 .stage_interest_owner(
20234 HandlerId::new("owner-a"),
20235 std::slice::from_ref(&interest),
20236 SubscriberOwnerStart::Live,
20237 )
20238 .unwrap();
20239 let owner_b = subscriber
20240 .stage_interest_owner(
20241 HandlerId::new("owner-b"),
20242 &[interest],
20243 SubscriberOwnerStart::Live,
20244 )
20245 .unwrap();
20246
20247 subscriber.enqueue_event(SubscriberEvent::Log {
20248 source_id: 0,
20249 log: rpc_log(false),
20250 });
20251 assert!(subscriber.abort_interest_owner(&owner_a));
20252
20253 let batch = subscriber
20254 .next_scoped_batch()
20255 .await
20256 .unwrap()
20257 .expect("shared canonical delivery remains queued");
20258 assert_eq!(batch.records.len(), 1);
20259 assert_eq!(
20260 batch.records[0].scope,
20261 SubscriberInputScope::Canonical {
20262 owners: vec![owner_b]
20263 }
20264 );
20265 }
20266
20267 #[tokio::test(flavor = "multi_thread")]
20268 #[cfg(feature = "reactive-ws")]
20269 async fn owner_backfill_dedupe_never_suppresses_canonical_delivery() {
20270 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20271 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20272 provider,
20273 SubscriberMode::PubSub,
20274 SubscriberConfig::default(),
20275 );
20276 subscriber.chain_id = Some(1);
20277 let epoch = subscriber
20278 .stage_interest_owner(
20279 HandlerId::new("owner"),
20280 &[ReactiveInterest::Logs(LogInterest {
20281 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
20282 local_matcher: None,
20283 route_key: None,
20284 })],
20285 SubscriberOwnerStart::Live,
20286 )
20287 .unwrap();
20288 let log = rpc_log(false);
20289
20290 subscriber.enqueue_owner_record(
20291 log_input_record(log.clone(), InputSource::Backfill),
20292 epoch.clone(),
20293 );
20294 subscriber.enqueue_event(SubscriberEvent::Log { source_id: 0, log });
20295
20296 let batch = subscriber
20297 .next_scoped_batch()
20298 .await
20299 .unwrap()
20300 .expect("owner backfill and canonical live delivery");
20301 assert_eq!(batch.records.len(), 2);
20302 assert_eq!(
20303 batch.records[0].scope,
20304 SubscriberInputScope::OwnerOnly {
20305 owners: vec![epoch]
20306 }
20307 );
20308 assert_eq!(
20309 batch.records[1].scope,
20310 SubscriberInputScope::Canonical { owners: Vec::new() },
20311 "owner replay dedupe must not suppress the global live record"
20312 );
20313 }
20314
20315 #[tokio::test(flavor = "multi_thread")]
20316 #[cfg(feature = "reactive-polling")]
20317 async fn reconcile_fetch_drains_live_burst_beyond_output_batch_capacity() {
20318 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20319 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20320 provider,
20321 SubscriberMode::Polling,
20322 SubscriberConfig {
20323 max_batch_size: 2,
20324 ..SubscriberConfig::default()
20325 },
20326 );
20327 let interest = ReactiveInterest::Logs(LogInterest {
20328 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
20329 local_matcher: None,
20330 route_key: None,
20331 });
20332 let epoch = subscriber
20333 .stage_interest_owner(
20334 HandlerId::new("owner"),
20335 std::slice::from_ref(&interest),
20336 SubscriberOwnerStart::Live,
20337 )
20338 .unwrap();
20339 subscriber.sources_dirty = false;
20340
20341 let mut duplicate = rpc_log(false);
20342 duplicate.transaction_hash = Some(B256::repeat_byte(1));
20343 duplicate.log_index = Some(0);
20344 let events = (0u8..10).map(|index| {
20345 let mut log = rpc_log(false);
20346 log.transaction_hash = Some(B256::repeat_byte(index.saturating_add(1)));
20347 log.log_index = Some(index as u64);
20348 SubscriberEvent::Log { source_id: 0, log }
20349 });
20350 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
20351 let mut streams = SubscriberStreams::new();
20352 streams.push(
20353 SubscriberStreamSource::PollingLog { filter },
20354 stream::iter(events).boxed(),
20355 );
20356 subscriber.state = AlloySubscriberState::Active(streams);
20357
20358 let mut polls = 0usize;
20359 let fetched_duplicate = duplicate.clone();
20360 let fetch = poll_fn(move |cx| {
20361 polls += 1;
20362 if polls > 10 {
20363 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>(fetched_duplicate.clone()))
20364 } else {
20365 cx.waker().wake_by_ref();
20366 std::task::Poll::Pending
20367 }
20368 });
20369 let target_epochs = HashSet::from([epoch.clone()]);
20370 let fetched_duplicate = subscriber
20371 .drive_reconcile_fetch(fetch, &target_epochs)
20372 .await
20373 .unwrap();
20374 subscriber.enqueue_owner_record_for_owners_unmerged(
20375 log_input_record(fetched_duplicate, InputSource::Backfill),
20376 vec![epoch.clone()],
20377 );
20378 subscriber.promote_reconcile_owner_records(&target_epochs);
20379
20380 assert_eq!(subscriber.pending_records.len(), 20);
20381 assert!(subscriber.pending_records.iter().take(10).all(|record| {
20382 record.scope == SubscriberInputScope::Canonical { owners: Vec::new() }
20383 }));
20384 assert!(subscriber.pending_records.iter().skip(10).all(|record| {
20385 record.scope
20386 == SubscriberInputScope::OwnerOnly {
20387 owners: vec![epoch.clone()],
20388 }
20389 }));
20390 }
20391
20392 #[tokio::test(flavor = "multi_thread")]
20393 #[cfg(feature = "reactive-polling")]
20394 async fn reconcile_fetch_waits_for_provider_when_live_topology_is_empty() {
20395 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20396 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20397 provider,
20398 SubscriberMode::Polling,
20399 SubscriberConfig::default(),
20400 );
20401 subscriber.chain_id = Some(1);
20402 subscriber.sources_dirty = false;
20403 let mut first_poll = true;
20404 let fetch = poll_fn(move |cx| {
20405 if first_poll {
20406 first_poll = false;
20407 cx.waker().wake_by_ref();
20408 std::task::Poll::Pending
20409 } else {
20410 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>("certified"))
20411 }
20412 });
20413
20414 let result = subscriber
20415 .drive_reconcile_fetch(fetch, &HashSet::new())
20416 .await
20417 .expect("an empty live topology must not be mistaken for termination");
20418 assert_eq!(result, "certified");
20419 }
20420
20421 #[tokio::test(flavor = "multi_thread")]
20422 #[cfg(all(feature = "reactive-polling", feature = "reactive-ws"))]
20423 async fn successful_owner_reconcile_seeds_its_live_filter_reconnect_anchor() {
20424 use alloy_rpc_types_eth::{Block, Header};
20425
20426 let asserter = Asserter::new();
20427 let baseline = BlockRef {
20428 number: 100,
20429 hash: B256::repeat_byte(0x64),
20430 parent_hash: Some(B256::repeat_byte(0x63)),
20431 timestamp: Some(1_700_000_100),
20432 };
20433 let through = BlockRef {
20434 number: 101,
20435 hash: B256::repeat_byte(0x65),
20436 parent_hash: Some(baseline.hash),
20437 timestamp: Some(1_700_000_101),
20438 };
20439 let rpc_block = || -> Block {
20440 Block::empty(Header {
20441 hash: through.hash,
20442 inner: alloy_consensus::Header {
20443 number: through.number,
20444 parent_hash: through.parent_hash.unwrap(),
20445 timestamp: through.timestamp.unwrap(),
20446 ..Default::default()
20447 },
20448 total_difficulty: None,
20449 size: None,
20450 })
20451 };
20452 asserter.push_success(&Some(rpc_block()));
20453 asserter.push_success(&Vec::<Log>::new());
20454 asserter.push_success(&Some(rpc_block()));
20455 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20456 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20457 provider,
20458 SubscriberMode::PubSub,
20459 SubscriberConfig::default(),
20460 );
20461 subscriber.chain_id = Some(1);
20462 let interest = ReactiveInterest::Logs(LogInterest {
20463 provider_filter: Filter::new().address(Address::repeat_byte(0xac)),
20464 local_matcher: None,
20465 route_key: None,
20466 });
20467 let epoch = subscriber
20468 .stage_interest_owner(
20469 HandlerId::new("reconnect-anchor"),
20470 std::slice::from_ref(&interest),
20471 SubscriberOwnerStart::PostBlock(baseline),
20472 )
20473 .unwrap();
20474 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
20475 let source = SubscriberStreamSource::PubSubLog {
20476 id: subscriber.log_source_id(&filter),
20477 filter: filter.clone(),
20478 };
20479 let mut streams = SubscriberStreams::new();
20480 streams.push(source, stream::pending().boxed());
20481 subscriber.state = AlloySubscriberState::Active(streams);
20482 subscriber.sources_dirty = false;
20483
20484 subscriber
20485 .reconcile_interest_owner(&epoch, through)
20486 .await
20487 .unwrap();
20488 assert_eq!(subscriber.log_anchor(&filter), Some(through.number));
20489 assert!(asserter.read_q().is_empty());
20490 }
20491
20492 #[tokio::test(flavor = "multi_thread")]
20493 #[cfg(feature = "reactive-polling")]
20494 async fn cancelled_reconcile_retains_hidden_owner_live_delivery_for_retry() {
20495 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20496 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20497 provider,
20498 SubscriberMode::Polling,
20499 SubscriberConfig::default(),
20500 );
20501 let interest = ReactiveInterest::Logs(LogInterest {
20502 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
20503 local_matcher: None,
20504 route_key: None,
20505 });
20506 let epoch = subscriber
20507 .stage_interest_owner(
20508 HandlerId::new("owner"),
20509 std::slice::from_ref(&interest),
20510 SubscriberOwnerStart::PostBlock(BlockRef {
20511 number: 100,
20512 hash: B256::repeat_byte(0x64),
20513 parent_hash: None,
20514 timestamp: None,
20515 }),
20516 )
20517 .unwrap();
20518 subscriber.sources_dirty = false;
20519
20520 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
20521 let event = SubscriberEvent::Log {
20522 source_id: 0,
20523 log: rpc_log(false),
20524 };
20525 let mut streams = SubscriberStreams::new();
20526 streams.push(
20527 SubscriberStreamSource::PollingLog { filter },
20528 stream::once(async move { event })
20529 .chain(stream::pending())
20530 .boxed(),
20531 );
20532 subscriber.state = AlloySubscriberState::Active(streams);
20533
20534 let targets = HashSet::from([epoch.clone()]);
20535 {
20536 let fetch = futures::future::pending::<Result<(), SubscriberOwnerError>>();
20537 let drive = subscriber.drive_reconcile_fetch(fetch, &targets);
20538 futures::pin_mut!(drive);
20539 poll_fn(|cx| {
20540 assert!(drive.as_mut().poll(cx).is_pending());
20541 std::task::Poll::Ready(())
20542 })
20543 .await;
20544 }
20545
20546 assert_eq!(subscriber.pending_records.len(), 1);
20547 assert_eq!(
20548 subscriber.pending_records[0].scope,
20549 SubscriberInputScope::Canonical { owners: Vec::new() },
20550 "canonical delivery commits immediately at a cancellation-safe boundary"
20551 );
20552 assert_eq!(subscriber.pending_reconcile_owner_records.len(), 1);
20553
20554 subscriber
20555 .drive_reconcile_fetch(futures::future::ready(Ok(())), &targets)
20556 .await
20557 .unwrap();
20558 subscriber.promote_reconcile_owner_records(&targets);
20559 assert!(subscriber.pending_reconcile_owner_records.is_empty());
20560 assert_eq!(subscriber.pending_records.len(), 2);
20561 assert_eq!(
20562 subscriber.pending_records[0].scope,
20563 SubscriberInputScope::Canonical { owners: Vec::new() },
20564 "canonical delivery remains target-excluded"
20565 );
20566 assert_eq!(
20567 subscriber.pending_records[1].scope,
20568 SubscriberInputScope::OwnerOnly {
20569 owners: vec![epoch]
20570 },
20571 "retry commit appends hidden owner delivery after historical catch-up"
20572 );
20573 }
20574
20575 #[tokio::test(flavor = "multi_thread")]
20576 #[cfg(feature = "reactive-ws")]
20577 async fn control_cancellation_preserves_terminated_source_reconcile_intent() {
20578 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20579 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20580 provider,
20581 SubscriberMode::PubSub,
20582 SubscriberConfig {
20583 reconnect: SubscriberReconnectConfig {
20584 initial_delay: Duration::from_secs(60),
20585 ..SubscriberReconnectConfig::default()
20586 },
20587 ..SubscriberConfig::default()
20588 },
20589 );
20590 subscriber.chain_id = Some(1);
20591 let interest = ReactiveInterest::Logs(LogInterest {
20592 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
20593 local_matcher: None,
20594 route_key: None,
20595 });
20596 let epoch = subscriber
20597 .stage_interest_owner(
20598 HandlerId::new("owner"),
20599 std::slice::from_ref(&interest),
20600 SubscriberOwnerStart::PostBlock(BlockRef {
20601 number: 7,
20602 hash: B256::repeat_byte(0x07),
20603 parent_hash: Some(B256::repeat_byte(0x06)),
20604 timestamp: Some(1_700_000_007),
20605 }),
20606 )
20607 .unwrap();
20608 subscriber.sources_dirty = false;
20609 subscriber.stream_revision = 1;
20610 let entry = subscriber
20611 .owned_interests
20612 .iter_mut()
20613 .find(|entry| entry.epoch.as_ref() == Some(&epoch))
20614 .unwrap();
20615 entry.progress = Some(SubscriberOwnerProgress {
20616 owner: epoch.clone(),
20617 through: entry.baseline.unwrap(),
20618 });
20619 entry.progress_stream_revision = Some(1);
20620
20621 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
20622 let source = SubscriberStreamSource::PubSubLog {
20623 id: subscriber.log_source_id(&filter),
20624 filter,
20625 };
20626 let mut streams = SubscriberStreams::new();
20627 streams.push(
20628 source.clone(),
20629 stream::iter([SubscriberEvent::StreamTerminated(source)]).boxed(),
20630 );
20631 subscriber.state = AlloySubscriberState::Active(streams);
20632 let prior_revision = subscriber.stream_revision;
20633
20634 let mut first_poll = true;
20635 let control = poll_fn(move |cx| {
20636 if first_poll {
20637 first_poll = false;
20638 cx.waker().wake_by_ref();
20639 std::task::Poll::Pending
20640 } else {
20641 std::task::Poll::Ready("stop")
20642 }
20643 });
20644 futures::pin_mut!(control);
20645 let outcome = subscriber
20646 .next_scoped_batch_or(control.as_mut())
20647 .await
20648 .unwrap();
20649
20650 assert!(matches!(outcome, SubscriberDriverPoll::Control("stop")));
20651 assert!(subscriber.sources_dirty);
20652 assert!(subscriber.stream_revision > prior_revision);
20653 assert!(
20654 !subscriber.activate_interest_owner(&epoch),
20655 "progress certified against the terminated stream revision is stale"
20656 );
20657 }
20658
20659 #[tokio::test]
20660 #[cfg(feature = "reactive-ws")]
20661 async fn pubsub_sources_assign_stable_log_ids_before_shared_streams() {
20662 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20663 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20664 provider,
20665 SubscriberMode::PubSub,
20666 SubscriberConfig::default(),
20667 );
20668 subscriber.chain_id = Some(1);
20669 subscriber
20670 .register_interests(&[
20671 ReactiveInterest::Logs(LogInterest {
20672 provider_filter: Filter::new().address(Address::repeat_byte(0x01)),
20673 local_matcher: None,
20674 route_key: None,
20675 }),
20676 ReactiveInterest::Logs(LogInterest {
20677 provider_filter: Filter::new().address(Address::repeat_byte(0x02)),
20678 local_matcher: None,
20679 route_key: None,
20680 }),
20681 ReactiveInterest::PendingTransactions(PendingTxInterest::default()),
20682 ])
20683 .await
20684 .expect("register base interests");
20685
20686 let sources = subscriber.stream_sources().expect("stream sources");
20690 assert_eq!(sources.len(), 2);
20691 assert!(matches!(
20692 &sources[0],
20693 SubscriberStreamSource::PubSubLog { id: 0, .. }
20694 ));
20695 assert!(matches!(
20696 sources[1],
20697 SubscriberStreamSource::PubSubPendingHashes
20698 ));
20699
20700 let again = subscriber.stream_sources().expect("stream sources again");
20702 assert!(again[0].same_key(&sources[0]));
20703 }
20704
20705 #[tokio::test(flavor = "multi_thread")]
20706 #[cfg(feature = "reactive-ws")]
20707 async fn pubsub_stream_termination_attempts_reconnect_before_error() {
20708 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20709 let mut subscriber = AlloySubscriber::new(
20710 provider,
20711 SubscriberMode::PubSub,
20712 SubscriberConfig {
20713 reconnect: SubscriberReconnectConfig {
20714 initial_delay: Duration::ZERO,
20715 retry_delay: Duration::ZERO,
20716 max_delay: Duration::ZERO,
20717 max_attempts: Some(1),
20718 ..SubscriberReconnectConfig::default()
20719 },
20720 ..SubscriberConfig::default()
20721 },
20722 );
20723 subscriber.chain_id = Some(1);
20724 subscriber.interests = vec![ReactiveInterest::PendingTransactions(
20725 PendingTxInterest::default(),
20726 )];
20727
20728 let mut streams = SubscriberStreams::new();
20729 let source = SubscriberStreamSource::PubSubPendingHashes;
20730 streams.push(
20731 source,
20732 stream::once(async {
20733 SubscriberEvent::<Ethereum>::StreamTerminated(
20734 SubscriberStreamSource::PubSubPendingHashes,
20735 )
20736 })
20737 .boxed(),
20738 );
20739 subscriber.state = AlloySubscriberState::Active(streams);
20740
20741 let result = subscriber.next_batch().await;
20742 assert!(
20743 matches!(result, Err(SubscriberError::Provider(ref message)) if message.contains("reconnect failed after 1 attempt")),
20744 "terminated pubsub streams should attempt reconnect before surfacing failure: {result:?}"
20745 );
20746 }
20747
20748 #[tokio::test]
20749 #[cfg(feature = "reactive-ws")]
20750 async fn flashblock_stream_termination_invalidates_before_reconnect_io() {
20751 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20752 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20753 provider,
20754 SubscriberMode::PubSub,
20755 SubscriberConfig {
20756 preconfirmations: PreconfirmationMode::Required,
20757 ..SubscriberConfig::default()
20758 },
20759 )
20760 .with_provider_ref(ProviderRef::new("base-paid", 7));
20761 subscriber.chain_id = Some(8_453);
20762 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20763 subscriber.interests = subscriber.base_interests.clone();
20764 subscriber.sources_dirty = false;
20765
20766 let preview: BaseFlashblockWirePayload = serde_json::from_str(
20767 r#"{
20768 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
20769 "number":"0x65",
20770 "parentHash":"0x6464646464646464646464646464646464646464646464646464646464646464",
20771 "stateRoot":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
20772 "transactionsRoot":"0x1111111111111111111111111111111111111111111111111111111111111111",
20773 "timestamp":"0x6553f165",
20774 "transactions":["0x4141414141414141414141414141414141414141414141414141414141414141"]
20775 }"#,
20776 )
20777 .unwrap();
20778 let (preview, _) = subscriber.accept_base_flashblock(preview).unwrap();
20779 subscriber.latest_preconfirmation = Some(preview);
20780
20781 let mut streams = SubscriberStreams::new();
20782 streams.push(
20783 SubscriberStreamSource::BaseFlashblocks,
20784 stream::once(async {
20785 SubscriberEvent::<Ethereum>::StreamTerminated(
20786 SubscriberStreamSource::BaseFlashblocks,
20787 )
20788 })
20789 .boxed(),
20790 );
20791 subscriber.state = AlloySubscriberState::Active(streams);
20792
20793 let batch = subscriber
20794 .next_scoped_batch()
20795 .await
20796 .expect("termination handling succeeds")
20797 .expect("invalidation is delivered");
20798 assert!(batch.preconfirmation_invalidated());
20799 assert!(subscriber.latest_preconfirmation.is_none());
20800 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 8);
20801 assert_eq!(subscriber.pending_flashblock_reconnects.len(), 2);
20802 assert!(
20803 subscriber
20804 .pending_flashblock_reconnect_sources
20805 .iter()
20806 .any(|source| matches!(source, SubscriberStreamSource::BaseFlashblocks))
20807 );
20808 assert!(
20809 subscriber
20810 .pending_flashblock_reconnect_sources
20811 .iter()
20812 .any(|source| matches!(source, SubscriberStreamSource::BasePendingLog { .. }))
20813 );
20814 let AlloySubscriberState::Active(streams) = &subscriber.state else {
20815 panic!("subscriber remains active while reconnect is pending")
20816 };
20817 assert!(
20818 streams
20819 .entries
20820 .iter()
20821 .all(|entry| !entry.source.is_flashblocks())
20822 );
20823 }
20824
20825 #[tokio::test]
20826 #[cfg(feature = "reactive-ws")]
20827 async fn preferred_initial_flashblock_rejection_retains_canonical_streams() {
20828 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20829 let filter = Filter::new().address(Address::repeat_byte(0x42));
20830 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20831 provider,
20832 SubscriberMode::PubSub,
20833 SubscriberConfig {
20834 preconfirmations: PreconfirmationMode::Preferred,
20835 reconnect: SubscriberReconnectConfig {
20836 enabled: false,
20837 ..SubscriberReconnectConfig::default()
20838 },
20839 ..SubscriberConfig::default()
20840 },
20841 )
20842 .with_provider_ref(ProviderRef::new("base-paid", 1));
20843 subscriber.chain_id = Some(8_453);
20844 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
20845 provider_filter: filter.clone(),
20846 local_matcher: None,
20847 route_key: None,
20848 })];
20849 subscriber.interests = subscriber.base_interests.clone();
20850 subscriber.log_source_ids.insert(filter.clone(), 0);
20851 subscriber.next_log_source_id = 1;
20852
20853 let canonical_source = SubscriberStreamSource::PubSubLog {
20854 id: 0,
20855 filter: filter.clone(),
20856 };
20857 let mut streams = SubscriberStreams::new();
20858 streams.push(
20859 canonical_source.clone(),
20860 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
20861 );
20862 subscriber.state = AlloySubscriberState::Active(streams);
20863 subscriber.sources_dirty = true;
20864
20865 subscriber
20866 .ensure_streams()
20867 .await
20868 .expect("preferred Flashblocks setup degrades to canonical-only");
20869 let AlloySubscriberState::Active(streams) = &subscriber.state else {
20870 panic!("canonical stream remains active")
20871 };
20872 assert!(streams.contains_source(&canonical_source));
20873 assert!(
20874 streams
20875 .entries
20876 .iter()
20877 .all(|entry| !entry.source.is_flashblocks())
20878 );
20879 assert!(subscriber.pending_flashblock_reconnects.is_empty());
20880 assert!(!subscriber.sources_dirty);
20881 }
20882
20883 #[tokio::test]
20884 #[cfg(feature = "reactive-ws")]
20885 async fn required_initial_flashblock_rejection_remains_fail_closed() {
20886 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20887 let filter = Filter::new().address(Address::repeat_byte(0x42));
20888 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20889 provider,
20890 SubscriberMode::PubSub,
20891 SubscriberConfig {
20892 preconfirmations: PreconfirmationMode::Required,
20893 reconnect: SubscriberReconnectConfig {
20894 enabled: false,
20895 ..SubscriberReconnectConfig::default()
20896 },
20897 ..SubscriberConfig::default()
20898 },
20899 )
20900 .with_provider_ref(ProviderRef::new("base-paid", 1));
20901 subscriber.chain_id = Some(8_453);
20902 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
20903 provider_filter: filter.clone(),
20904 local_matcher: None,
20905 route_key: None,
20906 })];
20907 subscriber.interests = subscriber.base_interests.clone();
20908 subscriber.log_source_ids.insert(filter.clone(), 0);
20909 subscriber.next_log_source_id = 1;
20910
20911 let canonical_source = SubscriberStreamSource::PubSubLog {
20912 id: 0,
20913 filter: filter.clone(),
20914 };
20915 let mut streams = SubscriberStreams::new();
20916 streams.push(
20917 canonical_source.clone(),
20918 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
20919 );
20920 subscriber.state = AlloySubscriberState::Active(streams);
20921 subscriber.sources_dirty = true;
20922
20923 let error = subscriber
20924 .ensure_streams()
20925 .await
20926 .expect_err("required Flashblocks setup must fail closed");
20927 assert!(matches!(error, SubscriberError::Provider(_)));
20928 let AlloySubscriberState::Active(streams) = &subscriber.state else {
20929 panic!("the already-connected canonical stream is retained")
20930 };
20931 assert!(streams.contains_source(&canonical_source));
20932 }
20933
20934 #[tokio::test]
20935 #[cfg(feature = "reactive-ws")]
20936 async fn preferred_flashblock_termination_preserves_canonical_delivery() {
20937 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20938 let filter = Filter::new().address(Address::repeat_byte(0x42));
20939 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20940 provider,
20941 SubscriberMode::PubSub,
20942 SubscriberConfig {
20943 preconfirmations: PreconfirmationMode::Preferred,
20944 reconnect: SubscriberReconnectConfig {
20945 enabled: false,
20946 ..SubscriberReconnectConfig::default()
20947 },
20948 ..SubscriberConfig::default()
20949 },
20950 )
20951 .with_provider_ref(ProviderRef::new("base-paid", 1));
20952 subscriber.chain_id = Some(8_453);
20953 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
20954 provider_filter: filter.clone(),
20955 local_matcher: None,
20956 route_key: None,
20957 })];
20958 subscriber.interests = subscriber.base_interests.clone();
20959 subscriber.log_source_ids.insert(filter.clone(), 0);
20960 subscriber.next_log_source_id = 1;
20961 subscriber.sources_dirty = false;
20962
20963 let mut streams = SubscriberStreams::new();
20964 streams.push(
20965 SubscriberStreamSource::BaseFlashblocks,
20966 stream::once(async {
20967 SubscriberEvent::<Ethereum>::StreamTerminated(
20968 SubscriberStreamSource::BaseFlashblocks,
20969 )
20970 })
20971 .boxed(),
20972 );
20973 streams.push(
20974 SubscriberStreamSource::PubSubLog {
20975 id: 0,
20976 filter: filter.clone(),
20977 },
20978 stream::once(async {
20979 SubscriberEvent::<Ethereum>::Log {
20980 source_id: 0,
20981 log: rpc_log(false),
20982 }
20983 })
20984 .boxed(),
20985 );
20986 subscriber.state = AlloySubscriberState::Active(streams);
20987
20988 let invalidation = subscriber
20989 .next_scoped_batch()
20990 .await
20991 .expect("preferred termination does not fail")
20992 .expect("invalidation is delivered");
20993 assert!(invalidation.preconfirmation_invalidated());
20994
20995 let canonical = subscriber
20996 .next_scoped_batch()
20997 .await
20998 .expect("canonical stream remains healthy")
20999 .expect("canonical log is delivered");
21000 assert!(!canonical.preconfirmation_invalidated());
21001 assert_eq!(canonical.records().len(), 1);
21002 assert_eq!(
21003 canonical.records()[0].record.context.source,
21004 InputSource::Subscription
21005 );
21006 }
21007
21008 #[tokio::test]
21009 #[cfg(feature = "reactive-ws")]
21010 async fn preferred_flashblock_reconnect_exhaustion_preserves_canonical_delivery() {
21011 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21012 let filter = Filter::new().address(Address::repeat_byte(0x42));
21013 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21014 provider,
21015 SubscriberMode::PubSub,
21016 SubscriberConfig {
21017 preconfirmations: PreconfirmationMode::Preferred,
21018 reconnect: SubscriberReconnectConfig {
21019 enabled: false,
21020 ..SubscriberReconnectConfig::default()
21021 },
21022 ..SubscriberConfig::default()
21023 },
21024 )
21025 .with_provider_ref(ProviderRef::new("base-paid", 1));
21026 subscriber.chain_id = Some(8_453);
21027 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
21028 provider_filter: filter.clone(),
21029 local_matcher: None,
21030 route_key: None,
21031 })];
21032 subscriber.interests = subscriber.base_interests.clone();
21033 subscriber.log_source_ids.insert(filter.clone(), 0);
21034 subscriber.next_log_source_id = 1;
21035 subscriber.sources_dirty = false;
21036
21037 let canonical_source = SubscriberStreamSource::PubSubLog { id: 0, filter };
21038 let mut streams = SubscriberStreams::new();
21039 streams.push(
21040 canonical_source,
21041 stream::once(async {
21042 tokio::time::sleep(Duration::from_millis(1)).await;
21043 SubscriberEvent::<Ethereum>::Log {
21044 source_id: 0,
21045 log: rpc_log(false),
21046 }
21047 })
21048 .boxed(),
21049 );
21050 subscriber.state = AlloySubscriberState::Active(streams);
21051
21052 let source = SubscriberStreamSource::BaseFlashblocks;
21053 subscriber
21054 .pending_flashblock_reconnect_sources
21055 .push(source.clone());
21056 subscriber
21057 .pending_flashblock_reconnects
21058 .push(Box::pin(async move {
21059 (
21060 source,
21061 Err(SubscriberError::Provider(
21062 "test reconnect window exhausted".to_owned(),
21063 )),
21064 )
21065 }));
21066
21067 let canonical = subscriber
21068 .next_scoped_batch()
21069 .await
21070 .expect("preferred reconnect exhaustion does not fail")
21071 .expect("canonical log is delivered");
21072 assert_eq!(canonical.records().len(), 1);
21073 assert!(subscriber.pending_flashblock_reconnects.is_empty());
21074 }
21075
21076 #[test]
21077 fn backfilled_logs_skip_recent_subscription_duplicates() {
21078 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21079 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21080 provider,
21081 SubscriberMode::PubSub,
21082 SubscriberConfig::default(),
21083 );
21084 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
21085 provider_filter: Filter::new()
21086 .address(Address::repeat_byte(0x42))
21087 .event_signature(B256::repeat_byte(0x01)),
21088 local_matcher: None,
21089 route_key: None,
21090 })];
21091
21092 let log = rpc_log(false);
21093 subscriber.enqueue_event(SubscriberEvent::Log {
21094 source_id: 0,
21095 log: log.clone(),
21096 });
21097 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
21098 source_id: 0,
21099 logs: vec![log],
21100 });
21101
21102 assert_eq!(subscriber.pending_records.len(), 1);
21103 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
21104 assert_eq!(
21105 subscriber.pending_records[0].context.source,
21106 InputSource::Subscription
21107 );
21108 }
21109
21110 #[test]
21111 fn backfilled_logs_surface_with_backfill_source() {
21112 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21117 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21118 provider,
21119 SubscriberMode::PubSub,
21120 SubscriberConfig::default(),
21121 );
21122 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
21123 provider_filter: Filter::new()
21124 .address(Address::repeat_byte(0x42))
21125 .event_signature(B256::repeat_byte(0x01)),
21126 local_matcher: None,
21127 route_key: None,
21128 })];
21129
21130 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
21131 source_id: 0,
21132 logs: vec![rpc_log(false)],
21133 });
21134
21135 assert_eq!(subscriber.pending_records.len(), 1);
21136 assert_eq!(
21137 subscriber.pending_records[0].context.source,
21138 InputSource::Backfill
21139 );
21140 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
21141 }
21142
21143 #[test]
21144 #[cfg(feature = "reactive-ws")]
21145 fn owner_removal_preserves_delivery_and_dedupe_state() {
21146 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21147 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21148 provider,
21149 SubscriberMode::PubSub,
21150 SubscriberConfig::default(),
21151 );
21152 subscriber
21153 .add_interest_owner(
21154 HandlerId::new("pool-a"),
21155 &[ReactiveInterest::Logs(LogInterest {
21156 provider_filter: Filter::new()
21157 .address(Address::repeat_byte(0x42))
21158 .event_signature(B256::repeat_byte(0x01)),
21159 local_matcher: None,
21160 route_key: None,
21161 })],
21162 )
21163 .expect("register pool-a owner");
21164 subscriber
21165 .add_interest_owner(
21166 HandlerId::new("pool-b"),
21167 &[ReactiveInterest::Logs(LogInterest {
21168 provider_filter: Filter::new()
21169 .address(Address::repeat_byte(0x24))
21170 .event_signature(B256::repeat_byte(0x02)),
21171 local_matcher: None,
21172 route_key: None,
21173 })],
21174 )
21175 .expect("register pool-b owner");
21176
21177 let sources = subscriber.stream_sources().expect("stream sources");
21180 subscriber.enqueue_event(SubscriberEvent::Log {
21181 source_id: 0,
21182 log: rpc_log(false),
21183 });
21184 let mut streams = SubscriberStreams::new();
21185 streams.push(
21186 sources[0].clone(),
21187 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
21188 );
21189 subscriber.state = AlloySubscriberState::Active(streams);
21190 assert_eq!(subscriber.pending_records.len(), 1);
21191 assert_eq!(subscriber.recent_input_refs.len(), 1);
21192 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
21193
21194 let removed = subscriber
21195 .remove_interest_owner(&HandlerId::new("pool-b"))
21196 .expect("pool-b should be removed");
21197
21198 assert_eq!(removed.len(), 1);
21199 assert_eq!(subscriber.pending_records.len(), 1);
21200 assert_eq!(subscriber.recent_input_refs.len(), 1);
21201 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
21202 assert!(
21203 subscriber
21204 .owner_interests(&HandlerId::new("pool-a"))
21205 .is_some()
21206 );
21207 assert!(
21208 subscriber
21209 .owner_interests(&HandlerId::new("pool-b"))
21210 .is_none()
21211 );
21212 assert_eq!(subscriber.registered_interests().len(), 1);
21213 }
21214
21215 #[test]
21216 #[cfg(feature = "reactive-ws")]
21217 fn owner_log_sources_fan_in_across_owners() {
21218 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21219 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21220 provider,
21221 SubscriberMode::PubSub,
21222 SubscriberConfig::default(),
21223 );
21224 subscriber
21225 .add_interest_owner(
21226 HandlerId::new("pool-a"),
21227 &[ReactiveInterest::Logs(LogInterest {
21228 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
21229 local_matcher: None,
21230 route_key: None,
21231 })],
21232 )
21233 .expect("register pool-a owner");
21234
21235 let initial_sources = subscriber.stream_sources().expect("initial sources");
21236 assert_eq!(initial_sources.len(), 1);
21237 let pool_a_source = initial_sources[0].clone();
21238 assert!(matches!(
21239 &pool_a_source,
21240 SubscriberStreamSource::PubSubLog { id: 0, .. }
21241 ));
21242
21243 subscriber
21244 .add_interest_owner(
21245 HandlerId::new("pool-b"),
21246 &[ReactiveInterest::Logs(LogInterest {
21247 provider_filter: Filter::new().address(Address::repeat_byte(0xb2)),
21248 local_matcher: None,
21249 route_key: None,
21250 })],
21251 )
21252 .expect("register pool-b owner");
21253
21254 let expanded_sources = subscriber.stream_sources().expect("expanded sources");
21255 assert_eq!(
21256 expanded_sources.len(),
21257 1,
21258 "compatible owner filters should share one provider subscription"
21259 );
21260 assert!(
21261 !expanded_sources[0].same_key(&pool_a_source),
21262 "the provider-facing superset changes while owner routing remains exact"
21263 );
21264
21265 subscriber
21266 .remove_interest_owner(&HandlerId::new("pool-b"))
21267 .expect("pool-b should be removed");
21268 let trimmed_sources = subscriber.stream_sources().expect("trimmed sources");
21269 assert_eq!(trimmed_sources.len(), 1);
21270 assert!(trimmed_sources[0].same_key(&pool_a_source));
21271 }
21272
21273 #[test]
21274 #[cfg(feature = "reactive-ws")]
21275 fn provider_log_fan_in_respects_address_ceiling() {
21276 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21277 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21278 provider,
21279 SubscriberMode::PubSub,
21280 SubscriberConfig {
21281 max_log_addresses_per_subscription: 2,
21282 ..SubscriberConfig::default()
21283 },
21284 );
21285 for index in 0..5 {
21286 subscriber
21287 .add_interest_owner(
21288 HandlerId::new(format!("pool-{index}")),
21289 &[log_interest_for(index + 1)],
21290 )
21291 .expect("register pool owner");
21292 }
21293
21294 let sources = subscriber.stream_sources().expect("stream sources");
21295 assert_eq!(sources.len(), 3);
21296 let mut address_counts: Vec<_> = sources
21297 .iter()
21298 .map(|source| match source {
21299 SubscriberStreamSource::PubSubLog { filter, .. } => filter.address.iter().count(),
21300 _ => panic!("expected log source"),
21301 })
21302 .collect();
21303 address_counts.sort_unstable();
21304 assert_eq!(address_counts, vec![1, 2, 2]);
21305 }
21306
21307 #[tokio::test(flavor = "multi_thread")]
21308 #[cfg(feature = "reactive-ws")]
21309 async fn owner_backfill_seeds_reconnect_anchor_before_live_log() {
21310 let asserter = Asserter::new();
21311 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
21312 asserter.push_success(&vec![rpc_log(false)]);
21313 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
21314 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
21315 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21316 provider,
21317 SubscriberMode::PubSub,
21318 SubscriberConfig::default(),
21319 );
21320 subscriber
21321 .add_interest_owner_with_backfill(
21322 HandlerId::new("pool-a"),
21323 &[ReactiveInterest::Logs(LogInterest {
21324 provider_filter: Filter::new()
21325 .address(Address::repeat_byte(0x42))
21326 .event_signature(B256::repeat_byte(0x01)),
21327 local_matcher: None,
21328 route_key: None,
21329 })],
21330 SubscriberBackfill::range(1, 7),
21331 )
21332 .expect("register pool-a with backfill");
21333
21334 subscriber
21335 .drain_pending_backfills()
21336 .await
21337 .expect("owner backfill should drain");
21338
21339 assert_eq!(subscriber.pending_records.len(), 1);
21340 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
21341 }
21342
21343 #[tokio::test(flavor = "multi_thread")]
21344 async fn subscriber_streams_poll_ready_sources_round_robin() {
21345 let first_hash = B256::repeat_byte(0x01);
21346 let second_hash = B256::repeat_byte(0x02);
21347 let mut streams = SubscriberStreams::new();
21348 streams.push(
21349 SubscriberStreamSource::PubSubPendingHashes,
21350 stream::iter([
21351 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
21352 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
21353 ])
21354 .boxed(),
21355 );
21356 streams.push(
21357 SubscriberStreamSource::PubSubBlockHeaders,
21358 stream::once(async move { SubscriberEvent::<Ethereum>::PendingHash(second_hash) })
21359 .boxed(),
21360 );
21361
21362 assert!(matches!(
21363 streams.next().await,
21364 Some(SubscriberEvent::PendingHash(hash)) if hash == first_hash
21365 ));
21366 assert!(matches!(
21367 streams.next().await,
21368 Some(SubscriberEvent::PendingHash(hash)) if hash == second_hash
21369 ));
21370 }
21371
21372 #[tokio::test(flavor = "multi_thread")]
21373 #[cfg(feature = "reactive-ws")]
21374 async fn owner_updates_ensure_streams_without_full_reset() {
21375 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21376 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21377 provider,
21378 SubscriberMode::PubSub,
21379 SubscriberConfig::default(),
21380 );
21381 subscriber.chain_id = Some(1);
21382 subscriber
21383 .register_interests(&[ReactiveInterest::PendingTransactions(
21384 PendingTxInterest::default(),
21385 )])
21386 .await
21387 .expect("register base pending interest");
21388 subscriber
21389 .add_interest_owner(
21390 HandlerId::new("headers"),
21391 &[ReactiveInterest::Blocks(BlockInterest::default())],
21392 )
21393 .expect("register header owner");
21394
21395 let mut streams = SubscriberStreams::new();
21396 streams.push(
21397 SubscriberStreamSource::PubSubPendingHashes,
21398 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
21399 );
21400 streams.push(
21401 SubscriberStreamSource::PubSubBlockHeaders,
21402 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
21403 );
21404 subscriber.state = AlloySubscriberState::Active(streams);
21405
21406 subscriber
21407 .remove_interest_owner(&HandlerId::new("headers"))
21408 .expect("header owner should be removed");
21409 assert!(matches!(
21410 &subscriber.state,
21411 AlloySubscriberState::Active(streams) if streams.len() == 2
21412 ));
21413
21414 subscriber
21415 .ensure_streams()
21416 .await
21417 .expect("pure removal reconciliation should not touch provider");
21418
21419 assert!(matches!(
21420 &subscriber.state,
21421 AlloySubscriberState::Active(streams)
21422 if streams.len() == 1
21423 && streams.contains_source(&SubscriberStreamSource::PubSubPendingHashes)
21424 && !streams.contains_source(&SubscriberStreamSource::PubSubBlockHeaders)
21425 ));
21426
21427 subscriber
21428 .add_interest_owner(
21429 HandlerId::new("headers"),
21430 &[ReactiveInterest::Blocks(BlockInterest::default())],
21431 )
21432 .expect("re-add header owner");
21433 assert!(matches!(
21434 &subscriber.state,
21435 AlloySubscriberState::Active(streams) if streams.len() == 1
21436 ));
21437 }
21438
21439 #[tokio::test(flavor = "multi_thread")]
21440 #[cfg(feature = "reactive-polling")]
21441 async fn ensure_streams_retains_each_successful_connection_across_later_failure() {
21442 let asserter = Asserter::new();
21443 asserter.push_success(&U256::from(1));
21444 asserter.push_failure_msg("second filter connection failed");
21445 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21446 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21447 provider,
21448 SubscriberMode::Polling,
21449 SubscriberConfig {
21450 max_log_addresses_per_subscription: 1,
21451 ..SubscriberConfig::default()
21452 },
21453 );
21454 subscriber.chain_id = Some(1);
21455 subscriber
21456 .register_interests(&[log_interest_for(0x41), log_interest_for(0x42)])
21457 .await
21458 .expect("register two independently connected filters");
21459
21460 let error = subscriber
21461 .ensure_streams()
21462 .await
21463 .expect_err("second provider connection is forced to fail");
21464 assert!(matches!(error, SubscriberError::Provider(_)));
21465 assert!(subscriber.sources_dirty);
21466 let retained_streams = match &subscriber.state {
21467 AlloySubscriberState::Active(streams) => Some(streams.len()),
21468 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => None,
21469 };
21470 assert_eq!(
21471 retained_streams,
21472 Some(1),
21473 "first connection must survive later error {error:?}; revision {}",
21474 subscriber.stream_revision
21475 );
21476
21477 asserter.push_success(&U256::from(2));
21478 subscriber
21479 .ensure_streams()
21480 .await
21481 .expect("retry connects only the missing source");
21482 assert!(!subscriber.sources_dirty);
21483 assert!(matches!(
21484 &subscriber.state,
21485 AlloySubscriberState::Active(streams) if streams.len() == 2
21486 ));
21487 assert!(asserter.read_q().is_empty());
21488 }
21489
21490 #[tokio::test(flavor = "multi_thread")]
21491 #[cfg(feature = "reactive-ws")]
21492 async fn cancelled_post_install_backfill_is_retried_without_reconnecting() {
21493 let asserter = Asserter::new();
21494 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21495 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21496 provider,
21497 SubscriberMode::PubSub,
21498 SubscriberConfig::default(),
21499 );
21500 subscriber.chain_id = Some(1);
21501 subscriber
21502 .register_interests(&[log_interest_for(0x43)])
21503 .await
21504 .expect("register log source");
21505 let source = subscriber
21506 .stream_sources()
21507 .expect("one desired source")
21508 .pop()
21509 .expect("log source");
21510 let SubscriberStreamSource::PubSubLog { id, .. } = source else {
21511 panic!("expected pubsub log source")
21512 };
21513 subscriber.last_seen_log_blocks.insert(id, 6);
21514
21515 {
21516 let source = SubscriberStreamSource::PubSubLog {
21517 id,
21518 filter: subscriber
21519 .log_stream_filters()
21520 .pop()
21521 .expect("provider filter"),
21522 };
21523 let interrupted = async {
21524 subscriber.install_source_stream(
21525 source.clone(),
21526 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
21527 );
21528 subscriber.queue_source_backfill(source);
21529 subscriber.sources_dirty = true;
21530 futures::future::pending::<()>().await;
21531 };
21532 futures::pin_mut!(interrupted);
21533 poll_fn(|cx| {
21534 assert!(interrupted.as_mut().poll(cx).is_pending());
21535 std::task::Poll::Ready(())
21536 })
21537 .await;
21538 }
21539
21540 assert_eq!(subscriber.pending_source_backfills.len(), 1);
21541 assert!(matches!(
21542 &subscriber.state,
21543 AlloySubscriberState::Active(streams) if streams.len() == 1
21544 ));
21545
21546 asserter.push_success(&7u64);
21547 asserter.push_success(&Vec::<Log>::new());
21548 subscriber
21549 .ensure_streams()
21550 .await
21551 .expect("retry completes only the pending historical window");
21552
21553 assert!(subscriber.pending_source_backfills.is_empty());
21554 assert!(!subscriber.sources_dirty);
21555 assert!(matches!(
21556 &subscriber.state,
21557 AlloySubscriberState::Active(streams) if streams.len() == 1
21558 ));
21559 assert!(asserter.read_q().is_empty());
21560 }
21561
21562 #[cfg(any(feature = "reactive-ws", feature = "reactive-polling"))]
21564 fn log_interest_matching_rpc_log() -> ReactiveInterest<Ethereum> {
21565 ReactiveInterest::Logs(LogInterest {
21566 provider_filter: Filter::new()
21567 .address(Address::repeat_byte(0x42))
21568 .event_signature(B256::repeat_byte(0x01)),
21569 local_matcher: None,
21570 route_key: None,
21571 })
21572 }
21573
21574 #[cfg(any(feature = "reactive-ws", feature = "reactive-polling"))]
21575 fn log_interest_for(address: u8) -> ReactiveInterest<Ethereum> {
21576 ReactiveInterest::Logs(LogInterest {
21577 provider_filter: Filter::new().address(Address::repeat_byte(address)),
21578 local_matcher: None,
21579 route_key: None,
21580 })
21581 }
21582
21583 #[tokio::test(flavor = "multi_thread")]
21586 #[cfg(feature = "reactive-ws")]
21587 async fn drain_backfill_retains_queue_entry_on_provider_error() {
21588 let asserter = Asserter::new();
21589 asserter.push_failure_msg("rate limited");
21590 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
21591 asserter.push_success(&vec![rpc_log(false)]);
21592 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
21593 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
21594 let mut subscriber = AlloySubscriber::new(
21595 provider,
21596 SubscriberMode::PubSub,
21597 SubscriberConfig::default(),
21598 );
21599 subscriber
21600 .add_interest_owner_with_backfill(
21601 HandlerId::new("pool"),
21602 &[log_interest_matching_rpc_log()],
21603 SubscriberBackfill::range(1, 7),
21604 )
21605 .expect("register owner with backfill");
21606 assert_eq!(subscriber.pending_backfills.len(), 1);
21607
21608 let first = subscriber.drain_pending_backfills().await;
21609 assert!(first.is_err(), "provider failure should surface");
21610 assert_eq!(
21611 subscriber.pending_backfills.len(),
21612 1,
21613 "failed fetch must leave the backfill queued for retry"
21614 );
21615 assert!(subscriber.pending_records.is_empty());
21616
21617 subscriber
21618 .drain_pending_backfills()
21619 .await
21620 .expect("retry should succeed");
21621 assert!(subscriber.pending_backfills.is_empty());
21622 assert_eq!(subscriber.pending_records.len(), 1);
21623 }
21624
21625 #[tokio::test(flavor = "multi_thread")]
21628 #[cfg(feature = "reactive-ws")]
21629 async fn drain_backfill_seeds_anchor_on_empty_window() {
21630 let asserter = Asserter::new();
21631 asserter.push_success(&Some(rpc_block(42, B256::repeat_byte(42))));
21632 asserter.push_success(&Vec::<Log>::new());
21633 asserter.push_success(&Some(rpc_block(42, B256::repeat_byte(42))));
21634 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
21635 let mut subscriber = AlloySubscriber::new(
21636 provider,
21637 SubscriberMode::PubSub,
21638 SubscriberConfig::default(),
21639 );
21640 subscriber
21641 .add_interest_owner_with_backfill(
21642 HandlerId::new("pool"),
21643 &[log_interest_matching_rpc_log()],
21644 SubscriberBackfill::range(1, 42),
21645 )
21646 .expect("register owner with backfill");
21647
21648 subscriber
21649 .drain_pending_backfills()
21650 .await
21651 .expect("empty backfill should drain");
21652
21653 assert!(subscriber.pending_records.is_empty());
21654 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
21655 .pop()
21656 .unwrap();
21657 assert_eq!(
21658 subscriber.log_anchor(&filter),
21659 Some(42),
21660 "empty window must still seed the anchor at its upper bound"
21661 );
21662 }
21663
21664 #[tokio::test(flavor = "multi_thread")]
21667 #[cfg(feature = "reactive-ws")]
21668 async fn drain_backfill_open_ended_resolves_head_and_seeds_anchor() {
21669 let asserter = Asserter::new();
21670 asserter.push_success(&100u64); asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(100))));
21672 asserter.push_success(&Vec::<Log>::new()); asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(100))));
21674 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
21675 let mut subscriber = AlloySubscriber::new(
21676 provider,
21677 SubscriberMode::PubSub,
21678 SubscriberConfig::default(),
21679 );
21680 subscriber
21681 .add_interest_owner_with_backfill(
21682 HandlerId::new("pool"),
21683 &[log_interest_matching_rpc_log()],
21684 SubscriberBackfill::from_block(10),
21685 )
21686 .expect("register owner with open-ended backfill");
21687
21688 subscriber
21689 .drain_pending_backfills()
21690 .await
21691 .expect("open-ended backfill should drain");
21692
21693 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
21694 .pop()
21695 .unwrap();
21696 assert_eq!(subscriber.log_anchor(&filter), Some(100));
21697 }
21698
21699 #[test]
21702 #[cfg(feature = "reactive-ws")]
21703 fn duplicate_filters_across_owners_map_to_single_source() {
21704 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21705 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21706 provider,
21707 SubscriberMode::PubSub,
21708 SubscriberConfig::default(),
21709 );
21710 subscriber
21711 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
21712 .expect("register pool-a");
21713 subscriber
21714 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xaa)])
21715 .expect("register pool-b with identical filter");
21716
21717 assert_eq!(
21718 subscriber.log_stream_filters().len(),
21719 1,
21720 "identical filters across owners must collapse to one"
21721 );
21722 let sources = subscriber.stream_sources().expect("stream sources");
21723 assert_eq!(sources.len(), 1);
21724 }
21725
21726 #[test]
21729 #[cfg(feature = "reactive-ws")]
21730 fn owner_removal_prunes_source_ids_and_anchors() {
21731 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21732 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21733 provider,
21734 SubscriberMode::PubSub,
21735 SubscriberConfig::default(),
21736 );
21737 subscriber
21738 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
21739 .expect("register pool-a");
21740 subscriber
21741 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xbb)])
21742 .expect("register pool-b");
21743
21744 let _ = subscriber.stream_sources().expect("stream sources");
21746 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
21747 let filter_b = log_filters(&[log_interest_for(0xbb)]).pop().unwrap();
21748 let id_a = subscriber.log_source_id(&filter_a);
21749 let id_b = subscriber.log_source_id(&filter_b);
21750 subscriber.last_seen_log_blocks.insert(id_a, 10);
21751 subscriber.last_seen_log_blocks.insert(id_b, 20);
21752 assert_eq!(
21753 subscriber.log_source_ids.len(),
21754 3,
21755 "one provider fan-in id plus two explicitly seeded logical ids"
21756 );
21757
21758 subscriber
21759 .remove_interest_owner(&HandlerId::new("pool-b"))
21760 .expect("remove pool-b");
21761
21762 assert_eq!(
21763 subscriber.log_source_ids.len(),
21764 1,
21765 "pool-b's filter id should be retired"
21766 );
21767 assert!(subscriber.log_source_ids.contains_key(&filter_a));
21768 assert_eq!(subscriber.last_seen_log_blocks.get(&id_a), Some(&10));
21769 assert_eq!(
21770 subscriber.last_seen_log_blocks.get(&id_b),
21771 None,
21772 "pool-b's anchor should be pruned"
21773 );
21774 }
21775
21776 #[test]
21781 #[cfg(feature = "reactive-ws")]
21782 fn owner_filter_growth_queues_continuity_backfill_from_prior_anchor() {
21783 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21784 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21785 provider,
21786 SubscriberMode::PubSub,
21787 SubscriberConfig::default(),
21788 );
21789 subscriber
21790 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
21791 .expect("register amm with pool A");
21792
21793 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
21796 let id_a = subscriber.log_source_id(&filter_a);
21797 subscriber.last_seen_log_blocks.insert(id_a, 50);
21798
21799 subscriber
21802 .add_interest_owner(
21803 HandlerId::new("amm"),
21804 &[log_interest_for(0xaa), log_interest_for(0xbb)],
21805 )
21806 .expect("grow amm to pools A+B");
21807
21808 assert_eq!(
21809 subscriber.pending_backfills.len(),
21810 1,
21811 "the changed merged filter should queue exactly one continuity backfill"
21812 );
21813 let queued = &subscriber.pending_backfills[0];
21814 assert_eq!(queued.owner, Some(HandlerId::new("amm")));
21815 assert_eq!(queued.backfill.start_block(), 50);
21816 assert_eq!(
21817 queued.backfill.end_block(),
21818 None,
21819 "continuity backfill runs open-ended to the current head"
21820 );
21821 }
21822
21823 #[test]
21826 #[cfg(feature = "reactive-ws")]
21827 fn unchanged_owner_filter_does_not_queue_continuity_backfill() {
21828 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21829 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21830 provider,
21831 SubscriberMode::PubSub,
21832 SubscriberConfig::default(),
21833 );
21834 subscriber
21835 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
21836 .expect("register amm");
21837 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
21838 let id_a = subscriber.log_source_id(&filter_a);
21839 subscriber.last_seen_log_blocks.insert(id_a, 50);
21840
21841 subscriber
21842 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
21843 .expect("re-register identical interests");
21844
21845 assert!(
21846 subscriber.pending_backfills.is_empty(),
21847 "an unchanged filter shape must not queue continuity backfill"
21848 );
21849 }
21850
21851 #[test]
21855 #[cfg(feature = "reactive-ws")]
21856 fn explicit_open_ended_backfill_below_anchor_suppresses_continuity() {
21857 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21858 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21859 provider,
21860 SubscriberMode::PubSub,
21861 SubscriberConfig::default(),
21862 );
21863 subscriber
21864 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
21865 .expect("register amm");
21866 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
21867 let id_a = subscriber.log_source_id(&filter_a);
21868 subscriber.last_seen_log_blocks.insert(id_a, 50);
21869
21870 subscriber
21872 .add_interest_owner_with_backfill(
21873 HandlerId::new("amm"),
21874 &[log_interest_for(0xaa), log_interest_for(0xbb)],
21875 SubscriberBackfill::from_block(10),
21876 )
21877 .expect("grow amm with explicit deep backfill");
21878
21879 assert_eq!(
21880 subscriber.pending_backfills.len(),
21881 1,
21882 "only the explicit backfill should be queued; continuity is subsumed"
21883 );
21884 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 10);
21885 }
21886
21887 #[tokio::test(flavor = "multi_thread")]
21890 #[cfg(feature = "reactive-ws")]
21891 async fn ensure_streams_is_noop_when_not_dirty() {
21892 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21893 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21894 provider,
21895 SubscriberMode::PubSub,
21896 SubscriberConfig::default(),
21897 );
21898 subscriber
21900 .add_interest_owner(
21901 HandlerId::new("headers"),
21902 &[ReactiveInterest::Blocks(BlockInterest::default())],
21903 )
21904 .expect("register header owner");
21905 subscriber.state = AlloySubscriberState::Empty;
21907 subscriber.sources_dirty = false;
21908
21909 subscriber
21910 .ensure_streams()
21911 .await
21912 .expect("clean reconcile must be a no-op");
21913
21914 assert!(
21915 matches!(subscriber.state, AlloySubscriberState::Empty),
21916 "not-dirty ensure_streams must not connect new sources"
21917 );
21918 }
21919}
21920
21921fn resolve_subscriber_transport(
21922 mode: SubscriberMode,
21923) -> Result<SubscriberTransport, SubscriberError> {
21924 match mode {
21925 SubscriberMode::PubSub => {
21926 #[cfg(feature = "reactive-ws")]
21927 {
21928 Ok(SubscriberTransport::PubSub)
21929 }
21930 #[cfg(not(feature = "reactive-ws"))]
21931 {
21932 Err(SubscriberError::Unsupported(
21933 "AlloySubscriber pubsub mode requires the reactive-ws feature",
21934 ))
21935 }
21936 }
21937 SubscriberMode::Polling => {
21938 #[cfg(feature = "reactive-polling")]
21939 {
21940 Ok(SubscriberTransport::Polling)
21941 }
21942 #[cfg(not(feature = "reactive-polling"))]
21943 {
21944 Err(SubscriberError::Unsupported(
21945 "AlloySubscriber polling mode requires the reactive-polling feature",
21946 ))
21947 }
21948 }
21949 SubscriberMode::Auto => resolve_auto_subscriber_transport(),
21950 }
21951}
21952
21953fn resolve_auto_subscriber_transport() -> Result<SubscriberTransport, SubscriberError> {
21954 #[cfg(feature = "reactive-ws")]
21955 {
21956 Ok(SubscriberTransport::PubSub)
21957 }
21958
21959 #[cfg(all(not(feature = "reactive-ws"), feature = "reactive-polling"))]
21960 {
21961 Ok(SubscriberTransport::Polling)
21962 }
21963
21964 #[cfg(not(any(feature = "reactive-ws", feature = "reactive-polling")))]
21965 {
21966 Err(SubscriberError::Unsupported(
21967 "AlloySubscriber requires either reactive-ws or reactive-polling",
21968 ))
21969 }
21970}
21971
21972fn validate_subscriber_config(config: &SubscriberConfig) -> Result<(), SubscriberError> {
21973 if config.preconfirmations != PreconfirmationMode::Disabled
21974 && config.canonical_head_poll_interval.is_zero()
21975 {
21976 return Err(SubscriberError::InvalidConfig(
21977 "SubscriberConfig::canonical_head_poll_interval must be greater than zero",
21978 ));
21979 }
21980 if config.preconfirmations != PreconfirmationMode::Disabled
21981 && config.flashblock_poll_interval.is_zero()
21982 {
21983 return Err(SubscriberError::InvalidConfig(
21984 "SubscriberConfig::flashblock_poll_interval must be greater than zero",
21985 ));
21986 }
21987 if config.preconfirmations != PreconfirmationMode::Disabled
21988 && config.max_consecutive_flashblock_poll_failures == 0
21989 {
21990 return Err(SubscriberError::InvalidConfig(
21991 "SubscriberConfig::max_consecutive_flashblock_poll_failures must be greater than zero",
21992 ));
21993 }
21994 if config.preconfirmations != PreconfirmationMode::Disabled
21995 && config.max_pending_transaction_receipts_per_tick == 0
21996 {
21997 return Err(SubscriberError::InvalidConfig(
21998 "SubscriberConfig::max_pending_transaction_receipts_per_tick must be greater than zero",
21999 ));
22000 }
22001 if config.preconfirmations != PreconfirmationMode::Disabled
22002 && config.max_flashblock_rpc_requests_per_second == 0
22003 {
22004 return Err(SubscriberError::InvalidConfig(
22005 "SubscriberConfig::max_flashblock_rpc_requests_per_second must be greater than zero",
22006 ));
22007 }
22008 if config.max_batch_size == 0 {
22009 return Err(SubscriberError::InvalidConfig(
22010 "SubscriberConfig::max_batch_size must be greater than zero",
22011 ));
22012 }
22013 if config.max_log_addresses_per_subscription == 0 {
22014 return Err(SubscriberError::InvalidConfig(
22015 "SubscriberConfig::max_log_addresses_per_subscription must be greater than zero",
22016 ));
22017 }
22018 if config.max_pending_records == 0 {
22019 return Err(SubscriberError::InvalidConfig(
22020 "SubscriberConfig::max_pending_records must be greater than zero",
22021 ));
22022 }
22023 if config.max_pending_backfills == 0 {
22024 return Err(SubscriberError::InvalidConfig(
22025 "SubscriberConfig::max_pending_backfills must be greater than zero",
22026 ));
22027 }
22028 if config.max_backfill_log_bytes == 0 {
22029 return Err(SubscriberError::InvalidConfig(
22030 "SubscriberConfig::max_backfill_log_bytes must be greater than zero",
22031 ));
22032 }
22033 if config.max_reconcile_requests_in_flight == 0 {
22034 return Err(SubscriberError::InvalidConfig(
22035 "SubscriberConfig::max_reconcile_requests_in_flight must be greater than zero",
22036 ));
22037 }
22038 if config.reconnect.enabled {
22039 if config.reconnect.retry_delay > config.reconnect.max_delay {
22040 return Err(SubscriberError::InvalidConfig(
22041 "SubscriberReconnectConfig::retry_delay must be less than or equal to max_delay",
22042 ));
22043 }
22044 if matches!(config.reconnect.max_attempts, Some(0)) {
22045 return Err(SubscriberError::InvalidConfig(
22046 "SubscriberReconnectConfig::max_attempts must be greater than zero when set",
22047 ));
22048 }
22049 }
22050 Ok(())
22051}
22052
22053fn validate_supported_interests<N: Network>(
22054 mode: SubscriberMode,
22055 config: &SubscriberConfig,
22056 interests: &[ReactiveInterest<N>],
22057) -> Result<(), SubscriberError> {
22058 let transport = resolve_subscriber_transport(mode)?;
22059
22060 for interest in interests {
22061 match interest {
22062 ReactiveInterest::Logs(_) => {}
22063 ReactiveInterest::PendingTransactions(interest)
22064 if !config.hydrate_pending_transactions && interest.matches_hash_only() => {}
22065 ReactiveInterest::PendingTransactions(_) => {
22066 return Err(SubscriberError::Unsupported(
22067 "AlloySubscriber currently supports pending transaction hash interests only (full pending-tx hydration is unimplemented)",
22068 ));
22069 }
22070 ReactiveInterest::Blocks(interest) => match (transport, interest.mode) {
22071 (SubscriberTransport::PubSub, BlockInterestMode::Header) => {}
22072 (_, BlockInterestMode::FullBlock) => {
22073 return Err(SubscriberError::Unsupported(
22074 "AlloySubscriber full block streams are not implemented in this transport slice",
22075 ));
22076 }
22077 (SubscriberTransport::Polling, BlockInterestMode::Header) => {
22078 return Err(SubscriberError::Unsupported(
22079 "AlloySubscriber polling block streams are not implemented in this transport slice",
22080 ));
22081 }
22082 },
22083 }
22084 }
22085
22086 Ok(())
22087}
22088
22089fn log_filters<N: Network>(interests: &[ReactiveInterest<N>]) -> Vec<Filter> {
22090 let mut filters = Vec::new();
22091 for interest in interests {
22092 if let ReactiveInterest::Logs(interest) = interest {
22093 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
22094 }
22095 }
22096 filters
22097}
22098
22099fn needs_header_block_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
22100 interests.iter().any(|interest| {
22101 matches!(
22102 interest,
22103 ReactiveInterest::Blocks(BlockInterest {
22104 mode: BlockInterestMode::Header,
22105 })
22106 )
22107 })
22108}
22109
22110fn needs_pending_hash_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
22111 interests.iter().any(|interest| {
22112 matches!(
22113 interest,
22114 ReactiveInterest::PendingTransactions(interest) if interest.matches_hash_only()
22115 )
22116 })
22117}
22118
22119fn log_matches_any_interest<N: Network>(log: &Log, interests: &[ReactiveInterest<N>]) -> bool {
22120 interests.iter().any(|interest| {
22121 matches!(
22122 interest,
22123 ReactiveInterest::Logs(interest) if interest.matches(log)
22124 )
22125 })
22126}
22127
22128fn validate_owner_backfill_logs(
22129 logs: &[Log],
22130 from_block: u64,
22131 through: &BlockRef,
22132) -> Result<(), SubscriberOwnerError> {
22133 for log in logs {
22134 if log.removed {
22135 return Err(SubscriberOwnerError::InvalidBackfillLog(
22136 "removed log in canonical catch-up",
22137 ));
22138 }
22139 let number = log
22140 .block_number
22141 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
22142 "log missing block number",
22143 ))?;
22144 let hash = log
22145 .block_hash
22146 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
22147 "log missing block hash",
22148 ))?;
22149 log.transaction_hash
22150 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
22151 "log missing transaction hash",
22152 ))?;
22153 log.transaction_index
22154 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
22155 "log missing transaction index",
22156 ))?;
22157 log.log_index
22158 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
22159 "log missing log index",
22160 ))?;
22161 if number < from_block || number > through.number {
22162 return Err(SubscriberOwnerError::InvalidBackfillLog(
22163 "log outside requested block range",
22164 ));
22165 }
22166 if number == through.number && hash != through.hash {
22167 return Err(SubscriberOwnerError::InvalidBackfillLog(
22168 "target-block log hash mismatch",
22169 ));
22170 }
22171 }
22172 Ok(())
22173}
22174
22175fn validate_backfill_resource_limits(
22176 logs: &[Log],
22177 max_logs: usize,
22178 max_log_bytes: usize,
22179) -> Result<usize, SubscriberError> {
22180 if logs.len() > max_logs {
22181 return Err(SubscriberError::ResourceExhausted(format!(
22182 "historical response returned {} logs, above the configured limit of {max_logs}",
22183 logs.len()
22184 )));
22185 }
22186 let bytes = logs.iter().fold(0usize, |total, log| {
22187 let fixed = 20usize + (32 * 3) + (8 * 4) + 1;
22191 total
22192 .saturating_add(fixed)
22193 .saturating_add(log.topics().len().saturating_mul(32))
22194 .saturating_add(log.inner.data.data.len())
22195 });
22196 if bytes > max_log_bytes {
22197 return Err(SubscriberError::ResourceExhausted(format!(
22198 "historical response retained approximately {bytes} log bytes, above the configured limit of {max_log_bytes}"
22199 )));
22200 }
22201 Ok(bytes)
22202}
22203
22204async fn fetch_provider_block_ref<P, N>(
22205 provider: &P,
22206 number: u64,
22207) -> Result<BlockRef, SubscriberError>
22208where
22209 P: Provider<N> + Send + Sync,
22210 N: Network,
22211{
22212 let block = provider
22213 .get_block_by_number(BlockNumberOrTag::Number(number))
22214 .await
22215 .map_err(provider_error)?
22216 .ok_or_else(|| {
22217 SubscriberError::InvalidBackfill(format!(
22218 "canonical target block {number} is unavailable"
22219 ))
22220 })?;
22221 let header = block.header();
22222 Ok(BlockRef {
22223 number: header.number(),
22224 hash: header.hash(),
22225 parent_hash: Some(header.parent_hash()),
22226 timestamp: Some(header.timestamp()),
22227 })
22228}
22229
22230fn block_ref_satisfies_expected(actual: &BlockRef, expected: &BlockRef) -> bool {
22231 actual.number == expected.number
22232 && actual.hash == expected.hash
22233 && optional_metadata_compatible(actual.parent_hash.as_ref(), expected.parent_hash.as_ref())
22234 && optional_metadata_compatible(actual.timestamp.as_ref(), expected.timestamp.as_ref())
22235}
22236
22237fn validate_owner_backfill_log_set(logs: &[Log]) -> Result<(), SubscriberOwnerError> {
22238 let mut positions = HashMap::new();
22239 let mut block_hashes = HashMap::new();
22240 let mut transaction_hashes = HashMap::new();
22241 let mut transaction_positions = HashMap::new();
22242 let mut ordering = BTreeMap::<u64, Vec<(u64, u64)>>::new();
22243 for log in logs {
22244 let number = log
22245 .block_number
22246 .expect("individual owner catch-up logs are validated before set validation");
22247 let block_hash = log
22248 .block_hash
22249 .expect("individual owner catch-up logs are validated before set validation");
22250 let transaction_hash = log
22251 .transaction_hash
22252 .expect("individual owner catch-up logs are validated before set validation");
22253 let transaction_index = log
22254 .transaction_index
22255 .expect("individual owner catch-up logs are validated before set validation");
22256 let log_index = log
22257 .log_index
22258 .expect("individual owner catch-up logs are validated before set validation");
22259 if block_hashes
22260 .insert(number, block_hash)
22261 .is_some_and(|prior| prior != block_hash)
22262 {
22263 return Err(SubscriberOwnerError::InvalidBackfillLog(
22264 "conflicting block identity in canonical catch-up",
22265 ));
22266 }
22267 if let Some(previous) = positions.insert((number, log_index), log)
22268 && previous != log
22269 {
22270 return Err(SubscriberOwnerError::InvalidBackfillLog(
22271 "conflicting logs at one canonical block position",
22272 ));
22273 }
22274 let conflicting_transaction = transaction_hashes
22275 .insert((number, transaction_index), transaction_hash)
22276 .is_some_and(|prior| prior != transaction_hash)
22277 || transaction_positions
22278 .insert((number, transaction_hash), transaction_index)
22279 .is_some_and(|prior| prior != transaction_index);
22280 if conflicting_transaction {
22281 return Err(SubscriberOwnerError::InvalidBackfillLog(
22282 "conflicting transaction identity at one canonical block position",
22283 ));
22284 }
22285 ordering
22286 .entry(number)
22287 .or_default()
22288 .push((log_index, transaction_index));
22289 }
22290 for positions in ordering.values_mut() {
22291 positions.sort_unstable();
22292 if positions.windows(2).any(|pair| pair[0].1 > pair[1].1) {
22293 return Err(SubscriberOwnerError::InvalidBackfillLog(
22294 "transaction and log positions disagree on canonical order",
22295 ));
22296 }
22297 }
22298 Ok(())
22299}
22300
22301fn merged_owner_reconcile_filters<N: Network>(
22302 plans: &[SubscriberOwnerReconcilePlan<N>],
22303 through: u64,
22304) -> Vec<SubscriberOwnerReconcileFilter> {
22305 let mut by_start = BTreeMap::<u64, Vec<Filter>>::new();
22306 for plan in plans.iter().filter(|plan| plan.from_block <= through) {
22307 let filters = by_start.entry(plan.from_block).or_default();
22308 filters.extend(
22309 log_filters(&plan.interests)
22310 .into_iter()
22311 .map(|filter| filter.from_block(plan.from_block).to_block(through)),
22312 );
22313 }
22314
22315 let mut chunks = Vec::new();
22316 for (from_block, filters) in by_start {
22317 for filters in filters.chunks(OWNER_RECONCILE_FILTERS_PER_CHUNK) {
22318 let mut merged = Vec::new();
22319 for filter in filters {
22320 merge_log_subscription_filter(&mut merged, filter);
22321 }
22322 chunks.extend(
22323 merged
22324 .into_iter()
22325 .map(|filter| SubscriberOwnerReconcileFilter { filter, from_block }),
22326 );
22327 }
22328 }
22329 chunks
22330}
22331
22332fn merged_lazy_backfill_filters(
22333 filters: &[Filter],
22334 from_block: u64,
22335 through: u64,
22336) -> Vec<SubscriberOwnerReconcileFilter> {
22337 let mut requests = Vec::new();
22338 for filters in filters.chunks(OWNER_RECONCILE_FILTERS_PER_CHUNK) {
22339 let mut merged = Vec::new();
22340 for filter in filters {
22341 merge_log_subscription_filter(
22342 &mut merged,
22343 &filter.clone().from_block(from_block).to_block(through),
22344 );
22345 }
22346 requests.extend(
22347 merged
22348 .into_iter()
22349 .map(|filter| SubscriberOwnerReconcileFilter { filter, from_block }),
22350 );
22351 }
22352 requests
22353}
22354
22355fn lazy_backfill_error(error: SubscriberOwnerError) -> SubscriberError {
22356 match error {
22357 SubscriberOwnerError::Subscriber(error) => error,
22358 error => SubscriberError::InvalidBackfill(error.to_string()),
22359 }
22360}
22361
22362fn global_backfill_barrier(backfill: SubscriberBackfill, certified: BlockRef) -> ChainControl {
22363 let mut id = b"alloy-global-backfill-v1".to_vec();
22364 id.extend_from_slice(&backfill.start_block().to_be_bytes());
22365 id.extend_from_slice(&certified.number.to_be_bytes());
22366 id.extend_from_slice(certified.hash.as_slice());
22367 ChainControl::Barrier {
22368 id,
22369 block: Some(certified),
22370 }
22371}
22372
22373async fn fetch_owner_catchup<P, N>(
22374 provider: P,
22375 filters: Vec<SubscriberOwnerReconcileFilter>,
22376 retained: Vec<BlockRef>,
22377 through: BlockRef,
22378 options: SubscriberOwnerCatchupOptions,
22379) -> Result<SubscriberOwnerCatchup, SubscriberOwnerError>
22380where
22381 P: Provider<N> + Send + Sync,
22382 N: Network,
22383{
22384 if !options.target_preverified {
22385 let _ = verify_provider_reconcile_target::<P, N>(&provider, &through).await?;
22386 }
22387 let mut certified_positions = HashSet::new();
22388 for position in retained {
22389 let target_certifies_position = position == through
22390 || (position.number.checked_add(1) == Some(through.number)
22391 && through.parent_hash == Some(position.hash));
22392 if !target_certifies_position && certified_positions.insert(position) {
22393 let _ = verify_provider_reconcile_target::<P, N>(&provider, &position).await?;
22394 }
22395 }
22396 let mut logs = Vec::new();
22397 let mut total_log_bytes = 0usize;
22398 let requests = stream::iter(filters.into_iter().map(|filter| {
22399 let provider = &provider;
22400 async move {
22401 let logs = provider
22402 .get_logs(&filter.filter)
22403 .await
22404 .map_err(provider_error)?;
22405 Ok::<_, SubscriberOwnerError>((filter.from_block, logs))
22406 }
22407 }))
22408 .buffer_unordered(options.max_requests_in_flight);
22409 futures::pin_mut!(requests);
22410 while let Some(result) = requests.next().await {
22411 let (from_block, fetched) = result?;
22412 let fetched_bytes =
22413 validate_backfill_resource_limits(&fetched, options.max_logs, options.max_log_bytes)?;
22414 validate_owner_backfill_logs(&fetched, from_block, &through)?;
22415 if logs.len().saturating_add(fetched.len()) > options.max_logs {
22416 return Err(SubscriberError::ResourceExhausted(format!(
22417 "bulk reconcile returned more than {} logs",
22418 options.max_logs
22419 ))
22420 .into());
22421 }
22422 total_log_bytes = total_log_bytes.saturating_add(fetched_bytes);
22423 if total_log_bytes > options.max_log_bytes {
22424 return Err(SubscriberError::ResourceExhausted(format!(
22425 "bulk reconcile retained approximately {total_log_bytes} log bytes, above the configured limit of {}",
22426 options.max_log_bytes
22427 ))
22428 .into());
22429 }
22430 logs.extend(fetched);
22431 }
22432 validate_owner_backfill_log_set(&logs)?;
22433 let certified = verify_provider_reconcile_target::<P, N>(&provider, &through).await?;
22434 Ok(SubscriberOwnerCatchup { logs, certified })
22435}
22436
22437async fn verify_provider_reconcile_target<P, N>(
22438 provider: &P,
22439 expected: &BlockRef,
22440) -> Result<BlockRef, SubscriberOwnerError>
22441where
22442 P: Provider<N> + Send + Sync,
22443 N: Network,
22444{
22445 let block = provider
22446 .get_block_by_number(BlockNumberOrTag::Number(expected.number))
22447 .await
22448 .map_err(provider_error)?
22449 .ok_or(SubscriberOwnerError::BlockUnavailable(expected.number))?;
22450 let header = block.header();
22451 let actual = BlockRef {
22452 number: header.number(),
22453 hash: header.hash(),
22454 parent_hash: Some(header.parent_hash()),
22455 timestamp: Some(header.timestamp()),
22456 };
22457 let exact_parent = expected
22458 .parent_hash
22459 .is_none_or(|parent| Some(parent) == actual.parent_hash);
22460 let exact_timestamp = expected
22461 .timestamp
22462 .is_none_or(|timestamp| Some(timestamp) == actual.timestamp);
22463 if actual.number != expected.number
22464 || actual.hash != expected.hash
22465 || !exact_parent
22466 || !exact_timestamp
22467 {
22468 return Err(SubscriberOwnerError::BlockMismatch {
22469 expected_number: expected.number,
22470 expected_hash: expected.hash,
22471 actual_number: actual.number,
22472 actual_hash: actual.hash,
22473 });
22474 }
22475 Ok(actual)
22476}
22477
22478fn log_input_record<N: Network>(log: Log, source: InputSource) -> ReactiveInputRecord<N> {
22479 let context = log_reactive_context(&log);
22480 ReactiveInputRecord::new(
22481 ReactiveInput::Log(log),
22482 ReactiveContext { source, ..context },
22483 )
22484}
22485
22486fn preconfirmed_log_input_record<N: Network>(
22487 log: Log,
22488 flashblock: FlashblockRef,
22489) -> ReactiveInputRecord<N> {
22490 let block = flashblock.block_ref();
22491 let provider = flashblock.provider.clone();
22492 ReactiveInputRecord::new(
22493 ReactiveInput::Log(log.clone()),
22494 ReactiveContext {
22495 chain_id: None,
22496 source: InputSource::Flashblocks,
22497 chain_status: ChainStatus::Preconfirmed {
22498 flashblock: Arc::new(flashblock),
22499 },
22500 block: Some(block),
22501 transaction_index: log.transaction_index,
22502 log_index: log.log_index,
22503 },
22504 )
22505 .with_provider(provider)
22506}
22507
22508fn log_reactive_context(log: &Log) -> ReactiveContext {
22509 let block = match (log.block_hash, log.block_number) {
22510 (Some(hash), Some(number)) => Some(BlockRef {
22511 number,
22512 hash,
22513 parent_hash: None,
22514 timestamp: log.block_timestamp,
22515 }),
22516 _ => None,
22517 };
22518
22519 let chain_status = match (&block, log.removed) {
22520 (Some(block), true) => ChainStatus::Reorged {
22521 dropped_from: *block,
22522 },
22523 (Some(block), false) => ChainStatus::Included {
22524 block: *block,
22525 confirmations: 0,
22526 },
22527 (None, _) => ChainStatus::Pending,
22528 };
22529
22530 ReactiveContext {
22531 chain_id: None,
22532 source: InputSource::Poll,
22533 chain_status,
22534 block,
22535 transaction_index: log.transaction_index,
22536 log_index: log.log_index,
22537 }
22538}
22539
22540fn block_header_input_record<N>(header: N::HeaderResponse) -> ReactiveInputRecord<N>
22541where
22542 N: Network,
22543{
22544 let block = BlockRef {
22545 number: header.number(),
22546 hash: HeaderResponseTrait::hash(&header),
22547 parent_hash: Some(header.parent_hash()),
22548 timestamp: Some(header.timestamp()),
22549 };
22550 ReactiveInputRecord::new(
22551 ReactiveInput::BlockHeader(header),
22552 ReactiveContext {
22553 chain_id: None,
22554 source: InputSource::Subscription,
22555 chain_status: ChainStatus::Included {
22556 block,
22557 confirmations: 0,
22558 },
22559 block: Some(block),
22560 transaction_index: None,
22561 log_index: None,
22562 },
22563 )
22564}
22565
22566fn pending_hash_input_record<N: Network>(
22567 hash: B256,
22568 source: InputSource,
22569) -> ReactiveInputRecord<N> {
22570 ReactiveInputRecord::new(
22571 ReactiveInput::PendingTxHash(hash),
22572 ReactiveContext {
22573 chain_id: None,
22574 source,
22575 chain_status: ChainStatus::Pending,
22576 block: None,
22577 transaction_index: None,
22578 log_index: None,
22579 },
22580 )
22581}
22582
22583#[cfg(feature = "reactive-ws")]
22584fn base_pending_log_filter(filter: &Filter) -> Result<serde_json::Value, SubscriberError> {
22585 let encoded = serde_json::to_value(filter)
22586 .map_err(|error| SubscriberError::Provider(error.to_string()))?;
22587 let serde_json::Value::Object(mut fields) = encoded else {
22588 return Err(SubscriberError::Provider(
22589 "Alloy log filter did not serialize as an object".into(),
22590 ));
22591 };
22592 fields.retain(|key, _| key == "address" || key == "topics");
22593 Ok(serde_json::Value::Object(fields))
22594}
22595
22596fn provider_error(error: impl fmt::Display) -> SubscriberError {
22597 SubscriberError::Provider(error.to_string())
22598}
22599
22600#[derive(Debug, thiserror::Error)]
22602#[non_exhaustive]
22603pub enum SubscriberError {
22604 #[error("{0}")]
22606 InvalidConfig(&'static str),
22607 #[error("{0}")]
22609 Unsupported(&'static str),
22610 #[error("subscriber chain mismatch: expected {expected}, got {actual}")]
22612 ChainMismatch {
22613 expected: u64,
22615 actual: u64,
22617 },
22618 #[error("provider error: {0}")]
22620 Provider(String),
22621 #[error("invalid canonical backfill: {0}")]
22624 InvalidBackfill(String),
22625 #[error("subscriber resource limit exceeded: {0}")]
22627 ResourceExhausted(String),
22628}