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#[cfg(feature = "reactive-ws")]
53use tokio::sync::broadcast;
54
55use crate::{
56 cache::{
57 AccountProof, BlockStateDiff, DurableCheckpointBlock, DurableCheckpointError,
58 DurableCheckpointIdentity, DurableCheckpointMetadata, DurableCheckpointStore, EvmCache,
59 EvmCacheStateSnapshot, LoadedDurableCheckpoint,
60 },
61 errors::{BlockContextError, StorageFetchResult},
62 events::{EventDecoder, StateView},
63 freshness::FreshnessRegistry,
64 state_update::{AccountPatch, PurgeScope, StateDiff, StateUpdate},
65};
66
67#[cfg(feature = "raw-flashblocks-json")]
68mod raw_json_flashblocks;
69#[cfg(feature = "raw-flashblocks-json")]
70pub use raw_json_flashblocks::{
71 BufferedRawJsonFlashblocksAdapter, FlashblockInvalidation, FlashblockInvalidationReason,
72 FlashblockSnapshot, FlashblockUpdate, FlashblockUpdateAcknowledgement,
73 FlashblockUpdateChannelError, FlashblockUpdateSender, RawJsonFlashblocksAdapter,
74 RawJsonFlashblocksError, RawJsonFlashblocksLimits, TimedFlashblockUpdate,
75};
76
77#[derive(Clone, Debug, PartialEq, Eq)]
79pub enum ReactiveInput<N: Network = Ethereum> {
80 Log(Log),
82 BlockHeader(N::HeaderResponse),
84 FullBlock(N::BlockResponse),
86 PendingTxHash(B256),
88 PendingTx(N::TransactionResponse),
90}
91
92#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
94pub struct ReactiveContext {
95 pub chain_id: Option<u64>,
97 pub source: InputSource,
99 pub chain_status: ChainStatus,
101 pub block: Option<BlockRef>,
103 pub transaction_index: Option<u64>,
105 pub log_index: Option<u64>,
107}
108
109#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
111pub struct BlockRef {
112 pub number: u64,
114 pub hash: B256,
116 pub parent_hash: Option<B256>,
118 pub timestamp: Option<u64>,
120}
121
122#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
129pub struct ProviderRef {
130 pub endpoint: EndpointId,
132 pub generation: u64,
134}
135
136impl ProviderRef {
137 pub fn new(endpoint: impl Into<EndpointId>, generation: u64) -> Self {
139 Self {
140 endpoint: endpoint.into(),
141 generation,
142 }
143 }
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub struct FlashblockIngressTiming {
153 source_ingress: Instant,
154}
155
156impl FlashblockIngressTiming {
157 pub const fn new(source_ingress: Instant) -> Self {
159 Self { source_ingress }
160 }
161
162 pub const fn source_ingress(self) -> Instant {
164 self.source_ingress
165 }
166
167 pub fn earliest(self, other: Self) -> Self {
169 Self::new(self.source_ingress.min(other.source_ingress))
170 }
171}
172
173#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
175pub struct FlashblockRef {
176 pub provider: ProviderRef,
178 pub payload_id: Option<FixedBytes<8>>,
182 pub index: Option<u64>,
184 pub block_number: u64,
186 pub content_hash: B256,
192 pub partial_block_hash: Option<B256>,
194 pub parent_hash: Option<B256>,
196 pub state_root: Option<B256>,
198 pub transactions_root: Option<B256>,
200 pub transaction_hashes: Vec<B256>,
202 pub timestamp: Option<u64>,
204 pub base_fee_per_gas: Option<u64>,
206 pub beneficiary: Option<Address>,
208 pub prevrandao: Option<B256>,
210 pub gas_limit: Option<u64>,
212}
213
214impl FlashblockRef {
215 pub const fn block_ref(&self) -> BlockRef {
220 BlockRef {
221 number: self.block_number,
222 hash: self.content_hash,
223 parent_hash: self.parent_hash,
224 timestamp: self.timestamp,
225 }
226 }
227
228 pub fn contains_transaction(&self, transaction_hash: &B256) -> bool {
230 self.transaction_hashes.contains(transaction_hash)
231 }
232
233 fn transaction_index(&self, transaction_hash: &B256) -> Option<u64> {
234 self.transaction_hashes
235 .iter()
236 .position(|candidate| candidate == transaction_hash)
237 .and_then(|index| u64::try_from(index).ok())
238 }
239
240 fn same_payload(&self, other: &Self) -> bool {
241 self.provider == other.provider
242 && match (self.payload_id, other.payload_id) {
243 (Some(left), Some(right)) => left == right,
244 _ => {
245 self.block_number == other.block_number && self.parent_hash == other.parent_hash
246 }
247 }
248 }
249
250 #[cfg(feature = "raw-flashblocks-json")]
258 pub fn same_base_identity(&self, other: &Self) -> bool {
259 self.block_number == other.block_number
260 && self.parent_hash == other.parent_hash
261 && self.timestamp == other.timestamp
262 && self.base_fee_per_gas == other.base_fee_per_gas
263 && self.beneficiary == other.beneficiary
264 && self.prevrandao == other.prevrandao
265 && self.gas_limit == other.gas_limit
266 }
267
268 fn is_cumulative_successor_of(&self, previous: &Self) -> bool {
269 self.same_payload(previous)
270 && self.transaction_hashes.len() >= previous.transaction_hashes.len()
271 && self
272 .transaction_hashes
273 .starts_with(&previous.transaction_hashes)
274 && match (previous.index, self.index) {
275 (Some(previous), Some(current)) => current >= previous,
276 _ => true,
277 }
278 }
279}
280
281#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
283pub enum PreconfirmationMode {
284 #[default]
286 Disabled,
287 Preferred,
290 Required,
292}
293
294#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
296pub struct BaseFlashblockPayload {
297 pub payload_id: FixedBytes<8>,
299 pub index: u64,
301 pub base: Option<BaseFlashblockBase>,
303 pub diff: BaseFlashblockDiff,
305 #[serde(default)]
307 pub metadata: Option<BaseFlashblockMetadata>,
308}
309
310#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
312pub struct BaseFlashblockBase {
313 pub parent_hash: B256,
315 #[serde(deserialize_with = "deserialize_rpc_u64")]
317 pub block_number: u64,
318 #[serde(deserialize_with = "deserialize_rpc_u64")]
320 pub timestamp: u64,
321 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
323 pub gas_limit: Option<u64>,
324 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
326 pub base_fee_per_gas: Option<u64>,
327 #[serde(default, alias = "fee_recipient", alias = "feeRecipient")]
329 pub beneficiary: Option<Address>,
330 #[serde(
332 default,
333 alias = "prev_randao",
334 alias = "prevRandao",
335 alias = "mixHash"
336 )]
337 pub prevrandao: Option<B256>,
338}
339
340#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
342pub struct BaseFlashblockDiff {
343 pub state_root: B256,
345 pub block_hash: B256,
347 #[serde(default)]
349 pub transactions: Vec<serde_json::Value>,
350 #[serde(default)]
352 pub transactions_root: Option<B256>,
353}
354
355#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
358pub struct BaseFlashblockMetadata {
359 #[serde(deserialize_with = "deserialize_rpc_u64")]
361 pub block_number: u64,
362}
363
364#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
367#[serde(rename_all = "camelCase")]
368struct BaseFlashblockBlockPayload {
369 hash: B256,
370 #[serde(deserialize_with = "deserialize_rpc_u64")]
371 number: u64,
372 parent_hash: B256,
373 state_root: B256,
374 #[serde(default)]
375 transactions_root: Option<B256>,
376 #[serde(default)]
377 transactions: Vec<serde_json::Value>,
378 #[serde(deserialize_with = "deserialize_rpc_u64")]
379 timestamp: u64,
380 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
381 base_fee_per_gas: Option<u64>,
382 #[serde(default, alias = "beneficiary", alias = "feeRecipient")]
383 miner: Option<Address>,
384 #[serde(default, alias = "prevRandao")]
385 mix_hash: Option<B256>,
386 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
387 gas_limit: Option<u64>,
388}
389
390#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
394#[serde(untagged)]
395enum BaseFlashblockWirePayload {
396 Indexed(BaseFlashblockPayload),
397 Block(BaseFlashblockBlockPayload),
398}
399
400fn deserialize_rpc_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
401where
402 D: serde::Deserializer<'de>,
403{
404 #[derive(serde::Deserialize)]
405 #[serde(untagged)]
406 enum RpcU64 {
407 Number(u64),
408 String(String),
409 }
410
411 match <RpcU64 as serde::Deserialize>::deserialize(deserializer)? {
412 RpcU64::Number(number) => Ok(number),
413 RpcU64::String(value) => {
414 let value = value.strip_prefix("0x").unwrap_or(&value);
415 u64::from_str_radix(value, 16).map_err(serde::de::Error::custom)
416 }
417 }
418}
419
420fn deserialize_optional_rpc_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
421where
422 D: serde::Deserializer<'de>,
423{
424 #[derive(serde::Deserialize)]
425 #[serde(untagged)]
426 enum RpcU64 {
427 Number(u64),
428 String(String),
429 }
430
431 let Some(value) = <Option<RpcU64> as serde::Deserialize>::deserialize(deserializer)? else {
432 return Ok(None);
433 };
434 match value {
435 RpcU64::Number(number) => Ok(Some(number)),
436 RpcU64::String(value) => {
437 let value = value.strip_prefix("0x").unwrap_or(&value);
438 u64::from_str_radix(value, 16)
439 .map(Some)
440 .map_err(serde::de::Error::custom)
441 }
442 }
443}
444
445fn non_placeholder_hash(hash: B256) -> Option<B256> {
446 (!hash.is_zero()).then_some(hash)
447}
448
449fn flashblock_transaction_hashes(
450 transactions: &[serde_json::Value],
451) -> Result<Vec<B256>, SubscriberError> {
452 let hashes: Vec<B256> = transactions
453 .iter()
454 .map(|transaction| {
455 let value = match transaction {
456 serde_json::Value::String(value) => value.as_str(),
457 serde_json::Value::Object(object) => object
458 .get("hash")
459 .or_else(|| object.get("transactionHash"))
460 .and_then(serde_json::Value::as_str)
461 .ok_or_else(|| {
462 SubscriberError::Provider(
463 "Flashblock transaction object is missing its hash".into(),
464 )
465 })?,
466 _ => {
467 return Err(SubscriberError::Provider(
468 "Flashblock transaction must be a hash, raw transaction, or object".into(),
469 ));
470 }
471 };
472 if value.len() == 66 {
473 return value.parse::<B256>().map_err(|error| {
474 SubscriberError::Provider(format!(
475 "Flashblock transaction hash is invalid: {error}"
476 ))
477 });
478 }
479 let encoded = value.strip_prefix("0x").unwrap_or(value);
480 let raw = alloy_primitives::hex::decode(encoded).map_err(|error| {
481 SubscriberError::Provider(format!(
482 "Flashblock raw transaction is invalid hex: {error}"
483 ))
484 })?;
485 Ok(alloy_primitives::keccak256(raw))
486 })
487 .collect::<Result<_, _>>()?;
488 let mut unique = HashSet::with_capacity(hashes.len());
489 if hashes.iter().any(|hash| !unique.insert(*hash)) {
490 return Err(SubscriberError::Provider(
491 "Flashblock cumulative transaction membership contains a duplicate hash".into(),
492 ));
493 }
494 Ok(hashes)
495}
496
497struct FlashblockContentCommitment<'a> {
498 provider: &'a ProviderRef,
499 payload_id: Option<FixedBytes<8>>,
500 index: Option<u64>,
501 block_number: u64,
502 partial_block_hash: Option<B256>,
503 parent_hash: Option<B256>,
504 state_root: Option<B256>,
505 transactions_root: Option<B256>,
506 transaction_hashes: &'a [B256],
507 timestamp: Option<u64>,
508 base_fee_per_gas: Option<u64>,
509 beneficiary: Option<Address>,
510 prevrandao: Option<B256>,
511 gas_limit: Option<u64>,
512}
513
514fn flashblock_content_hash(content: FlashblockContentCommitment<'_>) -> B256 {
515 let mut commitment = Keccak256::new();
516 commitment.update(b"evm-fork-cache/flashblock-content/v1");
517 let endpoint = content.provider.endpoint.as_str().as_bytes();
518 commitment.update((endpoint.len() as u64).to_be_bytes());
519 commitment.update(endpoint);
520 commitment.update(content.provider.generation.to_be_bytes());
521 commitment.update(content.block_number.to_be_bytes());
522 commit_optional_bytes(
523 &mut commitment,
524 content.payload_id.as_ref().map(FixedBytes::as_slice),
525 );
526 commit_optional_u64(&mut commitment, content.index);
527 commit_optional_bytes(
528 &mut commitment,
529 content
530 .partial_block_hash
531 .as_ref()
532 .map(FixedBytes::as_slice),
533 );
534 commit_optional_bytes(
535 &mut commitment,
536 content.parent_hash.as_ref().map(FixedBytes::as_slice),
537 );
538 commit_optional_bytes(
539 &mut commitment,
540 content.state_root.as_ref().map(FixedBytes::as_slice),
541 );
542 commit_optional_bytes(
543 &mut commitment,
544 content.transactions_root.as_ref().map(FixedBytes::as_slice),
545 );
546 commitment.update((content.transaction_hashes.len() as u64).to_be_bytes());
547 for transaction_hash in content.transaction_hashes {
548 commitment.update(transaction_hash);
549 }
550 commit_optional_u64(&mut commitment, content.timestamp);
551 commit_optional_u64(&mut commitment, content.base_fee_per_gas);
552 commit_optional_bytes(
553 &mut commitment,
554 content
555 .beneficiary
556 .as_ref()
557 .map(|address| address.as_slice()),
558 );
559 commit_optional_bytes(
560 &mut commitment,
561 content.prevrandao.as_ref().map(FixedBytes::as_slice),
562 );
563 commit_optional_u64(&mut commitment, content.gas_limit);
564 let hash = commitment.finalize();
565 if hash.is_zero() {
566 B256::with_last_byte(1)
567 } else {
568 hash
569 }
570}
571
572#[cfg(feature = "raw-flashblocks-json")]
573fn validate_standard_flashblock_snapshot(
574 snapshot: &FlashblockSnapshot,
575) -> Result<(), SubscriberError> {
576 let flashblock = &snapshot.flashblock;
577 if flashblock.payload_id.is_none() || flashblock.index.is_none() {
578 return Err(SubscriberError::Provider(
579 "external Flashblock snapshot is missing its indexed payload identity".into(),
580 ));
581 }
582 let expected_content_hash = flashblock_content_hash(FlashblockContentCommitment {
583 provider: &flashblock.provider,
584 payload_id: flashblock.payload_id,
585 index: flashblock.index,
586 block_number: flashblock.block_number,
587 partial_block_hash: flashblock.partial_block_hash,
588 parent_hash: flashblock.parent_hash,
589 state_root: flashblock.state_root,
590 transactions_root: flashblock.transactions_root,
591 transaction_hashes: &flashblock.transaction_hashes,
592 timestamp: flashblock.timestamp,
593 base_fee_per_gas: flashblock.base_fee_per_gas,
594 beneficiary: flashblock.beneficiary,
595 prevrandao: flashblock.prevrandao,
596 gas_limit: flashblock.gas_limit,
597 });
598 if flashblock.content_hash != expected_content_hash {
599 return Err(SubscriberError::Provider(
600 "external Flashblock content commitment is invalid".into(),
601 ));
602 }
603
604 let mut transactions = HashSet::with_capacity(flashblock.transaction_hashes.len());
605 if flashblock
606 .transaction_hashes
607 .iter()
608 .any(|hash| !transactions.insert(*hash))
609 {
610 return Err(SubscriberError::Provider(
611 "external Flashblock cumulative transaction membership contains a duplicate hash"
612 .into(),
613 ));
614 }
615
616 let mut log_ids = HashSet::with_capacity(snapshot.logs.len());
617 for log in &snapshot.logs {
618 if log.removed || log.block_number != Some(flashblock.block_number) {
619 return Err(SubscriberError::Provider(
620 "external pre-confirmed log disagrees with its Flashblock block identity".into(),
621 ));
622 }
623 if log.block_hash != Some(flashblock.content_hash) {
624 return Err(SubscriberError::Provider(
625 "external pre-confirmed log is not bound to its Flashblock content commitment"
626 .into(),
627 ));
628 }
629 let transaction_hash = log.transaction_hash.ok_or_else(|| {
630 SubscriberError::Provider(
631 "external pre-confirmed log is missing its transaction hash".into(),
632 )
633 })?;
634 let expected_transaction_index = flashblock
635 .transaction_index(&transaction_hash)
636 .ok_or_else(|| {
637 SubscriberError::Provider(
638 "external pre-confirmed log transaction is absent from the cumulative Flashblock"
639 .into(),
640 )
641 })?;
642 if log.transaction_index != Some(expected_transaction_index) {
643 return Err(SubscriberError::Provider(
644 "external pre-confirmed log transaction index disagrees with cumulative membership"
645 .into(),
646 ));
647 }
648 let log_index = log.log_index.ok_or_else(|| {
649 SubscriberError::Provider("external pre-confirmed log is missing its log index".into())
650 })?;
651 if !log_ids.insert((transaction_hash, log_index)) {
652 return Err(SubscriberError::Provider(
653 "external Flashblock snapshot contains a duplicate log identity".into(),
654 ));
655 }
656 }
657 Ok(())
658}
659
660fn commit_optional_bytes(commitment: &mut Keccak256, value: Option<&[u8]>) {
661 match value {
662 Some(value) => {
663 commitment.update([1]);
664 commitment.update((value.len() as u64).to_be_bytes());
665 commitment.update(value);
666 }
667 None => commitment.update([0]),
668 }
669}
670
671fn commit_optional_u64(commitment: &mut Keccak256, value: Option<u64>) {
672 match value {
673 Some(value) => {
674 commitment.update([1]);
675 commitment.update(value.to_be_bytes());
676 }
677 None => commitment.update([0]),
678 }
679}
680
681#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
684pub struct ReactiveCanonicalBaseline {
685 pub chain_id: u64,
687 pub block: BlockRef,
689}
690
691impl ReactiveCanonicalBaseline {
692 pub const fn new(chain_id: u64, block: BlockRef) -> Self {
694 Self { chain_id, block }
695 }
696}
697
698#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
707#[non_exhaustive]
708pub enum ChainControl {
709 Reorg {
711 common_ancestor: BlockRef,
713 old_tip: BlockRef,
715 new_tip: BlockRef,
717 },
718 Safe(BlockRef),
720 Finalized(BlockRef),
722 CanonicalProgress(BlockRef),
729 LogCoverage(BlockRef),
753 Barrier {
755 id: Vec<u8>,
757 block: Option<BlockRef>,
759 },
760}
761
762#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
781pub struct CanonicalSequenceState {
782 retained_canonical_history: Vec<BlockRef>,
783 coverage_head: Option<BlockRef>,
784 safe_head: Option<BlockRef>,
785 finalized_head: Option<BlockRef>,
786 log_coverage_head: Option<BlockRef>,
787}
788
789impl CanonicalSequenceState {
790 pub fn new(
796 retained_canonical_history: Vec<BlockRef>,
797 coverage_head: Option<BlockRef>,
798 safe_head: Option<BlockRef>,
799 finalized_head: Option<BlockRef>,
800 ) -> Self {
801 Self {
802 retained_canonical_history,
803 coverage_head,
804 safe_head,
805 finalized_head,
806 log_coverage_head: None,
807 }
808 }
809
810 #[must_use]
816 pub fn with_log_coverage_head(mut self, log_coverage_head: Option<BlockRef>) -> Self {
817 self.log_coverage_head = log_coverage_head;
818 self
819 }
820
821 pub fn retained_canonical_history(&self) -> &[BlockRef] {
823 &self.retained_canonical_history
824 }
825
826 pub const fn coverage_head(&self) -> Option<&BlockRef> {
828 self.coverage_head.as_ref()
829 }
830
831 pub const fn safe_head(&self) -> Option<&BlockRef> {
833 self.safe_head.as_ref()
834 }
835
836 pub const fn finalized_head(&self) -> Option<&BlockRef> {
838 self.finalized_head.as_ref()
839 }
840
841 pub const fn log_coverage_head(&self) -> Option<&BlockRef> {
847 self.log_coverage_head.as_ref()
848 }
849
850 pub fn retain_recent_history(&mut self, max_entries: usize) {
859 let remove = self
860 .retained_canonical_history
861 .len()
862 .saturating_sub(max_entries);
863 self.retained_canonical_history.drain(..remove);
864 }
865
866 pub fn validate(&self) -> Result<(), ReactiveError> {
880 validate_canonical_sequence_snapshot(self)
881 }
882}
883
884#[derive(Clone, Debug, PartialEq, Eq)]
886#[non_exhaustive]
887pub enum CanonicalSequenceMutation {
888 Rewind {
890 common_ancestor: Option<BlockRef>,
896 dropped: Vec<BlockRef>,
898 },
899 Canonical(BlockRef),
901 Safe(BlockRef),
903 Finalized(BlockRef),
905 LogCoverage(BlockRef),
907}
908
909#[derive(Clone, Debug, PartialEq, Eq)]
911pub struct CanonicalSequenceValidation {
912 pre_record_state: CanonicalSequenceState,
913 next_state: CanonicalSequenceState,
914 mutations: Vec<CanonicalSequenceMutation>,
915 normalized_chain_controls: Vec<ChainControl>,
916}
917
918impl CanonicalSequenceValidation {
919 pub const fn pre_record_state(&self) -> &CanonicalSequenceState {
921 &self.pre_record_state
922 }
923
924 pub const fn next_state(&self) -> &CanonicalSequenceState {
926 &self.next_state
927 }
928
929 pub fn mutations(&self) -> &[CanonicalSequenceMutation] {
931 &self.mutations
932 }
933
934 pub fn normalized_chain_controls(&self) -> &[ChainControl] {
944 &self.normalized_chain_controls
945 }
946}
947
948#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
950#[non_exhaustive]
951pub enum ChainStatus {
952 Pending,
954 Preconfirmed {
959 flashblock: Arc<FlashblockRef>,
963 },
964 Included {
966 block: BlockRef,
968 confirmations: u64,
970 },
971 Safe {
973 block: BlockRef,
975 },
976 Finalized {
978 block: BlockRef,
980 },
981 Reorged {
983 dropped_from: BlockRef,
985 },
986}
987
988#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
990#[non_exhaustive]
991pub enum InputSource {
992 Batch,
994 Subscription,
996 Poll,
998 Backfill,
1000 Flashblocks,
1002 Synthetic,
1004}
1005
1006#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1008pub enum InputRef {
1009 Log {
1011 chain_id: Option<u64>,
1013 block_hash: B256,
1015 transaction_hash: B256,
1017 log_index: u64,
1019 },
1020 PendingTx {
1022 chain_id: Option<u64>,
1024 hash: B256,
1026 },
1027 Block {
1029 chain_id: Option<u64>,
1031 hash: B256,
1033 number: u64,
1035 },
1036}
1037
1038#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1045#[non_exhaustive]
1046pub enum ReactiveInputKind {
1047 CanonicalLog,
1049 ReorgSignalLog,
1051 BlockHeader,
1053 FullBlock,
1055 PendingTxHash,
1057 PendingTx,
1059}
1060
1061#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1068pub struct ReactiveInputIdentity {
1069 input_ref: InputRef,
1070 kind: ReactiveInputKind,
1071}
1072
1073impl ReactiveInputIdentity {
1074 pub fn try_from_parts(
1088 input_ref: InputRef,
1089 kind: ReactiveInputKind,
1090 ) -> Result<Self, ReactiveInputIdentityError> {
1091 let compatible = matches!(
1092 (input_ref, kind),
1093 (
1094 InputRef::Log { .. },
1095 ReactiveInputKind::CanonicalLog | ReactiveInputKind::ReorgSignalLog
1096 ) | (
1097 InputRef::Block { .. },
1098 ReactiveInputKind::BlockHeader | ReactiveInputKind::FullBlock
1099 ) | (
1100 InputRef::PendingTx { .. },
1101 ReactiveInputKind::PendingTxHash | ReactiveInputKind::PendingTx
1102 )
1103 );
1104 if !compatible {
1105 return Err(ReactiveInputIdentityError { input_ref, kind });
1106 }
1107 Ok(Self { input_ref, kind })
1108 }
1109
1110 pub const fn input_ref(&self) -> InputRef {
1112 self.input_ref
1113 }
1114
1115 pub const fn kind(&self) -> ReactiveInputKind {
1117 self.kind
1118 }
1119}
1120
1121#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
1124#[error("reactive input kind {kind:?} is incompatible with input reference {input_ref:?}")]
1125pub struct ReactiveInputIdentityError {
1126 input_ref: InputRef,
1127 kind: ReactiveInputKind,
1128}
1129
1130impl ReactiveInputIdentityError {
1131 pub const fn input_ref(&self) -> InputRef {
1133 self.input_ref
1134 }
1135
1136 pub const fn kind(&self) -> ReactiveInputKind {
1138 self.kind
1139 }
1140}
1141
1142#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1144pub enum StateEffectQuality {
1145 ExactFromInput,
1147 AppliedWithPendingResync,
1149 ResyncedAuthoritatively,
1151 RequiresRepair,
1153 NoStateEffect,
1155}
1156
1157#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize)]
1159pub struct HandlerId(String);
1160
1161impl HandlerId {
1162 pub fn new(id: impl Into<String>) -> Self {
1169 Self::try_new(id).expect("handler id must not be empty")
1170 }
1171
1172 pub fn try_new(id: impl Into<String>) -> Result<Self, HandlerIdError> {
1179 let id = id.into();
1180 if id.is_empty() {
1181 return Err(HandlerIdError);
1182 }
1183 Ok(Self(id))
1184 }
1185
1186 pub fn as_str(&self) -> &str {
1188 &self.0
1189 }
1190}
1191
1192impl<'de> serde::Deserialize<'de> for HandlerId {
1193 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1194 where
1195 D: serde::Deserializer<'de>,
1196 {
1197 let id = <String as serde::Deserialize>::deserialize(deserializer)?;
1198 Self::try_new(id).map_err(serde::de::Error::custom)
1199 }
1200}
1201
1202#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
1205#[error("handler id must not be empty")]
1206pub struct HandlerIdError;
1207
1208impl fmt::Display for HandlerId {
1209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1210 self.0.fmt(f)
1211 }
1212}
1213
1214#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1216pub struct ReportTag {
1217 pub key: String,
1219 pub value: String,
1221}
1222
1223impl ReportTag {
1224 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1226 Self {
1227 key: key.into(),
1228 value: value.into(),
1229 }
1230 }
1231}
1232
1233#[derive(Clone)]
1235pub struct HookSignal {
1236 pub namespace: Cow<'static, str>,
1238 pub kind: Cow<'static, str>,
1240 pub labels: Vec<ReportTag>,
1242 pub payload: Option<Arc<dyn Any + Send + Sync>>,
1244}
1245
1246impl fmt::Debug for HookSignal {
1247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1248 f.debug_struct("HookSignal")
1249 .field("namespace", &self.namespace)
1250 .field("kind", &self.kind)
1251 .field("labels", &self.labels)
1252 .field("payload", &self.payload.as_ref().map(|_| "<payload>"))
1253 .finish()
1254 }
1255}
1256
1257#[derive(Clone, Debug)]
1259pub enum ReactiveEffect {
1260 StateUpdate(StateUpdate),
1262 Resync(ResyncRequest),
1264 Invalidate(InvalidationRequest),
1266 Hook(HookSignal),
1268 Speculative(SpeculativeRequest),
1270}
1271
1272#[derive(Clone, Debug)]
1274pub struct HandlerOutcome {
1275 pub effects: Vec<ReactiveEffect>,
1277 pub quality: StateEffectQuality,
1279 pub tags: Vec<ReportTag>,
1281}
1282
1283impl HandlerOutcome {
1284 pub fn empty(quality: StateEffectQuality) -> Self {
1286 Self {
1287 effects: Vec::new(),
1288 quality,
1289 tags: Vec::new(),
1290 }
1291 }
1292}
1293
1294#[derive(Clone, Debug)]
1296pub struct ReactiveInputRecord<N: Network = Ethereum> {
1297 pub input: ReactiveInput<N>,
1299 pub context: ReactiveContext,
1301 pub provider: Option<ProviderRef>,
1304}
1305
1306impl<N: Network> ReactiveInputRecord<N> {
1307 pub fn new(input: ReactiveInput<N>, context: ReactiveContext) -> Self {
1309 Self {
1310 input,
1311 context,
1312 provider: None,
1313 }
1314 }
1315
1316 #[must_use]
1318 pub fn with_provider(mut self, provider: ProviderRef) -> Self {
1319 self.provider = Some(provider);
1320 self
1321 }
1322
1323 pub fn input_ref(&self) -> InputRef {
1325 input_ref(&self.input, &self.context)
1326 }
1327
1328 pub fn validated_identity(&self) -> Result<ReactiveInputIdentity, ReactiveError> {
1345 validate_input_record(self)?;
1346 let kind = match &self.input {
1347 ReactiveInput::Log(log)
1348 if log.removed
1349 || matches!(self.context.chain_status, ChainStatus::Reorged { .. }) =>
1350 {
1351 ReactiveInputKind::ReorgSignalLog
1352 }
1353 ReactiveInput::Log(_) => ReactiveInputKind::CanonicalLog,
1354 ReactiveInput::BlockHeader(_) => ReactiveInputKind::BlockHeader,
1355 ReactiveInput::FullBlock(_) => ReactiveInputKind::FullBlock,
1356 ReactiveInput::PendingTxHash(_) => ReactiveInputKind::PendingTxHash,
1357 ReactiveInput::PendingTx(_) => ReactiveInputKind::PendingTx,
1358 };
1359 ReactiveInputIdentity::try_from_parts(self.input_ref(), kind).map_err(|error| {
1360 ReactiveError::InvalidInputRecord {
1361 message: error.to_string(),
1362 }
1363 })
1364 }
1365
1366 pub fn same_deduplicable_payload(&self, other: &Self) -> bool {
1380 match (&self.input, &other.input) {
1381 (ReactiveInput::Log(left), ReactiveInput::Log(right)) => {
1382 left.inner == right.inner
1383 && left.block_hash == right.block_hash
1384 && left.block_number == right.block_number
1385 && optional_metadata_compatible(
1386 left.block_timestamp.as_ref(),
1387 right.block_timestamp.as_ref(),
1388 )
1389 && left.transaction_hash == right.transaction_hash
1390 && left.transaction_index == right.transaction_index
1391 && left.log_index == right.log_index
1392 && left.removed == right.removed
1393 }
1394 (ReactiveInput::BlockHeader(left), ReactiveInput::BlockHeader(right)) => {
1395 left.hash() == right.hash()
1396 }
1397 (ReactiveInput::FullBlock(_), ReactiveInput::FullBlock(_)) => false,
1398 (ReactiveInput::PendingTxHash(left), ReactiveInput::PendingTxHash(right)) => {
1399 left == right
1400 }
1401 (ReactiveInput::PendingTx(_), ReactiveInput::PendingTx(_)) => false,
1402 _ => false,
1403 }
1404 }
1405
1406 pub fn is_payload_deduplicable(&self) -> bool {
1412 matches!(
1413 &self.input,
1414 ReactiveInput::Log(_) | ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_)
1415 )
1416 }
1417
1418 pub fn merge_compatible_duplicate(&mut self, other: &Self) -> Result<bool, ReactiveError> {
1433 let identity = self.validated_identity()?;
1434 let other_identity = other.validated_identity()?;
1435 if identity != other_identity
1436 || !self.is_payload_deduplicable()
1437 || !other.is_payload_deduplicable()
1438 {
1439 return Ok(false);
1440 }
1441 if !self.same_deduplicable_payload(other) || !self.dedupe_context_is_compatible(other) {
1442 return Err(ReactiveError::InvalidInputRecord {
1443 message: format!(
1444 "conflicting payload or semantic context for identity {identity:?}"
1445 ),
1446 });
1447 }
1448 let mut merged = self.clone();
1449 merge_deduplicable_record(&mut merged, other);
1450 merged.validated_identity()?;
1451 *self = merged;
1452 Ok(true)
1453 }
1454
1455 pub fn dedupe_context_is_compatible(&self, other: &Self) -> bool {
1463 let left = &self.context;
1464 let right = &other.context;
1465 left.chain_id == right.chain_id
1466 && optional_block_refs_are_compatible(left.block.as_ref(), right.block.as_ref())
1467 && left.transaction_index == right.transaction_index
1468 && left.log_index == right.log_index
1469 && chain_statuses_are_dedupe_compatible(&left.chain_status, &right.chain_status)
1470 }
1471}
1472
1473fn chain_statuses_are_dedupe_compatible(left: &ChainStatus, right: &ChainStatus) -> bool {
1474 match (left, right) {
1475 (ChainStatus::Pending, ChainStatus::Pending)
1476 | (ChainStatus::Reorged { .. }, ChainStatus::Reorged { .. }) => true,
1477 (
1478 ChainStatus::Preconfirmed { flashblock: left },
1479 ChainStatus::Preconfirmed { flashblock: right },
1480 ) => left == right,
1481 (
1482 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. },
1483 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. },
1484 ) => true,
1485 _ => false,
1486 }
1487}
1488
1489fn optional_metadata_compatible<T: PartialEq>(left: Option<&T>, right: Option<&T>) -> bool {
1490 left.zip(right).is_none_or(|(left, right)| left == right)
1491}
1492
1493fn optional_block_refs_are_compatible(left: Option<&BlockRef>, right: Option<&BlockRef>) -> bool {
1494 match (left, right) {
1495 (None, None) => true,
1496 (Some(left), Some(right)) => {
1497 left.number == right.number
1498 && left.hash == right.hash
1499 && optional_metadata_compatible(
1500 left.parent_hash.as_ref(),
1501 right.parent_hash.as_ref(),
1502 )
1503 && optional_metadata_compatible(left.timestamp.as_ref(), right.timestamp.as_ref())
1504 }
1505 _ => false,
1506 }
1507}
1508
1509fn merge_deduplicable_record<N: Network>(
1510 retained: &mut ReactiveInputRecord<N>,
1511 incoming: &ReactiveInputRecord<N>,
1512) {
1513 if let (ReactiveInput::Log(retained), ReactiveInput::Log(incoming)) =
1514 (&mut retained.input, &incoming.input)
1515 && retained.block_timestamp.is_none()
1516 {
1517 retained.block_timestamp = incoming.block_timestamp;
1518 }
1519 if let (Some(retained), Some(incoming)) =
1520 (&mut retained.context.block, incoming.context.block.as_ref())
1521 {
1522 enrich_block_ref(retained, incoming);
1523 }
1524 retained.context.chain_status = merged_chain_status(
1525 &retained.context.chain_status,
1526 &incoming.context.chain_status,
1527 );
1528 if input_source_rank(incoming.context.source) > input_source_rank(retained.context.source) {
1529 retained.context.source = incoming.context.source;
1530 }
1531 if retained.provider.is_none() {
1532 retained.provider = incoming.provider.clone();
1533 }
1534}
1535
1536fn enrich_block_ref(retained: &mut BlockRef, incoming: &BlockRef) {
1537 if retained.parent_hash.is_none() {
1538 retained.parent_hash = incoming.parent_hash;
1539 }
1540 if retained.timestamp.is_none() {
1541 retained.timestamp = incoming.timestamp;
1542 }
1543}
1544
1545fn merged_chain_status(retained: &ChainStatus, incoming: &ChainStatus) -> ChainStatus {
1546 let merged_block = |left: &BlockRef, right: &BlockRef| {
1547 let mut block = *left;
1548 enrich_block_ref(&mut block, right);
1549 block
1550 };
1551 match (retained, incoming) {
1552 (ChainStatus::Pending, ChainStatus::Pending) => ChainStatus::Pending,
1553 (
1554 ChainStatus::Preconfirmed { flashblock: left },
1555 ChainStatus::Preconfirmed { flashblock: right },
1556 ) => {
1557 debug_assert_eq!(left, right, "compatible pre-confirmed records agree");
1558 ChainStatus::Preconfirmed {
1559 flashblock: left.clone(),
1560 }
1561 }
1562 (
1563 ChainStatus::Reorged { dropped_from: left },
1564 ChainStatus::Reorged {
1565 dropped_from: right,
1566 },
1567 ) => ChainStatus::Reorged {
1568 dropped_from: merged_block(left, right),
1569 },
1570 (left, right) => {
1571 let (left_block, left_rank, left_confirmations) = canonical_status_parts(left)
1572 .expect("compatible duplicate has a canonical lifecycle");
1573 let (right_block, right_rank, right_confirmations) = canonical_status_parts(right)
1574 .expect("compatible duplicate has a canonical lifecycle");
1575 let block = merged_block(left_block, right_block);
1576 let rank = left_rank.max(right_rank);
1577 match rank {
1578 3 => ChainStatus::Finalized { block },
1579 2 => ChainStatus::Safe { block },
1580 _ => ChainStatus::Included {
1581 block,
1582 confirmations: left_confirmations.max(right_confirmations),
1583 },
1584 }
1585 }
1586 }
1587}
1588
1589fn canonical_status_parts(status: &ChainStatus) -> Option<(&BlockRef, u8, u64)> {
1590 match status {
1591 ChainStatus::Included {
1592 block,
1593 confirmations,
1594 } => Some((block, 1, *confirmations)),
1595 ChainStatus::Safe { block } => Some((block, 2, 0)),
1596 ChainStatus::Finalized { block } => Some((block, 3, 0)),
1597 ChainStatus::Pending | ChainStatus::Preconfirmed { .. } | ChainStatus::Reorged { .. } => {
1598 None
1599 }
1600 }
1601}
1602
1603fn input_source_rank(source: InputSource) -> u8 {
1604 match source {
1605 InputSource::Backfill => 0,
1606 InputSource::Poll => 1,
1607 InputSource::Subscription => 2,
1608 InputSource::Flashblocks => 3,
1609 InputSource::Batch => 4,
1610 InputSource::Synthetic => 5,
1611 }
1612}
1613
1614#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1624pub struct SubscriberDeliveryToken(Vec<u8>);
1625
1626impl SubscriberDeliveryToken {
1627 pub fn new(bytes: Vec<u8>) -> Self {
1629 Self(bytes)
1630 }
1631
1632 pub fn as_bytes(&self) -> &[u8] {
1634 &self.0
1635 }
1636
1637 pub fn into_bytes(self) -> Vec<u8> {
1639 self.0
1640 }
1641}
1642
1643#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1649pub struct SubscriberCheckpoint(Vec<u8>);
1650
1651impl SubscriberCheckpoint {
1652 pub fn new(bytes: Vec<u8>) -> Self {
1654 Self(bytes)
1655 }
1656
1657 pub fn as_bytes(&self) -> &[u8] {
1659 &self.0
1660 }
1661
1662 pub fn into_bytes(self) -> Vec<u8> {
1664 self.0
1665 }
1666}
1667
1668#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1678pub struct SubscriberPayloadCommitment(B256);
1679
1680impl SubscriberPayloadCommitment {
1681 pub const fn new(commitment: B256) -> Self {
1683 Self(commitment)
1684 }
1685
1686 pub const fn digest(&self) -> B256 {
1688 self.0
1689 }
1690}
1691
1692#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1698#[non_exhaustive]
1699pub struct SubscriberResumePosition {
1700 pub chain_id: u64,
1702 pub coverage_head: BlockRef,
1704 pub canonical_history: Vec<BlockRef>,
1706 pub delivery_token: Option<SubscriberDeliveryToken>,
1710 pub subscriber_checkpoint: Option<SubscriberCheckpoint>,
1712}
1713
1714impl SubscriberResumePosition {
1715 pub fn new(
1717 chain_id: u64,
1718 coverage_head: BlockRef,
1719 canonical_history: Vec<BlockRef>,
1720 delivery_token: Option<SubscriberDeliveryToken>,
1721 subscriber_checkpoint: Option<SubscriberCheckpoint>,
1722 ) -> Self {
1723 Self {
1724 chain_id,
1725 coverage_head,
1726 canonical_history,
1727 delivery_token,
1728 subscriber_checkpoint,
1729 }
1730 }
1731}
1732
1733#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1741#[non_exhaustive]
1742pub enum DeliveryAudience {
1743 #[default]
1745 All,
1746 Owners(Vec<HandlerId>),
1748 AllExcept(Vec<HandlerId>),
1753}
1754
1755#[derive(
1763 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
1764)]
1765#[non_exhaustive]
1766pub enum DeliveryScope {
1767 #[default]
1769 Canonical,
1770 CanonicalProgress,
1772 OwnerCatchup,
1774 Preconfirmed,
1777}
1778
1779impl DeliveryScope {
1780 const fn advances_canonical_state(self) -> bool {
1781 matches!(self, Self::Canonical | Self::CanonicalProgress)
1782 }
1783}
1784
1785#[derive(Clone, Debug)]
1787pub struct ReactiveInputDelivery<N: Network = Ethereum> {
1788 record: ReactiveInputRecord<N>,
1789 audience: DeliveryAudience,
1790 scope: DeliveryScope,
1791}
1792
1793impl<N: Network> ReactiveInputDelivery<N> {
1794 pub fn new(
1796 record: ReactiveInputRecord<N>,
1797 audience: DeliveryAudience,
1798 scope: DeliveryScope,
1799 ) -> Self {
1800 Self {
1801 record,
1802 audience,
1803 scope,
1804 }
1805 }
1806
1807 pub const fn record(&self) -> &ReactiveInputRecord<N> {
1809 &self.record
1810 }
1811
1812 pub const fn audience(&self) -> &DeliveryAudience {
1814 &self.audience
1815 }
1816
1817 pub const fn scope(&self) -> DeliveryScope {
1819 self.scope
1820 }
1821
1822 pub fn into_parts(self) -> (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope) {
1824 (self.record, self.audience, self.scope)
1825 }
1826}
1827
1828#[derive(Clone, Debug)]
1833#[non_exhaustive]
1834pub struct ReactiveInputBatchParts<N: Network = Ethereum> {
1835 pub chain_id: Option<u64>,
1837 pub deliveries: Vec<ReactiveInputDelivery<N>>,
1839 pub delivery_token: Option<SubscriberDeliveryToken>,
1841 pub subscriber_checkpoint: Option<SubscriberCheckpoint>,
1843 pub payload_commitment: Option<SubscriberPayloadCommitment>,
1845 pub chain_controls: Vec<ChainControl>,
1847 pub preconfirmation_timing: Option<FlashblockIngressTiming>,
1849}
1850
1851#[derive(Clone, Debug)]
1853pub struct ReactiveInputBatch<N: Network = Ethereum> {
1854 records: Vec<ReactiveInputRecord<N>>,
1855 chain_id: Option<u64>,
1856 delivery_token: Option<SubscriberDeliveryToken>,
1857 subscriber_checkpoint: Option<SubscriberCheckpoint>,
1858 payload_commitment: Option<SubscriberPayloadCommitment>,
1859 audience: DeliveryAudience,
1860 record_audiences: Option<Vec<DeliveryAudience>>,
1861 delivery_scope: DeliveryScope,
1862 record_delivery_scopes: Option<Vec<DeliveryScope>>,
1863 chain_controls: Vec<ChainControl>,
1864 preconfirmation_timing: Option<FlashblockIngressTiming>,
1865}
1866
1867type RuntimeInputDelivery<N> = (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope);
1868
1869impl<N: Network> ReactiveInputBatch<N> {
1870 pub fn new(records: Vec<ReactiveInputRecord<N>>) -> Self {
1872 let chain_id = common_record_chain_id(&records);
1873 Self {
1874 records,
1875 chain_id,
1876 delivery_token: None,
1877 subscriber_checkpoint: None,
1878 payload_commitment: None,
1879 audience: DeliveryAudience::All,
1880 record_audiences: None,
1881 delivery_scope: DeliveryScope::Canonical,
1882 record_delivery_scopes: None,
1883 chain_controls: Vec::new(),
1884 preconfirmation_timing: None,
1885 }
1886 }
1887
1888 pub fn with_chain_id(mut self, chain_id: u64) -> Self {
1891 self.chain_id = Some(chain_id);
1892 self
1893 }
1894
1895 pub const fn chain_id(&self) -> Option<u64> {
1898 self.chain_id
1899 }
1900
1901 pub fn with_delivery_token(mut self, token: SubscriberDeliveryToken) -> Self {
1903 self.delivery_token = Some(token);
1904 self
1905 }
1906
1907 pub fn delivery_token(&self) -> Option<&SubscriberDeliveryToken> {
1909 self.delivery_token.as_ref()
1910 }
1911
1912 pub fn with_subscriber_checkpoint(mut self, checkpoint: SubscriberCheckpoint) -> Self {
1914 self.subscriber_checkpoint = Some(checkpoint);
1915 self
1916 }
1917
1918 pub fn subscriber_checkpoint(&self) -> Option<&SubscriberCheckpoint> {
1920 self.subscriber_checkpoint.as_ref()
1921 }
1922
1923 pub fn with_payload_commitment(mut self, commitment: SubscriberPayloadCommitment) -> Self {
1926 self.payload_commitment = Some(commitment);
1927 self
1928 }
1929
1930 pub const fn payload_commitment(&self) -> Option<&SubscriberPayloadCommitment> {
1932 self.payload_commitment.as_ref()
1933 }
1934
1935 pub fn with_audience(mut self, audience: DeliveryAudience) -> Self {
1937 self.audience = audience;
1938 self.record_audiences = None;
1939 self
1940 }
1941
1942 pub const fn audience(&self) -> &DeliveryAudience {
1944 &self.audience
1945 }
1946
1947 pub fn from_scoped_records(
1949 records: impl IntoIterator<Item = (ReactiveInputRecord<N>, DeliveryAudience)>,
1950 ) -> Self {
1951 let (records, record_audiences): (Vec<_>, Vec<_>) = records.into_iter().unzip();
1952 let chain_id = common_record_chain_id(&records);
1953 Self {
1954 records,
1955 chain_id,
1956 delivery_token: None,
1957 subscriber_checkpoint: None,
1958 payload_commitment: None,
1959 audience: DeliveryAudience::All,
1960 record_audiences: Some(record_audiences),
1961 delivery_scope: DeliveryScope::Canonical,
1962 record_delivery_scopes: None,
1963 chain_controls: Vec::new(),
1964 preconfirmation_timing: None,
1965 }
1966 }
1967
1968 pub fn from_deliveries(deliveries: impl IntoIterator<Item = ReactiveInputDelivery<N>>) -> Self {
1970 Self::from_scoped_records_with_delivery_scope(
1971 deliveries
1972 .into_iter()
1973 .map(ReactiveInputDelivery::into_parts),
1974 )
1975 }
1976
1977 pub fn record_audience(&self, index: usize) -> Option<&DeliveryAudience> {
1979 if index >= self.records.len() {
1980 return None;
1981 }
1982 Some(
1983 self.record_audiences
1984 .as_ref()
1985 .and_then(|audiences| audiences.get(index))
1986 .unwrap_or(&self.audience),
1987 )
1988 }
1989
1990 pub fn with_delivery_scope(mut self, scope: DeliveryScope) -> Self {
1992 self.delivery_scope = scope;
1993 self.record_delivery_scopes = None;
1994 self
1995 }
1996
1997 pub fn record_delivery_scope(&self, index: usize) -> Option<DeliveryScope> {
1999 if index >= self.records.len() {
2000 return None;
2001 }
2002 Some(
2003 self.record_delivery_scopes
2004 .as_ref()
2005 .and_then(|scopes| scopes.get(index))
2006 .copied()
2007 .unwrap_or(self.delivery_scope),
2008 )
2009 }
2010
2011 fn from_scoped_records_with_delivery_scope(
2012 records: impl IntoIterator<Item = (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
2013 ) -> Self {
2014 let mut input_records = Vec::new();
2015 let mut audiences = Vec::new();
2016 let mut scopes = Vec::new();
2017 for (record, audience, scope) in records {
2018 input_records.push(record);
2019 audiences.push(audience);
2020 scopes.push(scope);
2021 }
2022 let chain_id = common_record_chain_id(&input_records);
2023 Self {
2024 records: input_records,
2025 chain_id,
2026 delivery_token: None,
2027 subscriber_checkpoint: None,
2028 payload_commitment: None,
2029 audience: DeliveryAudience::All,
2030 record_audiences: Some(audiences),
2031 delivery_scope: DeliveryScope::Canonical,
2032 record_delivery_scopes: Some(scopes),
2033 chain_controls: Vec::new(),
2034 preconfirmation_timing: None,
2035 }
2036 }
2037
2038 pub fn with_preconfirmation_timing(mut self, timing: FlashblockIngressTiming) -> Self {
2040 self.preconfirmation_timing = Some(timing);
2041 self
2042 }
2043
2044 pub const fn preconfirmation_timing(&self) -> Option<FlashblockIngressTiming> {
2046 self.preconfirmation_timing
2047 }
2048
2049 pub fn with_chain_controls(mut self, controls: impl IntoIterator<Item = ChainControl>) -> Self {
2056 self.chain_controls = controls.into_iter().collect();
2057 self
2058 }
2059
2060 pub fn chain_controls(&self) -> &[ChainControl] {
2062 &self.chain_controls
2063 }
2064
2065 pub fn records(&self) -> &[ReactiveInputRecord<N>] {
2067 &self.records
2068 }
2069
2070 pub fn into_records(self) -> Vec<ReactiveInputRecord<N>> {
2077 self.records
2078 }
2079
2080 pub fn into_parts(self) -> ReactiveInputBatchParts<N> {
2082 let chain_id = self.chain_id;
2083 let delivery_token = self.delivery_token;
2084 let subscriber_checkpoint = self.subscriber_checkpoint;
2085 let payload_commitment = self.payload_commitment;
2086 let chain_controls = self.chain_controls;
2087 let preconfirmation_timing = self.preconfirmation_timing;
2088 let audiences = self
2089 .record_audiences
2090 .unwrap_or_else(|| vec![self.audience; self.records.len()]);
2091 let scopes = self
2092 .record_delivery_scopes
2093 .unwrap_or_else(|| vec![self.delivery_scope; self.records.len()]);
2094 let deliveries = self
2095 .records
2096 .into_iter()
2097 .zip(audiences)
2098 .zip(scopes)
2099 .map(|((record, audience), scope)| ReactiveInputDelivery::new(record, audience, scope))
2100 .collect();
2101 ReactiveInputBatchParts {
2102 chain_id,
2103 deliveries,
2104 delivery_token,
2105 subscriber_checkpoint,
2106 payload_commitment,
2107 chain_controls,
2108 preconfirmation_timing,
2109 }
2110 }
2111
2112 fn into_runtime_parts(self) -> (Vec<RuntimeInputDelivery<N>>, Vec<ChainControl>, Option<u64>) {
2113 let audiences = self
2114 .record_audiences
2115 .unwrap_or_else(|| vec![self.audience; self.records.len()]);
2116 let scopes = self
2117 .record_delivery_scopes
2118 .unwrap_or_else(|| vec![self.delivery_scope; self.records.len()]);
2119 let records = self
2120 .records
2121 .into_iter()
2122 .zip(audiences)
2123 .zip(scopes)
2124 .map(|((record, audience), scope)| (record, audience, scope))
2125 .collect();
2126 (records, self.chain_controls, self.chain_id)
2127 }
2128
2129 fn take_delivery_token(&mut self) -> Option<SubscriberDeliveryToken> {
2130 self.delivery_token.take()
2131 }
2132
2133 fn take_subscriber_checkpoint(&mut self) -> Option<SubscriberCheckpoint> {
2134 self.subscriber_checkpoint.take()
2135 }
2136}
2137
2138fn common_record_chain_id<N: Network>(records: &[ReactiveInputRecord<N>]) -> Option<u64> {
2139 let chain_id = records.first()?.context.chain_id?;
2140 records
2141 .iter()
2142 .all(|record| record.context.chain_id == Some(chain_id))
2143 .then_some(chain_id)
2144}
2145
2146pub trait ReactiveHandler<N: Network = Ethereum>: Send + Sync {
2148 fn id(&self) -> HandlerId;
2150
2151 fn interests(&self) -> Vec<ReactiveInterest<N>>;
2153
2154 fn log_route_index(&self) -> Option<LogRouteIndex> {
2161 None
2162 }
2163
2164 fn handle(
2166 &self,
2167 ctx: &ReactiveContext,
2168 input: &ReactiveInput<N>,
2169 state: &dyn StateView,
2170 ) -> Result<HandlerOutcome, HandlerError>;
2171}
2172
2173pub trait ReactiveHook<N: Network = Ethereum>: Send + Sync {
2183 fn on_report(&self, report: Arc<ReactiveReport<N>>);
2185}
2186
2187#[allow(clippy::large_enum_variant)]
2189#[derive(Clone)]
2190pub enum ReactiveInterest<N: Network = Ethereum> {
2191 Logs(LogInterest),
2193 Blocks(BlockInterest),
2195 PendingTransactions(PendingTxInterest<N>),
2197}
2198
2199impl<N: Network> fmt::Debug for ReactiveInterest<N> {
2200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2201 match self {
2202 Self::Logs(interest) => f.debug_tuple("Logs").field(interest).finish(),
2203 Self::Blocks(interest) => f.debug_tuple("Blocks").field(interest).finish(),
2204 Self::PendingTransactions(interest) => f
2205 .debug_tuple("PendingTransactions")
2206 .field(interest)
2207 .finish(),
2208 }
2209 }
2210}
2211
2212#[derive(Clone)]
2214pub struct LogInterest {
2215 pub provider_filter: Filter,
2217 pub local_matcher: Option<Arc<dyn LogMatcher>>,
2219 pub route_key: Option<RouteKeySpec>,
2221}
2222
2223impl LogInterest {
2224 pub fn matches(&self, log: &Log) -> bool {
2226 self.provider_filter.rpc_matches(log)
2227 && self
2228 .local_matcher
2229 .as_ref()
2230 .is_none_or(|matcher| matcher.matches(log))
2231 }
2232
2233 pub fn route_key(&self, log: &Log) -> Option<RouteKey> {
2235 self.route_key.as_ref().and_then(|spec| spec.extract(log))
2236 }
2237}
2238
2239impl fmt::Debug for LogInterest {
2240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2241 f.debug_struct("LogInterest")
2242 .field("provider_filter", &self.provider_filter)
2243 .field(
2244 "local_matcher",
2245 &self.local_matcher.as_ref().map(|_| "<matcher>"),
2246 )
2247 .field("route_key", &self.route_key)
2248 .finish()
2249 }
2250}
2251
2252pub trait LogMatcher: Send + Sync {
2254 fn matches(&self, log: &Log) -> bool;
2256}
2257
2258#[derive(Clone)]
2260pub enum RouteKeySpec {
2261 EmitterAddress,
2263 Topic {
2265 index: usize,
2267 },
2268 DataSlice {
2270 offset: usize,
2272 len: usize,
2274 },
2275 Custom(Arc<dyn RouteKeyExtractor>),
2277}
2278
2279impl RouteKeySpec {
2280 pub fn extract(&self, log: &Log) -> Option<RouteKey> {
2282 match self {
2283 Self::EmitterAddress => Some(RouteKey::Address(log.address())),
2284 Self::Topic { index } => log.topics().get(*index).copied().map(RouteKey::Bytes32),
2285 Self::DataSlice { offset, len } => {
2286 let data = log.inner.data.data.as_ref();
2287 let end = offset.checked_add(*len)?;
2288 data.get(*offset..end)
2289 .map(|bytes| RouteKey::Bytes(bytes.to_vec()))
2290 }
2291 Self::Custom(extractor) => extractor.extract(log),
2292 }
2293 }
2294}
2295
2296impl fmt::Debug for RouteKeySpec {
2297 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2298 match self {
2299 Self::EmitterAddress => f.write_str("EmitterAddress"),
2300 Self::Topic { index } => f.debug_struct("Topic").field("index", index).finish(),
2301 Self::DataSlice { offset, len } => f
2302 .debug_struct("DataSlice")
2303 .field("offset", offset)
2304 .field("len", len)
2305 .finish(),
2306 Self::Custom(_) => f.write_str("Custom(<extractor>)"),
2307 }
2308 }
2309}
2310
2311pub trait RouteKeyExtractor: Send + Sync {
2313 fn extract(&self, log: &Log) -> Option<RouteKey>;
2315}
2316
2317#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2319pub enum RouteKey {
2320 Address(Address),
2322 Bytes32(B256),
2324 Bytes(Vec<u8>),
2326}
2327
2328#[non_exhaustive]
2330#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2331pub enum LogRouteKey {
2332 Emitter(Address),
2334 Topic {
2336 index: usize,
2338 value: B256,
2340 },
2341 DataSlice {
2343 offset: usize,
2345 value: Vec<u8>,
2347 },
2348}
2349
2350#[derive(Clone, Debug, PartialEq, Eq)]
2352pub struct LogRouteIndex {
2353 keys: Vec<LogRouteKey>,
2354}
2355
2356impl LogRouteIndex {
2357 pub fn new(primary: LogRouteKey, additional: impl IntoIterator<Item = LogRouteKey>) -> Self {
2359 let mut keys = vec![primary];
2360 for key in additional {
2361 if !keys.contains(&key) {
2362 keys.push(key);
2363 }
2364 }
2365 Self { keys }
2366 }
2367
2368 pub fn single(key: LogRouteKey) -> Self {
2370 Self { keys: vec![key] }
2371 }
2372
2373 pub fn keys(&self) -> &[LogRouteKey] {
2375 &self.keys
2376 }
2377}
2378
2379#[derive(Clone, Debug, PartialEq, Eq)]
2381pub struct ReactiveLogRoute {
2382 pub handler_id: HandlerId,
2384 pub route_key: Option<RouteKey>,
2386}
2387
2388#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2390pub struct BlockInterest {
2391 pub mode: BlockInterestMode,
2393}
2394
2395impl Default for BlockInterest {
2396 fn default() -> Self {
2397 Self {
2398 mode: BlockInterestMode::Header,
2399 }
2400 }
2401}
2402
2403#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2405pub enum BlockInterestMode {
2406 Header,
2408 FullBlock,
2410}
2411
2412#[derive(Clone)]
2414pub struct PendingTxInterest<N: Network = Ethereum> {
2415 pub full_transactions: bool,
2417 pub from: AddressMatcher,
2419 pub to: AddressMatcher,
2421 pub selectors: SelectorMatcher,
2423 pub local_matcher: Option<Arc<dyn PendingTxMatcher<N>>>,
2425}
2426
2427impl<N: Network> Default for PendingTxInterest<N> {
2428 fn default() -> Self {
2429 Self {
2430 full_transactions: false,
2431 from: AddressMatcher::Any,
2432 to: AddressMatcher::Any,
2433 selectors: SelectorMatcher::Any,
2434 local_matcher: None,
2435 }
2436 }
2437}
2438
2439impl<N: Network> PendingTxInterest<N> {
2440 fn matches_hash_only(&self) -> bool {
2441 !self.full_transactions
2442 && self.from.is_any()
2443 && self.to.is_any()
2444 && self.selectors.is_any()
2445 && self.local_matcher.is_none()
2446 }
2447
2448 fn matches_tx(&self, tx: &N::TransactionResponse) -> bool {
2449 self.from.matches(tx.from())
2450 && self.to.matches_option(tx.to())
2451 && self.selectors.matches(tx.input())
2452 && self
2453 .local_matcher
2454 .as_ref()
2455 .is_none_or(|matcher| matcher.matches(tx))
2456 }
2457}
2458
2459impl<N: Network> fmt::Debug for PendingTxInterest<N> {
2460 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2461 f.debug_struct("PendingTxInterest")
2462 .field("full_transactions", &self.full_transactions)
2463 .field("from", &self.from)
2464 .field("to", &self.to)
2465 .field("selectors", &self.selectors)
2466 .field(
2467 "local_matcher",
2468 &self.local_matcher.as_ref().map(|_| "<matcher>"),
2469 )
2470 .finish()
2471 }
2472}
2473
2474#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2476pub enum AddressMatcher {
2477 Any,
2479 Exact(Address),
2481 AnyOf(Vec<Address>),
2483}
2484
2485impl AddressMatcher {
2486 pub fn is_any(&self) -> bool {
2488 matches!(self, Self::Any)
2489 }
2490
2491 pub fn matches(&self, address: Address) -> bool {
2493 match self {
2494 Self::Any => true,
2495 Self::Exact(expected) => *expected == address,
2496 Self::AnyOf(addresses) => addresses.contains(&address),
2497 }
2498 }
2499
2500 pub fn matches_option(&self, address: Option<Address>) -> bool {
2502 match (self, address) {
2503 (Self::Any, _) => true,
2504 (_, Some(address)) => self.matches(address),
2505 _ => false,
2506 }
2507 }
2508}
2509
2510#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2512pub enum SelectorMatcher {
2513 Any,
2515 AnyOf(Vec<[u8; 4]>),
2517}
2518
2519impl SelectorMatcher {
2520 pub fn is_any(&self) -> bool {
2522 matches!(self, Self::Any)
2523 }
2524
2525 pub fn matches(&self, input: &Bytes) -> bool {
2527 match self {
2528 Self::Any => true,
2529 Self::AnyOf(selectors) => input
2530 .get(..4)
2531 .and_then(|bytes| bytes.try_into().ok())
2532 .is_some_and(|selector| selectors.contains(&selector)),
2533 }
2534 }
2535}
2536
2537pub trait PendingTxMatcher<N: Network = Ethereum>: Send + Sync {
2539 fn matches(&self, tx: &N::TransactionResponse) -> bool;
2541}
2542
2543#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
2561#[non_exhaustive]
2562pub enum TrackingPolicy {
2563 Slots {
2568 slots: Vec<U256>,
2570 },
2571 WholeAccount,
2576 Scalars,
2581}
2582
2583#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2596pub enum RootGateCadence {
2597 EveryNBlocks(NonZeroU64),
2601 Disabled,
2603}
2604
2605impl RootGateCadence {
2606 #[must_use]
2617 pub fn every_n_blocks(n: u64) -> Self {
2618 Self::EveryNBlocks(NonZeroU64::new(n.max(1)).expect("clamped to at least 1"))
2619 }
2620}
2621
2622impl Default for RootGateCadence {
2623 fn default() -> Self {
2627 Self::every_n_blocks(16)
2628 }
2629}
2630
2631#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
2638struct TrackedRoot {
2639 last_root: B256,
2640 last_block: u64,
2641 balance: U256,
2642 nonce: u64,
2643 code_hash: B256,
2644}
2645
2646#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2648pub struct ResyncRequest {
2649 pub id: ResyncId,
2651 pub reason: ResyncReason,
2653 pub block: ResyncBlock,
2655 pub targets: Vec<ResyncTarget>,
2657 pub priority: ResyncPriority,
2659}
2660
2661#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2663pub struct ResyncId(String);
2664
2665impl ResyncId {
2666 pub fn new(id: impl Into<String>) -> Self {
2668 Self(id.into())
2669 }
2670}
2671
2672#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2674#[non_exhaustive]
2675pub enum ResyncReason {
2676 HandlerRequested,
2678 SkippedStateEffect,
2680 MissedBlockRange,
2687 RootMoved,
2697 Custom(String),
2699}
2700
2701#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2703pub enum ResyncBlock {
2704 Latest,
2706 Pending,
2708 Safe,
2710 Finalized,
2712 Number(u64),
2714 Hash {
2716 number: u64,
2718 hash: B256,
2720 require_canonical: bool,
2722 },
2723}
2724
2725#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2727pub enum ResyncTarget {
2728 StorageSlot {
2730 address: Address,
2732 slot: U256,
2734 },
2735 StorageSlots {
2737 address: Address,
2739 slots: Vec<U256>,
2741 },
2742 Account {
2744 address: Address,
2746 fields: AccountFieldMask,
2748 },
2749}
2750
2751#[derive(
2753 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
2754)]
2755pub struct AccountFieldMask {
2756 pub balance: bool,
2758 pub nonce: bool,
2760 pub code: bool,
2762}
2763
2764#[derive(
2766 Clone,
2767 Copy,
2768 Debug,
2769 Default,
2770 PartialEq,
2771 Eq,
2772 Hash,
2773 PartialOrd,
2774 Ord,
2775 serde::Serialize,
2776 serde::Deserialize,
2777)]
2778pub enum ResyncPriority {
2779 Low,
2781 #[default]
2783 Normal,
2784 High,
2786}
2787
2788#[derive(Clone, Debug, PartialEq, Eq)]
2790pub struct InvalidationRequest {
2791 pub scope: PurgeScope,
2793 pub address: Address,
2795 pub reason: InvalidationReason,
2797}
2798
2799#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2801pub enum InvalidationReason {
2802 HandlerRequested,
2804 Reorg,
2806 Custom(String),
2808}
2809
2810#[derive(Clone, Debug, PartialEq, Eq)]
2812pub struct SpeculativeRequest {
2813 pub id: SpeculativeId,
2815 pub input_ref: InputRef,
2817 pub labels: Vec<ReportTag>,
2819}
2820
2821#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2823pub struct SpeculativeId(String);
2824
2825impl SpeculativeId {
2826 pub fn new(id: impl Into<String>) -> Self {
2828 Self(id.into())
2829 }
2830}
2831
2832#[derive(Clone, Debug, PartialEq, Eq)]
2834pub struct ReactiveConfig {
2835 pub hook_backpressure: HookBackpressure,
2840 pub journal_depth: usize,
2857}
2858
2859impl Default for ReactiveConfig {
2860 fn default() -> Self {
2861 Self {
2862 hook_backpressure: HookBackpressure::Block,
2863 journal_depth: 64,
2864 }
2865 }
2866}
2867
2868#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2870pub enum HookBackpressure {
2871 Block,
2873 DropNewest,
2875 DropOldest,
2877 Error,
2879}
2880
2881#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2889#[non_exhaustive]
2890pub enum CacheHealth {
2891 #[default]
2893 Healthy,
2894 Degraded {
2898 since_block: u64,
2900 },
2901 Unhealthy {
2904 since_block: u64,
2906 },
2907}
2908
2909#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2916#[non_exhaustive]
2917pub struct CacheMetricsSnapshot {
2918 pub deep_reorgs: u64,
2921 pub reorgs_recovered: u64,
2923 pub resync_requests: u64,
2925 pub resync_failures: u64,
2927 pub missed_ranges: u64,
2929 pub coverage_gaps: u64,
2931 pub pending_contamination: u64,
2933 pub stale_verdicts: u64,
2935}
2936
2937#[derive(Debug, Default)]
2943struct CacheMetrics {
2944 deep_reorgs: AtomicU64,
2945 reorgs_recovered: AtomicU64,
2946 resync_requests: AtomicU64,
2947 resync_failures: AtomicU64,
2948 missed_ranges: AtomicU64,
2949 coverage_gaps: AtomicU64,
2950 pending_contamination: AtomicU64,
2951 stale_verdicts: AtomicU64,
2952}
2953
2954impl CacheMetrics {
2955 fn snapshot(&self) -> CacheMetricsSnapshot {
2956 CacheMetricsSnapshot {
2957 deep_reorgs: self.deep_reorgs.load(Ordering::Relaxed),
2958 reorgs_recovered: self.reorgs_recovered.load(Ordering::Relaxed),
2959 resync_requests: self.resync_requests.load(Ordering::Relaxed),
2960 resync_failures: self.resync_failures.load(Ordering::Relaxed),
2961 missed_ranges: self.missed_ranges.load(Ordering::Relaxed),
2962 coverage_gaps: self.coverage_gaps.load(Ordering::Relaxed),
2963 pending_contamination: self.pending_contamination.load(Ordering::Relaxed),
2964 stale_verdicts: self.stale_verdicts.load(Ordering::Relaxed),
2965 }
2966 }
2967
2968 fn restore(&self, snapshot: CacheMetricsSnapshot) {
2969 self.deep_reorgs
2970 .store(snapshot.deep_reorgs, Ordering::Relaxed);
2971 self.reorgs_recovered
2972 .store(snapshot.reorgs_recovered, Ordering::Relaxed);
2973 self.resync_requests
2974 .store(snapshot.resync_requests, Ordering::Relaxed);
2975 self.resync_failures
2976 .store(snapshot.resync_failures, Ordering::Relaxed);
2977 self.missed_ranges
2978 .store(snapshot.missed_ranges, Ordering::Relaxed);
2979 self.coverage_gaps
2980 .store(snapshot.coverage_gaps, Ordering::Relaxed);
2981 self.pending_contamination
2982 .store(snapshot.pending_contamination, Ordering::Relaxed);
2983 self.stale_verdicts
2984 .store(snapshot.stale_verdicts, Ordering::Relaxed);
2985 }
2986}
2987
2988#[derive(Clone, Debug)]
2990#[non_exhaustive]
2991pub enum ReactiveReport<N: Network = Ethereum> {
2992 Input(InputReport<N>),
2994 Decoded(DecodedReport<N>),
2996 Applied(AppliedReport<N>),
2998 Resynced(ResyncReport),
3000 BlockCommitted(BlockReport<N>),
3002 Reorg(ReorgReport<N>),
3004 ChainControl(ChainControlReport),
3006 MissedBlockRange(MissedRangeReport<N>),
3009 Health(HealthReport<N>),
3011 CoverageGap(CoverageGapReport<N>),
3014 Error(ReactiveErrorReport<N>),
3016}
3017
3018#[derive(Clone, Debug, PartialEq, Eq)]
3020pub struct ChainControlReport {
3021 pub control: ChainControl,
3023}
3024
3025#[derive(Clone, Debug)]
3027pub struct InputReport<N: Network = Ethereum> {
3028 pub input_ref: InputRef,
3030 pub context: ReactiveContext,
3032 pub provider: Option<ProviderRef>,
3034 pub _network: PhantomData<N>,
3036}
3037
3038#[derive(Clone, Debug)]
3040pub struct DecodedReport<N: Network = Ethereum> {
3041 pub input_ref: InputRef,
3043 pub handler_ids: Vec<HandlerId>,
3045 pub _network: PhantomData<N>,
3047}
3048
3049#[derive(Clone, Debug)]
3051pub struct AppliedReport<N: Network = Ethereum> {
3052 pub input_ref: InputRef,
3054 pub handler_id: HandlerId,
3056 pub quality: StateEffectQuality,
3058 pub tags: Vec<ReportTag>,
3060 pub diff: StateDiff,
3062 pub state_updates: Vec<StateUpdate>,
3064 pub invalidations: Vec<InvalidationRequest>,
3066 pub resyncs: Vec<ResyncRequest>,
3068 pub speculative: Vec<SpeculativeRequest>,
3070 pub hook_signals: Vec<HookSignal>,
3072 pub _network: PhantomData<N>,
3074}
3075
3076#[derive(Clone, Debug, Default, PartialEq, Eq)]
3080pub struct ResyncReport {
3081 pub requested: Vec<ResyncRequest>,
3083 pub state_updates: Vec<StateUpdate>,
3085 pub diff: StateDiff,
3087 pub failed: Vec<ResyncFailure>,
3089}
3090
3091#[derive(Clone, Debug, PartialEq, Eq)]
3093pub struct ResyncFailure {
3094 pub request_id: ResyncId,
3096 pub block: ResyncBlock,
3098 pub target: ResyncTarget,
3100 pub kind: ResyncFailureKind,
3102 pub message: String,
3104}
3105
3106#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
3108#[non_exhaustive]
3109pub enum ResyncFailureKind {
3110 MissingStorageFetcher,
3112 StorageFetchFailed,
3114 StorageFetchOmitted,
3116 MissingAccountFetcher,
3118 AccountFetchFailed,
3120 AccountFetchOmitted,
3122}
3123
3124#[derive(Clone, Debug)]
3126pub struct BlockReport<N: Network = Ethereum> {
3127 pub block: Option<BlockRef>,
3129 pub inputs: Vec<InputRef>,
3131 pub _network: PhantomData<N>,
3133}
3134
3135#[derive(Clone, Debug)]
3151pub struct ReorgReport<N: Network = Ethereum> {
3152 pub dropped: Option<BlockRef>,
3154 pub dropped_blocks: Vec<BlockRef>,
3156 pub dropped_inputs: Vec<InputRef>,
3158 pub rollback_updates: Vec<StateUpdate>,
3160 pub rollback_diff: StateDiff,
3162 pub purge_updates: Vec<StateUpdate>,
3164 pub purge_diff: StateDiff,
3166 pub canceled_resyncs: Vec<ResyncRequest>,
3168 pub reason: ReorgReason,
3170 pub _network: PhantomData<N>,
3172}
3173
3174#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
3176pub enum ReorgReason {
3177 RemovedLog,
3179 ReorgedInput,
3181 ParentMismatch,
3183 Explicit,
3185}
3186
3187#[derive(Clone, Debug)]
3195pub struct MissedRangeReport<N: Network = Ethereum> {
3196 pub from: u64,
3198 pub to: u64,
3200 pub block: u64,
3202 pub _network: PhantomData<N>,
3204}
3205
3206#[derive(Clone, Debug)]
3209pub struct HealthReport<N: Network = Ethereum> {
3210 pub from: CacheHealth,
3212 pub to: CacheHealth,
3214 pub block: Option<u64>,
3216 pub _network: PhantomData<N>,
3218}
3219
3220#[derive(Clone, Debug)]
3233pub struct CoverageGapReport<N: Network = Ethereum> {
3234 pub address: Address,
3236 pub block: u64,
3238 pub _network: PhantomData<N>,
3240}
3241
3242#[derive(Clone, Debug)]
3245pub struct ReactiveErrorReport<N: Network = Ethereum> {
3246 pub input_ref: Option<InputRef>,
3248 pub message: String,
3250 pub _network: PhantomData<N>,
3252}
3253
3254#[derive(Clone, Debug)]
3257pub struct ReactiveBatchReport<N: Network = Ethereum> {
3258 pub applied: Vec<AppliedReport<N>>,
3260 pub resyncs: Vec<ResyncRequest>,
3262 pub speculative: Vec<SpeculativeRequest>,
3264 pub reports: Vec<Arc<ReactiveReport<N>>>,
3266}
3267
3268impl<N: Network> Default for ReactiveBatchReport<N> {
3269 fn default() -> Self {
3270 Self {
3271 applied: Vec::new(),
3272 resyncs: Vec::new(),
3273 speculative: Vec::new(),
3274 reports: Vec::new(),
3275 }
3276 }
3277}
3278
3279#[derive(Clone, Debug, PartialEq, Eq)]
3281pub struct HandlerError {
3282 message: String,
3283}
3284
3285impl HandlerError {
3286 pub fn new(message: impl Into<String>) -> Self {
3288 Self {
3289 message: message.into(),
3290 }
3291 }
3292}
3293
3294impl fmt::Display for HandlerError {
3295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3296 self.message.fmt(f)
3297 }
3298}
3299
3300impl std::error::Error for HandlerError {}
3301
3302impl From<String> for HandlerError {
3303 fn from(message: String) -> Self {
3304 Self::new(message)
3305 }
3306}
3307
3308impl From<&str> for HandlerError {
3309 fn from(message: &str) -> Self {
3310 Self::new(message)
3311 }
3312}
3313
3314#[derive(Debug, thiserror::Error)]
3316#[non_exhaustive]
3317pub enum ReactiveError {
3318 #[error("handler `{handler_id}` failed: {source}")]
3320 HandlerFailed {
3321 handler_id: HandlerId,
3323 source: HandlerError,
3325 },
3326 #[error(
3328 "conflicting effects for input {input_ref:?} on target {target:?}: `{first}` vs `{second}`"
3329 )]
3330 ConflictingEffects {
3331 input_ref: Box<InputRef>,
3333 target: Box<EffectTarget>,
3335 first: HandlerId,
3337 second: HandlerId,
3339 },
3340 #[error(
3342 "pending input {input_ref:?} emitted invalid canonical effect `{effect_kind}` from `{handler_id}`"
3343 )]
3344 InvalidPendingEffect {
3345 input_ref: Box<InputRef>,
3347 handler_id: HandlerId,
3349 effect_kind: &'static str,
3351 },
3352 #[error("invalid reactive input record: {message}")]
3355 InvalidInputRecord {
3356 message: String,
3358 },
3359 #[error("invalid chain control: {message}")]
3361 InvalidChainControl {
3362 message: String,
3364 },
3365 #[error(
3368 "owner catch-up block {number} {hash} is outside the retained canonical rollback journal"
3369 )]
3370 OwnerCatchupOutsideJournal {
3371 number: u64,
3373 hash: B256,
3375 },
3376 #[error(transparent)]
3378 Register(#[from] RegisterError),
3379}
3380
3381#[derive(Debug, thiserror::Error)]
3383#[non_exhaustive]
3384pub enum RegisterError {
3385 #[error("handler id `{0}` is already registered")]
3387 DuplicateHandler(HandlerId),
3388}
3389
3390#[derive(Debug, thiserror::Error)]
3393#[non_exhaustive]
3394pub enum ReactiveEngineRegisterError {
3395 #[error(transparent)]
3397 Register(#[from] RegisterError),
3398 #[error(transparent)]
3400 Subscriber(#[from] SubscriberError),
3401 #[error(
3404 "owner backfill {start_block}..={end_block:?} must target exactly one hash-certified block in the retained rollback journal"
3405 )]
3406 BackfillOutsideJournal {
3407 start_block: u64,
3409 end_block: Option<u64>,
3411 retained_anchor: Option<BlockRef>,
3413 },
3414}
3415
3416#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
3419#[non_exhaustive]
3420pub enum ReactiveBaselineError {
3421 #[error("cannot adopt a canonical baseline after reactive processing has started")]
3423 ActiveRuntime,
3424 #[error(
3427 "canonical baseline conflicts with existing block {existing_number} {existing_hash} (requested {requested_number} {requested_hash})"
3428 )]
3429 ConflictingBaseline {
3430 existing_number: u64,
3432 existing_hash: B256,
3434 requested_number: u64,
3436 requested_hash: B256,
3438 },
3439 #[error("baseline chain id {baseline_chain_id} does not match cache chain id {cache_chain_id}")]
3441 CacheChainMismatch {
3442 baseline_chain_id: u64,
3444 cache_chain_id: u64,
3446 },
3447 #[error("cache block selector is not canonically hash-pinned to baseline {number} {hash}")]
3449 CacheBlockMismatch {
3450 number: u64,
3452 hash: B256,
3454 },
3455}
3456
3457#[derive(Debug, thiserror::Error)]
3460#[non_exhaustive]
3461pub enum ReactiveEngineError {
3462 #[error(transparent)]
3464 Subscriber(#[from] SubscriberError),
3465 #[error(transparent)]
3467 Runtime(ReactiveError),
3468 #[error(transparent)]
3470 Baseline(#[from] ReactiveBaselineError),
3471 #[error("runtime ingestion succeeded but subscriber acknowledgement failed: {0}")]
3474 Acknowledgement(#[source] SubscriberError),
3475 #[error("runtime ingestion succeeded but durable checkpoint commit failed: {0}")]
3479 Checkpoint(#[source] DurableCheckpointError),
3480 #[error("cannot durably checkpoint reactive state before observing a canonical block")]
3482 MissingCheckpointBlock,
3483 #[error("pre-confirmed Flashblock batches cannot be durably checkpointed")]
3486 PreconfirmationNotCheckpointable,
3487 #[error("failed to encode durable reactive runtime state: {0}")]
3489 RuntimeCheckpoint(String),
3490 #[error("cannot use ordinary ingestion while a durable checkpoint commit is pending")]
3493 PendingCheckpointCommit,
3494 #[error("cannot use checkpointed ingestion while an ordinary acknowledgement is pending")]
3497 PendingAcknowledgementCommit,
3498 #[error(
3502 "raw engine ingestion cannot consume delivery tokens or subscriber checkpoints; use a combined next_ingest helper"
3503 )]
3504 UncommittedDeliveryMetadata,
3505 #[error(
3507 "subscriber chain id {subscriber_chain_id} does not match cache chain id {cache_chain_id}"
3508 )]
3509 SubscriberChainMismatch {
3510 subscriber_chain_id: u64,
3512 cache_chain_id: u64,
3514 },
3515 #[error("subscriber does not advertise durable replay support")]
3517 SubscriberNotDurable,
3518 #[error(
3521 "committed delivery token has no delivery witness; replay cannot be acknowledged safely"
3522 )]
3523 MissingReplayWitness,
3524 #[error("replayed delivery token does not match its committed delivery witness")]
3527 ReplayDeliveryMismatch,
3528 #[error("failed to encode durable delivery witness: {0}")]
3530 DeliveryWitness(String),
3531 #[error(
3534 "tokened block-header, full-block, or hydrated-transaction delivery requires an exact payload commitment"
3535 )]
3536 MissingPayloadCommitment,
3537 #[error(
3540 "cache changed while durable checkpoint commit was pending (staged generation {staged_generation}, current generation {current_generation})"
3541 )]
3542 PendingCheckpointCacheChanged {
3543 staged_generation: u64,
3545 current_generation: u64,
3547 },
3548 #[error(
3551 "reorg after block {common_ancestor} exceeds the retained rollback journal (oldest retained block {oldest_journaled:?}, configured depth {journal_depth})"
3552 )]
3553 CheckpointReorgOutsideJournal {
3554 common_ancestor: u64,
3556 oldest_journaled: Option<u64>,
3558 journal_depth: usize,
3560 },
3561 #[error(
3564 "owner catch-up block {number} {hash} is outside the retained canonical rollback journal"
3565 )]
3566 OwnerCatchupOutsideJournal {
3567 number: u64,
3569 hash: B256,
3571 },
3572}
3573
3574impl From<ReactiveError> for ReactiveEngineError {
3575 fn from(error: ReactiveError) -> Self {
3576 match error {
3577 ReactiveError::OwnerCatchupOutsideJournal { number, hash } => {
3578 Self::OwnerCatchupOutsideJournal { number, hash }
3579 }
3580 error => Self::Runtime(error),
3581 }
3582 }
3583}
3584
3585#[derive(Debug, thiserror::Error)]
3587#[non_exhaustive]
3588pub enum ReactiveCheckpointRestoreError {
3589 #[error("cannot restore a durable checkpoint into a runtime with canonical journal state")]
3591 ActiveRuntime,
3592 #[error("invalid durable reactive runtime state: {0}")]
3594 InvalidRuntimeCheckpoint(String),
3595 #[error(transparent)]
3597 Checkpoint(#[from] DurableCheckpointError),
3598 #[error("subscriber rejected durable resume position: {0}")]
3600 Subscriber(#[source] SubscriberError),
3601 #[error(
3603 "subscriber chain id {subscriber_chain_id} does not match checkpoint chain id {checkpoint_chain_id}"
3604 )]
3605 SubscriberChainMismatch {
3606 subscriber_chain_id: u64,
3608 checkpoint_chain_id: u64,
3610 },
3611 #[error("subscriber does not advertise durable replay support")]
3613 SubscriberNotDurable,
3614}
3615
3616#[derive(Clone, Debug)]
3618#[non_exhaustive]
3619pub enum CheckpointedIngest<N: Network = Ethereum> {
3620 Applied(ReactiveBatchReport<N>),
3622 ReplayAcknowledged,
3625}
3626
3627#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3629pub enum EffectTarget {
3630 StorageSlot {
3632 address: Address,
3634 slot: U256,
3636 },
3637 AccountBalance {
3639 address: Address,
3641 },
3642 AccountNonce {
3644 address: Address,
3646 },
3647 AccountCode {
3649 address: Address,
3651 },
3652 MaskedStorageSlot {
3654 address: Address,
3656 slot: U256,
3658 mask: U256,
3660 },
3661}
3662
3663#[derive(Clone, Debug, PartialEq, Eq)]
3664enum AbsoluteValue {
3665 U256(U256),
3666 U64(u64),
3667 Bytes(Bytes),
3668}
3669
3670pub struct ReactiveRuntime<N: Network = Ethereum> {
3672 registry: ReactiveRegistry<N>,
3673 hooks: Vec<Arc<dyn ReactiveHook<N>>>,
3674 config: ReactiveConfig,
3675 journal: VecDeque<BlockJournal<N>>,
3676 coverage_head: Option<BlockRef>,
3677 log_coverage_head: Option<BlockRef>,
3680 pending_resyncs: Vec<ResyncRequest>,
3681 health: CacheHealth,
3682 safe_head: Option<BlockRef>,
3683 finalized_head: Option<BlockRef>,
3684 metrics: CacheMetrics,
3685 freshness: Option<FreshnessRegistry>,
3694 tracking: HashMap<Address, TrackingPolicy>,
3698 tracked_roots: HashMap<Address, TrackedRoot>,
3701 root_gate_cadence: RootGateCadence,
3703 last_gate_block: Option<u64>,
3707 touched_since_gate: HashSet<Address>,
3713 preconfirmed_branch: Option<PreconfirmedBranch>,
3716}
3717
3718#[derive(Clone)]
3719struct PreconfirmedBranch {
3720 flashblock: FlashblockRef,
3721 canonical_cache: EvmCacheStateSnapshot,
3722}
3723
3724#[derive(Clone, Debug)]
3725struct BlockJournal<N: Network = Ethereum> {
3726 block: BlockRef,
3727 inputs: Vec<InputRef>,
3728 applied: Vec<AppliedReport<N>>,
3729 handler_ids: Vec<HandlerId>,
3730 resynced: Vec<ResyncReport>,
3731 rollback_diffs: Vec<StateDiff>,
3732}
3733
3734const DURABLE_RUNTIME_CHECKPOINT_VERSION: u32 = 4;
3736
3737#[derive(serde::Serialize, serde::Deserialize)]
3738struct DurableRuntimeCheckpoint {
3739 version: u32,
3740 safe_head: Option<BlockRef>,
3741 finalized_head: Option<BlockRef>,
3742 health: CacheHealth,
3743 pending_resyncs: Vec<ResyncRequest>,
3744 coverage_head: Option<BlockRef>,
3745 log_coverage_head: Option<BlockRef>,
3746 journal: Vec<DurableBlockJournal>,
3747 freshness: Option<FreshnessRegistry>,
3748 tracking: HashMap<Address, TrackingPolicy>,
3749 tracked_roots: HashMap<Address, TrackedRoot>,
3750 root_gate_cadence: RootGateCadence,
3751 last_gate_block: Option<u64>,
3752 touched_since_gate: HashSet<Address>,
3753 metrics: CacheMetricsSnapshot,
3754}
3755
3756#[derive(serde::Serialize, serde::Deserialize)]
3757struct DurableBlockJournal {
3758 block: BlockRef,
3759 handler_ids: Vec<HandlerId>,
3760 rollback_diffs: Vec<StateDiff>,
3761}
3762
3763struct DurableRuntimeRestorePlan {
3764 checkpoint: Option<DurableRuntimeCheckpoint>,
3765 fallback_history: Vec<BlockRef>,
3766}
3767
3768impl DurableRuntimeRestorePlan {
3769 fn canonical_history(&self) -> Vec<BlockRef> {
3770 self.checkpoint.as_ref().map_or_else(
3771 || self.fallback_history.clone(),
3772 |checkpoint| checkpoint.journal.iter().map(|entry| entry.block).collect(),
3773 )
3774 }
3775}
3776
3777#[derive(Clone)]
3778struct ReactiveRuntimeState<N: Network> {
3779 journal: VecDeque<BlockJournal<N>>,
3780 coverage_head: Option<BlockRef>,
3781 log_coverage_head: Option<BlockRef>,
3782 pending_resyncs: Vec<ResyncRequest>,
3783 health: CacheHealth,
3784 safe_head: Option<BlockRef>,
3785 finalized_head: Option<BlockRef>,
3786 freshness: Option<FreshnessRegistry>,
3787 tracking: HashMap<Address, TrackingPolicy>,
3788 tracked_roots: HashMap<Address, TrackedRoot>,
3789 root_gate_cadence: RootGateCadence,
3790 last_gate_block: Option<u64>,
3791 touched_since_gate: HashSet<Address>,
3792 metrics: CacheMetricsSnapshot,
3793}
3794
3795#[derive(Clone)]
3796struct ChainControlState {
3797 journal_invalidated_from: Option<u64>,
3798 resolved_canonical_blocks: HashMap<(u64, B256), BlockRef>,
3799}
3800
3801#[derive(Default)]
3810struct BatchDroppedCanonical {
3811 identities: HashSet<(u64, B256)>,
3812 implicit_spans: Vec<(u64, u64)>,
3813}
3814
3815impl BatchDroppedCanonical {
3816 fn covers_implicit_number(&self, number: u64) -> bool {
3817 self.implicit_spans
3818 .iter()
3819 .any(|(from, through)| number >= *from && number <= *through)
3820 }
3821
3822 fn contains(&self, block: &BlockRef) -> bool {
3823 self.identities.contains(&(block.number, block.hash))
3824 }
3825
3826 fn record_identity(&mut self, block: &BlockRef) {
3827 self.identities.insert((block.number, block.hash));
3828 }
3829
3830 fn record_explicit(&mut self, _common_ancestor: &BlockRef, old_tip: &BlockRef) {
3831 self.identities.insert((old_tip.number, old_tip.hash));
3832 }
3833
3834 fn record_drained(&mut self, blocks: &[BlockRef]) {
3835 let Some(from) = blocks.iter().map(|block| block.number).min() else {
3836 return;
3837 };
3838 let through = blocks
3839 .iter()
3840 .map(|block| block.number)
3841 .max()
3842 .expect("a non-empty drained set has a maximum");
3843 self.implicit_spans.push((from, through));
3844 self.identities
3845 .extend(blocks.iter().map(|block| (block.number, block.hash)));
3846 }
3847}
3848
3849pub struct ReactiveRegistry<N: Network = Ethereum> {
3857 handlers: BTreeMap<u128, RegisteredHandler<N>>,
3858 handler_positions: HashMap<HandlerId, u128>,
3859 next_handler_position: u128,
3860 indexed_log_handlers: HashMap<LogRouteKey, BTreeSet<u128>>,
3861 fallback_log_handlers: BTreeSet<u128>,
3862 data_slice_shapes: HashMap<(usize, usize), usize>,
3863}
3864
3865struct RegisteredHandler<N: Network = Ethereum> {
3866 id: HandlerId,
3867 handler: Arc<dyn ReactiveHandler<N>>,
3868 interests: Vec<ReactiveInterest<N>>,
3869 has_log_interests: bool,
3870 log_route_index: Option<LogRouteIndex>,
3871}
3872
3873impl<N: Network> Default for ReactiveRegistry<N> {
3874 fn default() -> Self {
3875 Self::new()
3876 }
3877}
3878
3879impl<N: Network> ReactiveRegistry<N> {
3880 pub fn new() -> Self {
3882 Self {
3883 handlers: BTreeMap::new(),
3884 handler_positions: HashMap::new(),
3885 next_handler_position: 0,
3886 indexed_log_handlers: HashMap::new(),
3887 fallback_log_handlers: BTreeSet::new(),
3888 data_slice_shapes: HashMap::new(),
3889 }
3890 }
3891
3892 pub fn register_handler(
3902 &mut self,
3903 handler: Arc<dyn ReactiveHandler<N>>,
3904 ) -> Result<(), RegisterError> {
3905 let id = handler.id();
3906 if self.handler_positions.contains_key(&id) {
3907 return Err(RegisterError::DuplicateHandler(id));
3908 }
3909 let interests = handler.interests();
3910 self.insert_handler_prepared(id, handler, interests);
3911 Ok(())
3912 }
3913
3914 fn insert_handler_prepared(
3915 &mut self,
3916 id: HandlerId,
3917 handler: Arc<dyn ReactiveHandler<N>>,
3918 interests: Vec<ReactiveInterest<N>>,
3919 ) {
3920 debug_assert!(!self.handler_positions.contains_key(&id));
3921 let has_log_interests = interests
3922 .iter()
3923 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)));
3924 let log_route_index = handler.log_route_index();
3925 if self.next_handler_position == u128::MAX {
3926 self.compact_handler_positions();
3927 }
3928 let position = self.next_handler_position;
3929 self.next_handler_position += 1;
3930 self.handler_positions.insert(id.clone(), position);
3931 if let Some(index) = &log_route_index {
3932 for key in index.keys() {
3933 if let LogRouteKey::DataSlice { offset, value } = key {
3934 *self
3935 .data_slice_shapes
3936 .entry((*offset, value.len()))
3937 .or_default() += 1;
3938 }
3939 self.indexed_log_handlers
3940 .entry(key.clone())
3941 .or_default()
3942 .insert(position);
3943 }
3944 } else if has_log_interests {
3945 self.fallback_log_handlers.insert(position);
3946 }
3947 self.handlers.insert(
3948 position,
3949 RegisteredHandler {
3950 id,
3951 handler,
3952 interests,
3953 has_log_interests,
3954 log_route_index,
3955 },
3956 );
3957 }
3958
3959 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
3965 let position = self.handler_positions.remove(id)?;
3966 let registered = self.handlers.remove(&position)?;
3967 if let Some(index) = ®istered.log_route_index {
3968 for key in index.keys() {
3969 let remove_bucket = self
3970 .indexed_log_handlers
3971 .get_mut(key)
3972 .is_some_and(|owners| {
3973 owners.remove(&position);
3974 owners.is_empty()
3975 });
3976 if remove_bucket {
3977 self.indexed_log_handlers.remove(key);
3978 }
3979 if let LogRouteKey::DataSlice { offset, value } = key {
3980 let shape = (*offset, value.len());
3981 let remove_shape =
3982 self.data_slice_shapes.get_mut(&shape).is_some_and(|count| {
3983 *count -= 1;
3984 *count == 0
3985 });
3986 if remove_shape {
3987 self.data_slice_shapes.remove(&shape);
3988 }
3989 }
3990 }
3991 } else {
3992 self.fallback_log_handlers.remove(&position);
3993 }
3994 Some(registered.handler)
3995 }
3996
3997 pub fn contains_handler(&self, id: &HandlerId) -> bool {
3999 self.handler_positions.contains_key(id)
4000 }
4001
4002 pub fn handler_ids(&self) -> Vec<HandlerId> {
4004 self.handlers
4005 .values()
4006 .map(|handler| handler.id.clone())
4007 .collect()
4008 }
4009
4010 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
4012 self.handler_positions
4013 .get(id)
4014 .and_then(|position| self.handlers.get(position))
4015 .map(|registered| registered.interests.as_slice())
4016 }
4017
4018 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
4020 self.handlers
4021 .values()
4022 .flat_map(|handler| handler.interests.clone())
4023 .collect()
4024 }
4025
4026 pub fn log_subscription_filters(&self) -> Vec<Filter> {
4033 let mut filters = Vec::new();
4034 for interest in self.log_interests() {
4035 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
4036 }
4037 filters
4038 }
4039
4040 pub fn route_log(&self, log: &Log) -> Vec<ReactiveLogRoute> {
4046 self.log_handler_candidates(log)
4047 .into_iter()
4048 .filter_map(|handler| handler.route_log(log))
4049 .collect()
4050 }
4051
4052 fn log_handler_candidates(&self, log: &Log) -> Vec<&RegisteredHandler<N>> {
4053 let mut indexed_positions = Vec::new();
4054 if let Some(indexed) = self
4055 .indexed_log_handlers
4056 .get(&LogRouteKey::Emitter(log.address()))
4057 {
4058 indexed_positions.extend(indexed.iter().copied());
4059 }
4060 for (index, value) in log.topics().iter().copied().enumerate() {
4061 if let Some(indexed) = self
4062 .indexed_log_handlers
4063 .get(&LogRouteKey::Topic { index, value })
4064 {
4065 indexed_positions.extend(indexed.iter().copied());
4066 }
4067 }
4068 let data = log.inner.data.data.as_ref();
4069 for &(offset, len) in self.data_slice_shapes.keys() {
4070 let Some(end) = offset.checked_add(len) else {
4071 continue;
4072 };
4073 let Some(value) = data.get(offset..end) else {
4074 continue;
4075 };
4076 if let Some(indexed) = self.indexed_log_handlers.get(&LogRouteKey::DataSlice {
4077 offset,
4078 value: value.to_vec(),
4079 }) {
4080 indexed_positions.extend(indexed.iter().copied());
4081 }
4082 }
4083 if indexed_positions.is_empty() {
4084 if self.fallback_log_handlers.is_empty() {
4085 return Vec::new();
4086 }
4087 if !self.indexed_log_handlers.is_empty() {
4088 return self
4089 .fallback_log_handlers
4090 .iter()
4091 .filter_map(|position| self.handlers.get(position))
4092 .collect();
4093 }
4094 return self
4095 .handlers
4096 .values()
4097 .filter(|handler| handler.has_log_interests && handler.log_route_index.is_none())
4098 .collect();
4099 }
4100
4101 indexed_positions.extend(self.fallback_log_handlers.iter().copied());
4102 indexed_positions.sort_unstable();
4103 indexed_positions.dedup();
4104 indexed_positions
4105 .into_iter()
4106 .filter_map(|position| self.handlers.get(&position))
4107 .collect()
4108 }
4109
4110 fn handlers(&self) -> impl Iterator<Item = &RegisteredHandler<N>> {
4111 self.handlers.values()
4112 }
4113
4114 fn log_interests(&self) -> impl Iterator<Item = &LogInterest> {
4115 self.handlers.values().flat_map(|handler| {
4116 handler
4117 .interests
4118 .iter()
4119 .filter_map(|interest| match interest {
4120 ReactiveInterest::Logs(interest) => Some(interest),
4121 ReactiveInterest::Blocks(_) | ReactiveInterest::PendingTransactions(_) => None,
4122 })
4123 })
4124 }
4125
4126 fn compact_handler_positions(&mut self) {
4127 let handlers = std::mem::take(&mut self.handlers);
4128 self.handler_positions.clear();
4129 self.indexed_log_handlers.clear();
4130 self.fallback_log_handlers.clear();
4131 self.data_slice_shapes.clear();
4132
4133 for (position, (_, handler)) in handlers.into_iter().enumerate() {
4134 let position = position as u128;
4135 self.handler_positions.insert(handler.id.clone(), position);
4136 if let Some(index) = &handler.log_route_index {
4137 for key in index.keys() {
4138 if let LogRouteKey::DataSlice { offset, value } = key {
4139 *self
4140 .data_slice_shapes
4141 .entry((*offset, value.len()))
4142 .or_default() += 1;
4143 }
4144 self.indexed_log_handlers
4145 .entry(key.clone())
4146 .or_default()
4147 .insert(position);
4148 }
4149 } else if handler.has_log_interests {
4150 self.fallback_log_handlers.insert(position);
4151 }
4152 self.handlers.insert(position, handler);
4153 }
4154 self.next_handler_position = self.handlers.len() as u128;
4155 }
4156}
4157
4158impl<N: Network> ReactiveRuntime<N> {
4159 pub fn new(config: ReactiveConfig) -> Self {
4161 Self {
4162 registry: ReactiveRegistry::new(),
4163 hooks: Vec::new(),
4164 config,
4165 journal: VecDeque::new(),
4166 coverage_head: None,
4167 log_coverage_head: None,
4168 pending_resyncs: Vec::new(),
4169 health: CacheHealth::Healthy,
4170 safe_head: None,
4171 finalized_head: None,
4172 metrics: CacheMetrics::default(),
4173 freshness: None,
4174 tracking: HashMap::new(),
4175 tracked_roots: HashMap::new(),
4176 root_gate_cadence: RootGateCadence::default(),
4177 last_gate_block: None,
4178 touched_since_gate: HashSet::new(),
4179 preconfirmed_branch: None,
4180 }
4181 }
4182
4183 fn checkpoint_state(&self) -> ReactiveRuntimeState<N> {
4184 ReactiveRuntimeState {
4185 journal: self.journal.clone(),
4186 coverage_head: self.coverage_head,
4187 log_coverage_head: self.log_coverage_head,
4188 pending_resyncs: self.pending_resyncs.clone(),
4189 health: self.health,
4190 safe_head: self.safe_head,
4191 finalized_head: self.finalized_head,
4192 freshness: self.freshness.clone(),
4193 tracking: self.tracking.clone(),
4194 tracked_roots: self.tracked_roots.clone(),
4195 root_gate_cadence: self.root_gate_cadence,
4196 last_gate_block: self.last_gate_block,
4197 touched_since_gate: self.touched_since_gate.clone(),
4198 metrics: self.metrics.snapshot(),
4199 }
4200 }
4201
4202 fn is_pristine_for_checkpoint_restore(&self) -> bool {
4203 self.preconfirmed_branch.is_none()
4204 && self.journal.is_empty()
4205 && self.coverage_head.is_none()
4206 && self.pending_resyncs.is_empty()
4207 && self.health == CacheHealth::Healthy
4208 && self.safe_head.is_none()
4209 && self.finalized_head.is_none()
4210 && self.tracked_roots.is_empty()
4211 && self.last_gate_block.is_none()
4212 && self.touched_since_gate.is_empty()
4213 && self.metrics.snapshot() == CacheMetricsSnapshot::default()
4214 }
4215
4216 fn adopted_baseline_only(&self) -> Option<BlockRef> {
4217 let baseline = self.coverage_head?;
4218 let journal_is_baseline_only = if self.config.journal_depth == 0 {
4219 self.journal.is_empty()
4220 } else {
4221 self.journal.len() == 1
4222 && self.journal.front().is_some_and(|entry| {
4223 entry.block == baseline
4224 && entry.inputs.is_empty()
4225 && entry.applied.is_empty()
4226 && entry.handler_ids.is_empty()
4227 && entry.resynced.is_empty()
4228 && entry.rollback_diffs.is_empty()
4229 })
4230 };
4231 (self.preconfirmed_branch.is_none()
4232 && journal_is_baseline_only
4233 && self.pending_resyncs.is_empty()
4234 && self.health == CacheHealth::Healthy
4235 && self.safe_head.is_none()
4236 && self.finalized_head.is_none()
4237 && self.tracked_roots.is_empty()
4238 && self.last_gate_block.is_none()
4239 && self.touched_since_gate.is_empty()
4240 && self.metrics.snapshot() == CacheMetricsSnapshot::default())
4241 .then_some(baseline)
4242 }
4243
4244 fn restore_state(&mut self, state: ReactiveRuntimeState<N>) {
4245 self.journal = state.journal;
4246 self.coverage_head = state.coverage_head;
4247 self.log_coverage_head = state.log_coverage_head;
4248 self.pending_resyncs = state.pending_resyncs;
4249 self.health = state.health;
4250 self.safe_head = state.safe_head;
4251 self.finalized_head = state.finalized_head;
4252 self.freshness = state.freshness;
4253 self.tracking = state.tracking;
4254 self.tracked_roots = state.tracked_roots;
4255 self.root_gate_cadence = state.root_gate_cadence;
4256 self.last_gate_block = state.last_gate_block;
4257 self.touched_since_gate = state.touched_since_gate;
4258 self.metrics.restore(state.metrics);
4259 }
4260
4261 fn restore_transaction_state(&mut self, state: ReactiveRuntimeState<N>) {
4262 let metrics = self.metrics.snapshot();
4267 self.restore_state(state);
4268 self.metrics.restore(metrics);
4269 }
4270
4271 fn durable_checkpoint_bytes(&self) -> Result<Vec<u8>, ReactiveEngineError> {
4272 let checkpoint = DurableRuntimeCheckpoint {
4273 version: DURABLE_RUNTIME_CHECKPOINT_VERSION,
4274 safe_head: self.safe_head,
4275 finalized_head: self.finalized_head,
4276 health: self.health,
4277 pending_resyncs: self.pending_resyncs.clone(),
4278 coverage_head: self.coverage_head,
4279 log_coverage_head: self.log_coverage_head,
4280 journal: self
4281 .journal
4282 .iter()
4283 .map(|entry| DurableBlockJournal {
4284 block: entry.block,
4285 handler_ids: entry.handler_ids.clone(),
4286 rollback_diffs: entry.rollback_diffs.clone(),
4287 })
4288 .collect(),
4289 freshness: self.freshness.clone(),
4290 tracking: self.tracking.clone(),
4291 tracked_roots: self.tracked_roots.clone(),
4292 root_gate_cadence: self.root_gate_cadence,
4293 last_gate_block: self.last_gate_block,
4294 touched_since_gate: self.touched_since_gate.clone(),
4295 metrics: self.metrics.snapshot(),
4296 };
4297 bincode::serialize(&checkpoint)
4298 .map_err(|error| ReactiveEngineError::RuntimeCheckpoint(error.to_string()))
4299 }
4300
4301 fn plan_durable_checkpoint_restore(
4302 &self,
4303 bytes: &[u8],
4304 expected_coverage: &BlockRef,
4305 ) -> Result<DurableRuntimeRestorePlan, ReactiveCheckpointRestoreError> {
4306 let mut cursor = std::io::Cursor::new(bytes);
4307 let mut checkpoint: DurableRuntimeCheckpoint = bincode::DefaultOptions::new()
4308 .with_fixint_encoding()
4309 .with_limit(bytes.len() as u64)
4310 .deserialize_from(&mut cursor)
4311 .map_err(|error| {
4312 ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(error.to_string())
4313 })?;
4314 if cursor.position() != bytes.len() as u64 {
4315 return Err(ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(
4316 "runtime checkpoint has trailing bytes".to_owned(),
4317 ));
4318 }
4319 if checkpoint.version != DURABLE_RUNTIME_CHECKPOINT_VERSION {
4320 return Err(ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(
4321 format!(
4322 "unsupported runtime checkpoint version {}",
4323 checkpoint.version
4324 ),
4325 ));
4326 }
4327 self.validate_durable_runtime_checkpoint(&checkpoint, expected_coverage)?;
4328
4329 let retained = self.config.journal_depth.min(checkpoint.journal.len());
4330 let discard = checkpoint.journal.len() - retained;
4331 checkpoint.journal.drain(..discard);
4332 Ok(DurableRuntimeRestorePlan {
4333 checkpoint: Some(checkpoint),
4334 fallback_history: Vec::new(),
4335 })
4336 }
4337
4338 fn apply_durable_checkpoint_restore(&mut self, plan: DurableRuntimeRestorePlan) {
4339 let Some(checkpoint) = plan.checkpoint else {
4340 self.journal = plan
4341 .fallback_history
4342 .into_iter()
4343 .map(|block| BlockJournal {
4344 block,
4345 inputs: Vec::new(),
4346 applied: Vec::new(),
4347 handler_ids: Vec::new(),
4348 resynced: Vec::new(),
4349 rollback_diffs: Vec::new(),
4350 })
4351 .collect();
4352 return;
4353 };
4354 self.safe_head = checkpoint.safe_head;
4355 self.finalized_head = checkpoint.finalized_head;
4356 self.health = checkpoint.health;
4357 self.pending_resyncs = checkpoint.pending_resyncs;
4358 self.coverage_head = checkpoint.coverage_head;
4359 self.log_coverage_head = checkpoint.log_coverage_head;
4360 self.journal = checkpoint
4361 .journal
4362 .into_iter()
4363 .map(|entry| BlockJournal {
4364 block: entry.block,
4365 inputs: Vec::new(),
4366 applied: Vec::new(),
4367 handler_ids: entry.handler_ids,
4368 resynced: Vec::new(),
4369 rollback_diffs: entry.rollback_diffs,
4370 })
4371 .collect();
4372 self.freshness = checkpoint.freshness;
4373 self.tracking = checkpoint.tracking;
4374 self.tracked_roots = checkpoint.tracked_roots;
4375 self.root_gate_cadence = checkpoint.root_gate_cadence;
4376 self.last_gate_block = checkpoint.last_gate_block;
4377 self.touched_since_gate = checkpoint.touched_since_gate;
4378 self.metrics.restore(checkpoint.metrics);
4379 }
4380
4381 fn validate_durable_runtime_checkpoint(
4382 &self,
4383 checkpoint: &DurableRuntimeCheckpoint,
4384 expected_coverage: &BlockRef,
4385 ) -> Result<(), ReactiveCheckpointRestoreError> {
4386 let invalid =
4387 |message: String| ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(message);
4388 let Some(coverage) = checkpoint.coverage_head.as_ref() else {
4389 return Err(invalid(
4390 "runtime checkpoint is missing its canonical coverage head".into(),
4391 ));
4392 };
4393 if !optional_block_refs_are_compatible(Some(coverage), Some(expected_coverage)) {
4394 return Err(invalid(format!(
4395 "runtime coverage {}:{:?} conflicts with checkpoint metadata {}:{:?}",
4396 coverage.number, coverage.hash, expected_coverage.number, expected_coverage.hash
4397 )));
4398 }
4399 for (label, head) in [
4400 ("safe", checkpoint.safe_head.as_ref()),
4401 ("finalized", checkpoint.finalized_head.as_ref()),
4402 ] {
4403 let Some(head) = head else { continue };
4404 if head.number > coverage.number
4405 || (head.number == coverage.number && head.hash != coverage.hash)
4406 {
4407 return Err(invalid(format!(
4408 "{label} head {}:{:?} lies beyond or conflicts with canonical coverage {}:{:?}",
4409 head.number, head.hash, coverage.number, coverage.hash
4410 )));
4411 }
4412 if head.number.checked_add(1) == Some(coverage.number)
4413 && coverage
4414 .parent_hash
4415 .is_some_and(|parent| parent != head.hash)
4416 {
4417 return Err(invalid(format!(
4418 "canonical coverage does not descend from adjacent {label} head"
4419 )));
4420 }
4421 }
4422 if let (Some(finalized), Some(safe)) = (
4423 checkpoint.finalized_head.as_ref(),
4424 checkpoint.safe_head.as_ref(),
4425 ) {
4426 if finalized.number > safe.number
4427 || (finalized.number == safe.number && finalized.hash != safe.hash)
4428 {
4429 return Err(invalid(
4430 "finalized head is above or conflicts with the safe head".into(),
4431 ));
4432 }
4433 if finalized.number.checked_add(1) == Some(safe.number)
4434 && safe.parent_hash != Some(finalized.hash)
4435 {
4436 return Err(invalid(
4437 "adjacent safe head does not descend from finalized head".into(),
4438 ));
4439 }
4440 }
4441
4442 let mut previous: Option<&DurableBlockJournal> = None;
4443 for entry in &checkpoint.journal {
4444 if entry.block.number > coverage.number
4445 || (entry.block.number == coverage.number && entry.block.hash != coverage.hash)
4446 {
4447 return Err(invalid(format!(
4448 "journal block {}:{:?} lies beyond or conflicts with canonical coverage",
4449 entry.block.number, entry.block.hash
4450 )));
4451 }
4452 if let Some(previous) = previous {
4453 if entry.block.number <= previous.block.number {
4454 return Err(invalid(
4455 "runtime journal block numbers are not strictly increasing".into(),
4456 ));
4457 }
4458 if previous.block.number.checked_add(1) == Some(entry.block.number)
4459 && entry.block.parent_hash.is_some()
4460 && entry.block.parent_hash != Some(previous.block.hash)
4461 {
4462 return Err(invalid(
4463 "adjacent runtime journal blocks are not parent-linked".into(),
4464 ));
4465 }
4466 }
4467 for (label, head) in [
4468 ("safe", checkpoint.safe_head.as_ref()),
4469 ("finalized", checkpoint.finalized_head.as_ref()),
4470 ] {
4471 if let Some(head) = head
4472 && head.number == entry.block.number
4473 && !optional_block_refs_are_compatible(Some(head), Some(&entry.block))
4474 {
4475 return Err(invalid(format!(
4476 "{label} head conflicts with the retained journal at block {}",
4477 head.number
4478 )));
4479 }
4480 }
4481 let mut handler_ids = HashSet::new();
4482 if entry
4483 .handler_ids
4484 .iter()
4485 .any(|handler_id| !handler_ids.insert(handler_id))
4486 {
4487 return Err(invalid(
4488 "runtime journal contains duplicate handler generation ids".into(),
4489 ));
4490 }
4491 previous = Some(entry);
4492 }
4493 if let Some(tail) = checkpoint.journal.last()
4494 && tail.block.number == coverage.number
4495 && !optional_block_refs_are_compatible(Some(&tail.block), Some(coverage))
4496 {
4497 return Err(invalid(format!(
4498 "runtime journal tail conflicts with canonical coverage at block {}",
4499 coverage.number
4500 )));
4501 }
4502 if let Some(tail) = checkpoint.journal.last()
4503 && tail.block.number.checked_add(1) == Some(coverage.number)
4504 && coverage
4505 .parent_hash
4506 .is_some_and(|parent_hash| parent_hash != tail.block.hash)
4507 {
4508 return Err(invalid(format!(
4509 "canonical coverage does not descend from adjacent runtime journal tail at block {}",
4510 tail.block.number
4511 )));
4512 }
4513
4514 if let Some(last_gate_block) = checkpoint.last_gate_block {
4515 if last_gate_block > coverage.number {
4516 return Err(invalid(
4517 "root-gate cursor lies beyond canonical coverage".into(),
4518 ));
4519 }
4520 } else if !checkpoint.tracked_roots.is_empty() {
4521 return Err(invalid(
4522 "root-gate baselines exist without a completed gate cursor".into(),
4523 ));
4524 }
4525 for (address, baseline) in &checkpoint.tracked_roots {
4526 let Some(policy) = checkpoint.tracking.get(address) else {
4527 return Err(invalid(
4528 "root-gate baseline has no corresponding tracking policy".into(),
4529 ));
4530 };
4531 if matches!(policy, TrackingPolicy::Slots { .. }) {
4532 return Err(invalid(
4533 "slot-only tracking cannot carry an account root baseline".into(),
4534 ));
4535 }
4536 if baseline.last_block > coverage.number
4537 || checkpoint
4538 .last_gate_block
4539 .is_some_and(|last_gate| baseline.last_block > last_gate)
4540 {
4541 return Err(invalid(
4542 "root-gate baseline lies beyond the committed gate window".into(),
4543 ));
4544 }
4545 }
4546 Ok(())
4547 }
4548
4549 pub fn track_account(&mut self, address: Address, policy: TrackingPolicy) {
4570 self.tracking.insert(address, policy);
4571 self.tracked_roots.remove(&address);
4572 }
4573
4574 pub fn untrack_account(&mut self, address: Address) -> bool {
4578 self.tracked_roots.remove(&address);
4579 self.tracking.remove(&address).is_some()
4580 }
4581
4582 pub fn set_root_gate_cadence(&mut self, cadence: RootGateCadence) {
4590 self.root_gate_cadence = cadence;
4591 self.last_gate_block = None;
4592 self.touched_since_gate.clear();
4593 }
4594
4595 pub const fn log_coverage_head(&self) -> Option<&BlockRef> {
4603 self.log_coverage_head.as_ref()
4604 }
4605
4606 pub fn root_gate_cadence(&self) -> RootGateCadence {
4608 self.root_gate_cadence
4609 }
4610
4611 pub fn enable_freshness_stamping(&mut self) {
4623 if self.freshness.is_none() {
4624 self.freshness = Some(FreshnessRegistry::new());
4625 }
4626 }
4627
4628 pub fn freshness(&self) -> Option<&FreshnessRegistry> {
4633 self.freshness.as_ref()
4634 }
4635
4636 pub fn freshness_mut(&mut self) -> Option<&mut FreshnessRegistry> {
4641 self.freshness.as_mut()
4642 }
4643
4644 pub fn health(&self) -> CacheHealth {
4646 self.health
4647 }
4648
4649 pub fn metrics(&self) -> CacheMetricsSnapshot {
4651 self.metrics.snapshot()
4652 }
4653
4654 pub fn reset_health(&mut self) {
4664 self.health = CacheHealth::Healthy;
4665 }
4666
4667 fn escalate_trust(&mut self, block: u64) -> Option<Arc<ReactiveReport<N>>> {
4681 let to = match self.health {
4682 CacheHealth::Healthy => CacheHealth::Degraded { since_block: block },
4683 CacheHealth::Degraded { .. } => CacheHealth::Unhealthy { since_block: block },
4684 CacheHealth::Unhealthy { .. } => return None,
4685 };
4686 self.transition_health(to, Some(block))
4687 }
4688
4689 fn transition_health(
4697 &mut self,
4698 to: CacheHealth,
4699 block: Option<u64>,
4700 ) -> Option<Arc<ReactiveReport<N>>> {
4701 if to == self.health {
4702 return None;
4703 }
4704 let from = self.health;
4705 self.health = to;
4706 Some(Arc::new(ReactiveReport::Health(HealthReport {
4707 from,
4708 to,
4709 block,
4710 _network: PhantomData,
4711 })))
4712 }
4713
4714 pub fn register_handler(
4721 &mut self,
4722 handler: Arc<dyn ReactiveHandler<N>>,
4723 ) -> Result<(), RegisterError> {
4724 self.registry.register_handler(handler)
4725 }
4726
4727 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
4735 self.registry.unregister_handler(id)
4736 }
4737
4738 pub fn contains_handler(&self, id: &HandlerId) -> bool {
4740 self.registry.contains_handler(id)
4741 }
4742
4743 pub fn handler_ids(&self) -> Vec<HandlerId> {
4745 self.registry.handler_ids()
4746 }
4747
4748 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
4750 self.registry.handler_interests(id)
4751 }
4752
4753 pub fn last_canonical_block(&self) -> Option<BlockRef> {
4763 self.coverage_head
4764 }
4765
4766 pub fn adopt_canonical_baseline(
4783 &mut self,
4784 baseline: BlockRef,
4785 ) -> Result<(), ReactiveBaselineError> {
4786 self.validate_canonical_baseline_adoption(baseline)?;
4787 if self.adopted_baseline_only().is_some() {
4788 return Ok(());
4789 }
4790
4791 self.coverage_head = Some(baseline);
4792 if self.config.journal_depth > 0 {
4793 self.journal.push_back(BlockJournal {
4794 block: baseline,
4795 inputs: Vec::new(),
4796 applied: Vec::new(),
4797 handler_ids: Vec::new(),
4798 resynced: Vec::new(),
4799 rollback_diffs: Vec::new(),
4800 });
4801 }
4802 Ok(())
4803 }
4804
4805 fn validate_canonical_baseline_adoption(
4806 &self,
4807 baseline: BlockRef,
4808 ) -> Result<(), ReactiveBaselineError> {
4809 if let Some(existing) = self.adopted_baseline_only() {
4810 return if existing == baseline {
4811 Ok(())
4812 } else {
4813 Err(ReactiveBaselineError::ConflictingBaseline {
4814 existing_number: existing.number,
4815 existing_hash: existing.hash,
4816 requested_number: baseline.number,
4817 requested_hash: baseline.hash,
4818 })
4819 };
4820 }
4821 if !self.is_pristine_for_checkpoint_restore() {
4822 return Err(ReactiveBaselineError::ActiveRuntime);
4823 }
4824 Ok(())
4825 }
4826
4827 pub const fn safe_head(&self) -> Option<&BlockRef> {
4829 self.safe_head.as_ref()
4830 }
4831
4832 pub const fn finalized_head(&self) -> Option<&BlockRef> {
4834 self.finalized_head.as_ref()
4835 }
4836
4837 pub fn has_journaled_handler_effects(&self, handler_id: &HandlerId) -> bool {
4845 self.journal
4846 .iter()
4847 .any(|entry| entry.handler_ids.contains(handler_id))
4848 }
4849
4850 pub fn journaled_handler_ids(&self) -> HashSet<HandlerId> {
4857 self.journal
4858 .iter()
4859 .flat_map(|entry| entry.handler_ids.iter().cloned())
4860 .collect()
4861 }
4862
4863 pub fn pending_resyncs(&self) -> &[ResyncRequest] {
4874 &self.pending_resyncs
4875 }
4876
4877 pub fn cancel_pending_resync(&mut self, id: &ResyncId) -> Vec<ResyncRequest> {
4886 self.cancel_pending_resyncs_by_id(std::slice::from_ref(id))
4887 }
4888
4889 pub fn cancel_pending_resyncs_by_id(&mut self, ids: &[ResyncId]) -> Vec<ResyncRequest> {
4896 if ids.is_empty() {
4897 return Vec::new();
4898 }
4899 let ids: HashSet<&ResyncId> = ids.iter().collect();
4900 let mut cancelled = Vec::new();
4901 self.pending_resyncs.retain(|request| {
4902 if ids.contains(&request.id) {
4903 cancelled.push(request.clone());
4904 false
4905 } else {
4906 true
4907 }
4908 });
4909 cancelled
4910 }
4911
4912 pub fn cancel_pending_resyncs(&mut self, address: Address) -> Vec<ResyncRequest> {
4927 let mut cancelled = Vec::new();
4928 self.pending_resyncs.retain_mut(|request| {
4929 let (matching, remaining): (Vec<_>, Vec<_>) = request
4930 .targets
4931 .drain(..)
4932 .partition(|target| resync_target_address(target) == address);
4933 request.targets = remaining;
4934 if !matching.is_empty() {
4935 cancelled.push(ResyncRequest {
4936 id: request.id.clone(),
4937 reason: request.reason.clone(),
4938 block: request.block.clone(),
4939 targets: matching,
4940 priority: request.priority,
4941 });
4942 }
4943 !request.targets.is_empty()
4944 });
4945 cancelled
4946 }
4947
4948 pub fn register_hook(&mut self, hook: Arc<dyn ReactiveHook<N>>) -> Result<(), RegisterError> {
4955 self.hooks.push(hook);
4956 Ok(())
4957 }
4958
4959 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
4961 self.registry.interests()
4962 }
4963
4964 pub fn ingest_batch(
4982 &mut self,
4983 cache: &mut EvmCache,
4984 batch: ReactiveInputBatch<N>,
4985 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4986 let preconfirmation = batch_preconfirmation(&batch)?;
4987 if let Some(flashblock) = preconfirmation.as_ref() {
4988 self.prepare_preconfirmed_branch(cache, flashblock)?;
4989 } else {
4990 self.discard_preconfirmed_branch(cache);
4991 }
4992 let cache_state = EvmCacheStateSnapshot::capture(cache);
4993 let runtime_state = self.checkpoint_state();
4994 let batch_report = match self.ingest_batch_direct(cache, batch) {
4995 Ok(report) => report,
4996 Err(error) => {
4997 cache_state.restore(cache);
4998 self.restore_transaction_state(runtime_state);
4999 return Err(error);
5000 }
5001 };
5002 if let Some(flashblock) = preconfirmation {
5003 self.restore_transaction_state(runtime_state);
5004 if let Some(branch) = self.preconfirmed_branch.as_mut() {
5005 branch.flashblock = flashblock;
5006 }
5007 }
5008 self.dispatch_reports(&batch_report.reports);
5009 let _ = &self.config;
5010 Ok(batch_report)
5011 }
5012
5013 pub fn ingest_batch_with_resync(
5030 &mut self,
5031 cache: &mut EvmCache,
5032 batch: ReactiveInputBatch<N>,
5033 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
5034 let preconfirmation = batch_preconfirmation(&batch)?;
5035 if let Some(flashblock) = preconfirmation.as_ref() {
5036 self.prepare_preconfirmed_branch(cache, flashblock)?;
5037 } else {
5038 self.discard_preconfirmed_branch(cache);
5039 }
5040 let cache_state = EvmCacheStateSnapshot::capture(cache);
5041 let runtime_state = self.checkpoint_state();
5042 let batch_report = match self.ingest_batch_with_resync_direct(cache, batch) {
5043 Ok(report) => report,
5044 Err(error) => {
5045 cache_state.restore(cache);
5046 self.restore_transaction_state(runtime_state);
5047 return Err(error);
5048 }
5049 };
5050
5051 if let Some(flashblock) = preconfirmation {
5052 self.restore_transaction_state(runtime_state);
5053 if let Some(branch) = self.preconfirmed_branch.as_mut() {
5054 branch.flashblock = flashblock;
5055 }
5056 }
5057
5058 self.dispatch_reports(&batch_report.reports);
5059 let _ = &self.config;
5060 Ok(batch_report)
5061 }
5062
5063 pub fn active_preconfirmation(&self) -> Option<&FlashblockRef> {
5066 self.preconfirmed_branch
5067 .as_ref()
5068 .map(|branch| &branch.flashblock)
5069 }
5070
5071 pub fn discard_preconfirmation(&mut self, cache: &mut EvmCache) {
5074 self.discard_preconfirmed_branch(cache);
5075 }
5076
5077 fn discard_preconfirmed_branch(&mut self, cache: &mut EvmCache) {
5078 if let Some(branch) = self.preconfirmed_branch.take() {
5079 branch.canonical_cache.restore(cache);
5080 }
5081 }
5082
5083 fn prepare_preconfirmed_branch(
5084 &mut self,
5085 cache: &mut EvmCache,
5086 incoming: &FlashblockRef,
5087 ) -> Result<(), ReactiveError> {
5088 let Some(canonical) = self.coverage_head else {
5089 self.discard_preconfirmed_branch(cache);
5090 return Err(ReactiveError::InvalidInputRecord {
5091 message: "pre-confirmed state requires an exact canonical coverage baseline".into(),
5092 });
5093 };
5094 if canonical.number.checked_add(1) != Some(incoming.block_number) {
5095 self.discard_preconfirmed_branch(cache);
5096 return Err(ReactiveError::InvalidInputRecord {
5097 message: format!(
5098 "pre-confirmed block {} is not the exact successor of canonical block {}",
5099 incoming.block_number, canonical.number
5100 ),
5101 });
5102 }
5103 if incoming.parent_hash != Some(canonical.hash) {
5104 self.discard_preconfirmed_branch(cache);
5105 return Err(ReactiveError::InvalidInputRecord {
5106 message: "pre-confirmed block parent does not match the canonical coverage hash"
5107 .into(),
5108 });
5109 }
5110 if let Some(active) = self.preconfirmed_branch.as_ref()
5111 && active.flashblock.same_payload(incoming)
5112 {
5113 if let (Some(active_index), Some(incoming_index)) =
5114 (active.flashblock.index, incoming.index)
5115 && incoming_index < active_index
5116 {
5117 return Err(ReactiveError::InvalidInputRecord {
5118 message: format!(
5119 "Flashblock index regressed from {active_index} to {incoming_index}"
5120 ),
5121 });
5122 }
5123 if active.flashblock.index.is_some()
5124 && active.flashblock.index == incoming.index
5125 && active.flashblock.content_hash != incoming.content_hash
5126 {
5127 self.discard_preconfirmed_branch(cache);
5128 return Err(ReactiveError::InvalidInputRecord {
5129 message: "same Flashblock payload/index carried conflicting cumulative content"
5130 .into(),
5131 });
5132 }
5133 install_preconfirmed_cache_context(cache, incoming);
5134 return Ok(());
5135 }
5136
5137 self.discard_preconfirmed_branch(cache);
5138 self.preconfirmed_branch = Some(PreconfirmedBranch {
5139 flashblock: incoming.clone(),
5140 canonical_cache: EvmCacheStateSnapshot::capture(cache),
5141 });
5142 install_preconfirmed_cache_context(cache, incoming);
5143 Ok(())
5144 }
5145
5146 fn ingest_batch_with_resync_direct(
5147 &mut self,
5148 cache: &mut EvmCache,
5149 batch: ReactiveInputBatch<N>,
5150 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
5151 let mut batch_report = self.ingest_batch_direct(cache, batch)?;
5152 if !batch_report.resyncs.is_empty() {
5153 let resync_report = execute_resync_requests(cache, &batch_report.resyncs);
5154 let unique_requests = resync_report
5158 .requested
5159 .iter()
5160 .map(|request| &request.id)
5161 .collect::<HashSet<_>>()
5162 .len();
5163 self.metrics
5164 .resync_requests
5165 .fetch_add(unique_requests as u64, Ordering::Relaxed);
5166 self.metrics
5167 .resync_failures
5168 .fetch_add(resync_report.failed.len() as u64, Ordering::Relaxed);
5169 self.remove_pending_resyncs(batch_report.resyncs.iter().map(|request| &request.id));
5170 self.record_journal_resync(&resync_report);
5171 batch_report
5172 .reports
5173 .push(Arc::new(ReactiveReport::Resynced(resync_report)));
5174 }
5175 Ok(batch_report)
5176 }
5177
5178 fn ingest_batch_direct(
5179 &mut self,
5180 cache: &mut EvmCache,
5181 batch: ReactiveInputBatch<N>,
5182 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
5183 let (records, chain_controls, batch_chain_id) = batch.into_runtime_parts();
5184 if let Some(chain_id) = batch_chain_id
5185 && chain_id != cache.chain_id()
5186 {
5187 return Err(ReactiveError::InvalidInputRecord {
5188 message: format!(
5189 "batch chain id {chain_id} does not match cache chain id {}",
5190 cache.chain_id()
5191 ),
5192 });
5193 }
5194 if !chain_controls.is_empty() && batch_chain_id.is_none() {
5195 return Err(ReactiveError::InvalidChainControl {
5196 message: "chain-control batches require an authoritative batch chain id".into(),
5197 });
5198 }
5199 for (record, _, _) in &records {
5200 record.validated_identity()?;
5201 if let Some(chain_id) = record.context.chain_id
5202 && chain_id != cache.chain_id()
5203 {
5204 return Err(ReactiveError::InvalidInputRecord {
5205 message: format!(
5206 "input chain id {chain_id} does not match cache chain id {}",
5207 cache.chain_id()
5208 ),
5209 });
5210 }
5211 }
5212 let records = sort_scoped_records(dedupe_scoped_records(records)?);
5213
5214 let mut batch_report = ReactiveBatchReport::default();
5215 let mut reports_to_dispatch = Vec::new();
5216 let control_split = validate_control_phase_order(&chain_controls)?;
5217 let (pre_record_controls, post_record_controls) = chain_controls.split_at(control_split);
5218 let pre_record_state =
5219 self.validate_ingest_sequence(pre_record_controls, post_record_controls, &records)?;
5220 self.validate_owner_catchup_against_journal(&pre_record_state, &records)?;
5221 let mut batch_dropped = BatchDroppedCanonical::default();
5222 for control in pre_record_controls {
5223 if let ChainControl::Reorg {
5224 common_ancestor,
5225 old_tip,
5226 ..
5227 } = control
5228 {
5229 batch_dropped.record_explicit(common_ancestor, old_tip);
5230 let drained = self
5231 .journal
5232 .iter()
5233 .filter(|entry| entry.block.number > common_ancestor.number)
5234 .map(|entry| entry.block)
5235 .collect::<Vec<_>>();
5236 batch_dropped.record_drained(&drained);
5237 }
5238 }
5239 let certified_progress_through = post_record_controls
5240 .iter()
5241 .filter_map(canonical_coverage_control_block)
5242 .map(|block| block.number)
5243 .max();
5244 for control in pre_record_controls.iter().cloned() {
5245 self.apply_chain_control(cache, control, &mut batch_report, &mut reports_to_dispatch);
5246 }
5247 let mut touched_addrs: HashSet<Address> = HashSet::new();
5252 let mut canonical_batch_block: Option<u64> = None;
5253
5254 for (record, audience, delivery_scope) in records {
5255 let raw_canonical_block = canonical_record_block(&record).copied();
5256 let canonical_block = raw_canonical_block.map(|block| {
5257 pre_record_state
5258 .resolved_canonical_blocks
5259 .get(&(block.number, block.hash))
5260 .copied()
5261 .unwrap_or(block)
5262 });
5263 let input_ref = record.input_ref();
5264 reports_to_dispatch.push(Arc::new(ReactiveReport::Input(InputReport {
5265 input_ref,
5266 context: record.context.clone(),
5267 provider: record.provider.clone(),
5268 _network: PhantomData,
5269 })));
5270
5271 let recovered_reorg = if delivery_scope.advances_canonical_state() {
5272 if let Some(block) = canonical_block.as_ref() {
5273 let gap_is_certified = delivery_scope == DeliveryScope::CanonicalProgress
5274 && certified_progress_through
5275 .is_some_and(|through| block.number <= through);
5276 let parentless_replacement_is_proven = raw_canonical_block.is_some_and(|raw| {
5277 raw.parent_hash.is_none()
5278 && batch_dropped.covers_implicit_number(raw.number)
5279 });
5280 self.recover_for_canonical_input(
5281 cache,
5282 block,
5283 gap_is_certified,
5284 parentless_replacement_is_proven,
5285 &mut reports_to_dispatch,
5286 )
5287 } else {
5288 None
5289 }
5290 } else {
5291 None
5292 };
5293 let recovered_reorg_for_input = recovered_reorg.is_some();
5294 if let Some(reorg_report) = recovered_reorg {
5295 self.metrics
5296 .reorgs_recovered
5297 .fetch_add(1, Ordering::Relaxed);
5298 remove_canceled_resyncs_from_batch(
5299 &mut batch_report.resyncs,
5300 &reorg_report.canceled_resyncs,
5301 );
5302 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5303 }
5304
5305 if reorg_signal_block(&record).is_some() {
5311 if delivery_scope.advances_canonical_state()
5312 && let Some(reorg_report) = self.recover_for_reorged_input(
5313 cache,
5314 &record,
5315 &mut batch_dropped,
5316 &mut reports_to_dispatch,
5317 )
5318 {
5319 self.metrics
5320 .reorgs_recovered
5321 .fetch_add(1, Ordering::Relaxed);
5322 remove_canceled_resyncs_from_batch(
5323 &mut batch_report.resyncs,
5324 &reorg_report.canceled_resyncs,
5325 );
5326 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5327 }
5328 continue;
5329 }
5330
5331 if delivery_scope == DeliveryScope::OwnerCatchup {
5338 self.validate_owner_catchup_record_against_current_journal(&record)?;
5339 }
5340
5341 if delivery_scope.advances_canonical_state()
5342 && let Some(block) = canonical_block.as_ref()
5343 {
5344 canonical_batch_block = Some(block.number);
5347 self.record_journal_input(block, input_ref);
5348 }
5349
5350 if delivery_scope.advances_canonical_state()
5356 && let Some(block) = canonical_block.as_ref()
5357 {
5358 match advance_block_for_canonical_record(cache, &record) {
5359 Some(Ok(())) => {
5360 cache.advance_compact_block(block.number, block.hash, block.timestamp, true)
5361 }
5362 Some(Err(err)) => {
5363 cache.advance_compact_block(
5364 block.number,
5365 block.hash,
5366 block.timestamp,
5367 false,
5368 );
5369 reports_to_dispatch.push(Arc::new(ReactiveReport::Error(
5370 ReactiveErrorReport {
5371 input_ref: Some(input_ref),
5372 message: err.to_string(),
5373 _network: PhantomData,
5374 },
5375 )));
5376 }
5377 None => cache.advance_compact_block(
5378 block.number,
5379 block.hash,
5380 block.timestamp,
5381 !recovered_reorg_for_input,
5382 ),
5383 }
5384 }
5385
5386 let executions = self.execute_handlers(cache, &record, input_ref, &audience)?;
5387 if executions.is_empty() {
5388 continue;
5389 }
5390
5391 reports_to_dispatch.push(Arc::new(ReactiveReport::Decoded(DecodedReport {
5392 input_ref,
5393 handler_ids: executions
5394 .iter()
5395 .map(|execution| execution.handler_id.clone())
5396 .collect(),
5397 _network: PhantomData,
5398 })));
5399
5400 detect_conflicts(input_ref, &executions)?;
5401
5402 let canonical_block_number = delivery_scope
5408 .advances_canonical_state()
5409 .then_some(canonical_block)
5410 .flatten()
5411 .map(|block| block.number);
5412
5413 for execution in executions {
5414 let diff = if execution.state_updates.is_empty() {
5415 StateDiff::default()
5416 } else {
5417 cache.apply_updates(&execution.state_updates)
5418 };
5419
5420 batch_report
5421 .resyncs
5422 .extend(execution.resyncs.iter().cloned());
5423 self.pending_resyncs
5424 .extend(execution.resyncs.iter().cloned());
5425 batch_report
5426 .speculative
5427 .extend(execution.speculative.iter().cloned());
5428
5429 let applied = AppliedReport {
5430 input_ref,
5431 handler_id: execution.handler_id,
5432 quality: execution.quality,
5433 tags: execution.tags,
5434 diff,
5435 state_updates: execution.state_updates,
5436 invalidations: execution.invalidations,
5437 resyncs: execution.resyncs,
5438 speculative: execution.speculative,
5439 hook_signals: execution.hook_signals,
5440 _network: PhantomData,
5441 };
5442 if let (Some(number), Some(registry)) =
5451 (canonical_block_number, self.freshness.as_mut())
5452 {
5453 for change in &applied.diff.slots {
5454 registry.valid_through_slot(change.address, change.slot, number);
5455 }
5456 }
5457
5458 if delivery_scope.advances_canonical_state() {
5466 collect_diff_addresses(&applied.diff, &mut touched_addrs);
5467 }
5468
5469 let report = Arc::new(ReactiveReport::Applied(applied.clone()));
5470 reports_to_dispatch.push(report);
5471 if let Some(block) = canonical_block.as_ref() {
5472 if delivery_scope.advances_canonical_state() {
5473 self.record_journal_applied(block, applied.clone());
5474 } else {
5475 self.record_journal_applied_if_present(block, applied.clone());
5476 }
5477 }
5478 batch_report.applied.push(applied);
5479 }
5480 }
5481
5482 for control in post_record_controls.iter().cloned() {
5487 if let Some(block) = canonical_coverage_control_block(&control) {
5488 canonical_batch_block = Some(
5489 canonical_batch_block.map_or(block.number, |current| current.max(block.number)),
5490 );
5491 }
5492 self.apply_chain_control(cache, control, &mut batch_report, &mut reports_to_dispatch);
5493 }
5494
5495 if self.root_gate_runnable(cache) {
5505 self.touched_since_gate
5506 .extend(touched_addrs.iter().copied());
5507 if self.root_gate_due(canonical_batch_block) {
5508 let accumulated = std::mem::take(&mut self.touched_since_gate);
5509 self.run_root_gate(
5510 cache,
5511 canonical_batch_block,
5512 &accumulated,
5513 &mut batch_report.resyncs,
5514 &mut reports_to_dispatch,
5515 );
5516 self.last_gate_block = canonical_batch_block;
5517 }
5518 } else {
5519 self.touched_since_gate.clear();
5526 }
5527
5528 batch_report.reports = reports_to_dispatch;
5529 Ok(batch_report)
5530 }
5531
5532 fn validate_owner_catchup_against_journal(
5546 &self,
5547 control_state: &ChainControlState,
5548 records: &[(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)],
5549 ) -> Result<(), ReactiveError> {
5550 for (record, _, delivery_scope) in records {
5551 if *delivery_scope != DeliveryScope::OwnerCatchup {
5552 continue;
5553 }
5554 if reorg_signal_block(record).is_some() {
5559 continue;
5560 }
5561 let context_block = canonical_record_block(record).ok_or_else(|| {
5562 ReactiveError::InvalidChainControl {
5563 message: "owner catch-up input has no canonical block identity".into(),
5564 }
5565 })?;
5566 let block = resolve_record_block_payload_metadata(record, *context_block)?;
5567 let invalidated_by_control = control_state
5568 .journal_invalidated_from
5569 .is_some_and(|from| block.number >= from);
5570 let rollbackable = !invalidated_by_control
5571 && self.journal.iter().any(|entry| {
5572 optional_block_refs_are_compatible(Some(&entry.block), Some(&block))
5573 });
5574 if !rollbackable {
5575 return Err(ReactiveError::OwnerCatchupOutsideJournal {
5576 number: block.number,
5577 hash: block.hash,
5578 });
5579 }
5580 }
5581 Ok(())
5582 }
5583
5584 fn validate_owner_catchup_record_against_current_journal(
5585 &self,
5586 record: &ReactiveInputRecord<N>,
5587 ) -> Result<(), ReactiveError> {
5588 let context_block =
5589 canonical_record_block(record).ok_or_else(|| ReactiveError::InvalidChainControl {
5590 message: "owner catch-up input has no canonical block identity".into(),
5591 })?;
5592 let block = resolve_record_block_payload_metadata(record, *context_block)?;
5593 if self
5594 .journal
5595 .iter()
5596 .any(|entry| optional_block_refs_are_compatible(Some(&entry.block), Some(&block)))
5597 {
5598 return Ok(());
5599 }
5600 Err(ReactiveError::OwnerCatchupOutsideJournal {
5601 number: block.number,
5602 hash: block.hash,
5603 })
5604 }
5605
5606 fn root_gate_runnable(&self, cache: &EvmCache) -> bool {
5611 if matches!(self.root_gate_cadence, RootGateCadence::Disabled) {
5612 return false;
5613 }
5614 let has_gated_targets = self
5615 .tracking
5616 .values()
5617 .any(|policy| !matches!(policy, TrackingPolicy::Slots { .. }));
5618 has_gated_targets && cache.account_proof_fetcher().is_some()
5619 }
5620
5621 fn root_gate_due(&self, canonical_block: Option<u64>) -> bool {
5625 let Some(block) = canonical_block else {
5626 return false;
5627 };
5628 match self.root_gate_cadence {
5629 RootGateCadence::Disabled => false,
5630 RootGateCadence::EveryNBlocks(n) => match self.last_gate_block {
5631 None => true,
5632 Some(last) => block >= last.saturating_add(n.get()),
5633 },
5634 }
5635 }
5636
5637 fn run_root_gate(
5663 &mut self,
5664 cache: &EvmCache,
5665 canonical_block: Option<u64>,
5666 touched: &HashSet<Address>,
5667 resyncs: &mut Vec<ResyncRequest>,
5668 reports: &mut Vec<Arc<ReactiveReport<N>>>,
5669 ) {
5670 if self.tracking.is_empty() {
5671 return;
5672 }
5673 let Some(block) = canonical_block else {
5674 return;
5675 };
5676 let Some(fetcher) = cache.account_proof_fetcher().cloned() else {
5677 return;
5678 };
5679
5680 let mut targets: Vec<(Address, bool)> = self
5683 .tracking
5684 .iter()
5685 .filter_map(|(address, policy)| match policy {
5686 TrackingPolicy::Slots { .. } => None,
5687 TrackingPolicy::WholeAccount => Some((*address, true)),
5688 TrackingPolicy::Scalars => Some((*address, false)),
5689 })
5690 .collect();
5691 if targets.is_empty() {
5692 return;
5693 }
5694 targets.sort_by_key(|(address, _)| *address);
5695
5696 let block_id = BlockId::number(block);
5697 let mut probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
5702 targets
5703 .iter()
5704 .map(|&(address, _)| (address, vec![]))
5705 .collect(),
5706 block_id,
5707 )
5708 .into_iter()
5709 .collect();
5710 for (address, whole_account) in targets {
5711 let Some(Ok(proof)) = probes.remove(&address) else {
5712 continue;
5715 };
5716
5717 let baseline = self.tracked_roots.get(&address).cloned();
5718 let Some(baseline) = baseline else {
5719 self.adopt_root(address, block, &proof);
5721 continue;
5722 };
5723
5724 if block <= baseline.last_block {
5729 continue;
5730 }
5731
5732 if whole_account {
5733 if proof.storage_hash == baseline.last_root {
5734 continue;
5736 }
5737 if !touched.contains(&address) {
5739 reports.push(Arc::new(ReactiveReport::CoverageGap(CoverageGapReport {
5741 address,
5742 block,
5743 _network: PhantomData,
5744 })));
5745 self.metrics.coverage_gaps.fetch_add(1, Ordering::Relaxed);
5746 resyncs.push(root_moved_account_resync(
5747 address,
5748 block,
5749 AccountFieldMask {
5750 balance: true,
5751 nonce: true,
5752 code: true,
5753 },
5754 ));
5755 }
5756 self.adopt_root(address, block, &proof);
5758 } else {
5759 let balance_moved = proof.balance != baseline.balance;
5762 let nonce_moved = proof.nonce != baseline.nonce;
5763 let code_moved = proof.code_hash != baseline.code_hash;
5764 if (balance_moved || nonce_moved || code_moved) && !touched.contains(&address) {
5765 resyncs.push(root_moved_account_resync(
5766 address,
5767 block,
5768 AccountFieldMask {
5769 balance: balance_moved,
5770 nonce: nonce_moved,
5771 code: code_moved,
5772 },
5773 ));
5774 }
5775 self.adopt_root(address, block, &proof);
5776 }
5777 }
5778 }
5779
5780 fn adopt_root(&mut self, address: Address, block: u64, proof: &AccountProof) {
5782 self.tracked_roots.insert(
5783 address,
5784 TrackedRoot {
5785 last_root: proof.storage_hash,
5786 last_block: block,
5787 balance: proof.balance,
5788 nonce: proof.nonce,
5789 code_hash: proof.code_hash,
5790 },
5791 );
5792 }
5793
5794 fn execute_handlers(
5795 &self,
5796 cache: &EvmCache,
5797 record: &ReactiveInputRecord<N>,
5798 input_ref: InputRef,
5799 audience: &DeliveryAudience,
5800 ) -> Result<Vec<HandlerExecution>, ReactiveError> {
5801 let mut executions = Vec::new();
5802 let candidates: Vec<_> = match &record.input {
5803 ReactiveInput::Log(log) => self.registry.log_handler_candidates(log),
5804 ReactiveInput::BlockHeader(_)
5805 | ReactiveInput::FullBlock(_)
5806 | ReactiveInput::PendingTxHash(_)
5807 | ReactiveInput::PendingTx(_) => self.registry.handlers().collect(),
5808 };
5809 for registered in candidates {
5810 match audience {
5811 DeliveryAudience::Owners(owners) if !owners.contains(®istered.id) => continue,
5812 DeliveryAudience::AllExcept(excluded) if excluded.contains(®istered.id) => {
5813 continue;
5814 }
5815 DeliveryAudience::All
5816 | DeliveryAudience::Owners(_)
5817 | DeliveryAudience::AllExcept(_) => {}
5818 }
5819 if !registered.matches(&record.input) {
5820 continue;
5821 }
5822
5823 let outcome = registered
5824 .handler
5825 .handle(&record.context, &record.input, cache)
5826 .map_err(|source| ReactiveError::HandlerFailed {
5827 handler_id: registered.id.clone(),
5828 source,
5829 })?;
5830
5831 if let Err(error) =
5832 validate_effects(input_ref, &record.context, ®istered.id, &outcome.effects)
5833 {
5834 if matches!(error, ReactiveError::InvalidPendingEffect { .. }) {
5835 self.metrics
5836 .pending_contamination
5837 .fetch_add(1, Ordering::Relaxed);
5838 }
5839 return Err(error);
5840 }
5841 executions.push(HandlerExecution::from_outcome(
5842 registered.id.clone(),
5843 input_ref,
5844 outcome,
5845 matches!(
5846 record.context.chain_status,
5847 ChainStatus::Preconfirmed { .. }
5848 ),
5849 ));
5850 }
5851 Ok(executions)
5852 }
5853
5854 fn dispatch_reports(&self, reports: &[Arc<ReactiveReport<N>>]) {
5855 for report in reports {
5856 for hook in &self.hooks {
5857 hook.on_report(report.clone());
5858 }
5859 }
5860 }
5861
5862 fn apply_chain_control(
5863 &mut self,
5864 cache: &mut EvmCache,
5865 control: ChainControl,
5866 batch_report: &mut ReactiveBatchReport<N>,
5867 reports: &mut Vec<Arc<ReactiveReport<N>>>,
5868 ) {
5869 match &control {
5870 ChainControl::Safe(block) => set_or_enrich_block_ref(&mut self.safe_head, block),
5871 ChainControl::Finalized(block) => {
5872 set_or_enrich_block_ref(&mut self.finalized_head, block);
5873 }
5874 ChainControl::CanonicalProgress(block)
5875 | ChainControl::Barrier {
5876 block: Some(block), ..
5877 } => {
5878 let preserve_env = self.coverage_head.as_ref().is_some_and(|current| {
5879 optional_block_refs_are_compatible(Some(current), Some(block))
5880 });
5881 cache.advance_compact_block(
5882 block.number,
5883 block.hash,
5884 block.timestamp,
5885 preserve_env,
5886 );
5887 advance_or_enrich_coverage(&mut self.coverage_head, block);
5888 let enriched = self.journal_entry_mut(block).block;
5889 advance_or_enrich_coverage(&mut self.coverage_head, &enriched);
5890 self.trim_journal();
5891 }
5892 ChainControl::LogCoverage(block) => {
5893 set_or_enrich_block_ref(&mut self.log_coverage_head, block);
5896 }
5897 ChainControl::Barrier { block: None, .. } => {}
5898 ChainControl::Reorg {
5899 common_ancestor,
5900 old_tip,
5901 ..
5902 } => {
5903 cache.invalidate_cached_block_hashes_from(common_ancestor.number.saturating_add(1));
5904 self.rebase_validation_state_from(common_ancestor.number.saturating_add(1));
5905 let dropped = if let Some(ancestor_index) = self.journal.iter().rposition(|entry| {
5906 entry.block.number == common_ancestor.number
5907 && entry.block.hash == common_ancestor.hash
5908 }) {
5909 self.drain_journal_after(ancestor_index)
5910 } else {
5911 if self
5916 .journal
5917 .front()
5918 .is_none_or(|entry| entry.block.number > common_ancestor.number)
5919 {
5920 reports.extend(
5921 self.warn_under_recovery(common_ancestor.number.saturating_add(1)),
5922 );
5923 }
5924 self.drain_journal_from_number(common_ancestor.number.saturating_add(1))
5925 };
5926
5927 let reorg_report = self
5928 .recover_dropped_journals(cache, dropped, ReorgReason::Explicit)
5929 .unwrap_or_else(|| ReorgReport {
5930 dropped: Some(*old_tip),
5931 dropped_blocks: Vec::new(),
5932 dropped_inputs: Vec::new(),
5933 rollback_updates: Vec::new(),
5934 rollback_diff: StateDiff::default(),
5935 purge_updates: Vec::new(),
5936 purge_diff: StateDiff::default(),
5937 canceled_resyncs: self
5938 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(old_tip)),
5939 reason: ReorgReason::Explicit,
5940 _network: PhantomData,
5941 });
5942 remove_canceled_resyncs_from_batch(
5943 &mut batch_report.resyncs,
5944 &reorg_report.canceled_resyncs,
5945 );
5946 self.metrics
5947 .reorgs_recovered
5948 .fetch_add(1, Ordering::Relaxed);
5949 reports.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5950
5951 if self.safe_head.as_ref().is_some_and(|head| {
5952 head.number > common_ancestor.number
5953 || (head.number == common_ancestor.number
5954 && head.hash != common_ancestor.hash)
5955 }) {
5956 self.safe_head = None;
5957 }
5958 if self.finalized_head.as_ref().is_some_and(|head| {
5959 head.number > common_ancestor.number
5960 || (head.number == common_ancestor.number
5961 && head.hash != common_ancestor.hash)
5962 }) {
5963 self.finalized_head = None;
5964 }
5965 let mut enriched_ancestor = *common_ancestor;
5966 if let Some(entry) = self.journal.iter().find(|entry| {
5967 entry.block.number == common_ancestor.number
5968 && entry.block.hash == common_ancestor.hash
5969 }) {
5970 enrich_block_ref(&mut enriched_ancestor, &entry.block);
5971 }
5972 if let Some(current) = self.coverage_head.as_ref()
5973 && current.number == common_ancestor.number
5974 && current.hash == common_ancestor.hash
5975 {
5976 enrich_block_ref(&mut enriched_ancestor, current);
5977 }
5978 self.coverage_head = Some(enriched_ancestor);
5979 cache.advance_compact_block(
5980 enriched_ancestor.number,
5981 enriched_ancestor.hash,
5982 enriched_ancestor.timestamp,
5983 false,
5984 );
5985 let enriched_ancestor = self.journal_entry_mut(&enriched_ancestor).block;
5986 self.coverage_head = Some(enriched_ancestor);
5987 self.trim_journal();
5988 }
5989 }
5990 reports.push(Arc::new(ReactiveReport::ChainControl(ChainControlReport {
5991 control,
5992 })));
5993 }
5994
5995 fn validate_ingest_sequence(
5996 &self,
5997 pre_record_controls: &[ChainControl],
5998 post_record_controls: &[ChainControl],
5999 records: &[(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)],
6000 ) -> Result<ChainControlState, ReactiveError> {
6001 let mut controls =
6002 Vec::with_capacity(pre_record_controls.len() + post_record_controls.len());
6003 controls.extend_from_slice(pre_record_controls);
6004 controls.extend_from_slice(post_record_controls);
6005 let state = CanonicalSequenceState::new(
6006 self.journal.iter().map(|entry| entry.block).collect(),
6007 self.coverage_head,
6008 self.safe_head,
6009 self.finalized_head,
6010 )
6011 .with_log_coverage_head(self.log_coverage_head);
6012 let record_metadata = records
6013 .iter()
6014 .map(|(record, _, scope)| (record, *scope))
6015 .collect::<Vec<_>>();
6016 let validation = validate_canonical_sequence_parts(
6017 &state,
6018 &controls,
6019 &record_metadata,
6020 CanonicalSequenceValidationPolicy::ObserveIncompleteRollback,
6021 )
6022 .map_err(CanonicalSequenceError::into_reactive_error)?;
6023 let mut resolved_canonical_blocks = HashMap::new();
6024 for mutation in validation.mutations() {
6025 if let CanonicalSequenceMutation::Canonical(block) = mutation {
6026 resolved_canonical_blocks
6027 .entry((block.number, block.hash))
6028 .and_modify(|known| enrich_block_ref(known, block))
6029 .or_insert(*block);
6030 }
6031 }
6032 Ok(ChainControlState {
6033 journal_invalidated_from: pre_record_controls
6034 .iter()
6035 .filter_map(|control| match control {
6036 ChainControl::Reorg {
6037 common_ancestor, ..
6038 } => Some(common_ancestor.number.saturating_add(1)),
6039 _ => None,
6040 })
6041 .min(),
6042 resolved_canonical_blocks,
6043 })
6044 }
6045
6046 fn recover_for_canonical_input(
6047 &mut self,
6048 cache: &mut EvmCache,
6049 block: &BlockRef,
6050 gap_is_certified: bool,
6051 parentless_replacement_is_proven: bool,
6052 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
6053 ) -> Option<ReorgReport<N>> {
6054 let latest = self
6055 .coverage_head
6056 .or_else(|| self.journal.back().map(|entry| entry.block))?;
6057
6058 if latest.number == block.number && latest.hash == block.hash {
6059 return None;
6060 }
6061
6062 if self
6063 .journal
6064 .iter()
6065 .any(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
6066 {
6067 return None;
6068 }
6069
6070 if latest.number.checked_add(1) == Some(block.number)
6071 && (block.parent_hash == Some(latest.hash)
6072 || (parentless_replacement_is_proven && block.parent_hash.is_none()))
6073 {
6074 return None;
6075 }
6076
6077 if latest
6078 .number
6079 .checked_add(1)
6080 .is_some_and(|next| block.number > next)
6081 {
6082 if !gap_is_certified {
6089 self.metrics.missed_ranges.fetch_add(1, Ordering::Relaxed);
6090 health_reports.extend(self.escalate_trust(block.number));
6091 health_reports.push(Arc::new(ReactiveReport::MissedBlockRange(
6092 MissedRangeReport {
6093 from: latest.number + 1,
6094 to: block.number - 1,
6095 block: block.number,
6096 _network: PhantomData,
6097 },
6098 )));
6099 }
6100 return None;
6101 }
6102
6103 let (dropped, authenticated_anchor) = if let Some(parent_hash) = block.parent_hash {
6104 if let Some(parent_index) = self.journal.iter().rposition(|entry| {
6105 entry.block.number.checked_add(1) == Some(block.number)
6106 && entry.block.hash == parent_hash
6107 }) {
6108 let parent = self.journal[parent_index].block;
6109 cache.invalidate_cached_block_hashes_from(parent.number.saturating_add(1));
6110 (self.drain_journal_after(parent_index), Some(parent))
6111 } else {
6112 let proven_finalized_anchor = self.finalized_head.filter(|finalized| {
6118 finalized.number.checked_add(1) == Some(block.number)
6119 && parent_hash == finalized.hash
6120 });
6121 let invalidated_from = proven_finalized_anchor
6122 .map_or(0, |finalized| finalized.number.saturating_add(1));
6123 cache.invalidate_cached_block_hashes_from(invalidated_from);
6124 if block.number > 0 {
6125 cache.set_cached_block_hash(block.number.saturating_sub(1), parent_hash);
6129 }
6130 health_reports.extend(self.warn_under_recovery(block.number));
6131 let dropped = if let Some(finalized) = proven_finalized_anchor {
6132 self.drain_journal_from_number(finalized.number.saturating_add(1))
6133 } else {
6134 self.drain_journal_from_number(0)
6135 };
6136 (dropped, proven_finalized_anchor)
6137 }
6138 } else {
6139 cache.invalidate_cached_block_hashes_from(0);
6141 health_reports.extend(self.warn_under_recovery(block.number));
6142 (self.drain_journal_from_number(0), None)
6143 };
6144
6145 self.rebase_validation_state_from(
6146 authenticated_anchor.map_or(0, |anchor| anchor.number.saturating_add(1)),
6147 );
6148 let report = self
6149 .recover_dropped_journals(cache, dropped, ReorgReason::ParentMismatch)
6150 .or_else(|| {
6151 Some(ReorgReport {
6152 dropped: Some(latest),
6153 dropped_blocks: Vec::new(),
6154 dropped_inputs: Vec::new(),
6155 rollback_updates: Vec::new(),
6156 rollback_diff: StateDiff::default(),
6157 purge_updates: Vec::new(),
6158 purge_diff: StateDiff::default(),
6159 canceled_resyncs: self
6160 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&latest)),
6161 reason: ReorgReason::ParentMismatch,
6162 _network: PhantomData,
6163 })
6164 });
6165 self.coverage_head = authenticated_anchor;
6166 for head in [&mut self.safe_head, &mut self.finalized_head] {
6167 if head.is_some_and(|head| {
6168 authenticated_anchor.is_none_or(|anchor| {
6169 head.number > anchor.number
6170 || (head.number == anchor.number && head.hash != anchor.hash)
6171 })
6172 }) {
6173 *head = None;
6174 }
6175 }
6176 if let Some(anchor) = authenticated_anchor {
6177 cache.advance_compact_block(anchor.number, anchor.hash, anchor.timestamp, false);
6178 }
6179 report
6180 }
6181
6182 fn recover_for_reorged_input(
6183 &mut self,
6184 cache: &mut EvmCache,
6185 record: &ReactiveInputRecord<N>,
6186 batch_dropped: &mut BatchDroppedCanonical,
6187 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
6188 ) -> Option<ReorgReport<N>> {
6189 let (incoming_dropped_block, reason) = reorg_signal_block(record)?;
6190 if batch_dropped.contains(&incoming_dropped_block) {
6191 let canceled_resyncs = self
6196 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&incoming_dropped_block));
6197 return (!canceled_resyncs.is_empty()).then(|| ReorgReport {
6198 dropped: Some(incoming_dropped_block),
6199 dropped_blocks: vec![incoming_dropped_block],
6200 dropped_inputs: Vec::new(),
6201 rollback_updates: Vec::new(),
6202 rollback_diff: StateDiff::default(),
6203 purge_updates: Vec::new(),
6204 purge_diff: StateDiff::default(),
6205 canceled_resyncs,
6206 reason,
6207 _network: PhantomData,
6208 });
6209 }
6210 let exact_index = self.journal.iter().position(|entry| {
6211 entry.block.number == incoming_dropped_block.number
6212 && entry.block.hash == incoming_dropped_block.hash
6213 });
6214 let mut dropped_block = exact_index
6215 .map(|index| self.journal[index].block)
6216 .or_else(|| {
6217 self.coverage_head.filter(|known| {
6218 known.number == incoming_dropped_block.number
6219 && known.hash == incoming_dropped_block.hash
6220 })
6221 })
6222 .unwrap_or(incoming_dropped_block);
6223 enrich_block_ref(&mut dropped_block, &incoming_dropped_block);
6224 let replacement_is_known = exact_index.is_none()
6225 && (self.journal.iter().any(|entry| {
6226 entry.block.number == dropped_block.number && entry.block.hash != dropped_block.hash
6227 }) || self.coverage_head.is_some_and(|head| {
6228 head.number == dropped_block.number && head.hash != dropped_block.hash
6229 }));
6230
6231 if replacement_is_known {
6232 let canceled_resyncs =
6236 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
6237 return (!canceled_resyncs.is_empty()).then(|| ReorgReport {
6238 dropped: Some(dropped_block),
6239 dropped_blocks: vec![dropped_block],
6240 dropped_inputs: Vec::new(),
6241 rollback_updates: Vec::new(),
6242 rollback_diff: StateDiff::default(),
6243 purge_updates: Vec::new(),
6244 purge_diff: StateDiff::default(),
6245 canceled_resyncs,
6246 reason,
6247 _network: PhantomData,
6248 });
6249 }
6250
6251 let authenticated_anchor = exact_index.and_then(|index| {
6252 let ancestor_number = dropped_block.number.checked_sub(1)?;
6253 let retained = self
6254 .journal
6255 .iter()
6256 .take(index)
6257 .rev()
6258 .find(|entry| entry.block.number == ancestor_number)
6259 .map(|entry| entry.block);
6260 let synthetic_parent = dropped_block.parent_hash.map(|hash| BlockRef {
6261 number: ancestor_number,
6262 hash,
6263 parent_hash: None,
6264 timestamp: None,
6265 });
6266 let finalized_fallback = self
6267 .finalized_head
6268 .filter(|head| head.number == ancestor_number);
6269 let mut anchor = retained.or(synthetic_parent).or(finalized_fallback)?;
6270 for head in [self.safe_head.as_ref(), self.finalized_head.as_ref()]
6271 .into_iter()
6272 .flatten()
6273 {
6274 if head.number == anchor.number && head.hash == anchor.hash {
6275 enrich_block_ref(&mut anchor, head);
6276 }
6277 }
6278 Some(anchor)
6279 });
6280
6281 cache.invalidate_cached_block_hashes_from(dropped_block.number);
6282 let dropped = if let Some(index) = exact_index {
6283 self.drain_journal_from(index)
6284 } else {
6285 health_reports.extend(self.warn_under_recovery(dropped_block.number));
6286 self.drain_journal_from_number(dropped_block.number)
6287 };
6288 let drained_blocks = dropped.iter().map(|entry| entry.block).collect::<Vec<_>>();
6289 batch_dropped.record_drained(&drained_blocks);
6290 batch_dropped.record_identity(&dropped_block);
6291 self.rebase_validation_state_from(dropped_block.number);
6292
6293 let recovered_journal = !dropped.is_empty();
6294 let report = if !recovered_journal {
6295 let canceled_resyncs =
6296 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
6297 Some(ReorgReport {
6298 dropped: Some(dropped_block),
6299 dropped_blocks: Vec::new(),
6300 dropped_inputs: Vec::new(),
6301 rollback_updates: Vec::new(),
6302 rollback_diff: StateDiff::default(),
6303 purge_updates: Vec::new(),
6304 purge_diff: StateDiff::default(),
6305 canceled_resyncs,
6306 reason,
6307 _network: PhantomData,
6308 })
6309 } else {
6310 self.recover_dropped_journals(cache, dropped, reason)
6311 };
6312
6313 if recovered_journal {
6314 if let Some(anchor) = authenticated_anchor {
6315 self.coverage_head = Some(anchor);
6316 }
6317 let coverage = self.coverage_head;
6318 for head in [&mut self.safe_head, &mut self.finalized_head] {
6319 if head.is_some_and(|head| {
6320 coverage.is_none_or(|coverage| {
6321 head.number > coverage.number
6322 || (head.number == coverage.number && head.hash != coverage.hash)
6323 })
6324 }) {
6325 *head = None;
6326 }
6327 }
6328 }
6329
6330 if recovered_journal
6331 && report.is_some()
6332 && let Some(head) = self.coverage_head
6333 {
6334 cache.advance_compact_block(head.number, head.hash, head.timestamp, false);
6335 }
6336 report
6337 }
6338
6339 fn warn_under_recovery(&mut self, reorg_number: u64) -> Option<Arc<ReactiveReport<N>>> {
6351 let oldest_journaled = self.journal.front().map(|entry| entry.block.number);
6352 tracing::warn!(
6353 reorg_block = reorg_number,
6354 oldest_journaled = ?oldest_journaled,
6355 journal_depth = self.config.journal_depth,
6356 "reactive reorg recovery is incomplete: the reorged block is no longer \
6357 in the journal, so effects from blocks aged out of the journal are \
6358 neither rolled back nor purged (the freshness/validation loop is the \
6359 backstop). Increase ReactiveConfig::journal_depth to recover deeper \
6360 reorgs precisely."
6361 );
6362
6363 self.metrics.deep_reorgs.fetch_add(1, Ordering::Relaxed);
6364
6365 self.escalate_trust(reorg_number)
6366 }
6367
6368 fn record_journal_input(&mut self, block: &BlockRef, input_ref: InputRef) {
6369 advance_or_enrich_coverage(&mut self.coverage_head, block);
6370 let entry = self.journal_entry_mut(block);
6371 let enriched = entry.block;
6372 if !entry.inputs.contains(&input_ref) {
6373 entry.inputs.push(input_ref);
6374 }
6375 advance_or_enrich_coverage(&mut self.coverage_head, &enriched);
6376 self.trim_journal();
6377 }
6378
6379 fn record_journal_applied(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
6380 let entry = self.journal_entry_mut(block);
6381 if !entry.handler_ids.contains(&applied.handler_id) {
6382 entry.handler_ids.push(applied.handler_id.clone());
6383 }
6384 entry.rollback_diffs.push(applied.diff.clone());
6385 entry.applied.push(applied);
6386 self.trim_journal();
6387 }
6388
6389 fn record_journal_applied_if_present(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
6390 let Some(entry) = self
6391 .journal
6392 .iter_mut()
6393 .find(|entry| entry.block.number == block.number && entry.block.hash == block.hash)
6394 else {
6395 return;
6396 };
6397 if !entry.handler_ids.contains(&applied.handler_id) {
6398 entry.handler_ids.push(applied.handler_id.clone());
6399 }
6400 entry.rollback_diffs.push(applied.diff.clone());
6401 entry.applied.push(applied);
6402 }
6403
6404 fn record_journal_resync(&mut self, report: &ResyncReport) {
6405 if report.diff.is_empty() {
6406 return;
6407 }
6408 let Some(block) = single_hash_pinned_resync_block(report) else {
6409 return;
6410 };
6411 let entry = self.journal_entry_mut(&block);
6412 entry.rollback_diffs.push(report.diff.clone());
6413 entry.resynced.push(report.clone());
6414 self.trim_journal();
6415 }
6416
6417 fn journal_entry_mut(&mut self, block: &BlockRef) -> &mut BlockJournal<N> {
6418 if let Some(index) = self
6419 .journal
6420 .iter()
6421 .position(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
6422 {
6423 enrich_block_ref(&mut self.journal[index].block, block);
6424 return &mut self.journal[index];
6425 }
6426
6427 self.journal.push_back(BlockJournal {
6428 block: *block,
6429 inputs: Vec::new(),
6430 applied: Vec::new(),
6431 handler_ids: Vec::new(),
6432 resynced: Vec::new(),
6433 rollback_diffs: Vec::new(),
6434 });
6435 let index = self.journal.len() - 1;
6436 &mut self.journal[index]
6437 }
6438
6439 fn trim_journal(&mut self) {
6440 if self.config.journal_depth == 0 {
6441 self.journal.clear();
6442 return;
6443 }
6444 while self.journal.len() > self.config.journal_depth {
6445 self.journal.pop_front();
6446 }
6447 }
6448
6449 fn drain_journal_after(&mut self, index: usize) -> Vec<BlockJournal<N>> {
6450 self.journal.drain((index + 1)..).collect()
6451 }
6452
6453 fn drain_journal_from(&mut self, index: usize) -> Vec<BlockJournal<N>> {
6454 self.journal.drain(index..).collect()
6455 }
6456
6457 fn drain_journal_from_number(&mut self, number: u64) -> Vec<BlockJournal<N>> {
6458 let Some(index) = self
6459 .journal
6460 .iter()
6461 .position(|entry| entry.block.number >= number)
6462 else {
6463 return Vec::new();
6464 };
6465 self.drain_journal_from(index)
6466 }
6467
6468 fn recover_dropped_journals(
6469 &mut self,
6470 cache: &mut EvmCache,
6471 dropped: Vec<BlockJournal<N>>,
6472 reason: ReorgReason,
6473 ) -> Option<ReorgReport<N>> {
6474 if dropped.is_empty() {
6475 return None;
6476 }
6477
6478 let first_dropped_block = dropped
6479 .iter()
6480 .map(|entry| entry.block.number)
6481 .min()
6482 .expect("non-empty dropped journal set");
6483 self.rebase_validation_state_from(first_dropped_block);
6484 if self
6485 .safe_head
6486 .is_some_and(|head| head.number >= first_dropped_block)
6487 {
6488 self.safe_head = None;
6489 }
6490
6491 let dropped_blocks: Vec<_> = dropped.iter().map(|entry| entry.block).collect();
6492 let dropped_inputs: Vec<_> = dropped
6493 .iter()
6494 .flat_map(|entry| entry.inputs.iter().copied())
6495 .collect();
6496 let canceled_resyncs = self.cancel_resyncs_for_dropped_blocks(&dropped_blocks);
6497 let purge_scopes = purge_scopes_for_dropped_journals(&dropped);
6498 let rollback_updates = rollback_updates_for_dropped_journals(&dropped, &purge_scopes);
6499 let purge_updates: Vec<_> = purge_scopes
6500 .iter()
6501 .map(|(address, scope)| StateUpdate::purge(*address, scope.clone()))
6502 .collect();
6503
6504 let rollback_diff = if rollback_updates.is_empty() {
6505 StateDiff::default()
6506 } else {
6507 cache.apply_updates(&rollback_updates)
6508 };
6509 let purge_diff = if purge_updates.is_empty() {
6510 StateDiff::default()
6511 } else {
6512 cache.apply_updates(&purge_updates)
6513 };
6514 self.coverage_head = self.journal.back().map(|entry| entry.block);
6515
6516 Some(ReorgReport {
6517 dropped: dropped_blocks.first().cloned(),
6518 dropped_blocks,
6519 dropped_inputs,
6520 rollback_updates,
6521 rollback_diff,
6522 purge_updates,
6523 purge_diff,
6524 canceled_resyncs,
6525 reason,
6526 _network: PhantomData,
6527 })
6528 }
6529
6530 fn rebase_validation_state_from(&mut self, first_dropped_block: u64) {
6531 if let Some(freshness) = self.freshness.as_mut() {
6532 freshness.invalidate_valid_through_from(first_dropped_block);
6533 }
6534 self.tracked_roots
6535 .retain(|_, baseline| baseline.last_block < first_dropped_block);
6536 if self
6537 .last_gate_block
6538 .is_some_and(|block| block >= first_dropped_block)
6539 {
6540 self.last_gate_block = self
6541 .tracked_roots
6542 .values()
6543 .map(|baseline| baseline.last_block)
6544 .max();
6545 }
6546 self.touched_since_gate.clear();
6550 }
6551
6552 fn cancel_resyncs_for_dropped_blocks(
6553 &mut self,
6554 dropped_blocks: &[BlockRef],
6555 ) -> Vec<ResyncRequest> {
6556 let mut canceled = Vec::new();
6557 self.pending_resyncs.retain(|request| {
6558 let should_cancel = resync_request_targets_dropped_block(request, dropped_blocks);
6559 if should_cancel {
6560 canceled.push(request.clone());
6561 }
6562 !should_cancel
6563 });
6564 canceled
6565 }
6566
6567 fn remove_pending_resyncs<'a>(&mut self, ids: impl IntoIterator<Item = &'a ResyncId>) {
6568 let ids: HashSet<_> = ids.into_iter().cloned().collect();
6569 self.pending_resyncs
6570 .retain(|request| !ids.contains(&request.id));
6571 }
6572}
6573
6574fn install_preconfirmed_cache_context(cache: &mut EvmCache, flashblock: &FlashblockRef) {
6575 cache.set_block(BlockId::pending());
6576 cache.set_block_context(Some(flashblock.block_number), flashblock.base_fee_per_gas);
6577 cache.set_coinbase(flashblock.beneficiary);
6578 cache.set_prevrandao(flashblock.prevrandao);
6579 cache.set_block_gas_limit(flashblock.gas_limit);
6580 cache.set_timestamp(flashblock.timestamp);
6581}
6582
6583pub fn validate_canonical_sequence<N: Network>(
6615 state: &CanonicalSequenceState,
6616 batch: &ReactiveInputBatch<N>,
6617) -> Result<CanonicalSequenceValidation, ReactiveError> {
6618 validate_canonical_sequence_diagnostic(state, batch)
6619 .map_err(CanonicalSequenceError::into_reactive_error)
6620}
6621
6622pub fn validate_canonical_sequence_diagnostic<N: Network>(
6637 state: &CanonicalSequenceState,
6638 batch: &ReactiveInputBatch<N>,
6639) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6640 validate_canonical_sequence_internal(
6641 state,
6642 batch,
6643 CanonicalSequenceValidationPolicy::RequireCompleteRollback,
6644 )
6645}
6646
6647pub fn normalize_and_validate_canonical_sequence<N: Network>(
6670 state: &CanonicalSequenceState,
6671 batch: &ReactiveInputBatch<N>,
6672) -> Result<CanonicalSequenceValidation, ReactiveError> {
6673 normalize_and_validate_canonical_sequence_diagnostic(state, batch)
6674 .map_err(CanonicalSequenceError::into_reactive_error)
6675}
6676
6677pub fn normalize_and_validate_canonical_sequence_diagnostic<N: Network>(
6692 state: &CanonicalSequenceState,
6693 batch: &ReactiveInputBatch<N>,
6694) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6695 validate_canonical_sequence_internal(
6696 state,
6697 batch,
6698 CanonicalSequenceValidationPolicy::RequireCompleteRollbackNormalizeCoverage,
6699 )
6700}
6701
6702fn validate_canonical_sequence_internal<N: Network>(
6703 state: &CanonicalSequenceState,
6704 batch: &ReactiveInputBatch<N>,
6705 policy: CanonicalSequenceValidationPolicy,
6706) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6707 let records = batch
6708 .records()
6709 .iter()
6710 .enumerate()
6711 .map(|(index, record)| {
6712 (
6713 record.clone(),
6714 DeliveryAudience::All,
6715 batch
6716 .record_delivery_scope(index)
6717 .expect("enumerated record always has a delivery scope"),
6718 )
6719 })
6720 .collect::<Vec<_>>();
6721 let records = sort_scoped_records(dedupe_scoped_records(records)?);
6722 let records = records
6723 .iter()
6724 .map(|(record, _, scope)| (record, *scope))
6725 .collect::<Vec<_>>();
6726 validate_canonical_sequence_parts(state, batch.chain_controls(), &records, policy)
6727}
6728
6729#[derive(Clone, Copy)]
6730enum CanonicalSequenceValidationPolicy {
6731 RequireCompleteRollback,
6732 RequireCompleteRollbackNormalizeCoverage,
6733 ObserveIncompleteRollback,
6734}
6735
6736#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6739#[non_exhaustive]
6740pub enum CanonicalRollbackKind {
6741 Explicit,
6743 Removed,
6745 ImplicitParent,
6747 MissingReplacement,
6749}
6750
6751#[derive(Debug, thiserror::Error)]
6756#[non_exhaustive]
6757pub enum CanonicalSequenceError {
6758 #[error(transparent)]
6760 Invalid(#[from] ReactiveError),
6761 #[error(
6764 "{kind:?} rollback after block {common_ancestor} exceeds retained canonical history starting at {oldest_retained:?}"
6765 )]
6766 IncompleteRollback {
6767 common_ancestor: u64,
6769 oldest_retained: Option<u64>,
6771 kind: CanonicalRollbackKind,
6773 },
6774}
6775
6776#[derive(Clone, Copy, Debug)]
6777struct RequiredReorgAnchor {
6778 number: u64,
6779 block: Option<BlockRef>,
6780 permits_missing_child_parent: bool,
6781 must_be_consumed: bool,
6782}
6783
6784#[derive(Debug)]
6785struct SequenceRewind {
6786 common_ancestor: Option<BlockRef>,
6787 dropped: Vec<BlockRef>,
6788}
6789
6790impl RequiredReorgAnchor {
6791 const fn hash(self) -> Option<B256> {
6792 match self.block {
6793 Some(block) => Some(block.hash),
6794 None => None,
6795 }
6796 }
6797}
6798
6799impl CanonicalSequenceError {
6800 pub const fn requires_history(&self) -> bool {
6803 matches!(self, Self::IncompleteRollback { .. })
6804 }
6805
6806 pub fn into_reactive_error(self) -> ReactiveError {
6808 match self {
6809 Self::Invalid(error) => error,
6810 Self::IncompleteRollback {
6811 common_ancestor,
6812 oldest_retained,
6813 kind,
6814 } => ReactiveError::InvalidChainControl {
6815 message: format!(
6816 "{kind:?} rollback after block {common_ancestor} exceeds retained canonical history starting at {oldest_retained:?}"
6817 ),
6818 },
6819 }
6820 }
6821}
6822
6823impl CanonicalSequenceValidationPolicy {
6824 const fn requires_complete_rollback(self) -> bool {
6825 matches!(
6826 self,
6827 Self::RequireCompleteRollback | Self::RequireCompleteRollbackNormalizeCoverage
6828 )
6829 }
6830
6831 const fn normalizes_coverage(self) -> bool {
6832 matches!(self, Self::RequireCompleteRollbackNormalizeCoverage)
6833 }
6834}
6835
6836fn validate_canonical_sequence_parts<N: Network>(
6837 initial: &CanonicalSequenceState,
6838 controls: &[ChainControl],
6839 records: &[(&ReactiveInputRecord<N>, DeliveryScope)],
6840 policy: CanonicalSequenceValidationPolicy,
6841) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6842 validate_canonical_sequence_snapshot(initial)?;
6843 let control_split = validate_control_phase_order(controls)?;
6844 let (pre_record_controls, post_record_controls) = controls.split_at(control_split);
6845 let mut state = initial.clone();
6846 let mut asserted_blocks = HashMap::<u64, BlockRef>::new();
6847 let mut mutations = Vec::new();
6848 let mut normalized_chain_controls = Vec::with_capacity(controls.len());
6849 let mut batch_dropped = BatchDroppedCanonical::default();
6850 let mut removed_assertions = HashMap::<(u64, B256), BlockRef>::new();
6851 let mut removed_heights_by_hash = HashMap::<B256, u64>::new();
6852 let mut record_proof_control_identities = HashSet::<(u64, B256)>::new();
6853 let rollback_oldest = initial
6854 .retained_canonical_history
6855 .first()
6856 .map(|block| block.number);
6857
6858 for control in pre_record_controls {
6859 normalized_chain_controls.push(control.clone());
6860 validate_sequence_control(&state, control)?;
6861 assert_chain_control_identities(&mut asserted_blocks, control)?;
6862 let ChainControl::Reorg {
6863 common_ancestor,
6864 old_tip,
6865 ..
6866 } = control
6867 else {
6868 unreachable!("phase validation leaves only reorg controls before records")
6869 };
6870 let exact_ancestor = state.retained_canonical_history.iter().any(|block| {
6871 block.number == common_ancestor.number && block.hash == common_ancestor.hash
6872 });
6873 let rollback_horizon_covers_ancestor = state
6874 .retained_canonical_history
6875 .first()
6876 .is_some_and(|oldest| oldest.number <= common_ancestor.number);
6877 if policy.requires_complete_rollback()
6878 && !exact_ancestor
6879 && !rollback_horizon_covers_ancestor
6880 {
6881 return Err(CanonicalSequenceError::IncompleteRollback {
6882 common_ancestor: common_ancestor.number,
6883 oldest_retained: rollback_oldest,
6884 kind: CanonicalRollbackKind::Explicit,
6885 });
6886 }
6887 let dropped = state
6888 .retained_canonical_history
6889 .iter()
6890 .copied()
6891 .filter(|block| block.number > common_ancestor.number)
6892 .collect::<Vec<_>>();
6893 state
6894 .retained_canonical_history
6895 .retain(|block| block.number <= common_ancestor.number);
6896 upsert_sequence_history(&mut state.retained_canonical_history, common_ancestor)?;
6897 let mut enriched_ancestor = *common_ancestor;
6898 if let Some(retained) = state.retained_canonical_history.iter().find(|block| {
6899 block.number == common_ancestor.number && block.hash == common_ancestor.hash
6900 }) {
6901 enrich_block_ref(&mut enriched_ancestor, retained);
6902 }
6903 if let Some(coverage) = state.coverage_head.as_ref()
6904 && coverage.number == common_ancestor.number
6905 && coverage.hash == common_ancestor.hash
6906 {
6907 enrich_block_ref(&mut enriched_ancestor, coverage);
6908 }
6909 upsert_sequence_history(&mut state.retained_canonical_history, &enriched_ancestor)?;
6910 state.coverage_head = Some(enriched_ancestor);
6911 clear_sequence_heads_above(&mut state, &enriched_ancestor);
6912 batch_dropped.record_explicit(common_ancestor, old_tip);
6913 batch_dropped.record_drained(&dropped);
6914 mutations.push(CanonicalSequenceMutation::Rewind {
6915 common_ancestor: Some(enriched_ancestor),
6916 dropped,
6917 });
6918 }
6919 let pre_record_state = state.clone();
6920 let mut required_reorg_anchor = None::<RequiredReorgAnchor>;
6921
6922 for (record, scope) in records {
6923 if !scope.advances_canonical_state() {
6924 continue;
6925 }
6926 if let Some((incoming_dropped_block, _)) = reorg_signal_block(record) {
6927 let incoming_dropped_block =
6928 resolve_record_block_payload_metadata(record, incoming_dropped_block)?;
6929 validate_sequence_matching_metadata(&state, &incoming_dropped_block, "removed record")?;
6930 validate_sequence_adjacent_parent_identity(
6931 &state,
6932 &incoming_dropped_block,
6933 "removed record",
6934 )?;
6935 let mut dropped_block = state
6936 .retained_canonical_history
6937 .iter()
6938 .find(|known| {
6939 known.number == incoming_dropped_block.number
6940 && known.hash == incoming_dropped_block.hash
6941 })
6942 .copied()
6943 .or_else(|| {
6944 state.coverage_head.filter(|known| {
6945 known.number == incoming_dropped_block.number
6946 && known.hash == incoming_dropped_block.hash
6947 })
6948 })
6949 .unwrap_or(incoming_dropped_block);
6950 enrich_block_ref(&mut dropped_block, &incoming_dropped_block);
6951 validate_sequence_implicit_finality(&state, record, None)?;
6952 if dropped_block.number == 0 {
6953 return Err(ReactiveError::InvalidChainControl {
6954 message: "a removed/reorged genesis block has no canonical parent anchor"
6955 .into(),
6956 }
6957 .into());
6958 }
6959 let removed_identity = (dropped_block.number, dropped_block.hash);
6960 if let Some(previous_number) =
6961 removed_heights_by_hash.insert(dropped_block.hash, dropped_block.number)
6962 && previous_number != dropped_block.number
6963 {
6964 return Err(ReactiveError::InvalidChainControl {
6965 message: format!(
6966 "removed hash {:?} is reused at heights {} and {}",
6967 dropped_block.hash, previous_number, dropped_block.number
6968 ),
6969 }
6970 .into());
6971 }
6972 if let Some(previous) = removed_assertions.get_mut(&removed_identity) {
6973 if !optional_block_refs_are_compatible(Some(previous), Some(&dropped_block)) {
6974 return Err(ReactiveError::InvalidChainControl {
6975 message: format!(
6976 "duplicate removed block {}:{:?} carries conflicting metadata",
6977 dropped_block.number, dropped_block.hash
6978 ),
6979 }
6980 .into());
6981 }
6982 enrich_block_ref(previous, &dropped_block);
6983 } else {
6984 removed_assertions.insert(removed_identity, dropped_block);
6985 }
6986 if asserted_blocks
6987 .get(&dropped_block.number)
6988 .is_some_and(|asserted| asserted.hash == dropped_block.hash)
6989 {
6990 return Err(ReactiveError::InvalidChainControl {
6991 message: format!(
6992 "removed block {}:{:?} is asserted canonical by the same envelope",
6993 dropped_block.number, dropped_block.hash
6994 ),
6995 }
6996 .into());
6997 }
6998 if batch_dropped.contains(&dropped_block) {
6999 continue;
7000 }
7001 if let Some(index) = state.retained_canonical_history.iter().position(|block| {
7002 block.number == dropped_block.number && block.hash == dropped_block.hash
7003 }) {
7004 let dropped = state.retained_canonical_history.split_off(index);
7005 batch_dropped.record_drained(&dropped);
7006 let ancestor_number = dropped_block
7007 .number
7008 .checked_sub(1)
7009 .expect("genesis removal was rejected above");
7010 let retained_anchor = state
7011 .retained_canonical_history
7012 .iter()
7013 .rev()
7014 .find(|head| head.number == ancestor_number)
7015 .copied();
7016 let authenticated_anchor = retained_anchor
7017 .or_else(|| {
7018 dropped_block.parent_hash.map(|hash| BlockRef {
7019 number: ancestor_number,
7020 hash,
7021 parent_hash: None,
7022 timestamp: None,
7023 })
7024 })
7025 .or_else(|| {
7026 state
7027 .finalized_head
7028 .filter(|head| head.number == ancestor_number)
7029 });
7030 let authenticated_anchor = authenticated_anchor.map(|mut anchor| {
7031 for head in [state.safe_head.as_ref(), state.finalized_head.as_ref()]
7032 .into_iter()
7033 .flatten()
7034 {
7035 if head.number == anchor.number && head.hash == anchor.hash {
7036 enrich_block_ref(&mut anchor, head);
7037 }
7038 }
7039 anchor
7040 });
7041 required_reorg_anchor = Some(RequiredReorgAnchor {
7042 number: ancestor_number,
7043 block: authenticated_anchor,
7044 permits_missing_child_parent: retained_anchor.is_some(),
7045 must_be_consumed: authenticated_anchor.is_none()
7046 && state.retained_canonical_history.is_empty(),
7047 });
7048 state.coverage_head = authenticated_anchor
7049 .or_else(|| state.retained_canonical_history.last().copied());
7050 if let Some(head) = state.coverage_head {
7051 clear_sequence_heads_above(&mut state, &head);
7052 } else {
7053 state.safe_head = None;
7054 state.finalized_head = None;
7055 }
7056 mutations.push(CanonicalSequenceMutation::Rewind {
7057 common_ancestor: state.coverage_head,
7058 dropped,
7059 });
7060 } else {
7061 let replacement_is_known = state.retained_canonical_history.iter().any(|block| {
7062 block.number == dropped_block.number && block.hash != dropped_block.hash
7063 }) || state.coverage_head.is_some_and(|head| {
7064 head.number == dropped_block.number && head.hash != dropped_block.hash
7065 });
7066 if !replacement_is_known {
7067 if policy.requires_complete_rollback() {
7072 return Err(CanonicalSequenceError::IncompleteRollback {
7073 common_ancestor: dropped_block
7074 .number
7075 .checked_sub(1)
7076 .expect("genesis removal was rejected above"),
7077 oldest_retained: rollback_oldest,
7078 kind: CanonicalRollbackKind::Removed,
7079 });
7080 }
7081 continue;
7082 }
7083 }
7084 continue;
7085 }
7086
7087 let Some(context_block) = canonical_record_block(record) else {
7088 continue;
7089 };
7090 let incoming_block = resolve_record_block_payload_metadata(record, *context_block)?;
7091 if post_record_controls
7092 .iter()
7093 .filter_map(canonical_coverage_control_block)
7094 .any(|asserted| {
7095 asserted.number == incoming_block.number
7096 && asserted.hash == incoming_block.hash
7097 && optional_block_refs_are_compatible(Some(asserted), Some(&incoming_block))
7098 && ((incoming_block.parent_hash.is_none() && asserted.parent_hash.is_some())
7099 || (incoming_block.timestamp.is_none() && asserted.timestamp.is_some()))
7100 })
7101 {
7102 record_proof_control_identities.insert((incoming_block.number, incoming_block.hash));
7103 }
7104 let mut resolved_block = incoming_block;
7105 if let Some(asserted) = asserted_blocks
7106 .get(&incoming_block.number)
7107 .filter(|asserted| asserted.hash == incoming_block.hash)
7108 {
7109 if !optional_block_refs_are_compatible(Some(asserted), Some(&incoming_block)) {
7110 return Err(ReactiveError::InvalidChainControl {
7111 message: format!(
7112 "canonical record {}:{:?} conflicts with the same envelope's asserted metadata",
7113 incoming_block.number, incoming_block.hash
7114 ),
7115 }
7116 .into());
7117 }
7118 enrich_block_ref(&mut resolved_block, asserted);
7119 }
7120 for asserted in post_record_controls
7121 .iter()
7122 .filter_map(chain_control_canonical_assertion)
7123 .filter(|asserted| {
7124 asserted.number == incoming_block.number && asserted.hash == incoming_block.hash
7125 })
7126 {
7127 if !optional_block_refs_are_compatible(Some(&resolved_block), Some(asserted)) {
7128 return Err(ReactiveError::InvalidChainControl {
7129 message: format!(
7130 "canonical record {}:{:?} conflicts with the same envelope's asserted metadata",
7131 incoming_block.number, incoming_block.hash
7132 ),
7133 }
7134 .into());
7135 }
7136 enrich_block_ref(&mut resolved_block, asserted);
7137 }
7138 let replacement_anchor =
7139 required_reorg_anchor.filter(|required| resolved_block.number > required.number);
7140 if resolved_block.parent_hash.is_none()
7141 && replacement_anchor.is_some_and(|anchor| {
7142 anchor.permits_missing_child_parent
7143 && anchor.number.checked_add(1) == Some(resolved_block.number)
7144 })
7145 {
7146 resolved_block.parent_hash = replacement_anchor.and_then(RequiredReorgAnchor::hash);
7147 }
7148 let block = &resolved_block;
7149 if removed_assertions.contains_key(&(block.number, block.hash)) {
7150 return Err(ReactiveError::InvalidChainControl {
7151 message: format!(
7152 "canonical block {}:{:?} is also removed by the same envelope",
7153 block.number, block.hash
7154 ),
7155 }
7156 .into());
7157 }
7158 if let Some(removed_number) = removed_heights_by_hash.get(&block.hash)
7159 && *removed_number != block.number
7160 {
7161 return Err(ReactiveError::InvalidChainControl {
7162 message: format!(
7163 "canonical hash {:?} at height {} is removed at height {} by the same envelope",
7164 block.hash, block.number, removed_number
7165 ),
7166 }
7167 .into());
7168 }
7169 let replacement_proven_by_removal =
7170 validate_replacement_reorg_anchor(replacement_anchor, block, policy, rollback_oldest)?;
7171 if replacement_anchor.is_some() {
7172 required_reorg_anchor = None;
7173 }
7174 validate_sequence_matching_metadata(&state, block, "canonical record")?;
7175 validate_sequence_implicit_finality(&state, record, Some(block))?;
7176 let implicit_replacement_requires_history = if replacement_proven_by_removal {
7177 false
7178 } else {
7179 sequence_implicit_replacement_requires_history(&state, block, policy)?
7180 };
7181 if implicit_replacement_requires_history && policy.requires_complete_rollback() {
7182 return Err(CanonicalSequenceError::IncompleteRollback {
7183 common_ancestor: block.number.saturating_sub(1),
7184 oldest_retained: rollback_oldest,
7185 kind: CanonicalRollbackKind::ImplicitParent,
7186 });
7187 }
7188 assert_canonical_block_identity(&mut asserted_blocks, block, "canonical record")?;
7189 let allow_parentless_extension = replacement_anchor.is_some_and(|anchor| {
7190 anchor.permits_missing_child_parent
7191 && anchor.number.checked_add(1) == Some(block.number)
7192 });
7193 if let Some(rewind) =
7194 apply_sequence_canonical_block(&mut state, block, allow_parentless_extension)?
7195 {
7196 mutations.push(CanonicalSequenceMutation::Rewind {
7197 common_ancestor: rewind.common_ancestor,
7198 dropped: rewind.dropped,
7199 });
7200 }
7201 mutations.push(CanonicalSequenceMutation::Canonical(*block));
7202 }
7203
7204 for control in post_record_controls {
7205 if let Some(block) = chain_control_canonical_assertion(control)
7206 && removed_assertions.contains_key(&(block.number, block.hash))
7207 {
7208 return Err(ReactiveError::InvalidChainControl {
7209 message: format!(
7210 "canonical block {}:{:?} is also removed by the same envelope",
7211 block.number, block.hash
7212 ),
7213 }
7214 .into());
7215 }
7216 if let Some(block) = chain_control_canonical_assertion(control)
7217 && let Some(removed_number) = removed_heights_by_hash.get(&block.hash)
7218 && *removed_number != block.number
7219 {
7220 return Err(ReactiveError::InvalidChainControl {
7221 message: format!(
7222 "canonical hash {:?} at height {} is removed at height {} by the same envelope",
7223 block.hash, block.number, removed_number
7224 ),
7225 }
7226 .into());
7227 }
7228 let replacement_anchor = canonical_coverage_control_block(control).and_then(|block| {
7229 required_reorg_anchor.filter(|required| block.number > required.number)
7230 });
7231 if let Some(block) = canonical_coverage_control_block(control) {
7232 validate_replacement_reorg_anchor(replacement_anchor, block, policy, rollback_oldest)?;
7233 if replacement_anchor.is_some() {
7234 required_reorg_anchor = None;
7235 }
7236 }
7237 assert_chain_control_identities(&mut asserted_blocks, control)?;
7238 let preserves_record_proof =
7239 canonical_coverage_control_block(control).is_some_and(|block| {
7240 record_proof_control_identities.contains(&(block.number, block.hash))
7241 });
7242 if policy.normalizes_coverage()
7243 && !preserves_record_proof
7244 && let Some(block) = canonical_coverage_control_block(control)
7245 && state
7246 .coverage_head
7247 .is_some_and(|head| block.number <= head.number)
7248 {
7249 let is_equal_coverage = state
7250 .coverage_head
7251 .is_some_and(|head| block.number == head.number);
7252 let known = state
7253 .coverage_head
7254 .as_ref()
7255 .filter(|head| head.number == block.number && head.hash == block.hash)
7256 .or_else(|| {
7257 state
7258 .retained_canonical_history
7259 .iter()
7260 .find(|entry| entry.number == block.number && entry.hash == block.hash)
7261 });
7262 if let Some(known) = known
7263 && optional_block_refs_are_compatible(Some(known), Some(block))
7264 && (!is_equal_coverage || !sequence_block_adds_metadata(&state, block))
7265 {
7266 if let ChainControl::Barrier { id, .. } = control {
7267 normalized_chain_controls.push(ChainControl::Barrier {
7268 id: id.clone(),
7269 block: None,
7270 });
7271 }
7272 continue;
7273 }
7274 }
7275 validate_sequence_control(&state, control)?;
7276 normalized_chain_controls.push(control.clone());
7277 match control {
7278 ChainControl::Safe(block) => {
7279 set_or_enrich_block_ref(&mut state.safe_head, block);
7280 mutations.push(CanonicalSequenceMutation::Safe(
7281 state.safe_head.expect("safe head was just installed"),
7282 ));
7283 }
7284 ChainControl::Finalized(block) => {
7285 set_or_enrich_block_ref(&mut state.finalized_head, block);
7286 mutations.push(CanonicalSequenceMutation::Finalized(
7287 state
7288 .finalized_head
7289 .expect("finalized head was just installed"),
7290 ));
7291 }
7292 ChainControl::CanonicalProgress(block)
7293 | ChainControl::Barrier {
7294 block: Some(block), ..
7295 } => {
7296 let allow_parentless_extension = replacement_anchor.is_some_and(|anchor| {
7297 anchor.permits_missing_child_parent
7298 && anchor.number.checked_add(1) == Some(block.number)
7299 }) || (replacement_anchor.is_none()
7300 && block.parent_hash.is_none()
7301 && state
7302 .coverage_head
7303 .is_some_and(|head| head.number.checked_add(1) == Some(block.number)));
7304 if let Some(rewind) =
7305 apply_sequence_canonical_block(&mut state, block, allow_parentless_extension)?
7306 {
7307 mutations.push(CanonicalSequenceMutation::Rewind {
7308 common_ancestor: rewind.common_ancestor,
7309 dropped: rewind.dropped,
7310 });
7311 }
7312 mutations.push(CanonicalSequenceMutation::Canonical(*block));
7313 }
7314 ChainControl::LogCoverage(block) => {
7315 set_or_enrich_block_ref(&mut state.log_coverage_head, block);
7316 mutations.push(CanonicalSequenceMutation::LogCoverage(
7317 state
7318 .log_coverage_head
7319 .expect("log coverage head was just installed"),
7320 ));
7321 }
7322 ChainControl::Barrier { block: None, .. } => {}
7323 ChainControl::Reorg { .. } => {
7324 unreachable!("phase validation excludes post-record reorg controls")
7325 }
7326 }
7327 }
7328
7329 if let Some(required) = required_reorg_anchor
7330 && required.must_be_consumed
7331 && policy.requires_complete_rollback()
7332 {
7333 return Err(CanonicalSequenceError::IncompleteRollback {
7334 common_ancestor: required.number,
7335 oldest_retained: rollback_oldest,
7336 kind: CanonicalRollbackKind::MissingReplacement,
7337 });
7338 }
7339
7340 validate_canonical_sequence_snapshot(&state)?;
7341 Ok(CanonicalSequenceValidation {
7342 pre_record_state,
7343 next_state: state,
7344 mutations,
7345 normalized_chain_controls,
7346 })
7347}
7348
7349fn validate_canonical_sequence_snapshot(
7350 state: &CanonicalSequenceState,
7351) -> Result<(), ReactiveError> {
7352 let invalid = |message: String| ReactiveError::InvalidChainControl { message };
7353 let supplied_blocks = state
7354 .retained_canonical_history
7355 .iter()
7356 .chain(state.coverage_head.iter())
7357 .chain(state.safe_head.iter())
7358 .chain(state.finalized_head.iter())
7359 .collect::<Vec<_>>();
7360 validate_known_parent_hash_heights(&supplied_blocks)?;
7361 let mut prior = None::<BlockRef>;
7362 for block in &state.retained_canonical_history {
7363 if let Some(previous) = prior {
7364 if block.number < previous.number {
7365 return Err(invalid(
7366 "retained canonical history is not ordered by block number".into(),
7367 ));
7368 }
7369 if block.number == previous.number {
7370 let qualifier = if optional_block_refs_are_compatible(Some(&previous), Some(block))
7371 {
7372 "duplicate"
7373 } else {
7374 "conflicting"
7375 };
7376 return Err(invalid(format!(
7377 "retained canonical history contains {qualifier} identities at block {}",
7378 block.number
7379 )));
7380 }
7381 if previous.number.checked_add(1) == Some(block.number)
7382 && block.parent_hash.is_some()
7383 && block.parent_hash != Some(previous.hash)
7384 {
7385 return Err(invalid(format!(
7386 "adjacent retained block {}:{:?} does not descend from {}:{:?}",
7387 block.number, block.hash, previous.number, previous.hash
7388 )));
7389 }
7390 }
7391 prior = Some(*block);
7392 }
7393 if state.coverage_head.is_none() && !state.retained_canonical_history.is_empty() {
7394 return Err(invalid(
7395 "retained canonical history requires an authoritative coverage head".into(),
7396 ));
7397 }
7398 if let Some(head) = state.coverage_head.as_ref() {
7399 if let Some(retained) = state
7400 .retained_canonical_history
7401 .iter()
7402 .find(|entry| entry.number == head.number)
7403 && !optional_block_refs_are_compatible(Some(retained), Some(head))
7404 {
7405 return Err(invalid(format!(
7406 "coverage head {}:{:?} conflicts with retained identity {:?}",
7407 head.number, head.hash, retained
7408 )));
7409 }
7410 if state
7411 .retained_canonical_history
7412 .last()
7413 .is_some_and(|retained| retained.number > head.number)
7414 {
7415 return Err(invalid(
7416 "retained canonical history advances beyond the coverage head".into(),
7417 ));
7418 }
7419 if let Some(retained) = state.retained_canonical_history.last()
7420 && retained.number.checked_add(1) == Some(head.number)
7421 && head.parent_hash.is_some()
7422 && head.parent_hash != Some(retained.hash)
7423 {
7424 return Err(invalid(format!(
7425 "coverage head {}:{:?} does not descend from adjacent retained block {}:{:?}",
7426 head.number, head.hash, retained.number, retained.hash
7427 )));
7428 }
7429 }
7430 if let Some(safe) = state.safe_head.as_ref() {
7431 validate_sequence_known_identity(state, safe, "safe")?;
7432 validate_sequence_head_within_coverage(state, safe, "safe")?;
7433 validate_coverage_descends_from_adjacent_head(state.coverage_head.as_ref(), safe, "safe")?;
7434 }
7435 if let Some(finalized) = state.finalized_head.as_ref() {
7436 validate_sequence_known_identity(state, finalized, "finalized")?;
7437 validate_sequence_head_within_coverage(state, finalized, "finalized")?;
7438 validate_coverage_descends_from_adjacent_head(
7439 state.coverage_head.as_ref(),
7440 finalized,
7441 "finalized",
7442 )?;
7443 }
7444 validate_adjacent_finality(state.finalized_head.as_ref(), state.safe_head.as_ref())?;
7445 if let (Some(finalized), Some(safe)) = (state.finalized_head, state.safe_head)
7446 && (finalized.number > safe.number
7447 || (finalized.number == safe.number && finalized.hash != safe.hash))
7448 {
7449 return Err(invalid(
7450 "finalized head cannot advance beyond or conflict with safe head".into(),
7451 ));
7452 }
7453 Ok(())
7454}
7455
7456fn validate_known_parent_hash_heights(blocks: &[&BlockRef]) -> Result<(), ReactiveError> {
7457 let mut heights_by_hash = HashMap::<B256, u64>::with_capacity(blocks.len());
7458 let mut resolved_by_height = HashMap::<u64, BlockRef>::with_capacity(blocks.len());
7459 for block in blocks.iter().copied() {
7460 if let Some(previous_height) = heights_by_hash.insert(block.hash, block.number)
7461 && previous_height != block.number
7462 {
7463 return Err(ReactiveError::InvalidChainControl {
7464 message: format!(
7465 "canonical hash {:?} is reused at heights {} and {}",
7466 block.hash, previous_height, block.number
7467 ),
7468 });
7469 }
7470 if let Some(resolved) = resolved_by_height.get_mut(&block.number) {
7471 if !optional_block_refs_are_compatible(Some(resolved), Some(block)) {
7472 return Err(ReactiveError::InvalidChainControl {
7473 message: format!(
7474 "canonical aliases at height {} carry conflicting identities or metadata",
7475 block.number
7476 ),
7477 });
7478 }
7479 enrich_block_ref(resolved, block);
7480 } else {
7481 resolved_by_height.insert(block.number, *block);
7482 }
7483 }
7484 for child in resolved_by_height.values() {
7485 let Some(parent_hash) = child.parent_hash else {
7486 continue;
7487 };
7488 if let Some(parent_number) = heights_by_hash.get(&parent_hash)
7489 && parent_number.checked_add(1) != Some(child.number)
7490 {
7491 return Err(ReactiveError::InvalidChainControl {
7492 message: format!(
7493 "block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
7494 child.number, child.hash, parent_hash, parent_number
7495 ),
7496 });
7497 }
7498 if let Some(parent_number) = child.number.checked_sub(1)
7499 && let Some(parent) = resolved_by_height.get(&parent_number)
7500 && parent.hash != parent_hash
7501 {
7502 return Err(ReactiveError::InvalidChainControl {
7503 message: format!(
7504 "block {}:{:?} does not descend from supplied adjacent identity {}:{:?}",
7505 child.number, child.hash, parent.number, parent.hash
7506 ),
7507 });
7508 }
7509 }
7510 Ok(())
7511}
7512
7513fn validate_coverage_descends_from_adjacent_head(
7514 coverage: Option<&BlockRef>,
7515 head: &BlockRef,
7516 label: &str,
7517) -> Result<(), ReactiveError> {
7518 let Some(coverage) = coverage else {
7519 return Ok(());
7520 };
7521 if head.number.checked_add(1) == Some(coverage.number)
7522 && coverage
7523 .parent_hash
7524 .is_some_and(|parent| parent != head.hash)
7525 {
7526 return Err(ReactiveError::InvalidChainControl {
7527 message: format!(
7528 "canonical coverage {}:{:?} does not descend from adjacent {label} head {}:{:?}",
7529 coverage.number, coverage.hash, head.number, head.hash
7530 ),
7531 });
7532 }
7533 Ok(())
7534}
7535
7536fn validate_sequence_control(
7537 state: &CanonicalSequenceState,
7538 control: &ChainControl,
7539) -> Result<(), ReactiveError> {
7540 let invalid = |message: String| ReactiveError::InvalidChainControl { message };
7541 match control {
7542 ChainControl::Safe(block) => {
7543 validate_sequence_known_identity(state, block, "safe")?;
7544 validate_sequence_head_within_coverage(state, block, "safe")?;
7545 if let Some(current) = state.safe_head.as_ref()
7546 && (block.number < current.number
7547 || (block.number == current.number
7548 && (block.hash != current.hash
7549 || !optional_block_refs_are_compatible(Some(block), Some(current)))))
7550 {
7551 return Err(invalid(format!(
7552 "safe head {}:{:?} conflicts with current {}:{:?}",
7553 block.number, block.hash, current.number, current.hash
7554 )));
7555 }
7556 if let Some(finalized) = state.finalized_head.as_ref()
7557 && (block.number < finalized.number
7558 || (block.number == finalized.number && block.hash != finalized.hash))
7559 {
7560 return Err(invalid(
7561 "safe head cannot precede or conflict with finalized head".into(),
7562 ));
7563 }
7564 validate_adjacent_finality(state.finalized_head.as_ref(), Some(block))?;
7565 }
7566 ChainControl::Finalized(block) => {
7567 validate_sequence_known_identity(state, block, "finalized")?;
7568 validate_sequence_head_within_coverage(state, block, "finalized")?;
7569 if let Some(current) = state.finalized_head.as_ref()
7570 && (block.number < current.number
7571 || (block.number == current.number
7572 && (block.hash != current.hash
7573 || !optional_block_refs_are_compatible(Some(block), Some(current)))))
7574 {
7575 return Err(invalid(format!(
7576 "finalized head {}:{:?} conflicts with current {}:{:?}",
7577 block.number, block.hash, current.number, current.hash
7578 )));
7579 }
7580 if let Some(safe) = state.safe_head.as_ref()
7581 && (block.number > safe.number
7582 || (block.number == safe.number && block.hash != safe.hash))
7583 {
7584 return Err(invalid(
7585 "finalized head cannot advance beyond or conflict with safe head".into(),
7586 ));
7587 }
7588 validate_adjacent_finality(Some(block), state.safe_head.as_ref())?;
7589 }
7590 ChainControl::CanonicalProgress(block)
7591 | ChainControl::Barrier {
7592 block: Some(block), ..
7593 } => {
7594 validate_sequence_known_identity(state, block, "canonical coverage")?;
7595 if let Some(current) = state.coverage_head.as_ref()
7596 && (block.number < current.number
7597 || (block.number == current.number && block.hash != current.hash))
7598 {
7599 return Err(invalid(format!(
7600 "canonical coverage {}:{:?} conflicts with current {}:{:?}",
7601 block.number, block.hash, current.number, current.hash
7602 )));
7603 }
7604 if let Some(current) = state.coverage_head.as_ref()
7605 && current.number.checked_add(1) == Some(block.number)
7606 && block.parent_hash.is_some()
7607 && block.parent_hash != Some(current.hash)
7608 {
7609 return Err(invalid(format!(
7610 "canonical coverage {}:{:?} does not descend from current {}:{:?}",
7611 block.number, block.hash, current.number, current.hash
7612 )));
7613 }
7614 }
7615 ChainControl::LogCoverage(block) => {
7616 validate_sequence_known_identity(state, block, "log coverage")?;
7617 if let Some(current) = state.log_coverage_head.as_ref()
7621 && (block.number < current.number
7622 || (block.number == current.number && block.hash != current.hash))
7623 {
7624 return Err(invalid(format!(
7625 "log coverage {}:{:?} conflicts with current {}:{:?}",
7626 block.number, block.hash, current.number, current.hash
7627 )));
7628 }
7629 if let Some(coverage) = state.coverage_head.as_ref()
7632 && block.number > coverage.number
7633 {
7634 return Err(invalid(format!(
7635 "log coverage {}:{:?} is ahead of canonical coverage {}:{:?}",
7636 block.number, block.hash, coverage.number, coverage.hash
7637 )));
7638 }
7639 }
7640 ChainControl::Barrier { block: None, .. } => {}
7641 ChainControl::Reorg {
7642 common_ancestor,
7643 old_tip,
7644 new_tip,
7645 } => {
7646 validate_sequence_known_identity(state, common_ancestor, "reorg common ancestor")?;
7647 validate_reorg_ancestor_against_retained_branch(state, common_ancestor)?;
7648 validate_sequence_known_hash_height(state, old_tip, "reorg old tip")?;
7649 validate_sequence_known_hash_height(state, new_tip, "reorg new tip")?;
7650 validate_sequence_known_parent_height(state, old_tip, "reorg old tip")?;
7651 validate_sequence_known_parent_height(state, new_tip, "reorg new tip")?;
7652 validate_sequence_adjacent_parent_identity(state, old_tip, "reorg old tip")?;
7653 if let Some(current) = state.coverage_head.as_ref()
7654 && (old_tip.number != current.number
7655 || old_tip.hash != current.hash
7656 || !optional_block_refs_are_compatible(Some(old_tip), Some(current)))
7657 {
7658 return Err(invalid(format!(
7659 "reorg old tip {}:{:?} does not exactly match current metadata {}:{:?}",
7660 old_tip.number, old_tip.hash, current.number, current.hash
7661 )));
7662 }
7663 if common_ancestor.number > old_tip.number || common_ancestor.number > new_tip.number {
7664 return Err(invalid(
7665 "reorg common ancestor cannot be above either branch tip".into(),
7666 ));
7667 }
7668 if common_ancestor.number == old_tip.number || common_ancestor.number == new_tip.number
7669 {
7670 return Err(invalid(
7671 "reorg must replace non-empty old and new branches above the common ancestor"
7672 .into(),
7673 ));
7674 }
7675 if old_tip.number == new_tip.number && old_tip.hash == new_tip.hash {
7676 return Err(invalid(
7677 "reorg old and new tips cannot have the same canonical identity".into(),
7678 ));
7679 }
7680 for (label, tip) in [("old", old_tip), ("new", new_tip)] {
7681 if common_ancestor.number.checked_add(1) == Some(tip.number)
7682 && tip.parent_hash != Some(common_ancestor.hash)
7683 {
7684 return Err(invalid(format!(
7685 "reorg {label} tip does not descend from the common ancestor"
7686 )));
7687 }
7688 }
7689 if let Some(finalized) = state.finalized_head.as_ref()
7690 && (common_ancestor.number < finalized.number
7691 || (common_ancestor.number == finalized.number
7692 && common_ancestor.hash != finalized.hash))
7693 {
7694 return Err(invalid(
7695 "reorg would cross or conflict with the finalized head".into(),
7696 ));
7697 }
7698 }
7699 }
7700 Ok(())
7701}
7702
7703fn validate_sequence_known_identity(
7704 state: &CanonicalSequenceState,
7705 block: &BlockRef,
7706 label: &str,
7707) -> Result<(), ReactiveError> {
7708 validate_sequence_known_hash_height(state, block, label)?;
7709 validate_sequence_known_parent_height(state, block, label)?;
7710 let known = state
7711 .coverage_head
7712 .as_ref()
7713 .filter(|head| head.number == block.number)
7714 .or_else(|| {
7715 state
7716 .retained_canonical_history
7717 .iter()
7718 .find(|entry| entry.number == block.number)
7719 });
7720 if let Some(known) = known
7721 && !optional_block_refs_are_compatible(Some(known), Some(block))
7722 {
7723 return Err(ReactiveError::InvalidChainControl {
7724 message: format!(
7725 "{label} block {}:{:?} conflicts with known canonical block {:?}",
7726 block.number, block.hash, known
7727 ),
7728 });
7729 }
7730 Ok(())
7731}
7732
7733fn validate_sequence_known_parent_height(
7734 state: &CanonicalSequenceState,
7735 block: &BlockRef,
7736 label: &str,
7737) -> Result<(), ReactiveError> {
7738 let Some(parent_hash) = block.parent_hash else {
7739 return Ok(());
7740 };
7741 let known_parent = state
7742 .retained_canonical_history
7743 .iter()
7744 .chain(state.coverage_head.iter())
7745 .chain(state.safe_head.iter())
7746 .chain(state.finalized_head.iter())
7747 .find(|known| known.hash == parent_hash);
7748 if let Some(parent) = known_parent
7749 && parent.number.checked_add(1) != Some(block.number)
7750 {
7751 return Err(ReactiveError::InvalidChainControl {
7752 message: format!(
7753 "{label} block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
7754 block.number, block.hash, parent.hash, parent.number
7755 ),
7756 });
7757 }
7758 Ok(())
7759}
7760
7761fn validate_sequence_head_within_coverage(
7762 state: &CanonicalSequenceState,
7763 block: &BlockRef,
7764 label: &str,
7765) -> Result<(), ReactiveError> {
7766 let Some(coverage) = state.coverage_head.as_ref() else {
7767 return Err(ReactiveError::InvalidChainControl {
7768 message: format!("{label} head requires an authoritative coverage head"),
7769 });
7770 };
7771 if block.number > coverage.number
7772 || (block.number == coverage.number
7773 && !optional_block_refs_are_compatible(Some(block), Some(coverage)))
7774 {
7775 return Err(ReactiveError::InvalidChainControl {
7776 message: format!(
7777 "{label} head {}:{:?} advances beyond or conflicts with coverage {}:{:?}",
7778 block.number, block.hash, coverage.number, coverage.hash
7779 ),
7780 });
7781 }
7782 Ok(())
7783}
7784
7785fn validate_sequence_matching_metadata(
7786 state: &CanonicalSequenceState,
7787 block: &BlockRef,
7788 label: &str,
7789) -> Result<(), ReactiveError> {
7790 validate_sequence_known_hash_height(state, block, label)?;
7791 validate_sequence_known_parent_height(state, block, label)?;
7792 let known = state
7793 .coverage_head
7794 .as_ref()
7795 .filter(|head| head.number == block.number && head.hash == block.hash)
7796 .or_else(|| {
7797 state
7798 .retained_canonical_history
7799 .iter()
7800 .find(|entry| entry.number == block.number && entry.hash == block.hash)
7801 });
7802 if let Some(known) = known
7803 && !optional_block_refs_are_compatible(Some(known), Some(block))
7804 {
7805 return Err(ReactiveError::InvalidChainControl {
7806 message: format!(
7807 "{label} block {}:{:?} carries metadata conflicting with known canonical block {:?}",
7808 block.number, block.hash, known
7809 ),
7810 });
7811 }
7812 Ok(())
7813}
7814
7815fn validate_sequence_known_hash_height(
7816 state: &CanonicalSequenceState,
7817 block: &BlockRef,
7818 label: &str,
7819) -> Result<(), ReactiveError> {
7820 let known = state
7821 .retained_canonical_history
7822 .iter()
7823 .chain(state.coverage_head.iter())
7824 .chain(state.safe_head.iter())
7825 .chain(state.finalized_head.iter())
7826 .find(|known| known.hash == block.hash);
7827 if let Some(known) = known
7828 && known.number != block.number
7829 {
7830 return Err(ReactiveError::InvalidChainControl {
7831 message: format!(
7832 "{label} block {}:{:?} reuses a canonical hash already known at height {}",
7833 block.number, block.hash, known.number
7834 ),
7835 });
7836 }
7837 Ok(())
7838}
7839
7840fn validate_reorg_ancestor_against_retained_branch(
7841 state: &CanonicalSequenceState,
7842 ancestor: &BlockRef,
7843) -> Result<(), ReactiveError> {
7844 let adjacent_number = ancestor.number.checked_add(1);
7845 for retained in state
7846 .retained_canonical_history
7847 .iter()
7848 .chain(state.coverage_head.iter())
7849 .chain(state.safe_head.iter())
7850 .chain(state.finalized_head.iter())
7851 {
7852 if Some(retained.number) == adjacent_number
7853 && retained
7854 .parent_hash
7855 .is_some_and(|parent| parent != ancestor.hash)
7856 {
7857 return Err(ReactiveError::InvalidChainControl {
7858 message: format!(
7859 "reorg common ancestor {}:{:?} conflicts with retained child {}:{:?} parent {:?}",
7860 ancestor.number,
7861 ancestor.hash,
7862 retained.number,
7863 retained.hash,
7864 retained.parent_hash
7865 ),
7866 });
7867 }
7868 if retained.parent_hash == Some(ancestor.hash) && Some(retained.number) != adjacent_number {
7869 return Err(ReactiveError::InvalidChainControl {
7870 message: format!(
7871 "reorg common ancestor {}:{:?} is named as the non-adjacent parent of retained block {}:{:?}",
7872 ancestor.number, ancestor.hash, retained.number, retained.hash
7873 ),
7874 });
7875 }
7876 }
7877 Ok(())
7878}
7879
7880fn validate_sequence_adjacent_parent_identity(
7881 state: &CanonicalSequenceState,
7882 block: &BlockRef,
7883 label: &str,
7884) -> Result<(), ReactiveError> {
7885 let Some(parent_hash) = block.parent_hash else {
7886 return Ok(());
7887 };
7888 let Some(parent_number) = block.number.checked_sub(1) else {
7889 return Ok(());
7890 };
7891 let known_parent = state
7892 .retained_canonical_history
7893 .iter()
7894 .chain(state.coverage_head.iter())
7895 .chain(state.safe_head.iter())
7896 .chain(state.finalized_head.iter())
7897 .find(|known| known.number == parent_number);
7898 if let Some(known_parent) = known_parent
7899 && known_parent.hash != parent_hash
7900 {
7901 return Err(ReactiveError::InvalidChainControl {
7902 message: format!(
7903 "{label} block {}:{:?} names parent {:?}, which conflicts with known adjacent block {}:{:?}",
7904 block.number, block.hash, parent_hash, known_parent.number, known_parent.hash
7905 ),
7906 });
7907 }
7908 Ok(())
7909}
7910
7911fn sequence_block_adds_metadata(state: &CanonicalSequenceState, incoming: &BlockRef) -> bool {
7912 state
7913 .coverage_head
7914 .iter()
7915 .chain(state.retained_canonical_history.iter())
7916 .filter(|known| known.number == incoming.number && known.hash == incoming.hash)
7917 .any(|known| {
7918 (known.parent_hash.is_none() && incoming.parent_hash.is_some())
7919 || (known.timestamp.is_none() && incoming.timestamp.is_some())
7920 })
7921}
7922
7923fn validate_sequence_implicit_finality<N: Network>(
7924 state: &CanonicalSequenceState,
7925 record: &ReactiveInputRecord<N>,
7926 resolved_canonical_block: Option<&BlockRef>,
7927) -> Result<(), ReactiveError> {
7928 let Some(finalized) = state.finalized_head.as_ref() else {
7929 return Ok(());
7930 };
7931 if let Some((dropped, _)) = reorg_signal_block(record) {
7932 if dropped.number <= finalized.number {
7933 return Err(ReactiveError::InvalidChainControl {
7934 message: format!(
7935 "implicit reorg at {}:{:?} would cross finalized head {}:{:?}",
7936 dropped.number, dropped.hash, finalized.number, finalized.hash
7937 ),
7938 });
7939 }
7940 return Ok(());
7941 }
7942 let Some(block) = resolved_canonical_block.or_else(|| canonical_record_block(record)) else {
7943 return Ok(());
7944 };
7945 let Some(latest) = state.coverage_head.as_ref() else {
7946 return Ok(());
7947 };
7948 if (block.number == latest.number && block.hash == latest.hash)
7949 || state
7950 .retained_canonical_history
7951 .iter()
7952 .any(|entry| entry.number == block.number && entry.hash == block.hash)
7953 || (latest.number.checked_add(1) == Some(block.number)
7954 && block.parent_hash == Some(latest.hash))
7955 || latest
7956 .number
7957 .checked_add(1)
7958 .is_some_and(|next| block.number > next)
7959 {
7960 return Ok(());
7961 }
7962 let crosses_finalized = if block.number <= finalized.number {
7963 true
7964 } else if let Some(parent_hash) = block.parent_hash {
7965 if finalized.number.checked_add(1) == Some(block.number) && parent_hash == finalized.hash {
7966 false
7967 } else if let Some(parent_index) =
7968 state.retained_canonical_history.iter().rposition(|entry| {
7969 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
7970 })
7971 {
7972 state
7973 .retained_canonical_history
7974 .iter()
7975 .skip(parent_index + 1)
7976 .any(|entry| entry.number <= finalized.number)
7977 } else {
7978 true
7979 }
7980 } else {
7981 true
7982 };
7983 if crosses_finalized {
7984 return Err(ReactiveError::InvalidChainControl {
7985 message: format!(
7986 "canonical input {}:{:?} would replace finalized head {}:{:?}",
7987 block.number, block.hash, finalized.number, finalized.hash
7988 ),
7989 });
7990 }
7991 Ok(())
7992}
7993
7994fn validate_required_reorg_anchor(
7995 required: Option<RequiredReorgAnchor>,
7996 block: &BlockRef,
7997) -> Result<(), ReactiveError> {
7998 let Some(required) = required else {
7999 return Ok(());
8000 };
8001 let ancestor_hash = required.hash();
8002 let restores_ancestor =
8003 block.number == required.number && ancestor_hash.is_some_and(|hash| block.hash == hash);
8004 let replaces_removed_child = required.number.checked_add(1) == Some(block.number)
8005 && ancestor_hash.is_some()
8006 && (block.parent_hash == ancestor_hash
8007 || (block.parent_hash.is_none() && required.permits_missing_child_parent));
8008 if restores_ancestor || replaces_removed_child {
8009 return Ok(());
8010 }
8011 Err(ReactiveError::InvalidChainControl {
8012 message: format!(
8013 "canonical replacement {}:{:?} does not prove the removed tip's parent at block {}",
8014 block.number, block.hash, required.number
8015 ),
8016 })
8017}
8018
8019fn validate_replacement_reorg_anchor(
8020 required: Option<RequiredReorgAnchor>,
8021 block: &BlockRef,
8022 policy: CanonicalSequenceValidationPolicy,
8023 oldest_retained: Option<u64>,
8024) -> Result<bool, CanonicalSequenceError> {
8025 let Some(required) = required else {
8026 return Ok(false);
8027 };
8028 match validate_required_reorg_anchor(Some(required), block) {
8029 Ok(()) => Ok(true),
8030 Err(error) if required.block.is_some() => Err(error.into()),
8031 Err(_) if policy.requires_complete_rollback() => {
8032 Err(CanonicalSequenceError::IncompleteRollback {
8033 common_ancestor: required.number,
8034 oldest_retained,
8035 kind: CanonicalRollbackKind::MissingReplacement,
8036 })
8037 }
8038 Err(_) => Ok(false),
8039 }
8040}
8041
8042fn apply_sequence_canonical_block(
8043 state: &mut CanonicalSequenceState,
8044 block: &BlockRef,
8045 allow_parentless_adjacent_extension: bool,
8046) -> Result<Option<SequenceRewind>, ReactiveError> {
8047 let latest = state.coverage_head;
8048 let already_known = state
8049 .retained_canonical_history
8050 .iter()
8051 .any(|entry| entry.number == block.number && entry.hash == block.hash);
8052 let repeats_tip =
8053 latest.is_some_and(|head| head.number == block.number && head.hash == block.hash);
8054 let extends_tip = latest.is_some_and(|head| {
8055 head.number.checked_add(1) == Some(block.number)
8056 && (block.parent_hash == Some(head.hash)
8057 || (allow_parentless_adjacent_extension && block.parent_hash.is_none()))
8058 });
8059 let forward_gap = latest.is_some_and(|head| {
8060 head.number
8061 .checked_add(1)
8062 .is_some_and(|next| block.number > next)
8063 });
8064 let mut rewind = None;
8065
8066 if latest.is_some() && !already_known && !repeats_tip && !extends_tip && !forward_gap {
8067 let retained_parent = block.parent_hash.and_then(|parent_hash| {
8068 state
8069 .retained_canonical_history
8070 .iter()
8071 .rposition(|entry| {
8072 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
8073 })
8074 .map(|index| (index, state.retained_canonical_history[index]))
8075 });
8076 let finalized_parent = block.parent_hash.and_then(|parent_hash| {
8077 state.finalized_head.filter(|finalized| {
8078 finalized.number.checked_add(1) == Some(block.number)
8079 && finalized.hash == parent_hash
8080 })
8081 });
8082 let (common_ancestor, dropped) = if let Some((parent_index, parent)) = retained_parent {
8083 let dropped = state.retained_canonical_history.split_off(parent_index + 1);
8084 (Some(parent), dropped)
8085 } else if let Some(finalized) = finalized_parent {
8086 let dropped = state
8087 .retained_canonical_history
8088 .iter()
8089 .position(|entry| entry.number > finalized.number)
8090 .map_or_else(Vec::new, |index| {
8091 state.retained_canonical_history.split_off(index)
8092 });
8093 (Some(finalized), dropped)
8094 } else {
8095 (None, std::mem::take(&mut state.retained_canonical_history))
8101 };
8102 state.coverage_head = common_ancestor;
8103 if let Some(common_ancestor) = common_ancestor {
8104 clear_sequence_heads_above(state, &common_ancestor);
8105 } else {
8106 state.safe_head = None;
8107 state.finalized_head = None;
8108 }
8109 rewind = Some(SequenceRewind {
8110 common_ancestor,
8111 dropped,
8112 });
8113 }
8114 upsert_sequence_history(&mut state.retained_canonical_history, block)?;
8115 advance_or_enrich_coverage(&mut state.coverage_head, block);
8116 Ok(rewind)
8117}
8118
8119fn sequence_implicit_replacement_requires_history(
8120 state: &CanonicalSequenceState,
8121 block: &BlockRef,
8122 policy: CanonicalSequenceValidationPolicy,
8123) -> Result<bool, ReactiveError> {
8124 let Some(latest) = state.coverage_head else {
8125 return Ok(false);
8126 };
8127 let already_known = state
8128 .retained_canonical_history
8129 .iter()
8130 .any(|entry| entry.number == block.number && entry.hash == block.hash);
8131 let repeats_tip = block.number == latest.number && block.hash == latest.hash;
8132 let extends_tip = latest.number.checked_add(1) == Some(block.number)
8133 && block.parent_hash == Some(latest.hash);
8134 let forward_gap = latest
8135 .number
8136 .checked_add(1)
8137 .is_some_and(|next| block.number > next);
8138 if already_known || repeats_tip || extends_tip || forward_gap {
8139 return Ok(false);
8140 }
8141 let Some(parent_hash) = block.parent_hash else {
8142 if policy.requires_complete_rollback() {
8143 return Err(ReactiveError::InvalidChainControl {
8144 message: format!(
8145 "implicit canonical replacement {}:{:?} must identify its parent",
8146 block.number, block.hash
8147 ),
8148 });
8149 }
8150 return Ok(true);
8151 };
8152 let known_adjacent_parent = block.number.checked_sub(1).and_then(|parent_number| {
8153 state
8154 .retained_canonical_history
8155 .iter()
8156 .chain(state.coverage_head.iter())
8157 .chain(state.safe_head.iter())
8158 .chain(state.finalized_head.iter())
8159 .find(|known| known.number == parent_number)
8160 });
8161 if let Some(known_parent) = known_adjacent_parent
8162 && known_parent.hash != parent_hash
8163 && policy.requires_complete_rollback()
8164 {
8165 return Err(ReactiveError::InvalidChainControl {
8166 message: format!(
8167 "implicit canonical replacement {}:{:?} names parent {:?}, which conflicts with known adjacent block {}:{:?}",
8168 block.number, block.hash, parent_hash, known_parent.number, known_parent.hash
8169 ),
8170 });
8171 }
8172 let retained_parent = state.retained_canonical_history.iter().any(|entry| {
8173 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
8174 });
8175 let finalized_parent = state.finalized_head.is_some_and(|finalized| {
8176 finalized.number.checked_add(1) == Some(block.number) && parent_hash == finalized.hash
8177 });
8178 Ok(!retained_parent && !finalized_parent)
8179}
8180
8181fn upsert_sequence_history(
8182 history: &mut Vec<BlockRef>,
8183 block: &BlockRef,
8184) -> Result<(), ReactiveError> {
8185 if let Some(existing) = history
8186 .iter_mut()
8187 .find(|entry| entry.number == block.number)
8188 {
8189 if existing.hash != block.hash {
8190 return Err(ReactiveError::InvalidChainControl {
8191 message: format!(
8192 "canonical block {}:{:?} conflicts with retained identity {:?}",
8193 block.number, block.hash, existing
8194 ),
8195 });
8196 }
8197 if !optional_block_refs_are_compatible(Some(existing), Some(block)) {
8198 return Err(ReactiveError::InvalidChainControl {
8199 message: format!(
8200 "canonical block {}:{:?} carries conflicting retained metadata",
8201 block.number, block.hash
8202 ),
8203 });
8204 }
8205 enrich_block_ref(existing, block);
8206 } else {
8207 history.push(*block);
8208 history.sort_by_key(|entry| entry.number);
8209 }
8210 Ok(())
8211}
8212
8213fn clear_sequence_heads_above(state: &mut CanonicalSequenceState, ancestor: &BlockRef) {
8214 if state.safe_head.as_ref().is_some_and(|head| {
8215 head.number > ancestor.number
8216 || (head.number == ancestor.number && head.hash != ancestor.hash)
8217 }) {
8218 state.safe_head = None;
8219 }
8220 if state.finalized_head.as_ref().is_some_and(|head| {
8221 head.number > ancestor.number
8222 || (head.number == ancestor.number && head.hash != ancestor.hash)
8223 }) {
8224 state.finalized_head = None;
8225 }
8226}
8227
8228fn validate_control_phase_order(controls: &[ChainControl]) -> Result<usize, ReactiveError> {
8229 let split = controls
8230 .iter()
8231 .position(|control| !matches!(control, ChainControl::Reorg { .. }))
8232 .unwrap_or(controls.len());
8233 if controls[split..]
8234 .iter()
8235 .any(|control| matches!(control, ChainControl::Reorg { .. }))
8236 {
8237 return Err(ReactiveError::InvalidChainControl {
8238 message: "reorg controls must precede records and all post-record controls in a batch"
8239 .into(),
8240 });
8241 }
8242 Ok(split)
8243}
8244
8245fn canonical_coverage_control_block(control: &ChainControl) -> Option<&BlockRef> {
8246 match control {
8247 ChainControl::CanonicalProgress(block)
8248 | ChainControl::Barrier {
8249 block: Some(block), ..
8250 } => Some(block),
8251 ChainControl::Reorg { .. }
8253 | ChainControl::Safe(_)
8254 | ChainControl::Finalized(_)
8255 | ChainControl::LogCoverage(_)
8256 | ChainControl::Barrier { block: None, .. } => None,
8257 }
8258}
8259
8260fn chain_control_canonical_assertion(control: &ChainControl) -> Option<&BlockRef> {
8261 match control {
8262 ChainControl::Safe(block)
8263 | ChainControl::Finalized(block)
8264 | ChainControl::CanonicalProgress(block)
8265 | ChainControl::LogCoverage(block)
8266 | ChainControl::Barrier {
8267 block: Some(block), ..
8268 } => Some(block),
8269 ChainControl::Reorg { .. } | ChainControl::Barrier { block: None, .. } => None,
8270 }
8271}
8272
8273fn assert_chain_control_identities(
8274 asserted_blocks: &mut HashMap<u64, BlockRef>,
8275 control: &ChainControl,
8276) -> Result<(), ReactiveError> {
8277 match control {
8278 ChainControl::Safe(block)
8279 | ChainControl::Finalized(block)
8280 | ChainControl::CanonicalProgress(block)
8281 | ChainControl::LogCoverage(block)
8282 | ChainControl::Barrier {
8283 block: Some(block), ..
8284 } => assert_canonical_block_identity(asserted_blocks, block, "chain control"),
8285 ChainControl::Barrier { block: None, .. } => Ok(()),
8286 ChainControl::Reorg {
8287 common_ancestor,
8288 new_tip,
8289 ..
8290 } => {
8291 asserted_blocks.retain(|number, _| *number <= common_ancestor.number);
8292 assert_canonical_block_identity(
8293 asserted_blocks,
8294 common_ancestor,
8295 "reorg common ancestor",
8296 )?;
8297 assert_canonical_block_identity(asserted_blocks, new_tip, "reorg new tip")
8298 }
8299 }
8300}
8301
8302fn assert_canonical_block_identity(
8303 asserted_blocks: &mut HashMap<u64, BlockRef>,
8304 block: &BlockRef,
8305 label: &str,
8306) -> Result<(), ReactiveError> {
8307 for asserted in asserted_blocks.values() {
8308 if asserted.hash == block.hash && asserted.number != block.number {
8309 return Err(ReactiveError::InvalidChainControl {
8310 message: format!(
8311 "{label} hash {:?} is already asserted at height {}, not {}",
8312 block.hash, asserted.number, block.number
8313 ),
8314 });
8315 }
8316 if block
8317 .parent_hash
8318 .is_some_and(|parent| parent == asserted.hash)
8319 && asserted.number.checked_add(1) != Some(block.number)
8320 {
8321 return Err(ReactiveError::InvalidChainControl {
8322 message: format!(
8323 "{label} block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
8324 block.number, block.hash, asserted.hash, asserted.number
8325 ),
8326 });
8327 }
8328 if asserted
8329 .parent_hash
8330 .is_some_and(|parent| parent == block.hash)
8331 && block.number.checked_add(1) != Some(asserted.number)
8332 {
8333 return Err(ReactiveError::InvalidChainControl {
8334 message: format!(
8335 "block {}:{:?} asserted earlier names {label} hash {:?} from non-adjacent height {} as its parent",
8336 asserted.number, asserted.hash, block.hash, block.number
8337 ),
8338 });
8339 }
8340 }
8341 if let Some(known) = asserted_blocks.get_mut(&block.number) {
8342 if !optional_block_refs_are_compatible(Some(known), Some(block)) {
8343 return Err(ReactiveError::InvalidChainControl {
8344 message: format!(
8345 "{label} block {}:{:?} conflicts with block identity {:?} asserted earlier in the batch",
8346 block.number, block.hash, known
8347 ),
8348 });
8349 }
8350 enrich_block_ref(known, block);
8351 } else {
8352 asserted_blocks.insert(block.number, *block);
8353 }
8354 Ok(())
8355}
8356
8357fn set_or_enrich_block_ref(current: &mut Option<BlockRef>, incoming: &BlockRef) {
8358 match current {
8359 Some(current) if current.number == incoming.number && current.hash == incoming.hash => {
8360 enrich_block_ref(current, incoming);
8361 }
8362 _ => *current = Some(*incoming),
8363 }
8364}
8365
8366fn advance_or_enrich_coverage(current: &mut Option<BlockRef>, incoming: &BlockRef) {
8367 match current {
8368 Some(current) if current.number == incoming.number && current.hash == incoming.hash => {
8369 enrich_block_ref(current, incoming);
8370 }
8371 Some(current) if current.number >= incoming.number => {}
8372 _ => *current = Some(*incoming),
8373 }
8374}
8375
8376fn validate_adjacent_finality(
8377 finalized: Option<&BlockRef>,
8378 safe: Option<&BlockRef>,
8379) -> Result<(), ReactiveError> {
8380 let Some((finalized, safe)) = finalized.zip(safe) else {
8381 return Ok(());
8382 };
8383 if finalized.number.checked_add(1) == Some(safe.number)
8384 && safe.parent_hash != Some(finalized.hash)
8385 {
8386 return Err(ReactiveError::InvalidChainControl {
8387 message: "adjacent safe head does not descend from finalized head".into(),
8388 });
8389 }
8390 Ok(())
8391}
8392
8393fn collect_diff_addresses(diff: &StateDiff, into: &mut HashSet<Address>) {
8399 into.extend(diff.slots.iter().map(|change| change.address));
8400 into.extend(diff.accounts.iter().map(|change| change.address));
8401 into.extend(diff.purged.iter().map(|purge| purge.address));
8402 into.extend(diff.skipped.iter().map(|skipped| skipped.address));
8403 into.extend(diff.skipped_balances.iter().map(|skipped| skipped.address));
8404 into.extend(diff.skipped_masks.iter().map(|skipped| skipped.address));
8405 into.extend(diff.skipped_accounts.iter().map(|skipped| skipped.address));
8406}
8407
8408fn root_moved_account_resync(
8413 address: Address,
8414 block: u64,
8415 fields: AccountFieldMask,
8416) -> ResyncRequest {
8417 ResyncRequest {
8418 id: ResyncId::new(format!("root-moved:{address:#x}:{block}")),
8419 reason: ResyncReason::RootMoved,
8420 block: ResyncBlock::Number(block),
8421 targets: vec![ResyncTarget::Account { address, fields }],
8422 priority: ResyncPriority::Normal,
8423 }
8424}
8425
8426fn batch_preconfirmation<N: Network>(
8427 batch: &ReactiveInputBatch<N>,
8428) -> Result<Option<FlashblockRef>, ReactiveError> {
8429 let mut flashblock: Option<FlashblockRef> = None;
8430 let mut has_non_preconfirmed = false;
8431 for (index, record) in batch.records().iter().enumerate() {
8432 match &record.context.chain_status {
8433 ChainStatus::Preconfirmed {
8434 flashblock: current,
8435 } => {
8436 if batch.record_delivery_scope(index) != Some(DeliveryScope::Preconfirmed) {
8437 return Err(ReactiveError::InvalidInputRecord {
8438 message: "pre-confirmed input requires pre-confirmed delivery scope".into(),
8439 });
8440 }
8441 if flashblock
8442 .as_ref()
8443 .is_some_and(|known| known != current.as_ref())
8444 {
8445 return Err(ReactiveError::InvalidInputRecord {
8446 message: "one batch cannot mix distinct Flashblock snapshots".into(),
8447 });
8448 }
8449 flashblock.get_or_insert_with(|| current.as_ref().clone());
8450 }
8451 _ => has_non_preconfirmed = true,
8452 }
8453 }
8454 if flashblock.is_some() && (has_non_preconfirmed || !batch.chain_controls().is_empty()) {
8455 return Err(ReactiveError::InvalidInputRecord {
8456 message: "pre-confirmed delivery cannot mix canonical inputs or chain controls".into(),
8457 });
8458 }
8459 Ok(flashblock)
8460}
8461
8462fn canonical_record_block<N: Network>(record: &ReactiveInputRecord<N>) -> Option<&BlockRef> {
8463 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
8464 return None;
8465 }
8466 if is_canonical_status(&record.context.chain_status) {
8467 return context_block_ref(&record.context);
8468 }
8469 None
8470}
8471
8472fn resolve_record_block_payload_metadata<N: Network>(
8473 record: &ReactiveInputRecord<N>,
8474 mut block: BlockRef,
8475) -> Result<BlockRef, ReactiveError> {
8476 let ReactiveInput::Log(log) = &record.input else {
8477 return Ok(block);
8478 };
8479 if log.block_number != Some(block.number) || log.block_hash != Some(block.hash) {
8480 return Err(ReactiveError::InvalidInputRecord {
8481 message: "log payload and canonical context carry different block identities".into(),
8482 });
8483 }
8484 if let Some(timestamp) = log.block_timestamp {
8485 if block.timestamp.is_some_and(|known| known != timestamp) {
8486 return Err(ReactiveError::InvalidInputRecord {
8487 message: "log payload and canonical context carry different block timestamps"
8488 .into(),
8489 });
8490 }
8491 block.timestamp = Some(timestamp);
8492 }
8493 Ok(block)
8494}
8495
8496fn validate_input_record<N: Network>(record: &ReactiveInputRecord<N>) -> Result<(), ReactiveError> {
8497 let invalid = |message: String| ReactiveError::InvalidInputRecord { message };
8498 if let ChainStatus::Preconfirmed { flashblock } = &record.context.chain_status
8499 && record.context.block != Some(flashblock.block_ref())
8500 {
8501 return Err(invalid(
8502 "pre-confirmed status and context carry different partial block identities".into(),
8503 ));
8504 }
8505 let status_block = match &record.context.chain_status {
8506 ChainStatus::Included { block, .. }
8507 | ChainStatus::Safe { block }
8508 | ChainStatus::Finalized { block }
8509 | ChainStatus::Reorged {
8510 dropped_from: block,
8511 } => Some(block),
8512 ChainStatus::Preconfirmed { .. } => record.context.block.as_ref(),
8513 ChainStatus::Pending => None,
8514 };
8515 match (status_block, record.context.block.as_ref()) {
8516 (Some(status), Some(context)) if status == context => {}
8517 (Some(_), Some(_)) => {
8518 return Err(invalid(
8519 "chain status and context carry different block identities".into(),
8520 ));
8521 }
8522 (Some(_), None) => {
8523 return Err(invalid(
8524 "included or reorged input is missing its context block".into(),
8525 ));
8526 }
8527 (None, Some(_)) => {
8528 return Err(invalid(
8529 "pending input cannot carry a canonical context block".into(),
8530 ));
8531 }
8532 (None, None) => {}
8533 }
8534
8535 match &record.input {
8536 ReactiveInput::Log(log) => {
8537 let Some(block) = status_block else {
8538 return Err(invalid(
8539 "log input must carry an included or reorged block identity".into(),
8540 ));
8541 };
8542 if log.removed && !matches!(record.context.chain_status, ChainStatus::Reorged { .. }) {
8543 return Err(invalid(
8544 "removed log must carry reorged chain status".into(),
8545 ));
8546 }
8547 let block_number = log
8548 .block_number
8549 .ok_or_else(|| invalid("log is missing its block number".into()))?;
8550 let block_hash = log
8551 .block_hash
8552 .ok_or_else(|| invalid("log is missing its block hash".into()))?;
8553 log.transaction_hash
8554 .ok_or_else(|| invalid("log is missing its transaction hash".into()))?;
8555 let transaction_index = log
8556 .transaction_index
8557 .ok_or_else(|| invalid("log is missing its transaction index".into()))?;
8558 let log_index = log
8559 .log_index
8560 .ok_or_else(|| invalid("log is missing its log index".into()))?;
8561 if block_number != block.number
8562 || block_hash != block.hash
8563 || !optional_metadata_compatible(
8564 log.block_timestamp.as_ref(),
8565 block.timestamp.as_ref(),
8566 )
8567 {
8568 return Err(invalid(
8569 "log payload and context carry different block identities".into(),
8570 ));
8571 }
8572 if record.context.transaction_index != Some(transaction_index)
8573 || record.context.log_index != Some(log_index)
8574 {
8575 return Err(invalid(
8576 "log payload and context carry different transaction/log positions".into(),
8577 ));
8578 }
8579 }
8580 ReactiveInput::BlockHeader(header) => {
8581 if let Some(block) = status_block {
8582 if header.number() != block.number
8583 || header.hash() != block.hash
8584 || Some(header.parent_hash()) != block.parent_hash
8585 || Some(header.timestamp()) != block.timestamp
8586 {
8587 return Err(invalid(
8588 "block header payload and context carry different block identities".into(),
8589 ));
8590 }
8591 } else if !matches!(record.context.chain_status, ChainStatus::Pending) {
8592 return Err(invalid("block header has an unsupported lifecycle".into()));
8593 }
8594 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8595 return Err(invalid(
8596 "block header context cannot carry transaction/log positions".into(),
8597 ));
8598 }
8599 }
8600 ReactiveInput::FullBlock(block_response) => {
8601 let header = block_response.header();
8602 if let Some(block) = status_block {
8603 if header.number() != block.number
8604 || header.hash() != block.hash
8605 || Some(header.parent_hash()) != block.parent_hash
8606 || Some(header.timestamp()) != block.timestamp
8607 {
8608 return Err(invalid(
8609 "full-block payload and context carry different block identities".into(),
8610 ));
8611 }
8612 } else if !matches!(record.context.chain_status, ChainStatus::Pending) {
8613 return Err(invalid("full block has an unsupported lifecycle".into()));
8614 }
8615 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8616 return Err(invalid(
8617 "full-block context cannot carry transaction/log positions".into(),
8618 ));
8619 }
8620 if let Some(transactions) = block_response.transactions().as_transactions() {
8621 for (index, transaction) in transactions.iter().enumerate() {
8622 if transaction
8623 .block_hash()
8624 .is_some_and(|hash| hash != header.hash())
8625 || transaction
8626 .block_number()
8627 .is_some_and(|number| number != header.number())
8628 || transaction
8629 .transaction_index()
8630 .is_some_and(|position| position != index as u64)
8631 {
8632 return Err(invalid(format!(
8633 "full-block transaction {index} carries contradictory inclusion metadata"
8634 )));
8635 }
8636 if transaction
8637 .chain_id()
8638 .zip(record.context.chain_id)
8639 .is_some_and(|(transaction, context)| transaction != context)
8640 {
8641 return Err(invalid(format!(
8642 "full-block transaction {index} carries a chain id conflicting with its context"
8643 )));
8644 }
8645 }
8646 }
8647 }
8648 ReactiveInput::PendingTxHash(_) => {
8649 if !matches!(record.context.chain_status, ChainStatus::Pending) {
8650 return Err(invalid(
8651 "pending transaction input must carry pending chain status".into(),
8652 ));
8653 }
8654 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8655 return Err(invalid(
8656 "pending transaction context cannot carry canonical positions".into(),
8657 ));
8658 }
8659 }
8660 ReactiveInput::PendingTx(transaction) => {
8661 if !matches!(record.context.chain_status, ChainStatus::Pending) {
8662 return Err(invalid(
8663 "pending transaction input must carry pending chain status".into(),
8664 ));
8665 }
8666 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8667 return Err(invalid(
8668 "pending transaction context cannot carry canonical positions".into(),
8669 ));
8670 }
8671 if transaction.block_hash().is_some()
8672 || transaction.block_number().is_some()
8673 || transaction.transaction_index().is_some()
8674 {
8675 return Err(invalid(
8676 "hydrated pending transaction cannot carry inclusion metadata".into(),
8677 ));
8678 }
8679 if transaction
8680 .chain_id()
8681 .zip(record.context.chain_id)
8682 .is_some_and(|(transaction, context)| transaction != context)
8683 {
8684 return Err(invalid(
8685 "pending transaction carries a chain id conflicting with its context".into(),
8686 ));
8687 }
8688 }
8689 }
8690 Ok(())
8691}
8692
8693fn advance_block_for_canonical_record<N: Network>(
8702 cache: &mut EvmCache,
8703 record: &ReactiveInputRecord<N>,
8704) -> Option<Result<(), BlockContextError>> {
8705 if !is_canonical_status(&record.context.chain_status) {
8706 return None;
8707 }
8708 match &record.input {
8709 ReactiveInput::BlockHeader(header) => Some(cache.advance_block(header)),
8710 ReactiveInput::FullBlock(block) => Some(cache.advance_block(block.header())),
8711 _ => None,
8712 }
8713}
8714
8715fn context_block_ref(ctx: &ReactiveContext) -> Option<&BlockRef> {
8716 match &ctx.chain_status {
8717 ChainStatus::Included { block, .. }
8718 | ChainStatus::Safe { block }
8719 | ChainStatus::Finalized { block } => Some(block),
8720 ChainStatus::Reorged { dropped_from } => Some(dropped_from),
8721 ChainStatus::Preconfirmed { .. } => ctx.block.as_ref(),
8722 ChainStatus::Pending => ctx.block.as_ref(),
8723 }
8724}
8725
8726fn reorg_signal_block<N: Network>(
8727 record: &ReactiveInputRecord<N>,
8728) -> Option<(BlockRef, ReorgReason)> {
8729 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
8730 return block_ref_from_record(record).map(|block| (block, ReorgReason::RemovedLog));
8731 }
8732
8733 if let ChainStatus::Reorged { dropped_from } = &record.context.chain_status {
8734 return Some((*dropped_from, ReorgReason::ReorgedInput));
8735 }
8736
8737 None
8738}
8739
8740fn block_ref_from_record<N: Network>(record: &ReactiveInputRecord<N>) -> Option<BlockRef> {
8741 context_block_ref(&record.context)
8742 .cloned()
8743 .or_else(|| match &record.input {
8744 ReactiveInput::Log(log) => Some(BlockRef {
8745 number: log.block_number?,
8746 hash: log.block_hash?,
8747 parent_hash: None,
8748 timestamp: log.block_timestamp,
8749 }),
8750 ReactiveInput::BlockHeader(header) => Some(BlockRef {
8751 number: header.number(),
8752 hash: header.hash(),
8753 parent_hash: Some(header.parent_hash()),
8754 timestamp: Some(header.timestamp()),
8755 }),
8756 ReactiveInput::FullBlock(block) => {
8757 let header = block.header();
8758 Some(BlockRef {
8759 number: header.number(),
8760 hash: header.hash(),
8761 parent_hash: Some(header.parent_hash()),
8762 timestamp: Some(header.timestamp()),
8763 })
8764 }
8765 ReactiveInput::PendingTxHash(_) | ReactiveInput::PendingTx(_) => None,
8766 })
8767}
8768
8769fn remove_canceled_resyncs_from_batch(
8770 resyncs: &mut Vec<ResyncRequest>,
8771 canceled: &[ResyncRequest],
8772) {
8773 if canceled.is_empty() {
8774 return;
8775 }
8776 let canceled_ids: HashSet<_> = canceled.iter().map(|request| request.id.clone()).collect();
8777 resyncs.retain(|request| !canceled_ids.contains(&request.id));
8778}
8779
8780fn resync_target_address(target: &ResyncTarget) -> Address {
8781 match target {
8782 ResyncTarget::StorageSlot { address, .. }
8783 | ResyncTarget::StorageSlots { address, .. }
8784 | ResyncTarget::Account { address, .. } => *address,
8785 }
8786}
8787
8788fn resync_request_targets_dropped_block(
8789 request: &ResyncRequest,
8790 dropped_blocks: &[BlockRef],
8791) -> bool {
8792 let ResyncBlock::Hash { number, hash, .. } = &request.block else {
8793 return false;
8794 };
8795 dropped_blocks
8796 .iter()
8797 .any(|block| block.hash == *hash && block.number == *number)
8798}
8799
8800fn single_hash_pinned_resync_block(report: &ResyncReport) -> Option<BlockRef> {
8801 let first = report.requested.first()?.block.clone();
8802 if !report
8803 .requested
8804 .iter()
8805 .all(|request| request.block == first)
8806 {
8807 return None;
8808 }
8809
8810 let ResyncBlock::Hash { number, hash, .. } = first else {
8811 return None;
8812 };
8813
8814 Some(BlockRef {
8815 number,
8816 hash,
8817 parent_hash: None,
8818 timestamp: None,
8819 })
8820}
8821
8822fn purge_scopes_for_dropped_journals<N: Network>(
8823 dropped: &[BlockJournal<N>],
8824) -> Vec<(Address, PurgeScope)> {
8825 let mut scopes: Vec<(Address, PurgeScope)> = Vec::new();
8826 for entry in dropped.iter().rev() {
8827 for diff in entry.rollback_diffs.iter().rev() {
8828 merge_purge_scopes_for_diff(&mut scopes, diff);
8829 }
8830 }
8831 scopes
8832}
8833
8834fn rollback_updates_for_dropped_journals<N: Network>(
8835 dropped: &[BlockJournal<N>],
8836 purge_scopes: &[(Address, PurgeScope)],
8837) -> Vec<StateUpdate> {
8838 let purge_addresses: HashSet<_> = purge_scopes
8839 .iter()
8840 .map(|(address, _scope)| *address)
8841 .collect();
8842 let mut updates = Vec::new();
8843 for entry in dropped.iter().rev() {
8844 for diff in entry.rollback_diffs.iter().rev() {
8845 push_rollback_updates_for_diff(&mut updates, diff, &purge_addresses);
8846 }
8847 }
8848 updates
8849}
8850
8851fn merge_purge_scopes_for_diff(scopes: &mut Vec<(Address, PurgeScope)>, diff: &StateDiff) {
8852 for change in &diff.accounts {
8853 merge_purge_scope(scopes, change.address, PurgeScope::Account);
8854 }
8855 for record in &diff.purged {
8856 merge_purge_scope(scopes, record.address, record.scope.clone());
8857 }
8858}
8859
8860fn push_rollback_updates_for_diff(
8861 updates: &mut Vec<StateUpdate>,
8862 diff: &StateDiff,
8863 purge_addresses: &HashSet<Address>,
8864) {
8865 for change in diff.slots.iter().rev() {
8866 if purge_addresses.contains(&change.address) {
8867 continue;
8868 }
8869 updates.push(StateUpdate::slot(change.address, change.slot, change.old));
8870 }
8871}
8872
8873fn merge_purge_scope(scopes: &mut Vec<(Address, PurgeScope)>, address: Address, scope: PurgeScope) {
8874 if let Some((_existing_address, existing_scope)) = scopes
8875 .iter_mut()
8876 .find(|(existing_address, _scope)| *existing_address == address)
8877 {
8878 *existing_scope = merged_purge_scope(existing_scope.clone(), scope);
8879 } else {
8880 scopes.push((address, scope));
8881 }
8882}
8883
8884fn merged_purge_scope(left: PurgeScope, right: PurgeScope) -> PurgeScope {
8885 match (left, right) {
8886 (PurgeScope::Account, _) | (_, PurgeScope::Account) => PurgeScope::Account,
8887 (PurgeScope::AllStorage, _) | (_, PurgeScope::AllStorage) => PurgeScope::AllStorage,
8888 (PurgeScope::Slots(mut left), PurgeScope::Slots(right)) => {
8889 for slot in right {
8890 if !left.contains(&slot) {
8891 left.push(slot);
8892 }
8893 }
8894 PurgeScope::Slots(left)
8895 }
8896 }
8897}
8898
8899#[derive(Clone, Debug)]
8900struct StorageFetchSlot {
8901 address: Address,
8902 slot: U256,
8903 origins: Vec<StorageFetchOrigin>,
8904}
8905
8906#[derive(Clone, Debug)]
8907struct StorageFetchOrigin {
8908 request_id: ResyncId,
8909 target: ResyncTarget,
8910}
8911
8912#[derive(Clone, Debug)]
8913struct StorageFetchGroup {
8914 block: ResyncBlock,
8915 slots: Vec<StorageFetchSlot>,
8916 seen: HashSet<(Address, U256)>,
8917}
8918
8919#[derive(Clone, Debug)]
8922struct AccountResyncTarget {
8923 request_id: ResyncId,
8924 block: ResyncBlock,
8925 address: Address,
8926 fields: AccountFieldMask,
8927}
8928
8929fn resolve_trace_resyncs(
8930 cache: &EvmCache,
8931 storage_groups: &mut Vec<StorageFetchGroup>,
8932 account_targets: &mut Vec<AccountResyncTarget>,
8933 state_updates: &mut Vec<StateUpdate>,
8934) {
8935 let Some(fetcher) = cache.block_state_diff_fetcher().cloned() else {
8936 return;
8937 };
8938
8939 let mut blocks = Vec::new();
8940 let mut seen = HashSet::new();
8941 for block in storage_groups
8942 .iter()
8943 .map(|group| group.block.clone())
8944 .chain(account_targets.iter().map(|target| target.block.clone()))
8945 {
8946 if seen.insert(block.clone()) {
8947 blocks.push(block);
8948 }
8949 }
8950
8951 let mut traces = HashMap::new();
8952 for block in blocks {
8953 match (fetcher)(resync_block_to_block_id(&block)) {
8954 Ok(diff) => {
8955 traces.insert(block, diff);
8956 }
8957 Err(error) => {
8958 tracing::debug!(
8959 block = ?block,
8960 error = %error,
8961 "block trace resync source failed; falling back to point resync"
8962 );
8963 }
8964 }
8965 }
8966
8967 for group in storage_groups.iter_mut() {
8968 let Some(trace) = traces.get(&group.block) else {
8969 continue;
8970 };
8971 group.slots.retain(|slot| {
8972 if let Some(value) = trace_storage_value(trace, slot.address, slot.slot) {
8973 state_updates.push(StateUpdate::slot(slot.address, slot.slot, value));
8974 return false;
8975 }
8976 cache
8977 .cached_storage_value(slot.address, slot.slot)
8978 .is_none()
8979 });
8980 group.seen = group
8981 .slots
8982 .iter()
8983 .map(|slot| (slot.address, slot.slot))
8984 .collect();
8985 }
8986 storage_groups.retain(|group| !group.slots.is_empty());
8987
8988 let mut unresolved_accounts = Vec::new();
8989 for mut account in account_targets.drain(..) {
8990 let Some(trace) = traces.get(&account.block) else {
8991 unresolved_accounts.push(account);
8992 continue;
8993 };
8994 let Some(trace_account) = trace
8995 .accounts
8996 .iter()
8997 .find(|diff| diff.address == account.address)
8998 else {
8999 unresolved_accounts.push(account);
9000 continue;
9001 };
9002
9003 let mut patch = AccountPatch::default();
9004 let mut unresolved = AccountFieldMask::default();
9005 if account.fields.balance {
9006 if let Some(balance) = trace_account.balance {
9007 patch = patch.balance(balance);
9008 } else {
9009 unresolved.balance = true;
9010 }
9011 }
9012 if account.fields.nonce {
9013 if let Some(nonce) = trace_account.nonce {
9014 patch = patch.nonce(nonce);
9015 } else {
9016 unresolved.nonce = true;
9017 }
9018 }
9019 if account.fields.code {
9020 if let Some(code) = &trace_account.code {
9021 patch = patch.code(code.clone());
9022 } else {
9023 unresolved.code = true;
9024 }
9025 }
9026
9027 if patch.balance.is_some() || patch.nonce.is_some() || patch.code.is_some() {
9028 state_updates.push(StateUpdate::account_upsert(account.address, patch));
9029 }
9030 if !account_field_mask_empty(unresolved) {
9031 account.fields = unresolved;
9032 unresolved_accounts.push(account);
9033 }
9034 }
9035 *account_targets = unresolved_accounts;
9036}
9037
9038fn trace_storage_value(trace: &BlockStateDiff, address: Address, slot: U256) -> Option<U256> {
9039 trace
9040 .accounts
9041 .iter()
9042 .find(|account| account.address == address)
9043 .and_then(|account| {
9044 account
9045 .storage
9046 .iter()
9047 .find(|entry| entry.slot == slot)
9048 .map(|entry| entry.value)
9049 })
9050}
9051
9052fn account_field_mask_empty(mask: AccountFieldMask) -> bool {
9053 !mask.balance && !mask.nonce && !mask.code
9054}
9055
9056fn execute_resync_requests(cache: &mut EvmCache, requests: &[ResyncRequest]) -> ResyncReport {
9057 let mut failed = Vec::new();
9058 let mut storage_groups: Vec<StorageFetchGroup> = Vec::new();
9059 let mut account_targets: Vec<AccountResyncTarget> = Vec::new();
9060
9061 for request in requests {
9062 for target in &request.targets {
9063 match target {
9064 ResyncTarget::StorageSlot { address, slot } => {
9065 push_storage_resync_slot(
9066 &mut storage_groups,
9067 &request.id,
9068 &request.block,
9069 *address,
9070 *slot,
9071 );
9072 }
9073 ResyncTarget::StorageSlots { address, slots } => {
9074 for slot in slots {
9075 push_storage_resync_slot(
9076 &mut storage_groups,
9077 &request.id,
9078 &request.block,
9079 *address,
9080 *slot,
9081 );
9082 }
9083 }
9084 ResyncTarget::Account { address, fields } => {
9085 account_targets.push(AccountResyncTarget {
9086 request_id: request.id.clone(),
9087 block: request.block.clone(),
9088 address: *address,
9089 fields: *fields,
9090 });
9091 }
9092 }
9093 }
9094 }
9095
9096 let mut state_updates = Vec::new();
9097 resolve_trace_resyncs(
9098 cache,
9099 &mut storage_groups,
9100 &mut account_targets,
9101 &mut state_updates,
9102 );
9103
9104 if !storage_groups.is_empty() {
9105 if let Some(fetcher) = cache.storage_batch_fetcher().cloned() {
9106 for group in storage_groups {
9107 let block = group.block.clone();
9108 let fetches: Vec<(Address, U256)> = group
9109 .slots
9110 .iter()
9111 .map(|slot| (slot.address, slot.slot))
9112 .collect();
9113 let results = (fetcher)(fetches, resync_block_to_block_id(&block));
9114 let mut pending: HashMap<(Address, U256), StorageFetchSlot> = group
9115 .slots
9116 .iter()
9117 .cloned()
9118 .map(|slot| ((slot.address, slot.slot), slot))
9119 .collect();
9120
9121 for (address, slot, fetched) in results {
9122 let Some(requested_slot) = pending.remove(&(address, slot)) else {
9123 continue;
9124 };
9125 match fetched {
9126 Ok(value) => state_updates.push(StateUpdate::slot(address, slot, value)),
9127 Err(error) => {
9128 let message = error.to_string();
9129 push_resync_failures(
9130 &mut failed,
9131 &block,
9132 requested_slot.origins,
9133 ResyncFailureKind::StorageFetchFailed,
9134 message,
9135 );
9136 }
9137 }
9138 }
9139
9140 for requested_slot in group.slots {
9141 if pending
9142 .remove(&(requested_slot.address, requested_slot.slot))
9143 .is_some()
9144 {
9145 push_resync_failures(
9146 &mut failed,
9147 &block,
9148 requested_slot.origins,
9149 ResyncFailureKind::StorageFetchOmitted,
9150 "storage batch fetcher did not return a value for slot".to_string(),
9151 );
9152 }
9153 }
9154 }
9155 } else {
9156 for group in storage_groups {
9157 let block = group.block.clone();
9158 for slot in group.slots {
9159 push_resync_failures(
9160 &mut failed,
9161 &block,
9162 slot.origins,
9163 ResyncFailureKind::MissingStorageFetcher,
9164 "storage resync requires a storage batch fetcher".to_string(),
9165 );
9166 }
9167 }
9168 }
9169 }
9170
9171 if !account_targets.is_empty() {
9172 if let Some(fetcher) = cache.account_proof_fetcher().cloned() {
9173 let mut groups: Vec<(BlockId, Vec<_>)> = Vec::new();
9179 for account in account_targets {
9180 let block_id = resync_block_to_block_id(&account.block);
9181 match groups
9182 .iter_mut()
9183 .find(|(group_block, _)| *group_block == block_id)
9184 {
9185 Some((_, group)) => group.push(account),
9186 None => groups.push((block_id, vec![account])),
9187 }
9188 }
9189 for (block_id, group) in groups {
9190 let probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
9191 group
9192 .iter()
9193 .map(|account| (account.address, vec![]))
9194 .collect(),
9195 block_id,
9196 )
9197 .into_iter()
9198 .collect();
9199 for account in group {
9200 match probes.get(&account.address).cloned() {
9204 Some(Ok(proof)) => {
9205 let mut patch = AccountPatch::default();
9210 if account.fields.balance {
9211 patch = patch.balance(proof.balance);
9212 }
9213 if account.fields.nonce {
9214 patch = patch.nonce(proof.nonce);
9215 }
9216 state_updates.push(StateUpdate::account_upsert(account.address, patch));
9222 }
9223 Some(Err(error)) => {
9224 failed.push(ResyncFailure {
9225 request_id: account.request_id,
9226 block: account.block,
9227 target: ResyncTarget::Account {
9228 address: account.address,
9229 fields: account.fields,
9230 },
9231 kind: ResyncFailureKind::AccountFetchFailed,
9232 message: error.to_string(),
9233 });
9234 }
9235 None => {
9236 failed.push(ResyncFailure {
9237 request_id: account.request_id,
9238 block: account.block,
9239 target: ResyncTarget::Account {
9240 address: account.address,
9241 fields: account.fields,
9242 },
9243 kind: ResyncFailureKind::AccountFetchOmitted,
9244 message:
9245 "account proof fetcher did not return a result for address"
9246 .to_string(),
9247 });
9248 }
9249 }
9250 }
9251 }
9252 } else {
9253 for account in account_targets {
9254 failed.push(ResyncFailure {
9255 request_id: account.request_id,
9256 block: account.block,
9257 target: ResyncTarget::Account {
9258 address: account.address,
9259 fields: account.fields,
9260 },
9261 kind: ResyncFailureKind::MissingAccountFetcher,
9262 message: "account resync requires an account proof fetcher".to_string(),
9263 });
9264 }
9265 }
9266 }
9267
9268 let diff = if state_updates.is_empty() {
9269 StateDiff::default()
9270 } else {
9271 cache.apply_updates(&state_updates)
9272 };
9273
9274 ResyncReport {
9275 requested: requests.to_vec(),
9276 state_updates,
9277 diff,
9278 failed,
9279 }
9280}
9281
9282fn push_resync_failures(
9283 failed: &mut Vec<ResyncFailure>,
9284 block: &ResyncBlock,
9285 origins: Vec<StorageFetchOrigin>,
9286 kind: ResyncFailureKind,
9287 message: String,
9288) {
9289 for origin in origins {
9290 failed.push(ResyncFailure {
9291 request_id: origin.request_id,
9292 block: block.clone(),
9293 target: origin.target,
9294 kind,
9295 message: message.clone(),
9296 });
9297 }
9298}
9299
9300fn push_storage_resync_slot(
9301 groups: &mut Vec<StorageFetchGroup>,
9302 request_id: &ResyncId,
9303 block: &ResyncBlock,
9304 address: Address,
9305 slot: U256,
9306) {
9307 let group_index = if let Some(index) = groups.iter().position(|group| group.block == *block) {
9308 index
9309 } else {
9310 groups.push(StorageFetchGroup {
9311 block: block.clone(),
9312 slots: Vec::new(),
9313 seen: HashSet::new(),
9314 });
9315 groups.len() - 1
9316 };
9317
9318 let group = &mut groups[group_index];
9319 let origin = StorageFetchOrigin {
9320 request_id: request_id.clone(),
9321 target: ResyncTarget::StorageSlot { address, slot },
9322 };
9323 if group.seen.insert((address, slot)) {
9324 group.slots.push(StorageFetchSlot {
9325 address,
9326 slot,
9327 origins: vec![origin],
9328 });
9329 } else if let Some(existing) = group
9330 .slots
9331 .iter_mut()
9332 .find(|existing| existing.address == address && existing.slot == slot)
9333 {
9334 existing.origins.push(origin);
9335 }
9336}
9337
9338fn resync_block_to_block_id(block: &ResyncBlock) -> BlockId {
9339 match block {
9340 ResyncBlock::Latest => BlockId::latest(),
9341 ResyncBlock::Pending => BlockId::pending(),
9342 ResyncBlock::Safe => BlockId::safe(),
9343 ResyncBlock::Finalized => BlockId::finalized(),
9344 ResyncBlock::Number(number) => BlockId::number(*number),
9345 ResyncBlock::Hash {
9346 number: _,
9347 hash,
9348 require_canonical,
9349 } => BlockId::from((*hash, Some(*require_canonical))),
9350 }
9351}
9352
9353impl<N: Network> RegisteredHandler<N> {
9354 fn matches(&self, input: &ReactiveInput<N>) -> bool {
9355 self.interests
9356 .iter()
9357 .any(|interest| interest_matches(interest, input))
9358 }
9359
9360 fn route_log(&self, log: &Log) -> Option<ReactiveLogRoute> {
9361 self.interests.iter().find_map(|interest| match interest {
9362 ReactiveInterest::Logs(interest) if interest.matches(log) => Some(ReactiveLogRoute {
9363 handler_id: self.id.clone(),
9364 route_key: interest.route_key(log),
9365 }),
9366 ReactiveInterest::Logs(_)
9367 | ReactiveInterest::Blocks(_)
9368 | ReactiveInterest::PendingTransactions(_) => None,
9369 })
9370 }
9371}
9372
9373fn merge_log_subscription_filter(filters: &mut Vec<Filter>, next: &Filter) {
9374 let mut candidate = next.clone();
9375 let mut insertion_index = filters.len();
9376 let mut index = 0;
9377 while index < filters.len() {
9378 if filters[index].block_option != candidate.block_option {
9379 index += 1;
9380 continue;
9381 }
9382 if let Some(merged) = exact_filter_union(&candidate, &filters[index]) {
9383 candidate = merged;
9384 insertion_index = insertion_index.min(index);
9385 filters.remove(index);
9386 index = 0;
9387 } else {
9388 index += 1;
9389 }
9390 }
9391 filters.insert(insertion_index.min(filters.len()), candidate);
9392}
9393
9394fn exact_filter_union(left: &Filter, right: &Filter) -> Option<Filter> {
9395 if filter_subsumes(left, right) {
9396 return Some(left.clone());
9397 }
9398 if filter_subsumes(right, left) {
9399 return Some(right.clone());
9400 }
9401 let differing_dimensions = usize::from(left.address != right.address)
9402 + left
9403 .topics
9404 .iter()
9405 .zip(right.topics.iter())
9406 .filter(|(left, right)| left != right)
9407 .count();
9408 if differing_dimensions != 1 {
9409 return None;
9410 }
9411
9412 let mut merged = left.clone();
9413 if merged.address != right.address {
9414 merge_filter_set(&mut merged.address, &right.address);
9415 } else {
9416 for (merged_topic, right_topic) in merged.topics.iter_mut().zip(right.topics.iter()) {
9417 if merged_topic != right_topic {
9418 merge_filter_set(merged_topic, right_topic);
9419 break;
9420 }
9421 }
9422 }
9423 Some(merged)
9424}
9425
9426fn filter_subsumes(left: &Filter, right: &Filter) -> bool {
9427 filter_set_subsumes(&left.address, &right.address)
9428 && left
9429 .topics
9430 .iter()
9431 .zip(right.topics.iter())
9432 .all(|(left, right)| filter_set_subsumes(left, right))
9433}
9434
9435fn filter_set_subsumes<T: Eq + Hash>(left: &FilterSet<T>, right: &FilterSet<T>) -> bool {
9436 left.is_empty()
9437 || (!right.is_empty()
9438 && right
9439 .iter()
9440 .all(|value| left.iter().any(|known| known == value)))
9441}
9442
9443fn merge_filter_set<T: Clone + Eq + Hash>(target: &mut FilterSet<T>, source: &FilterSet<T>) {
9444 if target.is_empty() {
9445 return;
9446 }
9447 if source.is_empty() {
9448 *target = FilterSet::default();
9449 return;
9450 }
9451 for value in source.iter() {
9452 target.insert(value.clone());
9453 }
9454}
9455
9456#[derive(Clone, Debug)]
9457struct HandlerExecution {
9458 handler_id: HandlerId,
9459 quality: StateEffectQuality,
9460 tags: Vec<ReportTag>,
9461 state_updates: Vec<StateUpdate>,
9462 invalidations: Vec<InvalidationRequest>,
9463 resyncs: Vec<ResyncRequest>,
9464 speculative: Vec<SpeculativeRequest>,
9465 hook_signals: Vec<HookSignal>,
9466}
9467
9468impl HandlerExecution {
9469 fn from_outcome(
9470 handler_id: HandlerId,
9471 input_ref: InputRef,
9472 outcome: HandlerOutcome,
9473 preconfirmed: bool,
9474 ) -> Self {
9475 let mut state_updates = Vec::new();
9476 let mut invalidations = Vec::new();
9477 let mut resyncs = Vec::new();
9478 let mut speculative = Vec::new();
9479 let mut hook_signals = Vec::new();
9480
9481 for effect in outcome.effects {
9482 match effect {
9483 ReactiveEffect::StateUpdate(update) => state_updates.push(update),
9484 ReactiveEffect::Invalidate(invalidation) => {
9485 state_updates.push(StateUpdate::purge(
9486 invalidation.address,
9487 invalidation.scope.clone(),
9488 ));
9489 invalidations.push(invalidation);
9490 }
9491 ReactiveEffect::Resync(mut request) => {
9492 if preconfirmed {
9493 request.block = ResyncBlock::Pending;
9494 }
9495 resyncs.push(request);
9496 }
9497 ReactiveEffect::Hook(signal) => hook_signals.push(signal),
9498 ReactiveEffect::Speculative(mut request) => {
9499 request.input_ref = input_ref;
9500 speculative.push(request);
9501 }
9502 }
9503 }
9504
9505 Self {
9506 handler_id,
9507 quality: outcome.quality,
9508 tags: outcome.tags,
9509 state_updates,
9510 invalidations,
9511 resyncs,
9512 speculative,
9513 hook_signals,
9514 }
9515 }
9516}
9517
9518fn dedupe_records<N: Network>(
9519 records: Vec<ReactiveInputRecord<N>>,
9520) -> Result<Vec<ReactiveInputRecord<N>>, ReactiveError> {
9521 let mut positions = HashMap::<ReactiveInputIdentity, usize>::new();
9522 let mut deduped = Vec::with_capacity(records.len());
9523 for record in records {
9524 let identity = record.validated_identity()?;
9525 if !record.is_payload_deduplicable() {
9526 deduped.push(record);
9527 continue;
9528 }
9529 if let Some(index) = positions.get(&identity).copied() {
9530 let merged = deduped[index].merge_compatible_duplicate(&record)?;
9531 debug_assert!(merged, "same indexed identity is deduplicable");
9532 } else {
9533 positions.insert(identity, deduped.len());
9534 deduped.push(record);
9535 }
9536 }
9537 Ok(deduped)
9538}
9539
9540fn dedupe_scoped_records<N: Network>(
9541 records: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
9542) -> Result<Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>, ReactiveError> {
9543 let mut positions: HashMap<ReactiveInputIdentity, usize> = HashMap::new();
9544 let mut deduped: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)> =
9545 Vec::with_capacity(records.len());
9546 for (record, audience, delivery_scope) in records {
9547 let identity = record.validated_identity()?;
9548 if !record.is_payload_deduplicable() {
9549 deduped.push((record, audience, delivery_scope));
9550 continue;
9551 }
9552 if let Some(index) = positions.get(&identity).copied() {
9553 let merged = deduped[index].0.merge_compatible_duplicate(&record)?;
9554 debug_assert!(merged, "same indexed identity is deduplicable");
9555 merge_delivery_audience(&mut deduped[index].1, audience);
9556 merge_delivery_scope(&mut deduped[index].2, delivery_scope);
9557 } else {
9558 positions.insert(identity, deduped.len());
9559 deduped.push((record, audience, delivery_scope));
9560 }
9561 }
9562 Ok(deduped)
9563}
9564
9565fn merge_delivery_scope(into: &mut DeliveryScope, incoming: DeliveryScope) {
9566 *into = match (*into, incoming) {
9567 (DeliveryScope::Canonical, _) | (_, DeliveryScope::Canonical) => DeliveryScope::Canonical,
9568 (DeliveryScope::CanonicalProgress, _) | (_, DeliveryScope::CanonicalProgress) => {
9569 DeliveryScope::CanonicalProgress
9570 }
9571 (DeliveryScope::Preconfirmed, DeliveryScope::Preconfirmed)
9572 | (DeliveryScope::Preconfirmed, DeliveryScope::OwnerCatchup)
9573 | (DeliveryScope::OwnerCatchup, DeliveryScope::Preconfirmed) => DeliveryScope::Preconfirmed,
9574 (DeliveryScope::OwnerCatchup, DeliveryScope::OwnerCatchup) => DeliveryScope::OwnerCatchup,
9575 };
9576}
9577
9578fn merge_delivery_audience(into: &mut DeliveryAudience, incoming: DeliveryAudience) {
9579 match (&mut *into, incoming) {
9580 (DeliveryAudience::All, _) => {}
9581 (current, DeliveryAudience::All) => *current = DeliveryAudience::All,
9582 (DeliveryAudience::Owners(current), DeliveryAudience::Owners(incoming)) => {
9583 for owner in incoming {
9584 if !current.contains(&owner) {
9585 current.push(owner);
9586 }
9587 }
9588 }
9589 (DeliveryAudience::AllExcept(current), DeliveryAudience::AllExcept(incoming)) => {
9590 current.retain(|owner| incoming.contains(owner));
9591 }
9592 (DeliveryAudience::AllExcept(excluded), DeliveryAudience::Owners(included)) => {
9593 excluded.retain(|owner| !included.contains(owner));
9594 }
9595 (current @ DeliveryAudience::Owners(_), DeliveryAudience::AllExcept(mut excluded)) => {
9596 let DeliveryAudience::Owners(included) = current else {
9597 unreachable!("match arm restricts the audience variant")
9598 };
9599 excluded.retain(|owner| !included.contains(owner));
9600 *current = DeliveryAudience::AllExcept(excluded);
9601 }
9602 }
9603}
9604
9605fn sort_records<N: Network>(records: Vec<ReactiveInputRecord<N>>) -> Vec<ReactiveInputRecord<N>> {
9606 let mut indexed: Vec<(usize, ReactiveInputRecord<N>)> =
9607 records.into_iter().enumerate().collect();
9608 indexed.sort_by_key(|(index, record)| record_sort_key(*index, record));
9609 indexed.into_iter().map(|(_, record)| record).collect()
9610}
9611
9612fn sort_scoped_records<N: Network>(
9613 records: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
9614) -> Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)> {
9615 let mut indexed: Vec<_> = records.into_iter().enumerate().collect();
9616 indexed.sort_by_key(|(index, (record, _, _))| record_sort_key(*index, record));
9617 indexed
9618 .into_iter()
9619 .map(|(_, scoped_record)| scoped_record)
9620 .collect()
9621}
9622
9623fn record_sort_key<N: Network>(index: usize, record: &ReactiveInputRecord<N>) -> RecordSortKey {
9624 if let Some((block, _)) = reorg_signal_block(record) {
9625 return RecordSortKey {
9626 class: 0,
9627 block_number: block.number,
9628 record_class: 0,
9629 transaction_index: record.context.transaction_index.unwrap_or(u64::MAX),
9630 log_index: record.context.log_index.unwrap_or(u64::MAX),
9631 original_index: index,
9632 };
9633 }
9634 if is_canonical_status(&record.context.chain_status)
9635 && let Some(block) = record.context.block.as_ref()
9636 {
9637 let (record_class, transaction_index, log_index) = match &record.input {
9638 ReactiveInput::BlockHeader(_) | ReactiveInput::FullBlock(_) => (0, 0, 0),
9639 ReactiveInput::Log(log) if !log.removed => (
9640 1,
9641 log.transaction_index
9642 .or(record.context.transaction_index)
9643 .unwrap_or(u64::MAX),
9644 log.log_index
9645 .or(record.context.log_index)
9646 .unwrap_or(u64::MAX),
9647 ),
9648 ReactiveInput::Log(_)
9649 | ReactiveInput::PendingTxHash(_)
9650 | ReactiveInput::PendingTx(_) => (2, u64::MAX, u64::MAX),
9651 };
9652 return RecordSortKey {
9653 class: 1,
9654 block_number: block.number,
9655 record_class,
9656 transaction_index,
9657 log_index,
9658 original_index: index,
9659 };
9660 }
9661
9662 RecordSortKey {
9663 class: 2,
9664 block_number: 0,
9665 record_class: 0,
9666 transaction_index: 0,
9667 log_index: 0,
9668 original_index: index,
9669 }
9670}
9671
9672#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
9673struct RecordSortKey {
9674 class: u8,
9675 block_number: u64,
9676 record_class: u8,
9677 transaction_index: u64,
9678 log_index: u64,
9679 original_index: usize,
9680}
9681
9682fn interest_matches<N: Network>(interest: &ReactiveInterest<N>, input: &ReactiveInput<N>) -> bool {
9683 match (interest, input) {
9684 (ReactiveInterest::Logs(interest), ReactiveInput::Log(log)) => interest.matches(log),
9685 (
9686 ReactiveInterest::Blocks(BlockInterest {
9687 mode: BlockInterestMode::Header,
9688 }),
9689 ReactiveInput::BlockHeader(_),
9690 ) => true,
9691 (
9692 ReactiveInterest::Blocks(BlockInterest {
9693 mode: BlockInterestMode::FullBlock,
9694 }),
9695 ReactiveInput::FullBlock(_),
9696 ) => true,
9697 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTxHash(_)) => {
9698 interest.matches_hash_only()
9699 }
9700 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTx(tx)) => {
9701 interest.matches_tx(tx)
9702 }
9703 _ => false,
9704 }
9705}
9706
9707fn validate_effects(
9708 input_ref: InputRef,
9709 ctx: &ReactiveContext,
9710 handler_id: &HandlerId,
9711 effects: &[ReactiveEffect],
9712) -> Result<(), ReactiveError> {
9713 let pending = matches!(ctx.chain_status, ChainStatus::Pending)
9714 || matches!(input_ref, InputRef::PendingTx { .. });
9715 if !pending {
9716 return Ok(());
9717 }
9718
9719 for effect in effects {
9720 let effect_kind = match effect {
9721 ReactiveEffect::StateUpdate(_) => Some("state_update"),
9722 ReactiveEffect::Invalidate(_) => Some("invalidate"),
9723 ReactiveEffect::Resync(_) => Some("resync"),
9724 ReactiveEffect::Hook(_) | ReactiveEffect::Speculative(_) => None,
9725 };
9726 if let Some(effect_kind) = effect_kind {
9727 return Err(ReactiveError::InvalidPendingEffect {
9728 input_ref: Box::new(input_ref),
9729 handler_id: handler_id.clone(),
9730 effect_kind,
9731 });
9732 }
9733 }
9734 Ok(())
9735}
9736
9737fn detect_conflicts(
9738 input_ref: InputRef,
9739 executions: &[HandlerExecution],
9740) -> Result<(), ReactiveError> {
9741 let mut writes: HashMap<EffectTarget, (AbsoluteValue, HandlerId)> = HashMap::new();
9742 for execution in executions {
9743 for update in &execution.state_updates {
9744 for (target, value) in absolute_writes(update) {
9745 if let Some((previous_value, previous_handler)) = writes.get(&target) {
9746 if previous_value != &value {
9747 return Err(ReactiveError::ConflictingEffects {
9748 input_ref: Box::new(input_ref),
9749 target: Box::new(target),
9750 first: previous_handler.clone(),
9751 second: execution.handler_id.clone(),
9752 });
9753 }
9754 } else {
9755 writes.insert(target, (value, execution.handler_id.clone()));
9756 }
9757 }
9758 }
9759 }
9760 Ok(())
9761}
9762
9763fn absolute_writes(update: &StateUpdate) -> Vec<(EffectTarget, AbsoluteValue)> {
9764 match update {
9765 StateUpdate::Slot {
9766 address,
9767 slot,
9768 value,
9769 } => vec![(
9770 EffectTarget::StorageSlot {
9771 address: *address,
9772 slot: *slot,
9773 },
9774 AbsoluteValue::U256(*value),
9775 )],
9776 StateUpdate::SlotMasked {
9777 address,
9778 slot,
9779 mask,
9780 value,
9781 } => vec![(
9782 EffectTarget::MaskedStorageSlot {
9783 address: *address,
9784 slot: *slot,
9785 mask: *mask,
9786 },
9787 AbsoluteValue::U256(*value),
9788 )],
9789 StateUpdate::Account { address, patch } | StateUpdate::AccountUpsert { address, patch } => {
9790 account_patch_writes(*address, patch)
9791 }
9792 StateUpdate::SlotDelta { .. }
9793 | StateUpdate::BalanceDelta { .. }
9794 | StateUpdate::Purge { .. } => Vec::new(),
9795 }
9796}
9797
9798fn account_patch_writes(
9799 address: Address,
9800 patch: &AccountPatch,
9801) -> Vec<(EffectTarget, AbsoluteValue)> {
9802 let mut writes = Vec::new();
9803 if let Some(balance) = patch.balance {
9804 writes.push((
9805 EffectTarget::AccountBalance { address },
9806 AbsoluteValue::U256(balance),
9807 ));
9808 }
9809 if let Some(nonce) = patch.nonce {
9810 writes.push((
9811 EffectTarget::AccountNonce { address },
9812 AbsoluteValue::U64(nonce),
9813 ));
9814 }
9815 if let Some(code) = &patch.code {
9816 writes.push((
9817 EffectTarget::AccountCode { address },
9818 AbsoluteValue::Bytes(code.clone()),
9819 ));
9820 }
9821 writes
9822}
9823
9824fn input_ref<N: Network>(input: &ReactiveInput<N>, ctx: &ReactiveContext) -> InputRef {
9825 match input {
9826 ReactiveInput::Log(log) => InputRef::Log {
9827 chain_id: ctx.chain_id,
9828 block_hash: log
9829 .block_hash
9830 .or(ctx.block.as_ref().map(|block| block.hash))
9831 .unwrap_or_default(),
9832 transaction_hash: log.transaction_hash.unwrap_or_default(),
9833 log_index: log.log_index.or(ctx.log_index).unwrap_or_default(),
9834 },
9835 ReactiveInput::PendingTxHash(hash) => InputRef::PendingTx {
9836 chain_id: ctx.chain_id,
9837 hash: *hash,
9838 },
9839 ReactiveInput::PendingTx(tx) => InputRef::PendingTx {
9840 chain_id: ctx.chain_id,
9841 hash: tx.tx_hash(),
9842 },
9843 ReactiveInput::BlockHeader(header) => InputRef::Block {
9844 chain_id: ctx.chain_id,
9845 hash: header.hash(),
9846 number: header.number(),
9847 },
9848 ReactiveInput::FullBlock(block) => {
9849 let header = block.header();
9850 InputRef::Block {
9851 chain_id: ctx.chain_id,
9852 hash: header.hash(),
9853 number: header.number(),
9854 }
9855 }
9856 }
9857}
9858
9859fn is_canonical_status(status: &ChainStatus) -> bool {
9860 matches!(
9861 status,
9862 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. }
9863 )
9864}
9865
9866pub struct EventDecoderHandler {
9868 id: HandlerId,
9869 decoder: Arc<dyn EventDecoder>,
9870 interest: LogInterest,
9871}
9872
9873impl EventDecoderHandler {
9874 pub fn new(id: HandlerId, decoder: Arc<dyn EventDecoder>, interest: LogInterest) -> Self {
9876 Self {
9877 id,
9878 decoder,
9879 interest,
9880 }
9881 }
9882}
9883
9884impl<N: Network> ReactiveHandler<N> for EventDecoderHandler {
9885 fn id(&self) -> HandlerId {
9886 self.id.clone()
9887 }
9888
9889 fn interests(&self) -> Vec<ReactiveInterest<N>> {
9890 vec![ReactiveInterest::Logs(self.interest.clone())]
9891 }
9892
9893 fn handle(
9894 &self,
9895 _ctx: &ReactiveContext,
9896 input: &ReactiveInput<N>,
9897 state: &dyn StateView,
9898 ) -> Result<HandlerOutcome, HandlerError> {
9899 let ReactiveInput::Log(log) = input else {
9900 return Ok(HandlerOutcome::empty(StateEffectQuality::NoStateEffect));
9901 };
9902
9903 Ok(HandlerOutcome {
9904 effects: self
9905 .decoder
9906 .decode(&log.inner, state)
9907 .into_iter()
9908 .map(ReactiveEffect::StateUpdate)
9909 .collect(),
9910 quality: StateEffectQuality::ExactFromInput,
9911 tags: Vec::new(),
9912 })
9913 }
9914}
9915
9916#[derive(
9918 Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
9919)]
9920#[non_exhaustive]
9921pub enum SubscriberCapability {
9922 Logs,
9924 BlockHeaders,
9926 FullBlocks,
9928 PendingTransactionHashes,
9930 PendingTransactions,
9932 HistoricalBackfill,
9934 Live,
9936 LogCoverageAttestation,
9945 DurableReplay,
9955 OwnerScopedDelivery,
9957 DynamicInterests,
9959 ExplicitReorgs,
9961 FinalityUpdates,
9963 Barriers,
9965 Preconfirmations,
9967}
9968
9969#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9975pub struct SubscriberCapabilities {
9976 supported: BTreeSet<SubscriberCapability>,
9977}
9978
9979impl SubscriberCapabilities {
9980 pub fn new(capabilities: impl IntoIterator<Item = SubscriberCapability>) -> Self {
9982 Self {
9983 supported: capabilities.into_iter().collect(),
9984 }
9985 }
9986
9987 pub fn supports(&self, capability: SubscriberCapability) -> bool {
9989 self.supported.contains(&capability)
9990 }
9991
9992 pub fn iter(&self) -> impl Iterator<Item = SubscriberCapability> + '_ {
9994 self.supported.iter().copied()
9995 }
9996
9997 pub fn supports_live(&self) -> bool {
9999 self.supports(SubscriberCapability::Live)
10000 }
10001
10002 pub fn supports_durable_replay(&self) -> bool {
10005 self.supports(SubscriberCapability::DurableReplay)
10006 }
10007
10008 pub fn supports_explicit_reorgs(&self) -> bool {
10010 self.supports(SubscriberCapability::ExplicitReorgs)
10011 }
10012}
10013
10014impl FromIterator<SubscriberCapability> for SubscriberCapabilities {
10015 fn from_iter<T: IntoIterator<Item = SubscriberCapability>>(iter: T) -> Self {
10016 Self::new(iter)
10017 }
10018}
10019
10020pub trait EventSubscriber<N: Network = Ethereum>: Send {
10022 fn chain_id(&self) -> Option<u64> {
10029 None
10030 }
10031
10032 fn capabilities(&self) -> SubscriberCapabilities {
10034 SubscriberCapabilities::default()
10035 }
10036
10037 fn register_interests(
10056 &mut self,
10057 interests: &[ReactiveInterest<N>],
10058 ) -> SubscriberOperation<'_, ()>;
10059
10060 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N>;
10072
10073 fn restore_position(
10096 &mut self,
10097 _position: &SubscriberResumePosition,
10098 ) -> Result<(), SubscriberError> {
10099 Ok(())
10100 }
10101
10102 fn acknowledge_delivery(
10116 &mut self,
10117 _token: SubscriberDeliveryToken,
10118 ) -> SubscriberOperation<'_, ()> {
10119 Box::pin(async { Ok(()) })
10120 }
10121}
10122
10123pub type SubscriberOperation<'a, T> =
10128 Pin<Box<dyn Future<Output = Result<T, SubscriberError>> + Send + 'a>>;
10129
10130pub type SubscriberNextBatch<'a, N> = Pin<
10132 Box<dyn Future<Output = Result<Option<ReactiveInputBatch<N>>, SubscriberError>> + Send + 'a>,
10133>;
10134
10135pub type SubscriberNextScopedBatch<'a, N> = Pin<
10137 Box<dyn Future<Output = Result<Option<SubscriberInputBatch<N>>, SubscriberError>> + Send + 'a>,
10138>;
10139
10140#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
10142pub enum SubscriberMode {
10143 #[default]
10149 Auto,
10150 PubSub,
10152 Polling,
10154}
10155
10156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10157enum FlashblocksAdapter {
10158 NativeSubscriptions,
10159 PendingStatePolling,
10160}
10161
10162fn flashblocks_adapter(chain_id: u64) -> Option<FlashblocksAdapter> {
10163 match chain_id {
10164 8_453 | 84_532 => Some(FlashblocksAdapter::NativeSubscriptions),
10165 10 | 11_155_420 => Some(FlashblocksAdapter::PendingStatePolling),
10166 _ => None,
10167 }
10168}
10169
10170#[derive(Clone, Debug, PartialEq, Eq)]
10172pub struct SubscriberConfig {
10173 pub preconfirmations: PreconfirmationMode,
10176 pub canonical_head_poll_interval: Duration,
10179 pub canonical_head_request_timeout: Duration,
10182 pub flashblock_poll_interval: Duration,
10211 pub max_consecutive_flashblock_poll_failures: usize,
10217 pub max_pending_transaction_receipts_per_tick: usize,
10225 pub max_flashblock_rpc_requests_per_second: usize,
10235 pub log_channel_size: Option<usize>,
10245 pub hydrate_pending_transactions: bool,
10247 pub verify_log_block_context: bool,
10269 pub max_batch_size: usize,
10271 pub max_log_addresses_per_subscription: usize,
10275 pub max_pending_records: usize,
10280 pub max_pending_backfills: usize,
10282 pub max_backfill_log_bytes: usize,
10285 pub max_reconcile_requests_in_flight: usize,
10288 pub reconnect: SubscriberReconnectConfig,
10290}
10291
10292impl Default for SubscriberConfig {
10293 fn default() -> Self {
10294 Self {
10295 log_channel_size: None,
10296 preconfirmations: PreconfirmationMode::Disabled,
10297 canonical_head_poll_interval: Duration::from_millis(500),
10298 canonical_head_request_timeout: Duration::from_secs(3),
10299 flashblock_poll_interval: Duration::from_millis(250),
10300 max_consecutive_flashblock_poll_failures: 10,
10301 max_pending_transaction_receipts_per_tick: 32,
10302 max_flashblock_rpc_requests_per_second: 40,
10303 hydrate_pending_transactions: false,
10304 verify_log_block_context: false,
10305 max_batch_size: 1024,
10306 max_log_addresses_per_subscription: 1024,
10307 max_pending_records: 16_384,
10308 max_pending_backfills: 4_096,
10309 max_backfill_log_bytes: 64 * 1024 * 1024,
10310 max_reconcile_requests_in_flight: 8,
10311 reconnect: SubscriberReconnectConfig::default(),
10312 }
10313 }
10314}
10315
10316#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10318pub enum FlashblocksDelivery {
10319 NativeSubscriptions,
10321 PendingStatePolling,
10323 #[cfg(feature = "raw-flashblocks-json")]
10325 ExternalUpdates,
10326}
10327
10328#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10330pub struct FlashblocksRpcMetrics {
10331 capability_requests: u64,
10332 provider_pair_chain_requests: u64,
10333 canonical_head_requests: u64,
10334 pending_block_requests: u64,
10335 pending_log_requests: u64,
10336 pending_receipt_requests: u64,
10337 pending_receipts_completed: u64,
10338 pending_receipts_unavailable: u64,
10339 failed_requests: u64,
10340 raced_samples: u64,
10341 suppressed_canonical_head_polls: u64,
10342}
10343
10344impl FlashblocksRpcMetrics {
10345 pub const fn capability_requests(self) -> u64 {
10347 self.capability_requests
10348 }
10349
10350 pub const fn provider_pair_chain_requests(self) -> u64 {
10353 self.provider_pair_chain_requests
10354 }
10355
10356 pub const fn canonical_head_requests(self) -> u64 {
10358 self.canonical_head_requests
10359 }
10360
10361 pub const fn pending_block_requests(self) -> u64 {
10363 self.pending_block_requests
10364 }
10365
10366 pub const fn pending_log_requests(self) -> u64 {
10368 self.pending_log_requests
10369 }
10370
10371 pub const fn pending_receipt_requests(self) -> u64 {
10374 self.pending_receipt_requests
10375 }
10376
10377 pub const fn pending_receipts_completed(self) -> u64 {
10379 self.pending_receipts_completed
10380 }
10381
10382 pub const fn pending_receipts_unavailable(self) -> u64 {
10385 self.pending_receipts_unavailable
10386 }
10387
10388 pub const fn failed_requests(self) -> u64 {
10390 self.failed_requests
10391 }
10392
10393 pub const fn suppressed_canonical_head_polls(self) -> u64 {
10400 self.suppressed_canonical_head_polls
10401 }
10402
10403 pub const fn raced_samples(self) -> u64 {
10407 self.raced_samples
10408 }
10409
10410 pub const fn total_requests(self) -> u64 {
10413 self.capability_requests
10414 .saturating_add(self.provider_pair_chain_requests)
10415 .saturating_add(self.canonical_head_requests)
10416 .saturating_add(self.pending_block_requests)
10417 .saturating_add(self.pending_log_requests)
10418 .saturating_add(self.pending_receipt_requests)
10419 }
10420}
10421
10422#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
10429#[non_exhaustive]
10430pub enum SubscriberRpcMethod {
10431 EthChainId,
10433 EthBlockNumber,
10435 EthGetBlockByNumber,
10437 EthGetBlockByHash,
10439 EthGetLogs,
10441 EthGetTransactionReceipt,
10443 EthSubscribe,
10446 OpSupportedCapabilities,
10448}
10449
10450impl SubscriberRpcMethod {
10451 pub const ALL: [Self; 8] = [
10453 Self::EthChainId,
10454 Self::EthBlockNumber,
10455 Self::EthGetBlockByNumber,
10456 Self::EthGetBlockByHash,
10457 Self::EthGetLogs,
10458 Self::EthGetTransactionReceipt,
10459 Self::EthSubscribe,
10460 Self::OpSupportedCapabilities,
10461 ];
10462
10463 pub const COUNT: usize = Self::ALL.len();
10465
10466 pub const fn as_str(self) -> &'static str {
10468 match self {
10469 Self::EthChainId => "eth_chainId",
10470 Self::EthBlockNumber => "eth_blockNumber",
10471 Self::EthGetBlockByNumber => "eth_getBlockByNumber",
10472 Self::EthGetBlockByHash => "eth_getBlockByHash",
10473 Self::EthGetLogs => "eth_getLogs",
10474 Self::EthGetTransactionReceipt => "eth_getTransactionReceipt",
10475 Self::EthSubscribe => "eth_subscribe",
10476 Self::OpSupportedCapabilities => "op_supportedCapabilities",
10477 }
10478 }
10479
10480 const fn index(self) -> usize {
10481 match self {
10482 Self::EthChainId => 0,
10483 Self::EthBlockNumber => 1,
10484 Self::EthGetBlockByNumber => 2,
10485 Self::EthGetBlockByHash => 3,
10486 Self::EthGetLogs => 4,
10487 Self::EthGetTransactionReceipt => 5,
10488 Self::EthSubscribe => 6,
10489 Self::OpSupportedCapabilities => 7,
10490 }
10491 }
10492}
10493
10494impl fmt::Display for SubscriberRpcMethod {
10495 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10496 f.write_str(self.as_str())
10497 }
10498}
10499
10500#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
10507#[non_exhaustive]
10508pub enum SubscriberRpcCause {
10509 ChainIdentity,
10511 StreamSubscription,
10513 FlashblocksSetup,
10516 CanonicalHeadCertification,
10519 PendingStateSample,
10521 LogBlockVerification,
10524 OwnerReconcile,
10527 LazyBackfill,
10529 ReconnectBackfill,
10531 GapBackfill,
10535}
10536
10537impl SubscriberRpcCause {
10538 pub const ALL: [Self; 10] = [
10541 Self::ChainIdentity,
10542 Self::StreamSubscription,
10543 Self::FlashblocksSetup,
10544 Self::CanonicalHeadCertification,
10545 Self::PendingStateSample,
10546 Self::LogBlockVerification,
10547 Self::OwnerReconcile,
10548 Self::LazyBackfill,
10549 Self::ReconnectBackfill,
10550 Self::GapBackfill,
10551 ];
10552
10553 pub const COUNT: usize = Self::ALL.len();
10555
10556 pub const fn as_str(self) -> &'static str {
10558 match self {
10559 Self::ChainIdentity => "chain_identity",
10560 Self::StreamSubscription => "stream_subscription",
10561 Self::FlashblocksSetup => "flashblocks_setup",
10562 Self::CanonicalHeadCertification => "canonical_head_certification",
10563 Self::PendingStateSample => "pending_state_sample",
10564 Self::LogBlockVerification => "log_block_verification",
10565 Self::OwnerReconcile => "owner_reconcile",
10566 Self::LazyBackfill => "lazy_backfill",
10567 Self::ReconnectBackfill => "reconnect_backfill",
10568 Self::GapBackfill => "gap_backfill",
10569 }
10570 }
10571
10572 const fn index(self) -> usize {
10573 match self {
10574 Self::ChainIdentity => 0,
10575 Self::StreamSubscription => 1,
10576 Self::FlashblocksSetup => 2,
10577 Self::CanonicalHeadCertification => 3,
10578 Self::PendingStateSample => 4,
10579 Self::LogBlockVerification => 5,
10580 Self::OwnerReconcile => 6,
10581 Self::LazyBackfill => 7,
10582 Self::ReconnectBackfill => 8,
10583 Self::GapBackfill => 9,
10584 }
10585 }
10586}
10587
10588impl fmt::Display for SubscriberRpcCause {
10589 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10590 f.write_str(self.as_str())
10591 }
10592}
10593
10594#[derive(Clone, Debug, PartialEq, Eq)]
10622pub struct SubscriberRpcStats {
10623 counts: [[u64; SubscriberRpcMethod::COUNT]; SubscriberRpcCause::COUNT],
10624}
10625
10626impl Default for SubscriberRpcStats {
10627 fn default() -> Self {
10628 Self {
10629 counts: [[0; SubscriberRpcMethod::COUNT]; SubscriberRpcCause::COUNT],
10630 }
10631 }
10632}
10633
10634impl SubscriberRpcStats {
10635 pub const fn get(&self, cause: SubscriberRpcCause, method: SubscriberRpcMethod) -> u64 {
10637 self.counts[cause.index()][method.index()]
10638 }
10639
10640 pub fn by_cause(&self, cause: SubscriberRpcCause) -> u64 {
10642 self.counts[cause.index()]
10643 .iter()
10644 .fold(0u64, |total, count| total.saturating_add(*count))
10645 }
10646
10647 pub fn by_method(&self, method: SubscriberRpcMethod) -> u64 {
10649 self.counts
10650 .iter()
10651 .fold(0u64, |total, row| total.saturating_add(row[method.index()]))
10652 }
10653
10654 pub fn total(&self) -> u64 {
10656 SubscriberRpcCause::ALL
10657 .into_iter()
10658 .fold(0u64, |total, cause| {
10659 total.saturating_add(self.by_cause(cause))
10660 })
10661 }
10662
10663 pub fn entries(
10665 &self,
10666 ) -> impl Iterator<Item = (SubscriberRpcCause, SubscriberRpcMethod, u64)> + '_ {
10667 SubscriberRpcCause::ALL.into_iter().flat_map(move |cause| {
10668 SubscriberRpcMethod::ALL
10669 .into_iter()
10670 .map(move |method| (cause, method, self.get(cause, method)))
10671 })
10672 }
10673
10674 pub fn nonzero(
10677 &self,
10678 ) -> impl Iterator<Item = (SubscriberRpcCause, SubscriberRpcMethod, u64)> + '_ {
10679 self.entries().filter(|(_, _, requests)| *requests > 0)
10680 }
10681}
10682
10683#[derive(Debug)]
10691pub(crate) struct SubscriberRpcCounters {
10692 counts: [[AtomicU64; SubscriberRpcMethod::COUNT]; SubscriberRpcCause::COUNT],
10693}
10694
10695impl Default for SubscriberRpcCounters {
10696 fn default() -> Self {
10697 Self {
10698 counts: std::array::from_fn(|_| std::array::from_fn(|_| AtomicU64::new(0))),
10699 }
10700 }
10701}
10702
10703#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10711pub enum SubscriberStreamGap {
10712 Lagged {
10719 skipped: u64,
10721 },
10722 Undecodable,
10727}
10728
10729impl SubscriberStreamGap {
10730 pub const fn skipped(self) -> Option<u64> {
10732 match self {
10733 Self::Lagged { skipped } => Some(skipped),
10734 Self::Undecodable => None,
10735 }
10736 }
10737
10738 pub const fn as_str(self) -> &'static str {
10740 match self {
10741 Self::Lagged { .. } => "lagged",
10742 Self::Undecodable => "undecodable",
10743 }
10744 }
10745}
10746
10747impl fmt::Display for SubscriberStreamGap {
10748 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10749 match self {
10750 Self::Lagged { skipped } => write!(f, "lagged({skipped})"),
10751 Self::Undecodable => f.write_str("undecodable"),
10752 }
10753 }
10754}
10755
10756#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10766pub struct SubscriberStreamGapStats {
10767 lagged_notifications: u64,
10768 undecodable_notifications: u64,
10769 log_gaps_healed: u64,
10770 header_gaps: u64,
10771 preconfirmation_gaps: u64,
10772}
10773
10774impl SubscriberStreamGapStats {
10775 pub const fn lagged_notifications(self) -> u64 {
10777 self.lagged_notifications
10778 }
10779
10780 pub const fn undecodable_notifications(self) -> u64 {
10782 self.undecodable_notifications
10783 }
10784
10785 pub const fn log_gaps_healed(self) -> u64 {
10790 self.log_gaps_healed
10791 }
10792
10793 pub const fn header_gaps(self) -> u64 {
10800 self.header_gaps
10801 }
10802
10803 pub const fn preconfirmation_gaps(self) -> u64 {
10806 self.preconfirmation_gaps
10807 }
10808
10809 pub const fn total_gaps(self) -> u64 {
10811 self.lagged_notifications
10812 .saturating_add(self.undecodable_notifications)
10813 }
10814}
10815
10816#[derive(Debug, Default)]
10818pub(crate) struct SubscriberStreamGapCounters {
10819 lagged_notifications: AtomicU64,
10820 undecodable_notifications: AtomicU64,
10821 log_gaps_healed: AtomicU64,
10822 header_gaps: AtomicU64,
10823 preconfirmation_gaps: AtomicU64,
10824}
10825
10826impl SubscriberStreamGapCounters {
10827 fn bump(counter: &AtomicU64, amount: u64) {
10828 counter.fetch_add(amount, Ordering::Relaxed);
10829 }
10830
10831 #[cfg(feature = "reactive-ws")]
10832 pub(crate) fn record_gap(&self, gap: SubscriberStreamGap) {
10833 match gap {
10834 SubscriberStreamGap::Lagged { skipped } => {
10835 Self::bump(&self.lagged_notifications, skipped.max(1));
10836 }
10837 SubscriberStreamGap::Undecodable => {
10838 Self::bump(&self.undecodable_notifications, 1);
10839 }
10840 }
10841 }
10842
10843 pub(crate) fn record_log_gap_healed(&self) {
10844 Self::bump(&self.log_gaps_healed, 1);
10845 }
10846
10847 pub(crate) fn record_header_gap(&self) {
10848 Self::bump(&self.header_gaps, 1);
10849 }
10850
10851 pub(crate) fn record_preconfirmation_gap(&self) {
10852 Self::bump(&self.preconfirmation_gaps, 1);
10853 }
10854
10855 pub(crate) fn snapshot(&self) -> SubscriberStreamGapStats {
10856 let load = |counter: &AtomicU64| counter.load(Ordering::Relaxed);
10857 SubscriberStreamGapStats {
10858 lagged_notifications: load(&self.lagged_notifications),
10859 undecodable_notifications: load(&self.undecodable_notifications),
10860 log_gaps_healed: load(&self.log_gaps_healed),
10861 header_gaps: load(&self.header_gaps),
10862 preconfirmation_gaps: load(&self.preconfirmation_gaps),
10863 }
10864 }
10865
10866 pub(crate) fn reset(&self) {
10867 for counter in [
10868 &self.lagged_notifications,
10869 &self.undecodable_notifications,
10870 &self.log_gaps_healed,
10871 &self.header_gaps,
10872 &self.preconfirmation_gaps,
10873 ] {
10874 counter.store(0, Ordering::Relaxed);
10875 }
10876 }
10877}
10878
10879impl SubscriberRpcCounters {
10880 pub(crate) fn record(&self, cause: SubscriberRpcCause, method: SubscriberRpcMethod) {
10882 self.record_many(cause, method, 1);
10883 }
10884
10885 pub(crate) fn record_many(
10888 &self,
10889 cause: SubscriberRpcCause,
10890 method: SubscriberRpcMethod,
10891 requests: u64,
10892 ) {
10893 self.counts[cause.index()][method.index()].fetch_add(requests, Ordering::Relaxed);
10894 }
10895
10896 pub(crate) fn snapshot(&self) -> SubscriberRpcStats {
10898 SubscriberRpcStats {
10899 counts: std::array::from_fn(|cause| {
10900 std::array::from_fn(|method| self.counts[cause][method].load(Ordering::Relaxed))
10901 }),
10902 }
10903 }
10904
10905 pub(crate) fn reset(&self) {
10907 for row in &self.counts {
10908 for counter in row {
10909 counter.store(0, Ordering::Relaxed);
10910 }
10911 }
10912 }
10913}
10914
10915#[derive(Clone, Debug, PartialEq, Eq)]
10924pub struct FlashblocksPreflight {
10925 chain_id: u64,
10926 provider: ProviderRef,
10927 delivery: FlashblocksDelivery,
10928 pending_log_subscriptions: usize,
10929 pending_log_filters: usize,
10930 advertised_capabilities: Option<serde_json::Value>,
10931}
10932
10933impl FlashblocksPreflight {
10934 pub const fn chain_id(&self) -> u64 {
10936 self.chain_id
10937 }
10938
10939 pub const fn provider(&self) -> &ProviderRef {
10945 &self.provider
10946 }
10947
10948 pub const fn delivery(&self) -> FlashblocksDelivery {
10950 self.delivery
10951 }
10952
10953 pub const fn pending_log_subscriptions(&self) -> usize {
10957 self.pending_log_subscriptions
10958 }
10959
10960 pub const fn pending_log_filters(&self) -> usize {
10963 self.pending_log_filters
10964 }
10965
10966 pub const fn advertised_capabilities(&self) -> Option<&serde_json::Value> {
10969 self.advertised_capabilities.as_ref()
10970 }
10971}
10972
10973#[derive(Clone, Debug, PartialEq, Eq)]
10979pub struct SubscriberReconnectConfig {
10980 pub enabled: bool,
10982 pub initial_delay: Duration,
10984 pub retry_delay: Duration,
10987 pub max_delay: Duration,
10989 pub max_attempts: Option<usize>,
10991 pub dedupe_window: usize,
10994}
10995
10996impl Default for SubscriberReconnectConfig {
10997 fn default() -> Self {
10998 Self {
10999 enabled: true,
11000 initial_delay: Duration::ZERO,
11001 retry_delay: Duration::from_millis(250),
11002 max_delay: Duration::from_secs(30),
11003 max_attempts: Some(3),
11004 dedupe_window: 4096,
11005 }
11006 }
11007}
11008
11009#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11021pub struct SubscriberBackfill {
11022 from_block: u64,
11023 to_block: Option<u64>,
11024 retained_anchor: Option<BlockRef>,
11025}
11026
11027impl SubscriberBackfill {
11028 pub fn range(from_block: u64, to_block: u64) -> Self {
11030 Self {
11031 from_block,
11032 to_block: Some(to_block),
11033 retained_anchor: None,
11034 }
11035 }
11036
11037 pub fn from_block(from_block: u64) -> Self {
11039 Self {
11040 from_block,
11041 to_block: None,
11042 retained_anchor: None,
11043 }
11044 }
11045
11046 pub fn from_canonical_block(block: BlockRef) -> Self {
11053 Self {
11054 from_block: block.number,
11055 to_block: None,
11056 retained_anchor: Some(block),
11057 }
11058 }
11059
11060 pub fn from_canonical_block_through(
11068 block: BlockRef,
11069 to_block: u64,
11070 ) -> Result<Self, SubscriberError> {
11071 if to_block < block.number {
11072 return Err(SubscriberError::InvalidConfig(
11073 "inclusive backfill upper bound precedes its retained anchor",
11074 ));
11075 }
11076 Ok(Self {
11077 from_block: block.number,
11078 to_block: Some(to_block),
11079 retained_anchor: Some(block),
11080 })
11081 }
11082
11083 pub fn after_canonical_block(block: BlockRef) -> Result<Self, SubscriberError> {
11099 Self::after_canonical_block_inner(block, None)
11100 }
11101
11102 pub fn after_canonical_block_through(
11114 block: BlockRef,
11115 to_block: u64,
11116 ) -> Result<Self, SubscriberError> {
11117 if to_block < block.number {
11118 return Err(SubscriberError::InvalidConfig(
11119 "exclusive backfill upper bound precedes its retained baseline",
11120 ));
11121 }
11122 Self::after_canonical_block_inner(block, Some(to_block))
11123 }
11124
11125 fn after_canonical_block_inner(
11126 block: BlockRef,
11127 to_block: Option<u64>,
11128 ) -> Result<Self, SubscriberError> {
11129 let from_block = block
11130 .number
11131 .checked_add(1)
11132 .ok_or(SubscriberError::InvalidConfig(
11133 "cannot construct an exclusive backfill after block u64::MAX",
11134 ))?;
11135 Ok(Self {
11136 from_block,
11137 to_block,
11138 retained_anchor: Some(block),
11139 })
11140 }
11141
11142 pub fn start_block(&self) -> u64 {
11144 self.from_block
11145 }
11146
11147 pub fn end_block(&self) -> Option<u64> {
11149 self.to_block
11150 }
11151
11152 pub fn retained_anchor(&self) -> Option<&BlockRef> {
11154 self.retained_anchor.as_ref()
11155 }
11156}
11157
11158#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
11165pub struct SubscriberOwnerEpoch {
11166 owner: HandlerId,
11167 sequence: u64,
11168}
11169
11170#[derive(Clone, Debug, PartialEq, Eq)]
11177#[non_exhaustive]
11178pub enum SubscriberInputScope {
11179 Canonical {
11181 owners: Vec<SubscriberOwnerEpoch>,
11183 },
11184 CanonicalResidual {
11188 owners: Vec<SubscriberOwnerEpoch>,
11190 excluded: Vec<HandlerId>,
11192 },
11193 OwnerOnly {
11195 owners: Vec<SubscriberOwnerEpoch>,
11197 },
11198 OwnerOnlyHandlers {
11200 owners: Vec<HandlerId>,
11202 },
11203 Preconfirmed,
11206}
11207
11208impl SubscriberInputScope {
11209 pub fn owners(&self) -> &[SubscriberOwnerEpoch] {
11211 match self {
11212 Self::Canonical { owners }
11213 | Self::CanonicalResidual { owners, .. }
11214 | Self::OwnerOnly { owners } => owners,
11215 Self::OwnerOnlyHandlers { .. } | Self::Preconfirmed => &[],
11216 }
11217 }
11218
11219 pub const fn is_canonical(&self) -> bool {
11221 matches!(
11222 self,
11223 Self::Canonical { .. } | Self::CanonicalResidual { .. }
11224 )
11225 }
11226
11227 pub const fn is_preconfirmed(&self) -> bool {
11229 matches!(self, Self::Preconfirmed)
11230 }
11231}
11232
11233#[derive(Clone, Debug)]
11235pub struct SubscriberInputRecord<N: Network = Ethereum> {
11236 record: ReactiveInputRecord<N>,
11237 scope: SubscriberInputScope,
11238 preconfirmation_timing: Option<FlashblockIngressTiming>,
11239}
11240
11241impl<N: Network> SubscriberInputRecord<N> {
11242 pub const fn record(&self) -> &ReactiveInputRecord<N> {
11244 &self.record
11245 }
11246
11247 pub const fn scope(&self) -> &SubscriberInputScope {
11249 &self.scope
11250 }
11251
11252 pub const fn preconfirmation_timing(&self) -> Option<FlashblockIngressTiming> {
11254 self.preconfirmation_timing
11255 }
11256
11257 pub fn into_record(self) -> ReactiveInputRecord<N> {
11259 self.record
11260 }
11261}
11262
11263impl<N: Network> std::ops::Deref for SubscriberInputRecord<N> {
11264 type Target = ReactiveInputRecord<N>;
11265
11266 fn deref(&self) -> &Self::Target {
11267 &self.record
11268 }
11269}
11270
11271#[derive(Clone, Debug)]
11273pub struct SubscriberInputBatch<N: Network = Ethereum> {
11274 records: Vec<SubscriberInputRecord<N>>,
11275 chain_id: Option<u64>,
11276 chain_controls: Vec<ChainControl>,
11277 preconfirmation_invalidated: bool,
11278 preconfirmation_timing: Option<FlashblockIngressTiming>,
11279}
11280
11281#[derive(Debug)]
11284#[non_exhaustive]
11285pub enum SubscriberDriverPoll<C, N: Network = Ethereum> {
11286 Control(C),
11288 Batch(Option<SubscriberInputBatch<N>>),
11290}
11291
11292impl<N: Network> SubscriberInputBatch<N> {
11293 pub fn records(&self) -> &[SubscriberInputRecord<N>] {
11295 &self.records
11296 }
11297
11298 pub fn into_records(self) -> Vec<SubscriberInputRecord<N>> {
11300 self.records
11301 }
11302
11303 pub fn chain_controls(&self) -> &[ChainControl] {
11305 &self.chain_controls
11306 }
11307
11308 pub const fn preconfirmation_invalidated(&self) -> bool {
11311 self.preconfirmation_invalidated
11312 }
11313
11314 pub const fn preconfirmation_timing(&self) -> Option<FlashblockIngressTiming> {
11316 self.preconfirmation_timing
11317 }
11318
11319 pub fn into_reactive_batch(self) -> ReactiveInputBatch<N> {
11325 let chain_id = self.chain_id;
11326 let chain_controls = self.chain_controls;
11327 let preconfirmation_timing = self.preconfirmation_timing;
11328 let mut batch = ReactiveInputBatch::from_scoped_records_with_delivery_scope(
11329 self.records.into_iter().map(|scoped| {
11330 let source = scoped.record.context.source;
11331 let (audience, delivery_scope) = match scoped.scope {
11332 SubscriberInputScope::Canonical { .. } => (
11333 DeliveryAudience::All,
11334 if source == InputSource::Backfill {
11335 DeliveryScope::CanonicalProgress
11336 } else {
11337 DeliveryScope::Canonical
11338 },
11339 ),
11340 SubscriberInputScope::CanonicalResidual { excluded, .. } => (
11341 DeliveryAudience::AllExcept(excluded),
11342 if source == InputSource::Backfill {
11343 DeliveryScope::CanonicalProgress
11344 } else {
11345 DeliveryScope::Canonical
11346 },
11347 ),
11348 SubscriberInputScope::OwnerOnly { owners } => {
11349 let mut handler_ids = Vec::with_capacity(owners.len());
11350 for epoch in owners {
11351 if !handler_ids.contains(epoch.owner()) {
11352 handler_ids.push(epoch.owner().clone());
11353 }
11354 }
11355 (
11356 DeliveryAudience::Owners(handler_ids),
11357 DeliveryScope::OwnerCatchup,
11358 )
11359 }
11360 SubscriberInputScope::OwnerOnlyHandlers { owners } => (
11361 DeliveryAudience::Owners(owners),
11362 DeliveryScope::OwnerCatchup,
11363 ),
11364 SubscriberInputScope::Preconfirmed => {
11365 (DeliveryAudience::All, DeliveryScope::Preconfirmed)
11366 }
11367 };
11368 (scoped.record, audience, delivery_scope)
11369 }),
11370 )
11371 .with_chain_controls(chain_controls);
11372 if let Some(chain_id) = chain_id {
11373 batch = batch.with_chain_id(chain_id);
11374 }
11375 if let Some(timing) = preconfirmation_timing {
11376 batch = batch.with_preconfirmation_timing(timing);
11377 }
11378 batch
11379 }
11380}
11381
11382impl SubscriberOwnerEpoch {
11383 pub const fn owner(&self) -> &HandlerId {
11385 &self.owner
11386 }
11387
11388 pub const fn sequence(&self) -> u64 {
11390 self.sequence
11391 }
11392}
11393
11394#[derive(Clone, Debug, PartialEq, Eq)]
11396#[non_exhaustive]
11397pub enum SubscriberOwnerStart {
11398 Live,
11400 PostBlock(BlockRef),
11407}
11408
11409#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11411#[non_exhaustive]
11412pub enum SubscriberOwnerState {
11413 Staged,
11416 Active,
11418 Removing,
11420}
11421
11422#[derive(Clone, Debug, PartialEq, Eq)]
11428pub struct SubscriberOwnerProgress {
11429 owner: SubscriberOwnerEpoch,
11430 through: BlockRef,
11431}
11432
11433impl SubscriberOwnerProgress {
11434 pub const fn owner(&self) -> &SubscriberOwnerEpoch {
11436 &self.owner
11437 }
11438
11439 pub const fn through(&self) -> &BlockRef {
11441 &self.through
11442 }
11443}
11444
11445#[derive(Debug, thiserror::Error)]
11447#[non_exhaustive]
11448pub enum SubscriberOwnerError {
11449 #[error(transparent)]
11451 Subscriber(#[from] SubscriberError),
11452 #[error("subscriber interest owner `{0}` is already registered")]
11454 AlreadyRegistered(HandlerId),
11455 #[error("post-block subscriber baseline {0} has no following block")]
11457 PostBlockOverflow(u64),
11458 #[error("subscriber owner epoch sequence exhausted")]
11460 EpochExhausted,
11461 #[error("subscriber owner epoch is not staged")]
11463 NotStaged,
11464 #[error("subscriber owner was staged live-only and has no catch-up baseline")]
11466 MissingBaseline,
11467 #[error("post-block subscriber owners support log interests only")]
11469 UnsupportedPostBlockInterest,
11470 #[error("subscriber reconcile target block {0} was not found")]
11472 BlockUnavailable(u64),
11473 #[error(
11475 "subscriber reconcile target mismatch: expected block {expected_number} {expected_hash}, got block {actual_number} {actual_hash}"
11476 )]
11477 BlockMismatch {
11478 expected_number: u64,
11480 expected_hash: B256,
11482 actual_number: u64,
11484 actual_hash: B256,
11486 },
11487 #[error("subscriber reconcile target block {target} precedes current owner position {current}")]
11489 ProgressRegression {
11490 current: u64,
11492 target: u64,
11494 },
11495 #[error(
11498 "subscriber reconcile conflicts with retained block {number} {current_hash}: target chain references {target_hash}"
11499 )]
11500 ProgressConflict {
11501 number: u64,
11503 current_hash: B256,
11505 target_hash: B256,
11507 },
11508 #[error("subscriber reconcile returned an invalid catch-up log: {0}")]
11510 InvalidBackfillLog(&'static str),
11511}
11512
11513pub trait InterestOwnerSubscriber<N: Network = Ethereum>: EventSubscriber<N> {
11528 fn upsert_interest_owners(
11541 &mut self,
11542 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
11543 ) -> SubscriberOperation<'_, ()> {
11544 Box::pin(async {
11545 Err(SubscriberError::Unsupported(
11546 "subscriber does not implement atomic bulk owner upsert",
11547 ))
11548 })
11549 }
11550
11551 fn replace_interest_owners(
11564 &mut self,
11565 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
11566 ) -> SubscriberOperation<'_, ()> {
11567 Box::pin(async {
11568 Err(SubscriberError::Unsupported(
11569 "subscriber does not implement atomic exact owner replacement",
11570 ))
11571 })
11572 }
11573
11574 fn replace_interest_owners_with_global_backfill(
11598 &mut self,
11599 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
11600 _backfill: SubscriberBackfill,
11601 ) -> SubscriberOperation<'_, ()> {
11602 Box::pin(async {
11603 Err(SubscriberError::Unsupported(
11604 "subscriber does not implement atomic owner replacement with global backfill",
11605 ))
11606 })
11607 }
11608
11609 fn add_interest_owner(
11621 &mut self,
11622 owner: HandlerId,
11623 interests: &[ReactiveInterest<N>],
11624 ) -> SubscriberOperation<'_, ()>;
11625
11626 fn add_interest_owner_with_backfill(
11634 &mut self,
11635 owner: HandlerId,
11636 interests: &[ReactiveInterest<N>],
11637 backfill: SubscriberBackfill,
11638 ) -> SubscriberOperation<'_, ()>;
11639
11640 fn add_interest_owner_with_canonical_catchup(
11658 &mut self,
11659 _owner: HandlerId,
11660 _interests: &[ReactiveInterest<N>],
11661 _retained: BlockRef,
11662 ) -> SubscriberOperation<'_, ()> {
11663 Box::pin(async {
11664 Err(SubscriberError::Unsupported(
11665 "subscriber does not implement coordinated canonical owner catch-up",
11666 ))
11667 })
11668 }
11669
11670 fn remove_interest_owner(
11681 &mut self,
11682 owner: &HandlerId,
11683 ) -> SubscriberOperation<'_, Option<Vec<ReactiveInterest<N>>>>;
11684
11685 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]>;
11687}
11688
11689pub struct ReactiveEngine<S, N: Network = Ethereum> {
11730 runtime: ReactiveRuntime<N>,
11731 subscriber: S,
11732 pending_acknowledgement: Option<PendingAcknowledgement<N>>,
11733 pending_checkpoint: Option<PendingCheckpoint<N>>,
11734 last_checkpoint_block: Option<DurableCheckpointBlock>,
11735 last_checkpoint_delivery_token: Option<SubscriberDeliveryToken>,
11736 last_checkpoint_delivery_witness: Option<B256>,
11737 last_subscriber_checkpoint: Option<SubscriberCheckpoint>,
11738 checkpoint_identity: Option<DurableCheckpointIdentity>,
11739}
11740
11741struct PendingAcknowledgement<N: Network> {
11742 token: SubscriberDeliveryToken,
11743 report: ReactiveBatchReport<N>,
11744}
11745
11746struct PendingCheckpoint<N: Network> {
11747 metadata: DurableCheckpointMetadata,
11748 delivery_token: Option<SubscriberDeliveryToken>,
11749 report: ReactiveBatchReport<N>,
11750 saved_to: Option<PathBuf>,
11751 staged_generation: u64,
11752}
11753
11754struct CheckpointStage<N: Network> {
11755 incoming_block: Option<DurableCheckpointBlock>,
11756 delivery_token: Option<SubscriberDeliveryToken>,
11757 delivery_witness: Option<B256>,
11758 subscriber_checkpoint: Option<SubscriberCheckpoint>,
11759 staged_generation: u64,
11760 report: ReactiveBatchReport<N>,
11761}
11762
11763struct DurableResumePlan {
11764 runtime: DurableRuntimeRestorePlan,
11765 position: SubscriberResumePosition,
11766 delivery_witness: Option<B256>,
11767}
11768
11769enum HandlerRegistrationCatchup {
11770 LiveOnly,
11771 OwnerBackfill(SubscriberBackfill),
11772 CoordinatedCanonical(BlockRef),
11773}
11774
11775const DELIVERY_WITNESS_VERSION: u32 = 2;
11779const DELIVERY_WITNESS_DOMAIN: &[u8] = b"evm-fork-cache/reactive-delivery-witness";
11780
11781#[derive(serde::Serialize)]
11782struct DeliveryWitnessEnvelope<'a> {
11783 version: u32,
11784 chain_id: Option<u64>,
11785 records: Vec<DeliveryRecordWitness<'a>>,
11786 chain_controls: &'a [ChainControl],
11787 subscriber_checkpoint: Option<&'a [u8]>,
11788 payload_commitment: Option<B256>,
11789}
11790
11791#[derive(serde::Serialize)]
11792struct DeliveryRecordWitness<'a> {
11793 identity: ReactiveInputIdentity,
11794 context: &'a ReactiveContext,
11795 audience: &'a DeliveryAudience,
11796 scope: DeliveryScope,
11797 payload: DeliveryPayloadWitness<'a>,
11798}
11799
11800#[derive(serde::Serialize)]
11801enum DeliveryPayloadWitness<'a> {
11802 Log {
11805 address: Address,
11806 topics: &'a [B256],
11807 data: &'a Bytes,
11808 block_hash: Option<B256>,
11809 block_number: Option<u64>,
11810 block_timestamp: Option<u64>,
11811 transaction_hash: Option<B256>,
11812 transaction_index: Option<u64>,
11813 log_index: Option<u64>,
11814 removed: bool,
11815 },
11816 IdentityCommitted,
11823}
11824
11825fn durable_delivery_witness<N: Network>(
11826 batch: &ReactiveInputBatch<N>,
11827) -> Result<B256, ReactiveEngineError> {
11828 let requires_payload_commitment = batch.records.iter().any(|record| {
11829 matches!(
11830 &record.input,
11831 ReactiveInput::BlockHeader(_)
11832 | ReactiveInput::FullBlock(_)
11833 | ReactiveInput::PendingTx(_)
11834 )
11835 });
11836 if requires_payload_commitment && batch.payload_commitment.is_none() {
11837 return Err(ReactiveEngineError::MissingPayloadCommitment);
11838 }
11839 let records = batch
11840 .records
11841 .iter()
11842 .enumerate()
11843 .map(|(index, record)| {
11844 let payload = match &record.input {
11845 ReactiveInput::Log(log) => DeliveryPayloadWitness::Log {
11846 address: log.address(),
11847 topics: log.topics(),
11848 data: &log.inner.data.data,
11849 block_hash: log.block_hash,
11850 block_number: log.block_number,
11851 block_timestamp: log.block_timestamp,
11852 transaction_hash: log.transaction_hash,
11853 transaction_index: log.transaction_index,
11854 log_index: log.log_index,
11855 removed: log.removed,
11856 },
11857 ReactiveInput::BlockHeader(_)
11858 | ReactiveInput::FullBlock(_)
11859 | ReactiveInput::PendingTxHash(_)
11860 | ReactiveInput::PendingTx(_) => DeliveryPayloadWitness::IdentityCommitted,
11861 };
11862 Ok(DeliveryRecordWitness {
11863 identity: record.validated_identity()?,
11864 context: &record.context,
11865 audience: batch
11866 .record_audience(index)
11867 .expect("enumerated record always has an audience"),
11868 scope: batch
11869 .record_delivery_scope(index)
11870 .expect("enumerated record always has a delivery scope"),
11871 payload,
11872 })
11873 })
11874 .collect::<Result<Vec<_>, ReactiveError>>()?;
11875 let envelope = DeliveryWitnessEnvelope {
11876 version: DELIVERY_WITNESS_VERSION,
11877 chain_id: batch.chain_id,
11878 records,
11879 chain_controls: &batch.chain_controls,
11880 subscriber_checkpoint: batch
11881 .subscriber_checkpoint
11882 .as_ref()
11883 .map(SubscriberCheckpoint::as_bytes),
11884 payload_commitment: batch
11885 .payload_commitment
11886 .as_ref()
11887 .map(SubscriberPayloadCommitment::digest),
11888 };
11889 let encoded = bincode::DefaultOptions::new()
11890 .with_fixint_encoding()
11891 .serialize(&envelope)
11892 .map_err(|error| ReactiveEngineError::DeliveryWitness(error.to_string()))?;
11893 let mut witness = Keccak256::new();
11894 witness.update(DELIVERY_WITNESS_DOMAIN);
11895 witness.update(encoded);
11896 Ok(witness.finalize())
11897}
11898
11899impl<S, N> ReactiveEngine<S, N>
11900where
11901 N: Network,
11902 S: EventSubscriber<N>,
11903{
11904 pub fn new(runtime: ReactiveRuntime<N>, subscriber: S) -> Self {
11906 Self {
11907 runtime,
11908 subscriber,
11909 pending_acknowledgement: None,
11910 pending_checkpoint: None,
11911 last_checkpoint_block: None,
11912 last_checkpoint_delivery_token: None,
11913 last_checkpoint_delivery_witness: None,
11914 last_subscriber_checkpoint: None,
11915 checkpoint_identity: None,
11916 }
11917 }
11918
11919 pub fn into_parts(self) -> Result<(ReactiveRuntime<N>, S), Box<Self>> {
11933 if self.pending_acknowledgement.is_some() || self.pending_checkpoint.is_some() {
11934 return Err(Box::new(self));
11935 }
11936 Ok((self.runtime, self.subscriber))
11937 }
11938
11939 fn durable_resume_plan(
11940 &self,
11941 metadata: &DurableCheckpointMetadata,
11942 ) -> Result<DurableResumePlan, ReactiveCheckpointRestoreError> {
11943 if !self.subscriber.capabilities().supports_durable_replay() {
11944 return Err(ReactiveCheckpointRestoreError::SubscriberNotDurable);
11945 }
11946 self.ensure_subscriber_restore_chain(metadata.identity.chain_id)?;
11947 if !self.runtime.is_pristine_for_checkpoint_restore()
11948 || self.pending_acknowledgement.is_some()
11949 || self.pending_checkpoint.is_some()
11950 || self.last_checkpoint_block.is_some()
11951 || self.last_checkpoint_delivery_token.is_some()
11952 || self.last_checkpoint_delivery_witness.is_some()
11953 || self.last_subscriber_checkpoint.is_some()
11954 || self.checkpoint_identity.is_some()
11955 {
11956 return Err(ReactiveCheckpointRestoreError::ActiveRuntime);
11957 }
11958
11959 let block = BlockRef {
11960 number: metadata.block.number,
11961 hash: metadata.block.hash,
11962 parent_hash: metadata.block.parent_hash,
11963 timestamp: metadata.block.timestamp,
11964 };
11965 let runtime = match metadata.runtime_checkpoint.as_deref() {
11966 Some(bytes) => self
11967 .runtime
11968 .plan_durable_checkpoint_restore(bytes, &block)?,
11969 None => DurableRuntimeRestorePlan {
11970 checkpoint: None,
11971 fallback_history: (self.runtime.config.journal_depth > 0)
11972 .then_some(block)
11973 .into_iter()
11974 .collect(),
11975 },
11976 };
11977 let delivery_token = metadata
11978 .delivery_token
11979 .clone()
11980 .map(SubscriberDeliveryToken::new);
11981 let subscriber_checkpoint = metadata
11982 .subscriber_checkpoint
11983 .clone()
11984 .map(SubscriberCheckpoint::new);
11985 let position = SubscriberResumePosition::new(
11986 metadata.identity.chain_id,
11987 block,
11988 runtime.canonical_history(),
11989 delivery_token,
11990 subscriber_checkpoint,
11991 );
11992 Ok(DurableResumePlan {
11993 runtime,
11994 position,
11995 delivery_witness: metadata.delivery_witness,
11996 })
11997 }
11998
11999 pub fn preview_durable_resume_position(
12028 &self,
12029 metadata: &DurableCheckpointMetadata,
12030 ) -> Result<SubscriberResumePosition, ReactiveCheckpointRestoreError> {
12031 Ok(self.durable_resume_plan(metadata)?.position)
12032 }
12033
12034 pub fn resume_from_durable_checkpoint(
12056 &mut self,
12057 metadata: &DurableCheckpointMetadata,
12058 ) -> Result<(), ReactiveCheckpointRestoreError> {
12059 let plan = self.durable_resume_plan(metadata)?;
12060 let prior_runtime = self.runtime.checkpoint_state();
12061
12062 let DurableResumePlan {
12063 runtime,
12064 position,
12065 delivery_witness,
12066 } = plan;
12067 self.runtime.apply_durable_checkpoint_restore(runtime);
12068 self.runtime.coverage_head = Some(position.coverage_head);
12069 if let Err(error) = self.subscriber.restore_position(&position) {
12070 self.runtime.restore_state(prior_runtime);
12071 return Err(ReactiveCheckpointRestoreError::Subscriber(error));
12072 }
12073 if let Err(error) = self.ensure_subscriber_restore_chain(metadata.identity.chain_id) {
12074 self.runtime.restore_state(prior_runtime);
12075 return Err(error);
12076 }
12077 self.last_checkpoint_block = Some(metadata.block.clone());
12078 self.last_checkpoint_delivery_token = position.delivery_token;
12079 self.last_checkpoint_delivery_witness = delivery_witness;
12080 self.last_subscriber_checkpoint = position.subscriber_checkpoint;
12081 self.checkpoint_identity = Some(metadata.identity.clone());
12082 Ok(())
12083 }
12084
12085 pub fn restore_durable_checkpoint(
12101 &mut self,
12102 cache: &mut EvmCache,
12103 loaded: LoadedDurableCheckpoint,
12104 expected: &DurableCheckpointIdentity,
12105 ) -> Result<DurableCheckpointMetadata, ReactiveCheckpointRestoreError> {
12106 if !self.subscriber.capabilities().supports_durable_replay() {
12107 return Err(ReactiveCheckpointRestoreError::SubscriberNotDurable);
12108 }
12109 self.ensure_subscriber_restore_chain(expected.chain_id)?;
12110 if !self.runtime.is_pristine_for_checkpoint_restore()
12111 || self.pending_acknowledgement.is_some()
12112 || self.pending_checkpoint.is_some()
12113 || self.last_checkpoint_block.is_some()
12114 || self.last_checkpoint_delivery_token.is_some()
12115 || self.last_checkpoint_delivery_witness.is_some()
12116 || self.last_subscriber_checkpoint.is_some()
12117 || self.checkpoint_identity.is_some()
12118 {
12119 return Err(ReactiveCheckpointRestoreError::ActiveRuntime);
12120 }
12121
12122 let prior_cache = EvmCacheStateSnapshot::capture(cache);
12123 let metadata = loaded.restore_into(cache, expected)?;
12124 if let Err(error) = self.resume_from_durable_checkpoint(&metadata) {
12125 prior_cache.restore(cache);
12126 return Err(error);
12127 }
12128 Ok(metadata)
12129 }
12130
12131 pub fn runtime(&self) -> &ReactiveRuntime<N> {
12133 &self.runtime
12134 }
12135
12136 pub fn runtime_mut(&mut self) -> &mut ReactiveRuntime<N> {
12138 &mut self.runtime
12139 }
12140
12141 pub fn subscriber(&self) -> &S {
12143 &self.subscriber
12144 }
12145
12146 pub fn subscriber_mut(&mut self) -> &mut S {
12148 &mut self.subscriber
12149 }
12150
12151 pub fn adopt_canonical_baseline(
12169 &mut self,
12170 cache: &EvmCache,
12171 baseline: ReactiveCanonicalBaseline,
12172 ) -> Result<(), ReactiveEngineError> {
12173 if self.pending_acknowledgement.is_some()
12174 || self.pending_checkpoint.is_some()
12175 || self.last_checkpoint_block.is_some()
12176 || self.last_checkpoint_delivery_token.is_some()
12177 || self.last_checkpoint_delivery_witness.is_some()
12178 || self.last_subscriber_checkpoint.is_some()
12179 || self.checkpoint_identity.is_some()
12180 {
12181 return Err(ReactiveBaselineError::ActiveRuntime.into());
12182 }
12183 self.runtime
12187 .validate_canonical_baseline_adoption(baseline.block)?;
12188 if baseline.chain_id != cache.chain_id() {
12189 return Err(ReactiveBaselineError::CacheChainMismatch {
12190 baseline_chain_id: baseline.chain_id,
12191 cache_chain_id: cache.chain_id(),
12192 }
12193 .into());
12194 }
12195 self.ensure_subscriber_chain(cache)?;
12196 let exact_selector = BlockId::from((baseline.block.hash, Some(true)));
12197 let context_matches = cache.block_number() == Some(baseline.block.number)
12198 && baseline
12199 .block
12200 .timestamp
12201 .is_none_or(|timestamp| cache.timestamp() == Some(timestamp));
12202 if cache.block() != exact_selector || !context_matches {
12203 return Err(ReactiveBaselineError::CacheBlockMismatch {
12204 number: baseline.block.number,
12205 hash: baseline.block.hash,
12206 }
12207 .into());
12208 }
12209 self.runtime.adopt_canonical_baseline(baseline.block)?;
12210 Ok(())
12211 }
12212
12213 pub fn next_batch(
12226 &mut self,
12227 cache: &EvmCache,
12228 ) -> Result<SubscriberNextBatch<'_, N>, ReactiveEngineError> {
12229 if self.pending_checkpoint.is_some() {
12230 return Err(ReactiveEngineError::PendingCheckpointCommit);
12231 }
12232 if self.pending_acknowledgement.is_some() {
12233 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
12234 }
12235 self.ensure_subscriber_chain(cache)?;
12236 Ok(self.subscriber.next_batch())
12237 }
12238
12239 pub fn ingest_batch(
12248 &mut self,
12249 cache: &mut EvmCache,
12250 batch: ReactiveInputBatch<N>,
12251 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
12252 self.ensure_raw_ingest_is_safe(cache, &batch)?;
12253 Ok(self.runtime.ingest_batch(cache, batch)?)
12254 }
12255
12256 pub fn ingest_batch_with_resync(
12266 &mut self,
12267 cache: &mut EvmCache,
12268 batch: ReactiveInputBatch<N>,
12269 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
12270 self.ensure_raw_ingest_is_safe(cache, &batch)?;
12271 Ok(self.runtime.ingest_batch_with_resync(cache, batch)?)
12272 }
12273
12274 fn ensure_raw_ingest_is_safe(
12275 &self,
12276 cache: &EvmCache,
12277 batch: &ReactiveInputBatch<N>,
12278 ) -> Result<(), ReactiveEngineError> {
12279 if self.pending_checkpoint.is_some() {
12280 return Err(ReactiveEngineError::PendingCheckpointCommit);
12281 }
12282 if self.pending_acknowledgement.is_some() {
12283 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
12284 }
12285 if batch.delivery_token().is_some() || batch.subscriber_checkpoint().is_some() {
12286 return Err(ReactiveEngineError::UncommittedDeliveryMetadata);
12287 }
12288 self.ensure_subscriber_chain(cache)?;
12289 Ok(())
12290 }
12291
12292 fn ensure_subscriber_chain(&self, cache: &EvmCache) -> Result<(), ReactiveEngineError> {
12293 if let Some(subscriber_chain_id) = self.subscriber.chain_id()
12294 && subscriber_chain_id != cache.chain_id()
12295 {
12296 return Err(ReactiveEngineError::SubscriberChainMismatch {
12297 subscriber_chain_id,
12298 cache_chain_id: cache.chain_id(),
12299 });
12300 }
12301 Ok(())
12302 }
12303
12304 fn ensure_subscriber_restore_chain(
12305 &self,
12306 checkpoint_chain_id: u64,
12307 ) -> Result<(), ReactiveCheckpointRestoreError> {
12308 if let Some(subscriber_chain_id) = self.subscriber.chain_id()
12309 && subscriber_chain_id != checkpoint_chain_id
12310 {
12311 return Err(ReactiveCheckpointRestoreError::SubscriberChainMismatch {
12312 subscriber_chain_id,
12313 checkpoint_chain_id,
12314 });
12315 }
12316 Ok(())
12317 }
12318
12319 pub async fn next_ingest(
12329 &mut self,
12330 cache: &mut EvmCache,
12331 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
12332 self.ensure_subscriber_chain(cache)?;
12333 if self.pending_checkpoint.is_some() {
12334 return Err(ReactiveEngineError::PendingCheckpointCommit);
12335 }
12336 if self.pending_acknowledgement.is_some() {
12337 return self.commit_pending_acknowledgement().await.map(Some);
12338 }
12339 let batch = self.subscriber.next_batch().await?;
12340 self.ensure_subscriber_chain(cache)?;
12341 let Some(mut batch) = batch else {
12342 return Ok(None);
12343 };
12344 let delivery_token = batch.take_delivery_token();
12345 let report = self.runtime.ingest_batch(cache, batch)?;
12346 self.stage_or_return_acknowledgement(delivery_token, report)
12347 .await
12348 }
12349
12350 pub async fn next_ingest_with_resync(
12361 &mut self,
12362 cache: &mut EvmCache,
12363 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
12364 self.ensure_subscriber_chain(cache)?;
12365 if self.pending_checkpoint.is_some() {
12366 return Err(ReactiveEngineError::PendingCheckpointCommit);
12367 }
12368 if self.pending_acknowledgement.is_some() {
12369 return self.commit_pending_acknowledgement().await.map(Some);
12370 }
12371 let batch = self.subscriber.next_batch().await?;
12372 self.ensure_subscriber_chain(cache)?;
12373 let Some(mut batch) = batch else {
12374 return Ok(None);
12375 };
12376 let delivery_token = batch.take_delivery_token();
12377 let report = self.runtime.ingest_batch_with_resync(cache, batch)?;
12378 self.stage_or_return_acknowledgement(delivery_token, report)
12379 .await
12380 }
12381
12382 pub async fn next_ingest_checkpointed(
12413 &mut self,
12414 cache: &mut EvmCache,
12415 store: &DurableCheckpointStore,
12416 identity: &DurableCheckpointIdentity,
12417 ) -> Result<Option<CheckpointedIngest<N>>, ReactiveEngineError> {
12418 if !self.subscriber.capabilities().supports_durable_replay() {
12419 return Err(ReactiveEngineError::SubscriberNotDurable);
12420 }
12421 self.ensure_subscriber_chain(cache)?;
12422 if self.pending_acknowledgement.is_some() {
12423 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
12424 }
12425 self.ensure_checkpoint_identity(cache, identity)?;
12426 if self.pending_checkpoint.is_some() {
12427 return self.commit_pending_checkpoint(cache, store).await.map(Some);
12428 }
12429
12430 let batch = self.subscriber.next_batch().await?;
12431 self.ensure_subscriber_chain(cache)?;
12432 let Some(mut batch) = batch else {
12433 return Ok(None);
12434 };
12435 if batch_preconfirmation(&batch)?.is_some() {
12436 return Err(ReactiveEngineError::PreconfirmationNotCheckpointable);
12437 }
12438 self.runtime.discard_preconfirmed_branch(cache);
12439 let delivery_witness = batch
12440 .delivery_token()
12441 .map(|_| durable_delivery_witness(&batch))
12442 .transpose()?;
12443 let delivery_token = batch.take_delivery_token();
12444 let subscriber_checkpoint = batch.take_subscriber_checkpoint();
12445 if let (Some(replay_token), Some(committed_token)) = (
12446 delivery_token.as_ref(),
12447 self.last_checkpoint_delivery_token.as_ref(),
12448 ) && replay_token == committed_token
12449 {
12450 let committed_witness = self
12451 .last_checkpoint_delivery_witness
12452 .ok_or(ReactiveEngineError::MissingReplayWitness)?;
12453 if delivery_witness != Some(committed_witness) {
12454 return Err(ReactiveEngineError::ReplayDeliveryMismatch);
12455 }
12456 self.subscriber
12457 .acknowledge_delivery(replay_token.clone())
12458 .await
12459 .map_err(ReactiveEngineError::Acknowledgement)?;
12460 return Ok(Some(CheckpointedIngest::ReplayAcknowledged));
12461 }
12462
12463 self.ensure_checkpointable_reorgs(&batch)?;
12464
12465 let incoming_block = latest_canonical_batch_block(&batch);
12466 let cache_state = EvmCacheStateSnapshot::capture(cache);
12467 let runtime_state = self.runtime.checkpoint_state();
12468 let report = match self.runtime.ingest_batch_direct(cache, batch) {
12469 Ok(report) => report,
12470 Err(error) => {
12471 cache_state.restore(cache);
12472 self.runtime.restore_transaction_state(runtime_state);
12473 return Err(error.into());
12474 }
12475 };
12476 let reports = report.reports.clone();
12477 let stage = CheckpointStage {
12478 incoming_block,
12479 delivery_token,
12480 delivery_witness,
12481 subscriber_checkpoint,
12482 staged_generation: cache.snapshot_generation(),
12483 report,
12484 };
12485 if let Err(error) = self.stage_checkpoint(identity, stage) {
12486 cache_state.restore(cache);
12487 self.runtime.restore_transaction_state(runtime_state);
12488 return Err(error);
12489 }
12490 self.runtime.dispatch_reports(&reports);
12491 self.commit_pending_checkpoint(cache, store).await.map(Some)
12492 }
12493
12494 pub async fn next_ingest_with_resync_checkpointed(
12505 &mut self,
12506 cache: &mut EvmCache,
12507 store: &DurableCheckpointStore,
12508 identity: &DurableCheckpointIdentity,
12509 ) -> Result<Option<CheckpointedIngest<N>>, ReactiveEngineError> {
12510 if !self.subscriber.capabilities().supports_durable_replay() {
12511 return Err(ReactiveEngineError::SubscriberNotDurable);
12512 }
12513 self.ensure_subscriber_chain(cache)?;
12514 if self.pending_acknowledgement.is_some() {
12515 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
12516 }
12517 self.ensure_checkpoint_identity(cache, identity)?;
12518 if self.pending_checkpoint.is_some() {
12519 return self.commit_pending_checkpoint(cache, store).await.map(Some);
12520 }
12521
12522 let batch = self.subscriber.next_batch().await?;
12523 self.ensure_subscriber_chain(cache)?;
12524 let Some(mut batch) = batch else {
12525 return Ok(None);
12526 };
12527 if batch_preconfirmation(&batch)?.is_some() {
12528 return Err(ReactiveEngineError::PreconfirmationNotCheckpointable);
12529 }
12530 self.runtime.discard_preconfirmed_branch(cache);
12531 let delivery_witness = batch
12532 .delivery_token()
12533 .map(|_| durable_delivery_witness(&batch))
12534 .transpose()?;
12535 let delivery_token = batch.take_delivery_token();
12536 let subscriber_checkpoint = batch.take_subscriber_checkpoint();
12537 if let (Some(replay_token), Some(committed_token)) = (
12538 delivery_token.as_ref(),
12539 self.last_checkpoint_delivery_token.as_ref(),
12540 ) && replay_token == committed_token
12541 {
12542 let committed_witness = self
12543 .last_checkpoint_delivery_witness
12544 .ok_or(ReactiveEngineError::MissingReplayWitness)?;
12545 if delivery_witness != Some(committed_witness) {
12546 return Err(ReactiveEngineError::ReplayDeliveryMismatch);
12547 }
12548 self.subscriber
12549 .acknowledge_delivery(replay_token.clone())
12550 .await
12551 .map_err(ReactiveEngineError::Acknowledgement)?;
12552 return Ok(Some(CheckpointedIngest::ReplayAcknowledged));
12553 }
12554
12555 self.ensure_checkpointable_reorgs(&batch)?;
12556
12557 let incoming_block = latest_canonical_batch_block(&batch);
12558 let cache_state = EvmCacheStateSnapshot::capture(cache);
12559 let runtime_state = self.runtime.checkpoint_state();
12560 let report = match self.runtime.ingest_batch_with_resync_direct(cache, batch) {
12561 Ok(report) => report,
12562 Err(error) => {
12563 cache_state.restore(cache);
12564 self.runtime.restore_transaction_state(runtime_state);
12565 return Err(error.into());
12566 }
12567 };
12568 let reports = report.reports.clone();
12569 let stage = CheckpointStage {
12570 incoming_block,
12571 delivery_token,
12572 delivery_witness,
12573 subscriber_checkpoint,
12574 staged_generation: cache.snapshot_generation(),
12575 report,
12576 };
12577 if let Err(error) = self.stage_checkpoint(identity, stage) {
12578 cache_state.restore(cache);
12579 self.runtime.restore_transaction_state(runtime_state);
12580 return Err(error);
12581 }
12582 self.runtime.dispatch_reports(&reports);
12583 self.commit_pending_checkpoint(cache, store).await.map(Some)
12584 }
12585
12586 fn stage_checkpoint(
12587 &mut self,
12588 identity: &DurableCheckpointIdentity,
12589 stage: CheckpointStage<N>,
12590 ) -> Result<(), ReactiveEngineError> {
12591 let CheckpointStage {
12592 incoming_block,
12593 delivery_token,
12594 delivery_witness,
12595 subscriber_checkpoint,
12596 staged_generation,
12597 report,
12598 } = stage;
12599 if delivery_token.is_some() != delivery_witness.is_some() {
12600 return Err(ReactiveEngineError::DeliveryWitness(
12601 "delivery token and witness must be staged together".into(),
12602 ));
12603 }
12604 let runtime_checkpoint = self.runtime.durable_checkpoint_bytes()?;
12605 let block = self
12606 .runtime
12607 .last_canonical_block()
12608 .map(|block| DurableCheckpointBlock {
12609 number: block.number,
12610 hash: block.hash,
12611 parent_hash: block.parent_hash,
12612 timestamp: block.timestamp,
12613 })
12614 .or(incoming_block)
12615 .or_else(|| self.last_checkpoint_block.clone())
12616 .ok_or(ReactiveEngineError::MissingCheckpointBlock)?;
12617 let metadata = DurableCheckpointMetadata {
12618 identity: identity.clone(),
12619 block,
12620 delivery_token: delivery_token
12621 .as_ref()
12622 .or(self.last_checkpoint_delivery_token.as_ref())
12623 .map(|token| token.as_bytes().to_vec()),
12624 delivery_witness: if delivery_token.is_some() {
12625 delivery_witness
12626 } else {
12627 self.last_checkpoint_delivery_witness
12628 },
12629 subscriber_checkpoint: subscriber_checkpoint
12630 .as_ref()
12631 .or(self.last_subscriber_checkpoint.as_ref())
12632 .map(|checkpoint| checkpoint.as_bytes().to_vec()),
12633 runtime_checkpoint: Some(runtime_checkpoint),
12634 };
12635 self.pending_checkpoint = Some(PendingCheckpoint {
12636 metadata,
12637 delivery_token,
12638 report,
12639 saved_to: None,
12640 staged_generation,
12641 });
12642 Ok(())
12643 }
12644
12645 fn ensure_checkpointable_reorgs(
12646 &self,
12647 batch: &ReactiveInputBatch<N>,
12648 ) -> Result<(), ReactiveEngineError> {
12649 let state = CanonicalSequenceState::new(
12650 self.runtime
12651 .journal
12652 .iter()
12653 .map(|entry| entry.block)
12654 .collect(),
12655 self.runtime.coverage_head,
12656 self.runtime.safe_head,
12657 self.runtime.finalized_head,
12658 )
12659 .with_log_coverage_head(self.runtime.log_coverage_head);
12660 match validate_canonical_sequence_internal(
12661 &state,
12662 batch,
12663 CanonicalSequenceValidationPolicy::RequireCompleteRollback,
12664 ) {
12665 Ok(_) => Ok(()),
12666 Err(CanonicalSequenceError::Invalid(error)) => Err(error.into()),
12667 Err(CanonicalSequenceError::IncompleteRollback {
12668 common_ancestor,
12669 oldest_retained,
12670 ..
12671 }) => Err(ReactiveEngineError::CheckpointReorgOutsideJournal {
12672 common_ancestor,
12673 oldest_journaled: oldest_retained,
12674 journal_depth: self.runtime.config.journal_depth,
12675 }),
12676 }
12677 }
12678
12679 async fn stage_or_return_acknowledgement(
12680 &mut self,
12681 delivery_token: Option<SubscriberDeliveryToken>,
12682 report: ReactiveBatchReport<N>,
12683 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
12684 let Some(token) = delivery_token else {
12685 return Ok(Some(report));
12686 };
12687 self.pending_acknowledgement = Some(PendingAcknowledgement { token, report });
12688 self.commit_pending_acknowledgement().await.map(Some)
12689 }
12690
12691 async fn commit_pending_acknowledgement(
12692 &mut self,
12693 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
12694 let token = self
12695 .pending_acknowledgement
12696 .as_ref()
12697 .expect("caller checked pending acknowledgement")
12698 .token
12699 .clone();
12700 self.subscriber
12701 .acknowledge_delivery(token)
12702 .await
12703 .map_err(ReactiveEngineError::Acknowledgement)?;
12704 Ok(self
12705 .pending_acknowledgement
12706 .take()
12707 .expect("pending acknowledgement remains until commit")
12708 .report)
12709 }
12710
12711 async fn commit_pending_checkpoint(
12712 &mut self,
12713 cache: &EvmCache,
12714 store: &DurableCheckpointStore,
12715 ) -> Result<CheckpointedIngest<N>, ReactiveEngineError> {
12716 let pending = self
12717 .pending_checkpoint
12718 .as_mut()
12719 .expect("caller checked pending checkpoint");
12720 let cache_generation = cache.snapshot_generation();
12721 if cache_generation != pending.staged_generation {
12722 return Err(ReactiveEngineError::PendingCheckpointCacheChanged {
12723 staged_generation: pending.staged_generation,
12724 current_generation: cache_generation,
12725 });
12726 }
12727 if pending.saved_to.as_deref() != Some(store.path()) {
12728 store
12729 .save_async(cache, pending.metadata.clone())
12730 .await
12731 .map_err(ReactiveEngineError::Checkpoint)?;
12732 pending.saved_to = Some(store.path().to_path_buf());
12733 }
12734 if let Some(token) = pending.delivery_token.clone() {
12735 self.subscriber
12736 .acknowledge_delivery(token)
12737 .await
12738 .map_err(ReactiveEngineError::Acknowledgement)?;
12739 }
12740
12741 let pending = self
12742 .pending_checkpoint
12743 .take()
12744 .expect("pending checkpoint remains until commit");
12745 self.last_checkpoint_block = Some(pending.metadata.block);
12746 self.checkpoint_identity = Some(pending.metadata.identity);
12747 self.last_checkpoint_delivery_token = pending
12748 .metadata
12749 .delivery_token
12750 .map(SubscriberDeliveryToken::new);
12751 self.last_checkpoint_delivery_witness = pending.metadata.delivery_witness;
12752 self.last_subscriber_checkpoint = pending
12753 .metadata
12754 .subscriber_checkpoint
12755 .map(SubscriberCheckpoint::new);
12756 Ok(CheckpointedIngest::Applied(pending.report))
12757 }
12758
12759 fn ensure_checkpoint_identity(
12760 &self,
12761 cache: &EvmCache,
12762 identity: &DurableCheckpointIdentity,
12763 ) -> Result<(), ReactiveEngineError> {
12764 if identity.chain_id != cache.chain_id() {
12765 return Err(ReactiveEngineError::Checkpoint(
12766 DurableCheckpointError::CacheChainMismatch {
12767 cache_chain_id: cache.chain_id(),
12768 checkpoint_chain_id: identity.chain_id,
12769 },
12770 ));
12771 }
12772 if let Some(actual) = self.checkpoint_identity.as_ref()
12773 && actual != identity
12774 {
12775 return Err(ReactiveEngineError::Checkpoint(
12776 DurableCheckpointError::IdentityMismatch {
12777 expected: identity.clone(),
12778 actual: actual.clone(),
12779 },
12780 ));
12781 }
12782 if let Some(pending) = self.pending_checkpoint.as_ref()
12783 && &pending.metadata.identity != identity
12784 {
12785 return Err(ReactiveEngineError::Checkpoint(
12786 DurableCheckpointError::IdentityMismatch {
12787 expected: identity.clone(),
12788 actual: pending.metadata.identity.clone(),
12789 },
12790 ));
12791 }
12792 Ok(())
12793 }
12794}
12795
12796fn latest_canonical_batch_block<N: Network>(
12797 batch: &ReactiveInputBatch<N>,
12798) -> Option<DurableCheckpointBlock> {
12799 let record_block = batch
12800 .records()
12801 .iter()
12802 .enumerate()
12803 .filter(|(index, _)| {
12804 batch
12805 .record_delivery_scope(*index)
12806 .is_some_and(DeliveryScope::advances_canonical_state)
12807 })
12808 .filter_map(|(_, record)| canonical_record_block(record))
12809 .max_by_key(|block| block.number)
12810 .cloned();
12811 let control_block = batch
12812 .chain_controls()
12813 .iter()
12814 .filter_map(|control| match control {
12815 ChainControl::Reorg {
12816 common_ancestor, ..
12817 } => Some(common_ancestor),
12818 ChainControl::Barrier {
12819 block: Some(block), ..
12820 }
12821 | ChainControl::CanonicalProgress(block) => Some(block),
12822 ChainControl::Safe(_)
12823 | ChainControl::Finalized(_)
12824 | ChainControl::LogCoverage(_)
12825 | ChainControl::Barrier { block: None, .. } => None,
12826 })
12827 .max_by_key(|block| block.number)
12828 .cloned();
12829
12830 record_block
12831 .into_iter()
12832 .chain(control_block)
12833 .max_by_key(|block| block.number)
12834 .map(|block| DurableCheckpointBlock {
12835 number: block.number,
12836 hash: block.hash,
12837 parent_hash: block.parent_hash,
12838 timestamp: block.timestamp,
12839 })
12840}
12841
12842impl<S, N> ReactiveEngine<S, N>
12843where
12844 N: Network,
12845 S: InterestOwnerSubscriber<N>,
12846{
12847 pub async fn register_handler(
12873 &mut self,
12874 handler: Arc<dyn ReactiveHandler<N>>,
12875 ) -> Result<(), ReactiveEngineRegisterError> {
12876 let backfill = self
12877 .runtime
12878 .last_canonical_block()
12879 .filter(|retained| {
12880 self.runtime.journal.iter().any(|entry| {
12881 optional_block_refs_are_compatible(Some(&entry.block), Some(retained))
12882 })
12883 })
12884 .map(HandlerRegistrationCatchup::CoordinatedCanonical)
12885 .unwrap_or(HandlerRegistrationCatchup::LiveOnly);
12886 self.register_handler_inner(handler, backfill).await
12887 }
12888
12889 pub async fn register_handler_with_backfill(
12908 &mut self,
12909 handler: Arc<dyn ReactiveHandler<N>>,
12910 backfill: SubscriberBackfill,
12911 ) -> Result<(), ReactiveEngineRegisterError> {
12912 self.register_handler_inner(handler, HandlerRegistrationCatchup::OwnerBackfill(backfill))
12913 .await
12914 }
12915
12916 pub async fn register_handler_live_only(
12927 &mut self,
12928 handler: Arc<dyn ReactiveHandler<N>>,
12929 ) -> Result<(), ReactiveEngineRegisterError> {
12930 self.register_handler_inner(handler, HandlerRegistrationCatchup::LiveOnly)
12931 .await
12932 }
12933
12934 async fn register_handler_inner(
12935 &mut self,
12936 handler: Arc<dyn ReactiveHandler<N>>,
12937 catchup: HandlerRegistrationCatchup,
12938 ) -> Result<(), ReactiveEngineRegisterError> {
12939 let id = handler.id();
12940 if self.runtime.contains_handler(&id) {
12941 return Err(RegisterError::DuplicateHandler(id).into());
12942 }
12943 let interests = handler.interests();
12944
12945 if let HandlerRegistrationCatchup::OwnerBackfill(backfill) = &catchup {
12946 let retained_anchor = backfill.retained_anchor().copied();
12947 let is_exact_retained_block = retained_anchor.is_some_and(|anchor| {
12948 backfill.start_block() == anchor.number
12949 && backfill.end_block() == Some(anchor.number)
12950 && self.runtime.journal.iter().any(|entry| {
12951 optional_block_refs_are_compatible(Some(&entry.block), Some(&anchor))
12952 })
12953 });
12954 if !is_exact_retained_block {
12955 return Err(ReactiveEngineRegisterError::BackfillOutsideJournal {
12956 start_block: backfill.start_block(),
12957 end_block: backfill.end_block(),
12958 retained_anchor,
12959 });
12960 }
12961 }
12962
12963 let subscribed = match catchup {
12964 HandlerRegistrationCatchup::OwnerBackfill(backfill) => {
12965 self.subscriber
12966 .add_interest_owner_with_backfill(id.clone(), &interests, backfill)
12967 .await
12968 }
12969 HandlerRegistrationCatchup::CoordinatedCanonical(retained) => {
12970 self.subscriber
12971 .add_interest_owner_with_canonical_catchup(id.clone(), &interests, retained)
12972 .await
12973 }
12974 HandlerRegistrationCatchup::LiveOnly => {
12975 self.subscriber
12976 .add_interest_owner(id.clone(), &interests)
12977 .await
12978 }
12979 };
12980 if let Err(error) = subscribed {
12981 return Err(error.into());
12982 }
12983
12984 self.runtime
12989 .registry
12990 .insert_handler_prepared(id, handler, interests);
12991 Ok(())
12992 }
12993
12994 pub async fn sync_handler_interests(&mut self) -> Result<(), SubscriberError> {
13019 let owners = self
13020 .runtime
13021 .handler_ids()
13022 .into_iter()
13023 .map(|id| {
13024 let interests = self
13025 .runtime
13026 .handler_interests(&id)
13027 .map(<[ReactiveInterest<N>]>::to_vec)
13028 .unwrap_or_default();
13029 (id, interests)
13030 })
13031 .collect();
13032 self.subscriber.replace_interest_owners(owners).await
13033 }
13034
13035 pub async fn sync_handler_interests_with_backfill(&mut self) -> Result<(), SubscriberError> {
13063 let baseline =
13064 self.runtime
13065 .last_canonical_block()
13066 .ok_or(SubscriberError::InvalidConfig(
13067 "cannot continuity-sync handlers before a canonical runtime position exists",
13068 ))?;
13069 let backfill = SubscriberBackfill::after_canonical_block(baseline)?;
13070 let owners = self
13071 .runtime
13072 .handler_ids()
13073 .into_iter()
13074 .map(|id| {
13075 let interests = self
13076 .runtime
13077 .handler_interests(&id)
13078 .map(<[ReactiveInterest<N>]>::to_vec)
13079 .unwrap_or_default();
13080 (id, interests)
13081 })
13082 .collect();
13083 self.subscriber
13084 .replace_interest_owners_with_global_backfill(owners, backfill)
13085 .await
13086 }
13087
13088 pub async fn unregister_handler(
13120 &mut self,
13121 id: &HandlerId,
13122 ) -> Result<Option<Arc<dyn ReactiveHandler<N>>>, SubscriberError> {
13123 self.subscriber.remove_interest_owner(id).await?;
13124 Ok(self.runtime.unregister_handler(id))
13125 }
13126}
13127
13128type FlashblockReconnectFuture<N> = Pin<
13129 Box<
13130 dyn Future<
13131 Output = (
13132 SubscriberStreamSource,
13133 Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>,
13134 ),
13135 > + Send,
13136 >,
13137>;
13138
13139pub struct AlloySubscriber<P, N: Network = Ethereum> {
13160 provider: P,
13161 #[cfg(feature = "raw-flashblocks-json")]
13164 external_flashblocks_provider: Option<ProviderRef>,
13165 #[cfg(feature = "raw-flashblocks-json")]
13167 external_flashblock_updates:
13168 Option<tokio::sync::mpsc::Receiver<raw_json_flashblocks::QueuedFlashblockUpdate>>,
13169 #[cfg(feature = "raw-flashblocks-json")]
13171 external_flashblock_update_channel_opened: bool,
13172 #[cfg(feature = "raw-flashblocks-json")]
13174 rejected_external_flashblock_generation: Option<u64>,
13175 #[cfg(feature = "raw-flashblocks-json")]
13178 last_external_flashblock_snapshot: Option<FlashblockSnapshot>,
13179 flashblocks_state_provider: Option<P>,
13183 provider_ref: Option<ProviderRef>,
13186 log_verification_provider: Option<P>,
13190 chain_id: Option<u64>,
13192 mode: SubscriberMode,
13193 config: SubscriberConfig,
13194 base_interests: Vec<ReactiveInterest<N>>,
13195 owned_interests: Vec<OwnedSubscriberInterests<N>>,
13196 next_owner_epoch: u64,
13197 interests: Vec<ReactiveInterest<N>>,
13198 log_source_ids: HashMap<Filter, usize>,
13203 next_log_source_id: usize,
13204 pending_backfills: VecDeque<QueuedSubscriberBackfill>,
13205 pending_source_backfills: VecDeque<SubscriberStreamSource>,
13210 sources_dirty: bool,
13213 stream_revision: u64,
13216 state: AlloySubscriberState<N>,
13217 pending_records: VecDeque<SubscriberInputRecord<N>>,
13218 pending_chain_controls: VecDeque<ChainControl>,
13219 pending_reconcile_owner_records: VecDeque<BufferedSubscriberOwnerRecord<N>>,
13224 resource_error: Option<String>,
13227 last_seen_log_blocks: HashMap<usize, u64>,
13228 verified_log_blocks: HashMap<(u64, B256), BlockRef>,
13229 verified_log_block_order: VecDeque<(u64, B256)>,
13230 recent_input_refs: VecDeque<InputRef>,
13231 recent_input_ref_set: HashSet<InputRef>,
13232 recent_owner_input_refs: HashMap<SubscriberOwnerEpoch, VecDeque<InputRef>>,
13233 recent_owner_input_ref_sets: HashMap<SubscriberOwnerEpoch, HashSet<InputRef>>,
13234 recent_compat_owner_input_refs: HashMap<HandlerId, VecDeque<InputRef>>,
13235 recent_compat_owner_input_ref_sets: HashMap<HandlerId, HashSet<InputRef>>,
13236 base_flashblock_header: Option<(FixedBytes<8>, BaseFlashblockBase)>,
13237 base_flashblock_transactions: Option<(FixedBytes<8>, u64, Vec<B256>, Vec<B256>)>,
13238 unmatched_pending_logs: VecDeque<(usize, Log, FlashblockIngressTiming)>,
13239 latest_preconfirmation: Option<FlashblockRef>,
13240 preconfirmed_seen_logs: HashSet<(B256, u64)>,
13241 preconfirmed_receipted_transactions: HashSet<B256>,
13245 preconfirmed_unavailable_receipts: HashSet<B256>,
13250 last_certified_canonical_head: Option<BlockRef>,
13251 last_canonical_head_certification: Option<Instant>,
13259 sealed_block_pending_certification: bool,
13262 attestable_canonical_head: Option<BlockRef>,
13266 attested_log_coverage: Option<BlockRef>,
13267 pending_preconfirmation_invalidation: bool,
13268 pending_flashblock_reconnects: FuturesUnordered<FlashblockReconnectFuture<N>>,
13269 pending_flashblock_reconnect_sources: Vec<SubscriberStreamSource>,
13270 flashblocks_rpc_metrics: FlashblocksRpcMetrics,
13271 rpc_counters: Arc<SubscriberRpcCounters>,
13275 gap_counters: Arc<SubscriberStreamGapCounters>,
13279 consecutive_flashblock_poll_failures: usize,
13280 flashblock_rpc_request_times: VecDeque<Instant>,
13281 _network: PhantomData<N>,
13282}
13283
13284struct OwnedSubscriberInterests<N: Network = Ethereum> {
13285 owner: HandlerId,
13286 interests: Vec<ReactiveInterest<N>>,
13287 epoch: Option<SubscriberOwnerEpoch>,
13288 state: SubscriberOwnerState,
13289 baseline: Option<BlockRef>,
13290 progress: Option<SubscriberOwnerProgress>,
13291 progress_stream_revision: Option<u64>,
13292}
13293
13294#[derive(Clone)]
13295struct SubscriberOwnerReconcilePlan<N: Network = Ethereum> {
13296 epoch: SubscriberOwnerEpoch,
13297 interests: Vec<ReactiveInterest<N>>,
13298 retained: BlockRef,
13299 from_block: u64,
13300}
13301
13302struct SubscriberOwnerCatchup {
13303 logs: Vec<Log>,
13304 certified: BlockRef,
13305}
13306
13307#[derive(Clone, Copy)]
13308struct SubscriberOwnerCatchupOptions {
13309 target_preverified: bool,
13310 max_logs: usize,
13311 max_log_bytes: usize,
13312 max_requests_in_flight: usize,
13313 cause: SubscriberRpcCause,
13316}
13317
13318struct SubscriberOwnerReconcileFilter {
13319 filter: Filter,
13320 from_block: u64,
13321}
13322
13323struct BufferedSubscriberOwnerRecord<N: Network = Ethereum> {
13324 record: ReactiveInputRecord<N>,
13325 owners: Vec<SubscriberOwnerEpoch>,
13326}
13327
13328const OWNER_RECONCILE_FILTERS_PER_CHUNK: usize = 256;
13329
13330struct QueuedSubscriberBackfill {
13331 owner: Option<HandlerId>,
13334 epoch: Option<SubscriberOwnerEpoch>,
13335 filters: Vec<Filter>,
13337 backfill: SubscriberBackfill,
13338}
13339
13340#[cfg(feature = "reactive-ws")]
13346fn ensure_ring_crypto_provider() {
13347 use std::sync::Once;
13348 static INSTALL: Once = Once::new();
13349 INSTALL.call_once(|| {
13350 let _ = rustls::crypto::ring::default_provider().install_default();
13351 });
13352}
13353
13354impl<P, N: Network> AlloySubscriber<P, N> {
13355 pub fn new(provider: P, mode: SubscriberMode, config: SubscriberConfig) -> Self {
13357 #[cfg(feature = "reactive-ws")]
13358 ensure_ring_crypto_provider();
13359 Self {
13360 provider,
13361 #[cfg(feature = "raw-flashblocks-json")]
13362 external_flashblocks_provider: None,
13363 #[cfg(feature = "raw-flashblocks-json")]
13364 external_flashblock_updates: None,
13365 #[cfg(feature = "raw-flashblocks-json")]
13366 external_flashblock_update_channel_opened: false,
13367 #[cfg(feature = "raw-flashblocks-json")]
13368 rejected_external_flashblock_generation: None,
13369 #[cfg(feature = "raw-flashblocks-json")]
13370 last_external_flashblock_snapshot: None,
13371 flashblocks_state_provider: None,
13372 provider_ref: None,
13373 log_verification_provider: None,
13374 chain_id: None,
13375 mode,
13376 config,
13377 base_interests: Vec::new(),
13378 owned_interests: Vec::new(),
13379 next_owner_epoch: 0,
13380 interests: Vec::new(),
13381 log_source_ids: HashMap::new(),
13382 next_log_source_id: 0,
13383 pending_backfills: VecDeque::new(),
13384 pending_source_backfills: VecDeque::new(),
13385 sources_dirty: true,
13386 stream_revision: 0,
13387 state: AlloySubscriberState::Uninitialized,
13388 pending_records: VecDeque::new(),
13389 pending_chain_controls: VecDeque::new(),
13390 pending_reconcile_owner_records: VecDeque::new(),
13391 resource_error: None,
13392 last_seen_log_blocks: HashMap::new(),
13393 verified_log_blocks: HashMap::new(),
13394 verified_log_block_order: VecDeque::new(),
13395 recent_input_refs: VecDeque::new(),
13396 recent_input_ref_set: HashSet::new(),
13397 recent_owner_input_refs: HashMap::new(),
13398 recent_owner_input_ref_sets: HashMap::new(),
13399 recent_compat_owner_input_refs: HashMap::new(),
13400 recent_compat_owner_input_ref_sets: HashMap::new(),
13401 base_flashblock_header: None,
13402 base_flashblock_transactions: None,
13403 unmatched_pending_logs: VecDeque::new(),
13404 latest_preconfirmation: None,
13405 preconfirmed_seen_logs: HashSet::new(),
13406 preconfirmed_receipted_transactions: HashSet::new(),
13407 preconfirmed_unavailable_receipts: HashSet::new(),
13408 last_certified_canonical_head: None,
13409 last_canonical_head_certification: None,
13410 sealed_block_pending_certification: false,
13411 attestable_canonical_head: None,
13412 attested_log_coverage: None,
13413 pending_preconfirmation_invalidation: false,
13414 pending_flashblock_reconnects: FuturesUnordered::new(),
13415 pending_flashblock_reconnect_sources: Vec::new(),
13416 flashblocks_rpc_metrics: FlashblocksRpcMetrics::default(),
13417 rpc_counters: Arc::new(SubscriberRpcCounters::default()),
13418 gap_counters: Arc::new(SubscriberStreamGapCounters::default()),
13419 consecutive_flashblock_poll_failures: 0,
13420 flashblock_rpc_request_times: VecDeque::new(),
13421 _network: PhantomData,
13422 }
13423 }
13424
13425 pub fn provider(&self) -> &P {
13427 &self.provider
13428 }
13429
13430 #[must_use]
13434 pub fn with_provider_ref(mut self, provider: ProviderRef) -> Self {
13435 self.provider_ref = Some(provider);
13436 self
13437 }
13438
13439 #[cfg(feature = "raw-flashblocks-json")]
13465 pub fn configure_external_flashblock_updates(
13466 &mut self,
13467 provider: ProviderRef,
13468 ) -> Result<(), SubscriberError> {
13469 if self.external_flashblocks_provider.is_some() {
13470 return Err(SubscriberError::InvalidConfig(
13471 "external Flashblock updates were already configured",
13472 ));
13473 }
13474 if self.external_flashblock_update_channel_opened
13475 || self.external_flashblock_updates.is_some()
13476 || self.chain_id.is_some()
13477 || !self.base_interests.is_empty()
13478 || !self.owned_interests.is_empty()
13479 || !self.interests.is_empty()
13480 || !self.pending_records.is_empty()
13481 || !self.pending_chain_controls.is_empty()
13482 || !self.pending_backfills.is_empty()
13483 || !matches!(self.state, AlloySubscriberState::Uninitialized)
13484 {
13485 return Err(SubscriberError::InvalidConfig(
13486 "external Flashblock updates must be configured before subscriber registration",
13487 ));
13488 }
13489 self.external_flashblocks_provider = Some(provider);
13490 Ok(())
13491 }
13492
13493 #[cfg(feature = "raw-flashblocks-json")]
13508 pub fn open_external_flashblock_update_channel(
13509 &mut self,
13510 capacity: usize,
13511 ) -> Result<FlashblockUpdateSender, SubscriberError> {
13512 if capacity == 0 {
13513 return Err(SubscriberError::InvalidConfig(
13514 "external Flashblock update channel capacity must be greater than zero",
13515 ));
13516 }
13517 let provider = self.external_flashblocks_provider.clone().ok_or(
13518 SubscriberError::InvalidConfig(
13519 "external Flashblock update channel requires configure_external_flashblock_updates",
13520 ),
13521 )?;
13522 if self.external_flashblock_update_channel_opened {
13523 return Err(SubscriberError::InvalidConfig(
13524 "external Flashblock update channel was already opened",
13525 ));
13526 }
13527 let (sender, receiver) =
13528 raw_json_flashblocks::flashblock_update_channel(provider, capacity);
13529 self.external_flashblock_updates = Some(receiver);
13530 self.external_flashblock_update_channel_opened = true;
13531 self.sources_dirty = true;
13532 Ok(sender)
13533 }
13534
13535 fn uses_external_flashblock_updates(&self) -> bool {
13536 #[cfg(feature = "raw-flashblocks-json")]
13537 {
13538 self.external_flashblocks_provider.is_some()
13539 }
13540 #[cfg(not(feature = "raw-flashblocks-json"))]
13541 {
13542 false
13543 }
13544 }
13545
13546 #[must_use]
13554 pub fn with_flashblocks_state_provider(mut self, provider: P) -> Self {
13555 self.flashblocks_state_provider = Some(provider);
13556 self
13557 }
13558
13559 #[must_use]
13566 pub fn with_log_verification_provider(mut self, provider: P) -> Self {
13567 self.log_verification_provider = Some(provider);
13568 self
13569 }
13570
13571 pub fn mode(&self) -> SubscriberMode {
13573 self.mode
13574 }
13575
13576 pub fn config(&self) -> &SubscriberConfig {
13578 &self.config
13579 }
13580
13581 pub const fn flashblocks_rpc_metrics(&self) -> FlashblocksRpcMetrics {
13584 self.flashblocks_rpc_metrics
13585 }
13586
13587 pub fn rpc_stats(&self) -> SubscriberRpcStats {
13596 self.rpc_counters.snapshot()
13597 }
13598
13599 pub fn reset_rpc_stats(&self) {
13603 self.rpc_counters.reset();
13604 }
13605
13606 pub fn stream_gap_stats(&self) -> SubscriberStreamGapStats {
13615 self.gap_counters.snapshot()
13616 }
13617
13618 pub fn reset_stream_gap_stats(&self) {
13620 self.gap_counters.reset();
13621 }
13622
13623 fn record_rpc(&self, cause: SubscriberRpcCause, method: SubscriberRpcMethod) {
13625 self.rpc_counters.record(cause, method);
13626 }
13627
13628 #[cfg(feature = "reactive-ws")]
13630 fn log_channel_size(&self) -> usize {
13631 self.config
13632 .log_channel_size
13633 .unwrap_or(self.config.max_batch_size)
13634 .max(1)
13635 }
13636
13637 pub fn registered_interests(&self) -> &[ReactiveInterest<N>] {
13639 &self.interests
13640 }
13641
13642 pub fn stage_interest_owner(
13659 &mut self,
13660 owner: HandlerId,
13661 interests: &[ReactiveInterest<N>],
13662 start: SubscriberOwnerStart,
13663 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
13664 validate_subscriber_config(&self.config)?;
13665 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
13666 && interests
13667 .iter()
13668 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
13669 {
13670 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
13671 }
13672 if self
13673 .owned_interests
13674 .iter()
13675 .any(|entry| entry.owner == owner)
13676 {
13677 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
13678 }
13679
13680 let mut next_owned = self.clone_owned_interests();
13681 next_owned.push(OwnedSubscriberInterests {
13682 owner: owner.clone(),
13683 interests: interests.to_vec(),
13684 epoch: None,
13685 state: SubscriberOwnerState::Staged,
13686 baseline: None,
13687 progress: None,
13688 progress_stream_revision: None,
13689 });
13690 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
13691 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13692
13693 let baseline = match start {
13694 SubscriberOwnerStart::Live => None,
13695 SubscriberOwnerStart::PostBlock(block) => {
13696 block
13697 .number
13698 .checked_add(1)
13699 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
13700 Some(block)
13701 }
13702 };
13703 let sequence = self
13704 .next_owner_epoch
13705 .checked_add(1)
13706 .ok_or(SubscriberOwnerError::EpochExhausted)?;
13707 let epoch = SubscriberOwnerEpoch {
13708 owner: owner.clone(),
13709 sequence,
13710 };
13711
13712 self.next_owner_epoch = sequence;
13713 let entry = next_owned
13714 .last_mut()
13715 .expect("staged owner was appended during preflight");
13716 entry.epoch = Some(epoch.clone());
13717 entry.baseline = baseline;
13718 self.owned_interests = next_owned;
13719 self.interests = next_registered;
13720 self.sources_dirty = true;
13721
13722 Ok(epoch)
13723 }
13724
13725 pub fn stage_interest_owner_replacement(
13739 &mut self,
13740 owner: HandlerId,
13741 interests: &[ReactiveInterest<N>],
13742 start: SubscriberOwnerStart,
13743 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
13744 validate_subscriber_config(&self.config)?;
13745 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
13746 && interests
13747 .iter()
13748 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
13749 {
13750 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
13751 }
13752 let active_count = self
13753 .owned_interests
13754 .iter()
13755 .filter(|entry| {
13756 entry.owner == owner
13757 && entry.state == SubscriberOwnerState::Active
13758 && entry.epoch.is_some()
13759 })
13760 .count();
13761 if active_count != 1
13762 || self
13763 .owned_interests
13764 .iter()
13765 .any(|entry| entry.owner == owner && entry.state != SubscriberOwnerState::Active)
13766 {
13767 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
13768 }
13769
13770 let mut next_owned = self.clone_owned_interests();
13771 next_owned.push(OwnedSubscriberInterests {
13772 owner: owner.clone(),
13773 interests: interests.to_vec(),
13774 epoch: None,
13775 state: SubscriberOwnerState::Staged,
13776 baseline: None,
13777 progress: None,
13778 progress_stream_revision: None,
13779 });
13780 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
13781 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13782
13783 let baseline = match start {
13784 SubscriberOwnerStart::Live => None,
13785 SubscriberOwnerStart::PostBlock(block) => {
13786 block
13787 .number
13788 .checked_add(1)
13789 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
13790 Some(block)
13791 }
13792 };
13793 let sequence = self
13794 .next_owner_epoch
13795 .checked_add(1)
13796 .ok_or(SubscriberOwnerError::EpochExhausted)?;
13797 let epoch = SubscriberOwnerEpoch {
13798 owner: owner.clone(),
13799 sequence,
13800 };
13801
13802 self.next_owner_epoch = sequence;
13803 let entry = next_owned
13804 .last_mut()
13805 .expect("staged replacement owner was appended during preflight");
13806 entry.epoch = Some(epoch.clone());
13807 entry.baseline = baseline;
13808 self.owned_interests = next_owned;
13809 self.interests = next_registered;
13810 self.sources_dirty = true;
13811 Ok(epoch)
13812 }
13813
13814 pub fn interest_owner_state(
13816 &self,
13817 epoch: &SubscriberOwnerEpoch,
13818 ) -> Option<SubscriberOwnerState> {
13819 self.owned_interests
13820 .iter()
13821 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13822 .map(|entry| entry.state)
13823 }
13824
13825 pub fn interest_owner_progress(
13827 &self,
13828 epoch: &SubscriberOwnerEpoch,
13829 ) -> Option<&SubscriberOwnerProgress> {
13830 self.owned_interests
13831 .iter()
13832 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13833 .and_then(|entry| entry.progress.as_ref())
13834 }
13835
13836 pub fn activate_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
13840 let stream_revision = self.stream_revision;
13841 let sources_dirty = self.sources_dirty;
13842 let Some(entry) = self
13843 .owned_interests
13844 .iter_mut()
13845 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13846 else {
13847 return false;
13848 };
13849 if entry.state != SubscriberOwnerState::Staged
13850 || (entry.baseline.is_some()
13851 && (entry.progress.is_none()
13852 || entry.progress_stream_revision != Some(stream_revision)
13853 || sources_dirty))
13854 {
13855 return false;
13856 }
13857 entry.state = SubscriberOwnerState::Active;
13858 true
13859 }
13860
13861 pub fn commit_interest_owner_replacement(
13863 &mut self,
13864 active: &SubscriberOwnerEpoch,
13865 replacement: &SubscriberOwnerEpoch,
13866 ) -> bool {
13867 let Some(active_index) = self
13868 .owned_interests
13869 .iter()
13870 .position(|entry| entry.epoch.as_ref() == Some(active))
13871 else {
13872 return false;
13873 };
13874 let Some(replacement_index) = self
13875 .owned_interests
13876 .iter()
13877 .position(|entry| entry.epoch.as_ref() == Some(replacement))
13878 else {
13879 return false;
13880 };
13881 if active_index == replacement_index
13882 || active.owner() != replacement.owner()
13883 || self.owned_interests[active_index].state != SubscriberOwnerState::Active
13884 || self.owned_interests[replacement_index].state != SubscriberOwnerState::Staged
13885 || (self.owned_interests[replacement_index].baseline.is_some()
13886 && (self.owned_interests[replacement_index].progress.is_none()
13887 || self.owned_interests[replacement_index].progress_stream_revision
13888 != Some(self.stream_revision)
13889 || self.sources_dirty))
13890 {
13891 return false;
13892 }
13893
13894 self.owned_interests[replacement_index].state = SubscriberOwnerState::Active;
13895 self.owned_interests.remove(active_index);
13896 self.purge_owner_epoch(active);
13897 self.rebuild_registered_interests();
13898 self.retire_unreferenced_filters();
13899 self.sources_dirty = true;
13900 true
13901 }
13902
13903 pub fn prepare_interest_owner_removal(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
13912 let Some(entry) = self
13913 .owned_interests
13914 .iter_mut()
13915 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13916 else {
13917 return false;
13918 };
13919 if entry.state != SubscriberOwnerState::Active {
13920 return false;
13921 }
13922 entry.state = SubscriberOwnerState::Removing;
13923 true
13924 }
13925
13926 pub fn finalize_interest_owner_removal(
13932 &mut self,
13933 epoch: &SubscriberOwnerEpoch,
13934 ) -> Option<Vec<ReactiveInterest<N>>> {
13935 let index = self.owned_interests.iter().position(|entry| {
13936 entry.epoch.as_ref() == Some(epoch) && entry.state == SubscriberOwnerState::Removing
13937 })?;
13938 let removed = self.owned_interests.remove(index).interests;
13939 self.purge_owner_epoch(epoch);
13940 self.rebuild_registered_interests();
13941 self.retire_unreferenced_filters();
13942 self.sources_dirty = true;
13943 Some(removed)
13944 }
13945
13946 pub fn abort_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
13952 let Some(index) = self
13953 .owned_interests
13954 .iter()
13955 .position(|entry| entry.epoch.as_ref() == Some(epoch))
13956 else {
13957 return false;
13958 };
13959 match self.owned_interests[index].state {
13960 SubscriberOwnerState::Staged => {
13961 self.owned_interests.remove(index);
13962 self.purge_owner_epoch(epoch);
13963 self.rebuild_registered_interests();
13964 self.retire_unreferenced_filters();
13965 self.sources_dirty = true;
13966 true
13967 }
13968 SubscriberOwnerState::Removing => {
13969 self.owned_interests[index].state = SubscriberOwnerState::Active;
13970 true
13971 }
13972 SubscriberOwnerState::Active => false,
13973 }
13974 }
13975
13976 fn purge_owner_epoch(&mut self, epoch: &SubscriberOwnerEpoch) {
13977 self.pending_backfills
13978 .retain(|backfill| backfill.epoch.as_ref() != Some(epoch));
13979 self.pending_records
13980 .retain_mut(|pending| match &mut pending.scope {
13981 SubscriberInputScope::Canonical { owners }
13982 | SubscriberInputScope::CanonicalResidual { owners, .. } => {
13983 owners.retain(|owner| owner != epoch);
13984 true
13985 }
13986 SubscriberInputScope::OwnerOnly { owners } => {
13987 owners.retain(|owner| owner != epoch);
13988 !owners.is_empty()
13989 }
13990 SubscriberInputScope::OwnerOnlyHandlers { .. }
13991 | SubscriberInputScope::Preconfirmed => true,
13992 });
13993 self.pending_reconcile_owner_records.retain_mut(|pending| {
13994 pending.owners.retain(|owner| owner != epoch);
13995 !pending.owners.is_empty()
13996 });
13997 self.recent_owner_input_refs.remove(epoch);
13998 self.recent_owner_input_ref_sets.remove(epoch);
13999 }
14000
14001 pub fn upsert_interest_owners(
14009 &mut self,
14010 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14011 ) -> Result<(), SubscriberError> {
14012 self.upsert_interest_owners_inner(owners, None)
14013 }
14014
14015 pub fn upsert_interest_owners_with_backfill(
14024 &mut self,
14025 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14026 backfill: SubscriberBackfill,
14027 ) -> Result<(), SubscriberError> {
14028 self.upsert_interest_owners_inner(owners, Some(backfill))
14029 }
14030
14031 fn upsert_interest_owners_inner(
14032 &mut self,
14033 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14034 explicit_backfill: Option<SubscriberBackfill>,
14035 ) -> Result<(), SubscriberError> {
14036 validate_subscriber_config(&self.config)?;
14037 let mut seen = HashSet::with_capacity(owners.len());
14038 let mut next_owned = self.clone_owned_interests();
14039 for (owner, interests) in &owners {
14040 if !seen.insert(owner.clone()) {
14041 return Err(SubscriberError::InvalidConfig(
14042 "bulk owner upsert contains a duplicate owner",
14043 ));
14044 }
14045 if self
14046 .owned_interests
14047 .iter()
14048 .any(|entry| &entry.owner == owner && entry.epoch.is_some())
14049 {
14050 return Err(SubscriberError::InvalidConfig(
14051 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
14052 ));
14053 }
14054 if let Some(entry) = next_owned.iter_mut().find(|entry| &entry.owner == owner) {
14055 entry.interests = interests.clone();
14056 entry.state = SubscriberOwnerState::Active;
14057 entry.baseline = None;
14058 entry.progress = None;
14059 entry.progress_stream_revision = None;
14060 } else {
14061 next_owned.push(OwnedSubscriberInterests {
14062 owner: owner.clone(),
14063 interests: interests.clone(),
14064 epoch: None,
14065 state: SubscriberOwnerState::Active,
14066 baseline: None,
14067 progress: None,
14068 progress_stream_revision: None,
14069 });
14070 }
14071 }
14072 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
14073 validate_supported_interests(self.mode, &self.config, &next_registered)?;
14074
14075 let mut replacement_backfills = Vec::new();
14080 for (owner, interests) in &owners {
14081 let previous_filters: Vec<Filter> = self
14082 .owner_interests(owner)
14083 .map(log_filters)
14084 .unwrap_or_default();
14085 let continuity_anchor = previous_filters
14086 .iter()
14087 .filter_map(|filter| self.log_anchor(filter))
14088 .min();
14089 let filters = log_filters(interests);
14090 if let Some(backfill) = explicit_backfill
14091 && !filters.is_empty()
14092 {
14093 replacement_backfills.push(QueuedSubscriberBackfill {
14094 owner: Some(owner.clone()),
14095 epoch: None,
14096 filters: filters.clone(),
14097 backfill,
14098 });
14099 }
14100 let explicit_covers = explicit_backfill.is_some_and(|explicit| {
14101 explicit.end_block().is_none()
14102 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
14103 });
14104 let continuity_filters: Vec<_> = filters
14105 .into_iter()
14106 .filter(|filter| !previous_filters.contains(filter))
14107 .collect();
14108 if let Some(anchor) = continuity_anchor
14109 && !continuity_filters.is_empty()
14110 && !explicit_covers
14111 {
14112 replacement_backfills.push(QueuedSubscriberBackfill {
14113 owner: Some(owner.clone()),
14114 epoch: None,
14115 filters: continuity_filters,
14116 backfill: SubscriberBackfill::from_block(anchor),
14117 });
14118 }
14119 }
14120
14121 let retained_backfills = self
14122 .pending_backfills
14123 .iter()
14124 .filter(|queued| {
14125 queued
14126 .owner
14127 .as_ref()
14128 .is_none_or(|owner| !seen.contains(owner))
14129 })
14130 .map(|queued| queued.filters.len())
14131 .sum::<usize>();
14132 let replacement_units = replacement_backfills
14133 .iter()
14134 .map(|queued| queued.filters.len())
14135 .sum::<usize>();
14136 if retained_backfills.saturating_add(replacement_units) > self.config.max_pending_backfills
14137 {
14138 return Err(SubscriberError::ResourceExhausted(format!(
14139 "bulk owner update would queue more than {} lazy backfills",
14140 self.config.max_pending_backfills
14141 )));
14142 }
14143
14144 self.owned_interests = next_owned;
14148 self.interests = next_registered;
14149 for owner in &seen {
14150 self.recent_compat_owner_input_refs.remove(owner);
14151 self.recent_compat_owner_input_ref_sets.remove(owner);
14152 }
14153 self.retire_unreferenced_filters();
14154 self.sources_dirty = true;
14155 self.pending_backfills.retain(|queued| {
14156 queued
14157 .owner
14158 .as_ref()
14159 .is_none_or(|owner| !seen.contains(owner))
14160 });
14161 self.pending_backfills.extend(replacement_backfills);
14162 Ok(())
14163 }
14164
14165 pub fn replace_interest_owners(
14174 &mut self,
14175 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14176 ) -> Result<(), SubscriberError> {
14177 self.replace_interest_owners_inner(owners, None)
14178 }
14179
14180 pub fn replace_interest_owners_with_global_backfill(
14193 &mut self,
14194 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14195 backfill: SubscriberBackfill,
14196 ) -> Result<(), SubscriberError> {
14197 self.replace_interest_owners_inner(owners, Some(backfill))
14198 }
14199
14200 fn replace_interest_owners_inner(
14201 &mut self,
14202 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14203 backfill: Option<SubscriberBackfill>,
14204 ) -> Result<(), SubscriberError> {
14205 validate_subscriber_config(&self.config)?;
14206 if self
14207 .owned_interests
14208 .iter()
14209 .any(|entry| entry.epoch.is_some())
14210 {
14211 return Err(SubscriberError::InvalidConfig(
14212 "cannot replace compatibility owners while an epoch-scoped lifecycle exists",
14213 ));
14214 }
14215
14216 let mut seen = HashSet::with_capacity(owners.len());
14217 let mut next_owned = Vec::with_capacity(owners.len());
14218 for (owner, interests) in owners {
14219 if !seen.insert(owner.clone()) {
14220 return Err(SubscriberError::InvalidConfig(
14221 "owner replacement contains a duplicate owner",
14222 ));
14223 }
14224 next_owned.push(OwnedSubscriberInterests {
14225 owner,
14226 interests,
14227 epoch: None,
14228 state: SubscriberOwnerState::Active,
14229 baseline: None,
14230 progress: None,
14231 progress_stream_revision: None,
14232 });
14233 }
14234 let next_registered = aggregate_interests(&[], &next_owned);
14235 validate_supported_interests(self.mode, &self.config, &next_registered)?;
14236 let mut filters = log_filters(&next_registered);
14237 let mut unique_filters = Vec::with_capacity(filters.len());
14238 for filter in filters.drain(..) {
14239 if !unique_filters.contains(&filter) {
14240 unique_filters.push(filter);
14241 }
14242 }
14243 let replacement_backfills: VecDeque<_> = match backfill {
14244 Some(backfill) if !unique_filters.is_empty() => {
14245 VecDeque::from([QueuedSubscriberBackfill {
14246 owner: None,
14247 epoch: None,
14248 filters: unique_filters,
14249 backfill,
14250 }])
14251 }
14252 Some(_) | None => VecDeque::new(),
14253 };
14254 let replacement_units = replacement_backfills
14255 .iter()
14256 .map(|queued| queued.filters.len())
14257 .sum::<usize>();
14258 if replacement_units > self.config.max_pending_backfills {
14259 return Err(SubscriberError::ResourceExhausted(format!(
14260 "owner replacement would queue more than {} lazy backfills",
14261 self.config.max_pending_backfills
14262 )));
14263 }
14264
14265 let revoke_preconfirmation = self.latest_preconfirmation.is_some()
14270 || self.pending_preconfirmation_invalidation
14271 || self.pending_records.iter().any(|record| {
14272 record.scope == SubscriberInputScope::Preconfirmed
14273 || matches!(
14274 &record.record.context.chain_status,
14275 ChainStatus::Preconfirmed { .. }
14276 )
14277 });
14278 self.base_interests.clear();
14279 self.owned_interests = next_owned;
14280 self.interests = next_registered;
14281 self.reset_delivery_state();
14282 self.pending_preconfirmation_invalidation = revoke_preconfirmation;
14283 self.pending_backfills = replacement_backfills;
14284 self.reset_stream_topology();
14285 Ok(())
14286 }
14287
14288 pub fn add_interest_owner(
14311 &mut self,
14312 owner: HandlerId,
14313 interests: &[ReactiveInterest<N>],
14314 ) -> Result<(), SubscriberError> {
14315 self.set_interest_owner(owner, interests, None)
14316 }
14317
14318 pub fn add_interest_owner_with_backfill(
14337 &mut self,
14338 owner: HandlerId,
14339 interests: &[ReactiveInterest<N>],
14340 backfill: SubscriberBackfill,
14341 ) -> Result<(), SubscriberError> {
14342 self.set_interest_owner(owner, interests, Some(backfill))
14343 }
14344
14345 pub fn add_interest_owner_with_canonical_catchup(
14364 &mut self,
14365 owner: HandlerId,
14366 interests: &[ReactiveInterest<N>],
14367 retained: BlockRef,
14368 ) -> Result<(), SubscriberError> {
14369 validate_subscriber_config(&self.config)?;
14370 if self
14371 .owned_interests
14372 .iter()
14373 .any(|entry| entry.owner == owner && entry.epoch.is_some())
14374 {
14375 return Err(SubscriberError::InvalidConfig(
14376 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
14377 ));
14378 }
14379
14380 let mut next_owned = self.clone_owned_interests();
14381 if let Some(entry) = next_owned.iter_mut().find(|entry| entry.owner == owner) {
14382 entry.interests = interests.to_vec();
14383 entry.state = SubscriberOwnerState::Active;
14384 entry.baseline = None;
14385 entry.progress = None;
14386 entry.progress_stream_revision = None;
14387 entry.epoch = None;
14388 } else {
14389 next_owned.push(OwnedSubscriberInterests {
14390 owner: owner.clone(),
14391 interests: interests.to_vec(),
14392 epoch: None,
14393 state: SubscriberOwnerState::Active,
14394 baseline: None,
14395 progress: None,
14396 progress_stream_revision: None,
14397 });
14398 }
14399 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
14400 validate_supported_interests(self.mode, &self.config, &next_registered)?;
14401 if next_registered
14402 .iter()
14403 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
14404 {
14405 return Err(SubscriberError::Unsupported(
14406 "Alloy coordinated registration supports log-only interest topologies",
14407 ));
14408 }
14409
14410 let mut owner_filters = Vec::new();
14411 for filter in log_filters(interests) {
14412 if !owner_filters.contains(&filter) {
14413 owner_filters.push(filter);
14414 }
14415 }
14416 let mut global_filters = Vec::new();
14417 for filter in log_filters(&next_registered) {
14418 if !global_filters.contains(&filter) {
14419 global_filters.push(filter);
14420 }
14421 }
14422 let owner_backfill =
14423 SubscriberBackfill::from_canonical_block_through(retained, retained.number)?;
14424 let global_backfill = SubscriberBackfill::after_canonical_block(retained)?;
14425 let replacement_units = owner_filters.len().saturating_add(global_filters.len());
14426 let retained_units = self
14427 .pending_backfills
14428 .iter()
14429 .filter(|queued| queued.owner.as_ref() != Some(&owner))
14430 .map(|queued| queued.filters.len())
14431 .sum::<usize>();
14432 if retained_units.saturating_add(replacement_units) > self.config.max_pending_backfills {
14433 return Err(SubscriberError::ResourceExhausted(format!(
14434 "coordinated owner registration would queue more than {} lazy backfills",
14435 self.config.max_pending_backfills
14436 )));
14437 }
14438
14439 let mut replacement_backfills = VecDeque::new();
14440 if !owner_filters.is_empty() {
14441 replacement_backfills.push_back(QueuedSubscriberBackfill {
14442 owner: Some(owner.clone()),
14443 epoch: None,
14444 filters: owner_filters,
14445 backfill: owner_backfill,
14446 });
14447 }
14448 replacement_backfills.push_back(QueuedSubscriberBackfill {
14451 owner: None,
14452 epoch: None,
14453 filters: global_filters,
14454 backfill: global_backfill,
14455 });
14456
14457 self.owned_interests = next_owned;
14460 self.interests = next_registered;
14461 self.recent_compat_owner_input_refs.remove(&owner);
14462 self.recent_compat_owner_input_ref_sets.remove(&owner);
14463 self.pending_backfills
14464 .retain(|queued| queued.owner.as_ref() != Some(&owner));
14465 self.pending_backfills.extend(replacement_backfills);
14466 self.retire_unreferenced_filters();
14467 self.sources_dirty = true;
14468 Ok(())
14469 }
14470
14471 pub fn remove_interest_owner(&mut self, owner: &HandlerId) -> Option<Vec<ReactiveInterest<N>>> {
14480 let index = self
14481 .owned_interests
14482 .iter()
14483 .position(|entry| &entry.owner == owner && entry.epoch.is_none())?;
14484 let removed = self.owned_interests.remove(index);
14485 if let Some(epoch) = &removed.epoch {
14486 self.purge_owner_epoch(epoch);
14487 } else {
14488 self.pending_backfills
14489 .retain(|backfill| backfill.owner.as_ref() != Some(owner));
14490 self.recent_compat_owner_input_refs.remove(owner);
14491 self.recent_compat_owner_input_ref_sets.remove(owner);
14492 }
14493 self.rebuild_registered_interests();
14494 self.retire_unreferenced_filters();
14495 self.sources_dirty = true;
14496 Some(removed.interests)
14497 }
14498
14499 pub fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
14501 self.owned_interests
14502 .iter()
14503 .find(|entry| &entry.owner == owner)
14504 .map(|entry| entry.interests.as_slice())
14505 }
14506
14507 fn set_interest_owner(
14508 &mut self,
14509 owner: HandlerId,
14510 interests: &[ReactiveInterest<N>],
14511 backfill: Option<SubscriberBackfill>,
14512 ) -> Result<(), SubscriberError> {
14513 validate_subscriber_config(&self.config)?;
14514 if self
14515 .owned_interests
14516 .iter()
14517 .any(|entry| entry.owner == owner && entry.epoch.is_some())
14518 {
14519 return Err(SubscriberError::InvalidConfig(
14520 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
14521 ));
14522 }
14523
14524 let mut next_owned = self.clone_owned_interests();
14525 let replaced_epoch = match next_owned.iter_mut().find(|entry| entry.owner == owner) {
14526 Some(entry) => {
14527 entry.interests = interests.to_vec();
14528 entry.state = SubscriberOwnerState::Active;
14529 entry.baseline = None;
14530 entry.progress = None;
14531 entry.progress_stream_revision = None;
14532 entry.epoch.take()
14533 }
14534 None => {
14535 next_owned.push(OwnedSubscriberInterests {
14536 owner: owner.clone(),
14537 interests: interests.to_vec(),
14538 epoch: None,
14539 state: SubscriberOwnerState::Active,
14540 baseline: None,
14541 progress: None,
14542 progress_stream_revision: None,
14543 });
14544 None
14545 }
14546 };
14547 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
14548 validate_supported_interests(self.mode, &self.config, &next_registered)?;
14549
14550 let previous_filters: Vec<Filter> = self
14557 .owner_interests(&owner)
14558 .map(log_filters)
14559 .unwrap_or_default();
14560 let continuity_anchor: Option<u64> = previous_filters
14561 .iter()
14562 .filter_map(|filter| self.log_anchor(filter))
14563 .min();
14564
14565 let mut replacement_backfills = Vec::new();
14569 let filters = log_filters(interests);
14570 if let Some(backfill) = backfill
14571 && !filters.is_empty()
14572 {
14573 replacement_backfills.push(QueuedSubscriberBackfill {
14574 owner: Some(owner.clone()),
14575 epoch: None,
14576 filters: filters.clone(),
14577 backfill,
14578 });
14579 }
14580 let explicit_covers = backfill.is_some_and(|explicit| {
14581 explicit.end_block().is_none()
14582 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
14583 });
14584 let continuity_filters: Vec<_> = filters
14585 .into_iter()
14586 .filter(|filter| !previous_filters.contains(filter))
14587 .collect();
14588 if let Some(anchor) = continuity_anchor
14589 && !continuity_filters.is_empty()
14590 && !explicit_covers
14591 {
14592 replacement_backfills.push(QueuedSubscriberBackfill {
14593 owner: Some(owner.clone()),
14594 epoch: None,
14595 filters: continuity_filters,
14596 backfill: SubscriberBackfill::from_block(anchor),
14597 });
14598 }
14599 let retained_backfills = self
14600 .pending_backfills
14601 .iter()
14602 .filter(|queued| queued.owner.as_ref() != Some(&owner))
14603 .map(|queued| queued.filters.len())
14604 .sum::<usize>();
14605 let replacement_units = replacement_backfills
14606 .iter()
14607 .map(|queued| queued.filters.len())
14608 .sum::<usize>();
14609 if retained_backfills.saturating_add(replacement_units) > self.config.max_pending_backfills
14610 {
14611 return Err(SubscriberError::ResourceExhausted(format!(
14612 "owner update would queue more than {} lazy backfills",
14613 self.config.max_pending_backfills
14614 )));
14615 }
14616
14617 self.owned_interests = next_owned;
14618 self.interests = next_registered;
14619 if let Some(epoch) = replaced_epoch {
14620 self.purge_owner_epoch(&epoch);
14621 } else {
14622 self.recent_compat_owner_input_refs.remove(&owner);
14623 self.recent_compat_owner_input_ref_sets.remove(&owner);
14624 }
14625 self.retire_unreferenced_filters();
14626 self.sources_dirty = true;
14627
14628 self.pending_backfills
14631 .retain(|queued| queued.owner.as_ref() != Some(&owner));
14632 self.pending_backfills.extend(replacement_backfills);
14633 Ok(())
14634 }
14635
14636 fn clone_owned_interests(&self) -> Vec<OwnedSubscriberInterests<N>> {
14637 self.owned_interests
14638 .iter()
14639 .map(|entry| OwnedSubscriberInterests {
14640 owner: entry.owner.clone(),
14641 interests: entry.interests.clone(),
14642 epoch: entry.epoch.clone(),
14643 state: entry.state,
14644 baseline: entry.baseline,
14645 progress: entry.progress.clone(),
14646 progress_stream_revision: entry.progress_stream_revision,
14647 })
14648 .collect()
14649 }
14650
14651 fn rebuild_registered_interests(&mut self) {
14652 self.interests = aggregate_interests(&self.base_interests, &self.owned_interests);
14653 }
14654
14655 fn log_anchor(&self, filter: &Filter) -> Option<u64> {
14658 if let Some(anchor) = self
14659 .log_source_ids
14660 .get(filter)
14661 .and_then(|id| self.last_seen_log_blocks.get(id))
14662 {
14663 return Some(*anchor);
14664 }
14665
14666 self.log_source_ids
14670 .values()
14671 .filter_map(|id| self.last_seen_log_blocks.get(id).copied())
14672 .min()
14673 }
14674
14675 #[allow(clippy::mutable_key_type)]
14682 fn logical_log_filters(&self) -> Vec<Filter> {
14683 let mut filters = log_filters(&self.base_interests);
14684 for entry in &self.owned_interests {
14685 filters.extend(log_filters(&entry.interests));
14686 }
14687 let mut seen = HashSet::new();
14688 filters.retain(|filter| seen.insert(filter.clone()));
14689 filters
14690 }
14691
14692 fn log_stream_filters(&self) -> Vec<Filter> {
14698 let mut merged = Vec::new();
14699 for filter in self.logical_log_filters() {
14700 merge_log_subscription_filter(&mut merged, &filter);
14701 }
14702
14703 let max_addresses = self.config.max_log_addresses_per_subscription.max(1);
14704 let mut planned = Vec::new();
14705 for filter in merged {
14706 let mut addresses: Vec<_> = filter.address.iter().copied().collect();
14707 if addresses.len() <= max_addresses {
14708 planned.push(filter);
14709 continue;
14710 }
14711 addresses.sort_unstable();
14712 for chunk in addresses.chunks(max_addresses) {
14713 let mut split = filter.clone();
14714 split.address = FilterSet::default();
14715 for address in chunk {
14716 split.address.insert(*address);
14717 }
14718 planned.push(split);
14719 }
14720 }
14721 planned
14722 }
14723
14724 #[allow(clippy::mutable_key_type)]
14731 fn retire_unreferenced_filters(&mut self) {
14732 let mut live: HashSet<Filter> = self.log_stream_filters().into_iter().collect();
14733 if let AlloySubscriberState::Active(streams) = &self.state {
14734 for entry in &streams.entries {
14735 match &entry.source {
14736 SubscriberStreamSource::PubSubLog { filter, .. }
14737 | SubscriberStreamSource::BasePendingLog { filter, .. }
14738 | SubscriberStreamSource::PollingLog { filter } => {
14739 live.insert(filter.clone());
14740 }
14741 SubscriberStreamSource::BaseFlashblocks
14742 | SubscriberStreamSource::OpPendingFlashblocks
14743 | SubscriberStreamSource::CanonicalHeadPolling
14744 | SubscriberStreamSource::PubSubPendingHashes
14745 | SubscriberStreamSource::PubSubBlockHeaders
14746 | SubscriberStreamSource::PollingPendingHashes => {}
14747 #[cfg(feature = "raw-flashblocks-json")]
14748 SubscriberStreamSource::ExternalFlashblockUpdates => {}
14749 }
14750 }
14751 }
14752 self.log_source_ids
14753 .retain(|filter, _| live.contains(filter));
14754 let live_ids: HashSet<usize> = self.log_source_ids.values().copied().collect();
14755 self.last_seen_log_blocks
14756 .retain(|id, _| live_ids.contains(id));
14757 }
14758
14759 fn note_attestable_canonical_block(&mut self, record: &ReactiveInputRecord<N>) {
14766 if !self.attests_log_coverage() {
14767 return;
14768 }
14769 let Some(block) = record.context.block.as_ref() else {
14770 return;
14771 };
14772 let advances = self
14773 .attestable_canonical_head
14774 .as_ref()
14775 .is_none_or(|current| block.number > current.number);
14776 if advances {
14777 self.attestable_canonical_head = Some(*block);
14778 }
14779 }
14780
14781 fn reset_log_attestation(&mut self) {
14787 self.attestable_canonical_head = None;
14788 }
14789
14790 fn attests_log_coverage(&self) -> bool {
14796 matches!(
14797 resolve_subscriber_transport(self.mode),
14798 Ok(SubscriberTransport::PubSub)
14799 ) && self
14800 .interests
14801 .iter()
14802 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)))
14803 }
14804
14805 fn queue_log_coverage_attestation(&mut self) {
14807 if !self.attests_log_coverage() {
14808 return;
14809 }
14810 let Some(candidate) = self.attestable_canonical_head else {
14811 return;
14812 };
14813 let advances = self
14814 .attested_log_coverage
14815 .as_ref()
14816 .is_none_or(|attested| candidate.number > attested.number);
14817 if !advances {
14818 return;
14819 }
14820 self.attested_log_coverage = Some(candidate);
14821 self.pending_chain_controls
14822 .push_back(ChainControl::LogCoverage(candidate));
14823 }
14824
14825 fn drain_next_scoped_batch(&mut self) -> Option<SubscriberInputBatch<N>> {
14826 self.queue_log_coverage_attestation();
14830 if self.pending_records.is_empty()
14831 && self.pending_chain_controls.is_empty()
14832 && !self.pending_preconfirmation_invalidation
14833 {
14834 return None;
14835 }
14836
14837 let first_preconfirmation = self.pending_records.front().and_then(|record| {
14838 if record.scope != SubscriberInputScope::Preconfirmed {
14839 return None;
14840 }
14841 match &record.record.context.chain_status {
14842 ChainStatus::Preconfirmed { flashblock } => Some(flashblock.clone()),
14843 _ => None,
14844 }
14845 });
14846 let len = self
14847 .pending_records
14848 .iter()
14849 .take(self.config.max_batch_size)
14850 .take_while(|record| match &first_preconfirmation {
14851 Some(expected) => {
14852 record.scope == SubscriberInputScope::Preconfirmed
14853 && matches!(
14854 &record.record.context.chain_status,
14855 ChainStatus::Preconfirmed { flashblock } if flashblock == expected
14856 )
14857 }
14858 None => record.scope != SubscriberInputScope::Preconfirmed,
14859 })
14860 .count();
14861 let preconfirmation_timing = self
14862 .pending_records
14863 .iter()
14864 .take(len)
14865 .filter_map(SubscriberInputRecord::preconfirmation_timing)
14866 .reduce(FlashblockIngressTiming::earliest);
14867 let records = self.pending_records.drain(..len).collect();
14868 let chain_controls = if first_preconfirmation.is_none() && self.pending_records.is_empty() {
14869 self.pending_chain_controls.drain(..).collect()
14870 } else {
14871 Vec::new()
14872 };
14873 Some(SubscriberInputBatch {
14874 records,
14875 chain_id: self.chain_id,
14876 chain_controls,
14877 preconfirmation_invalidated: std::mem::take(
14878 &mut self.pending_preconfirmation_invalidation,
14879 ),
14880 preconfirmation_timing,
14881 })
14882 }
14883
14884 fn reset_delivery_state(&mut self) {
14885 self.pending_records.clear();
14886 self.pending_chain_controls.clear();
14887 self.pending_reconcile_owner_records.clear();
14888 self.resource_error = None;
14889 self.last_seen_log_blocks.clear();
14890 self.verified_log_blocks.clear();
14891 self.verified_log_block_order.clear();
14892 self.recent_input_refs.clear();
14893 self.recent_input_ref_set.clear();
14894 self.recent_owner_input_refs.clear();
14895 self.recent_owner_input_ref_sets.clear();
14896 self.recent_compat_owner_input_refs.clear();
14897 self.recent_compat_owner_input_ref_sets.clear();
14898 self.pending_backfills.clear();
14899 self.pending_source_backfills.clear();
14900 self.pending_preconfirmation_invalidation = false;
14901 self.pending_flashblock_reconnects.clear();
14902 self.pending_flashblock_reconnect_sources.clear();
14903 self.flashblocks_rpc_metrics = FlashblocksRpcMetrics::default();
14904 self.log_source_ids.clear();
14905 self.next_log_source_id = 0;
14906 self.sources_dirty = true;
14907 self.last_certified_canonical_head = None;
14908 self.last_canonical_head_certification = None;
14909 self.sealed_block_pending_certification = false;
14910 self.attestable_canonical_head = None;
14911 self.attested_log_coverage = None;
14912 self.reset_flashblock_tracking();
14913 }
14914
14915 fn reset_stream_topology(&mut self) {
14916 #[cfg(feature = "raw-flashblocks-json")]
14917 let external = match &mut self.state {
14918 AlloySubscriberState::Active(streams) => streams
14919 .entries
14920 .iter()
14921 .position(|entry| entry.source.is_external_flashblocks())
14922 .map(|index| streams.entries.remove(index)),
14923 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => None,
14924 };
14925
14926 #[cfg(feature = "raw-flashblocks-json")]
14927 if let Some(external) = external {
14928 let mut streams = SubscriberStreams::new();
14929 streams.entries.push(external);
14930 self.state = AlloySubscriberState::Active(streams);
14931 return;
14932 }
14933
14934 self.state = AlloySubscriberState::Uninitialized;
14935 }
14936
14937 fn reset_flashblock_tracking(&mut self) {
14938 self.base_flashblock_header = None;
14939 self.base_flashblock_transactions = None;
14940 self.unmatched_pending_logs.clear();
14941 self.latest_preconfirmation = None;
14942 self.preconfirmed_seen_logs.clear();
14943 self.preconfirmed_receipted_transactions.clear();
14944 self.preconfirmed_unavailable_receipts.clear();
14945 #[cfg(feature = "raw-flashblocks-json")]
14946 {
14947 self.last_external_flashblock_snapshot = None;
14948 }
14949 self.consecutive_flashblock_poll_failures = 0;
14950 }
14951
14952 fn invalidate_preconfirmation_snapshot(&mut self) {
14959 self.pending_records
14960 .retain(|record| record.scope != SubscriberInputScope::Preconfirmed);
14961 self.pending_preconfirmation_invalidation = true;
14962 self.latest_preconfirmation = None;
14963 self.preconfirmed_seen_logs.clear();
14964 self.preconfirmed_receipted_transactions.clear();
14965 self.preconfirmed_unavailable_receipts.clear();
14966 }
14967
14968 fn bump_stream_revision(&mut self) {
14969 self.stream_revision = self.stream_revision.saturating_add(1);
14970 }
14971}
14972
14973impl<P, N> InterestOwnerSubscriber<N> for AlloySubscriber<P, N>
14974where
14975 P: Provider<N> + Send + Sync,
14976 N: Network + 'static,
14977 N::HeaderResponse: Send + 'static,
14978{
14979 fn upsert_interest_owners(
14980 &mut self,
14981 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14982 ) -> SubscriberOperation<'_, ()> {
14983 Box::pin(async move {
14984 if !owners.is_empty() {
14985 self.ensure_chain_id().await?;
14986 }
14987 AlloySubscriber::upsert_interest_owners(self, owners)
14988 })
14989 }
14990
14991 fn replace_interest_owners(
14992 &mut self,
14993 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14994 ) -> SubscriberOperation<'_, ()> {
14995 Box::pin(async move {
14996 if owners.iter().any(|(_, interests)| !interests.is_empty()) {
14997 self.ensure_chain_id().await?;
14998 }
14999 AlloySubscriber::replace_interest_owners(self, owners)
15000 })
15001 }
15002
15003 fn replace_interest_owners_with_global_backfill(
15004 &mut self,
15005 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
15006 backfill: SubscriberBackfill,
15007 ) -> SubscriberOperation<'_, ()> {
15008 Box::pin(async move {
15009 if owners.iter().any(|(_, interests)| !interests.is_empty()) {
15010 self.ensure_chain_id().await?;
15011 }
15012 AlloySubscriber::replace_interest_owners_with_global_backfill(self, owners, backfill)
15013 })
15014 }
15015
15016 fn add_interest_owner(
15017 &mut self,
15018 owner: HandlerId,
15019 interests: &[ReactiveInterest<N>],
15020 ) -> SubscriberOperation<'_, ()> {
15021 let interests = interests.to_vec();
15022 Box::pin(async move {
15023 if !interests.is_empty() {
15024 self.ensure_chain_id().await?;
15025 }
15026 AlloySubscriber::add_interest_owner(self, owner, &interests)
15027 })
15028 }
15029
15030 fn add_interest_owner_with_backfill(
15031 &mut self,
15032 owner: HandlerId,
15033 interests: &[ReactiveInterest<N>],
15034 backfill: SubscriberBackfill,
15035 ) -> SubscriberOperation<'_, ()> {
15036 let interests = interests.to_vec();
15037 Box::pin(async move {
15038 if !interests.is_empty() {
15039 self.ensure_chain_id().await?;
15040 }
15041 AlloySubscriber::add_interest_owner_with_backfill(self, owner, &interests, backfill)
15042 })
15043 }
15044
15045 fn add_interest_owner_with_canonical_catchup(
15046 &mut self,
15047 owner: HandlerId,
15048 interests: &[ReactiveInterest<N>],
15049 retained: BlockRef,
15050 ) -> SubscriberOperation<'_, ()> {
15051 let interests = interests.to_vec();
15052 Box::pin(async move {
15053 self.ensure_chain_id().await?;
15056 AlloySubscriber::add_interest_owner_with_canonical_catchup(
15057 self, owner, &interests, retained,
15058 )
15059 })
15060 }
15061
15062 fn remove_interest_owner(
15063 &mut self,
15064 owner: &HandlerId,
15065 ) -> SubscriberOperation<'_, Option<Vec<ReactiveInterest<N>>>> {
15066 let owner = owner.clone();
15067 Box::pin(async move { Ok(AlloySubscriber::remove_interest_owner(self, &owner)) })
15068 }
15069
15070 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
15071 AlloySubscriber::owner_interests(self, owner)
15072 }
15073}
15074
15075enum AlloySubscriberState<N: Network> {
15076 Uninitialized,
15077 Active(SubscriberStreams<N>),
15078 Empty,
15079}
15080
15081struct SubscriberStreams<N: Network> {
15082 entries: Vec<SubscriberStreamEntry<N>>,
15083 next_index: usize,
15084}
15085
15086struct SubscriberStreamEntry<N: Network> {
15087 source: SubscriberStreamSource,
15088 stream: BoxStream<'static, SubscriberEvent<N>>,
15089}
15090
15091impl<N: Network> SubscriberStreams<N> {
15092 fn new() -> Self {
15093 Self {
15094 entries: Vec::new(),
15095 next_index: 0,
15096 }
15097 }
15098
15099 fn is_empty(&self) -> bool {
15100 self.entries.is_empty()
15101 }
15102
15103 fn push(
15104 &mut self,
15105 source: SubscriberStreamSource,
15106 stream: BoxStream<'static, SubscriberEvent<N>>,
15107 ) {
15108 self.entries.push(SubscriberStreamEntry { source, stream });
15109 }
15110
15111 #[cfg(all(test, any(feature = "reactive-polling", feature = "reactive-ws")))]
15112 fn len(&self) -> usize {
15113 self.entries.len()
15114 }
15115
15116 fn contains_source(&self, source: &SubscriberStreamSource) -> bool {
15117 self.entries
15118 .iter()
15119 .any(|entry| entry.source.same_key(source))
15120 }
15121
15122 fn retain_sources(&mut self, sources: &[SubscriberStreamSource]) {
15123 self.entries
15124 .retain(|entry| sources.iter().any(|source| entry.source.same_key(source)));
15125 self.normalize_next_index();
15126 }
15127
15128 fn normalize_next_index(&mut self) {
15129 if self.entries.is_empty() {
15130 self.next_index = 0;
15131 } else if self.next_index >= self.entries.len() {
15132 self.next_index %= self.entries.len();
15133 }
15134 }
15135
15136 async fn next(&mut self) -> Option<SubscriberEvent<N>> {
15137 poll_fn(|cx| {
15138 self.normalize_next_index();
15139 if self.entries.is_empty() {
15140 return std::task::Poll::Ready(None);
15141 }
15142
15143 let mut index = self.next_index;
15144 let mut checked = 0usize;
15145 while checked < self.entries.len() {
15146 if index >= self.entries.len() {
15147 index = 0;
15148 }
15149 match self.entries[index].stream.as_mut().poll_next(cx) {
15150 std::task::Poll::Ready(Some(event)) => {
15151 if matches!(event, SubscriberEvent::StreamTerminated(_)) {
15152 self.entries.remove(index);
15153 self.next_index = if self.entries.is_empty() {
15154 0
15155 } else {
15156 index % self.entries.len()
15157 };
15158 } else {
15159 self.next_index = (index + 1) % self.entries.len();
15160 }
15161 return std::task::Poll::Ready(Some(event));
15162 }
15163 std::task::Poll::Ready(None) => {
15164 self.entries.remove(index);
15165 if self.entries.is_empty() {
15166 self.next_index = 0;
15167 return std::task::Poll::Ready(None);
15168 }
15169 }
15170 std::task::Poll::Pending => {
15171 checked += 1;
15172 index += 1;
15173 }
15174 }
15175 }
15176
15177 if self.entries.is_empty() {
15178 std::task::Poll::Ready(None)
15179 } else {
15180 self.next_index = index % self.entries.len();
15181 std::task::Poll::Pending
15182 }
15183 })
15184 .await
15185 }
15186}
15187
15188#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15189#[allow(dead_code)]
15190enum SubscriberTransport {
15191 PubSub,
15192 Polling,
15193}
15194
15195#[derive(Clone, Debug)]
15196enum SubscriberStreamSource {
15197 PubSubLog {
15198 id: usize,
15199 filter: Filter,
15200 },
15201 BasePendingLog {
15202 id: usize,
15203 filter: Filter,
15204 },
15205 BaseFlashblocks,
15206 OpPendingFlashblocks,
15207 CanonicalHeadPolling,
15208 PubSubPendingHashes,
15209 PubSubBlockHeaders,
15210 PollingLog {
15211 filter: Filter,
15212 },
15213 PollingPendingHashes,
15214 #[cfg(feature = "raw-flashblocks-json")]
15215 ExternalFlashblockUpdates,
15216}
15217
15218impl SubscriberStreamSource {
15219 fn label(&self) -> &'static str {
15220 match self {
15221 Self::PubSubLog { .. } => "pubsub log",
15222 Self::BasePendingLog { .. } => "OP Stack pendingLogs",
15223 Self::BaseFlashblocks => "OP Stack newFlashblocks",
15224 Self::OpPendingFlashblocks => "Optimism pending Flashblocks",
15225 Self::CanonicalHeadPolling => "certified canonical head",
15226 Self::PubSubPendingHashes => "pubsub pending transaction hash",
15227 Self::PubSubBlockHeaders => "pubsub block header",
15228 Self::PollingLog { .. } => "polling log",
15229 Self::PollingPendingHashes => "polling pending transaction hash",
15230 #[cfg(feature = "raw-flashblocks-json")]
15231 Self::ExternalFlashblockUpdates => "external standardized Flashblock update",
15232 }
15233 }
15234
15235 fn is_pubsub(&self) -> bool {
15236 matches!(
15237 self,
15238 Self::PubSubLog { .. }
15239 | Self::BasePendingLog { .. }
15240 | Self::BaseFlashblocks
15241 | Self::OpPendingFlashblocks
15242 | Self::PubSubPendingHashes
15243 | Self::PubSubBlockHeaders
15244 )
15245 }
15246
15247 fn is_flashblocks(&self) -> bool {
15248 matches!(
15249 self,
15250 Self::BasePendingLog { .. } | Self::BaseFlashblocks | Self::OpPendingFlashblocks
15251 )
15252 }
15253
15254 fn same_key(&self, other: &Self) -> bool {
15255 match (self, other) {
15256 (Self::PubSubLog { filter: left, .. }, Self::PubSubLog { filter: right, .. })
15257 | (
15258 Self::BasePendingLog { filter: left, .. },
15259 Self::BasePendingLog { filter: right, .. },
15260 )
15261 | (Self::PollingLog { filter: left }, Self::PollingLog { filter: right }) => {
15262 left == right
15263 }
15264 (Self::BaseFlashblocks, Self::BaseFlashblocks)
15265 | (Self::OpPendingFlashblocks, Self::OpPendingFlashblocks)
15266 | (Self::CanonicalHeadPolling, Self::CanonicalHeadPolling)
15267 | (Self::PubSubPendingHashes, Self::PubSubPendingHashes)
15268 | (Self::PubSubBlockHeaders, Self::PubSubBlockHeaders)
15269 | (Self::PollingPendingHashes, Self::PollingPendingHashes) => true,
15270 #[cfg(feature = "raw-flashblocks-json")]
15271 (Self::ExternalFlashblockUpdates, Self::ExternalFlashblockUpdates) => true,
15272 _ => false,
15273 }
15274 }
15275
15276 fn is_external_flashblocks(&self) -> bool {
15277 #[cfg(feature = "raw-flashblocks-json")]
15278 {
15279 matches!(self, Self::ExternalFlashblockUpdates)
15280 }
15281 #[cfg(not(feature = "raw-flashblocks-json"))]
15282 {
15283 false
15284 }
15285 }
15286}
15287
15288#[allow(dead_code)]
15289enum SubscriberEvent<N: Network> {
15290 Log {
15291 source_id: usize,
15292 log: Log,
15293 },
15294 BackfilledLogs {
15295 source_id: usize,
15296 logs: Vec<Log>,
15297 },
15298 Logs(Vec<Log>),
15299 BlockHeader(N::HeaderResponse),
15300 PendingHash(B256),
15301 PendingHashes(Vec<B256>),
15302 BasePendingLog {
15303 source_id: usize,
15304 log: Log,
15305 },
15306 BasePendingLogTimed {
15307 source_id: usize,
15308 log: Log,
15309 timing: FlashblockIngressTiming,
15310 },
15311 BaseFlashblock(BaseFlashblockWirePayload),
15312 BaseFlashblockTimed {
15313 payload: BaseFlashblockWirePayload,
15314 timing: FlashblockIngressTiming,
15315 },
15316 OpFlashblockTick,
15317 OpFlashblockTickTimed(FlashblockIngressTiming),
15318 CanonicalHeadTick,
15319 PreconfirmedLogs {
15320 flashblock: FlashblockRef,
15321 logs: Vec<Log>,
15322 timing: FlashblockIngressTiming,
15323 },
15324 FlashblockInvalidated,
15325 FlashblockObserved,
15326 #[cfg(feature = "raw-flashblocks-json")]
15327 ExternalFlashblockUpdate(raw_json_flashblocks::QueuedFlashblockUpdate),
15328 StreamTerminated(SubscriberStreamSource),
15329 StreamGap {
15333 source: SubscriberStreamSource,
15334 gap: SubscriberStreamGap,
15335 },
15336}
15337
15338enum SubscriberReady<N: Network> {
15339 Event(Option<SubscriberEvent<N>>),
15340 FlashblockReconnect(
15341 SubscriberStreamSource,
15342 Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>,
15343 ),
15344}
15345
15346#[derive(Debug)]
15347enum PendingFlashblockPollError {
15348 Request(SubscriberError),
15349 Integrity(SubscriberError),
15350}
15351
15352impl PendingFlashblockPollError {
15353 fn into_subscriber(self) -> SubscriberError {
15354 match self {
15355 Self::Request(error) | Self::Integrity(error) => error,
15356 }
15357 }
15358}
15359
15360fn pending_flashblock_request_error(error: impl fmt::Display) -> PendingFlashblockPollError {
15361 PendingFlashblockPollError::Request(provider_error(error))
15362}
15363
15364fn normalize_op_pending_block<N: Network>(
15365 mut value: serde_json::Value,
15366) -> Result<N::BlockResponse, SubscriberError> {
15367 let object = value.as_object_mut().ok_or_else(|| {
15368 SubscriberError::Provider("OP pending block response is not an object".into())
15369 })?;
15370 let transactions = object
15371 .get_mut("transactions")
15372 .and_then(serde_json::Value::as_array_mut)
15373 .ok_or_else(|| {
15374 SubscriberError::Provider(
15375 "OP pending block response is missing its transaction array".into(),
15376 )
15377 })?;
15378 for transaction in transactions {
15379 if transaction.is_string() {
15380 continue;
15381 }
15382 let hash = transaction
15383 .as_object()
15384 .and_then(|object| object.get("hash"))
15385 .filter(|hash| hash.is_string())
15386 .cloned()
15387 .ok_or_else(|| {
15388 SubscriberError::Provider("OP pending block transaction is missing its hash".into())
15389 })?;
15390 *transaction = hash;
15391 }
15392 if object.get("hash").is_none_or(serde_json::Value::is_null) {
15393 object.insert(
15394 "hash".into(),
15395 serde_json::Value::String(B256::ZERO.to_string()),
15396 );
15397 }
15398 if object.get("nonce").is_none_or(serde_json::Value::is_null) {
15399 object.insert(
15400 "nonce".into(),
15401 serde_json::Value::String("0x0000000000000000".into()),
15402 );
15403 }
15404 if object.get("miner").is_none_or(serde_json::Value::is_null)
15405 || object
15406 .get("beneficiary")
15407 .is_none_or(serde_json::Value::is_null)
15408 {
15409 object.insert(
15410 "miner".into(),
15411 serde_json::Value::String(Address::ZERO.to_string()),
15412 );
15413 }
15414 serde_json::from_value(value).map_err(|error| {
15415 SubscriberError::Provider(format!(
15416 "failed to decode normalized OP pending block: {error}"
15417 ))
15418 })
15419}
15420
15421fn normalize_pending_transaction_receipt(
15422 expected_transaction_hash: B256,
15423 value: serde_json::Value,
15424) -> Result<Option<Vec<Log>>, SubscriberError> {
15425 if value.is_null() {
15426 return Ok(None);
15427 }
15428 let receipt = value.as_object().ok_or_else(|| {
15429 SubscriberError::Provider("pending transaction receipt response is not an object".into())
15430 })?;
15431 let transaction_hash: B256 =
15432 serde_json::from_value(receipt.get("transactionHash").cloned().ok_or_else(|| {
15433 SubscriberError::Provider(
15434 "pending transaction receipt is missing its transaction hash".into(),
15435 )
15436 })?)
15437 .map_err(|error| {
15438 SubscriberError::Provider(format!(
15439 "failed to decode pending transaction receipt hash: {error}"
15440 ))
15441 })?;
15442 if transaction_hash != expected_transaction_hash {
15443 return Err(SubscriberError::Provider(
15444 "pending transaction receipt hash disagrees with its request".into(),
15445 ));
15446 }
15447 let receipt_logs = receipt
15448 .get("logs")
15449 .and_then(serde_json::Value::as_array)
15450 .ok_or_else(|| {
15451 SubscriberError::Provider("pending transaction receipt is missing its log array".into())
15452 })?;
15453 let mut logs = Vec::new();
15454 for log in receipt_logs {
15455 let log: Log = serde_json::from_value(log.clone()).map_err(|error| {
15456 SubscriberError::Provider(format!(
15457 "failed to decode pending transaction receipt log: {error}"
15458 ))
15459 })?;
15460 if log.transaction_hash != Some(expected_transaction_hash) {
15461 return Err(SubscriberError::Provider(
15462 "pending transaction receipt log hash disagrees with its receipt".into(),
15463 ));
15464 }
15465 logs.push(log);
15466 }
15467 Ok(Some(logs))
15468}
15469
15470impl<P, N> EventSubscriber<N> for AlloySubscriber<P, N>
15471where
15472 P: Provider<N> + Send + Sync,
15473 N: Network + 'static,
15474 N::HeaderResponse: Send + 'static,
15475{
15476 fn chain_id(&self) -> Option<u64> {
15477 self.chain_id
15478 }
15479
15480 fn capabilities(&self) -> SubscriberCapabilities {
15481 let Ok(transport) = resolve_subscriber_transport(self.mode) else {
15482 return SubscriberCapabilities::default();
15483 };
15484 let mut capabilities = vec![
15485 SubscriberCapability::Logs,
15486 SubscriberCapability::PendingTransactionHashes,
15487 SubscriberCapability::HistoricalBackfill,
15488 SubscriberCapability::Live,
15489 SubscriberCapability::OwnerScopedDelivery,
15490 SubscriberCapability::DynamicInterests,
15491 ];
15492 if transport == SubscriberTransport::PubSub {
15493 capabilities.push(SubscriberCapability::BlockHeaders);
15494 capabilities.push(SubscriberCapability::LogCoverageAttestation);
15499 }
15500 if self.config.preconfirmations != PreconfirmationMode::Disabled
15501 && (self.uses_external_flashblock_updates()
15502 || (self.provider_ref.is_some()
15503 && self.chain_id.and_then(flashblocks_adapter).is_some()))
15504 {
15505 capabilities.push(SubscriberCapability::Preconfirmations);
15506 }
15507 SubscriberCapabilities::new(capabilities)
15508 }
15509
15510 fn register_interests(
15511 &mut self,
15512 interests: &[ReactiveInterest<N>],
15513 ) -> SubscriberOperation<'_, ()> {
15514 let interests = interests.to_vec();
15515 Box::pin(async move {
15516 validate_subscriber_config(&self.config)?;
15517 validate_supported_interests(self.mode, &self.config, &interests)?;
15518 if !interests.is_empty() {
15519 self.ensure_chain_id().await?;
15520 }
15521 self.validate_flashblocks_setup()?;
15522
15523 self.base_interests = interests;
15524 self.owned_interests.clear();
15525 self.rebuild_registered_interests();
15526 self.reset_delivery_state();
15527 self.reset_stream_topology();
15528 Ok(())
15529 })
15530 }
15531
15532 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N> {
15533 Box::pin(async {
15534 Ok(self
15535 .next_scoped_batch()
15536 .await?
15537 .map(SubscriberInputBatch::into_reactive_batch))
15538 })
15539 }
15540}
15541
15542impl<P, N> AlloySubscriber<P, N>
15543where
15544 P: Provider<N> + Send + Sync,
15545 N: Network + 'static,
15546 N::HeaderResponse: Send + 'static,
15547{
15548 pub async fn establish_flashblocks_preflight(
15570 &mut self,
15571 expected_chain_id: u64,
15572 ) -> Result<FlashblocksPreflight, SubscriberError> {
15573 validate_subscriber_config(&self.config)?;
15574 if self.config.preconfirmations == PreconfirmationMode::Disabled {
15575 return Err(SubscriberError::InvalidConfig(
15576 "Flashblocks preflight requires preconfirmations",
15577 ));
15578 }
15579 if !self
15580 .interests
15581 .iter()
15582 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)))
15583 {
15584 return Err(SubscriberError::InvalidConfig(
15585 "Flashblocks preflight requires at least one active log interest",
15586 ));
15587 }
15588 let chain_id = self.ensure_chain_id().await?;
15589 if chain_id != expected_chain_id {
15590 return Err(SubscriberError::ChainMismatch {
15591 expected: expected_chain_id,
15592 actual: chain_id,
15593 });
15594 }
15595 self.validate_flashblocks_setup()?;
15596 #[cfg(feature = "raw-flashblocks-json")]
15597 if let Some(provider) = self.external_flashblocks_provider.clone() {
15598 self.ensure_streams().await?;
15599 return Ok(FlashblocksPreflight {
15600 chain_id,
15601 provider,
15602 delivery: FlashblocksDelivery::ExternalUpdates,
15603 pending_log_subscriptions: 0,
15604 pending_log_filters: self.log_stream_filters().len(),
15605 advertised_capabilities: None,
15606 });
15607 }
15608 let adapter = flashblocks_adapter(chain_id).ok_or(SubscriberError::Unsupported(
15609 "Flashblocks are currently implemented for Base and OP chains",
15610 ))?;
15611 let provider = self
15612 .provider_ref
15613 .clone()
15614 .ok_or(SubscriberError::InvalidConfig(
15615 "Flashblocks preflight requires a stable provider ref",
15616 ))?;
15617 self.record_rpc(
15618 SubscriberRpcCause::FlashblocksSetup,
15619 SubscriberRpcMethod::OpSupportedCapabilities,
15620 );
15621 self.flashblocks_rpc_metrics.capability_requests = self
15622 .flashblocks_rpc_metrics
15623 .capability_requests
15624 .saturating_add(1);
15625 let capability_provider = if adapter == FlashblocksAdapter::PendingStatePolling {
15626 self.flashblocks_state_provider
15627 .as_ref()
15628 .unwrap_or(&self.provider)
15629 } else {
15630 &self.provider
15631 };
15632 let advertised_capabilities = capability_provider
15633 .client()
15634 .request::<_, serde_json::Value>("op_supportedCapabilities", ())
15635 .await
15636 .ok();
15637
15638 self.ensure_streams().await?;
15639 let pending_log_filters = self.log_stream_filters();
15640 if adapter == FlashblocksAdapter::PendingStatePolling
15641 && self.pending_receipt_requests_per_tick_capacity() == 0
15642 {
15643 return Err(SubscriberError::InvalidConfig(
15644 "Flashblocks RPC budget leaves no capacity for OP transaction receipts",
15645 ));
15646 }
15647 let (delivery, pending_log_subscriptions) = match adapter {
15648 FlashblocksAdapter::NativeSubscriptions => {
15649 if resolve_subscriber_transport(self.mode)? != SubscriberTransport::PubSub {
15650 return Err(SubscriberError::Unsupported(
15651 "Base Flashblocks preflight requires pubsub",
15652 ));
15653 }
15654 let pending_sources = self
15655 .pubsub_stream_sources()
15656 .into_iter()
15657 .filter(|source| {
15658 matches!(source, SubscriberStreamSource::BasePendingLog { .. })
15659 })
15660 .collect::<Vec<_>>();
15661 let AlloySubscriberState::Active(streams) = &self.state else {
15662 return Err(SubscriberError::Provider(
15663 "Flashblocks preflight subscriptions did not become active".to_owned(),
15664 ));
15665 };
15666 if !streams.contains_source(&SubscriberStreamSource::BaseFlashblocks)
15667 || pending_sources
15668 .iter()
15669 .any(|source| !streams.contains_source(source))
15670 {
15671 return Err(SubscriberError::Provider(
15672 "Base Flashblocks preflight did not retain both subscription lanes"
15673 .to_owned(),
15674 ));
15675 }
15676 (
15677 FlashblocksDelivery::NativeSubscriptions,
15678 pending_sources.len(),
15679 )
15680 }
15681 FlashblocksAdapter::PendingStatePolling => {
15682 if let Some(state_provider) = self.flashblocks_state_provider.as_ref() {
15683 self.record_rpc(
15684 SubscriberRpcCause::FlashblocksSetup,
15685 SubscriberRpcMethod::EthChainId,
15686 );
15687 self.flashblocks_rpc_metrics.provider_pair_chain_requests = self
15688 .flashblocks_rpc_metrics
15689 .provider_pair_chain_requests
15690 .saturating_add(1);
15691 let actual = state_provider
15692 .get_chain_id()
15693 .await
15694 .map_err(provider_error)?;
15695 if actual != expected_chain_id {
15696 return Err(SubscriberError::ChainMismatch {
15697 expected: expected_chain_id,
15698 actual,
15699 });
15700 }
15701 }
15702 let AlloySubscriberState::Active(streams) = &self.state else {
15703 return Err(SubscriberError::Provider(
15704 "Flashblocks preflight streams did not become active".to_owned(),
15705 ));
15706 };
15707 if !streams.contains_source(&SubscriberStreamSource::OpPendingFlashblocks) {
15708 return Err(SubscriberError::Provider(
15709 "Optimism Flashblocks preflight did not retain its pending-state sampler"
15710 .to_owned(),
15711 ));
15712 }
15713 self.probe_pending_state(&pending_log_filters).await?;
15714 (FlashblocksDelivery::PendingStatePolling, 0)
15715 }
15716 };
15717 Ok(FlashblocksPreflight {
15718 chain_id,
15719 provider,
15720 delivery,
15721 pending_log_subscriptions,
15722 pending_log_filters: pending_log_filters.len(),
15723 advertised_capabilities,
15724 })
15725 }
15726
15727 #[cfg(feature = "raw-flashblocks-json")]
15738 pub fn ingest_flashblock_update(
15739 &mut self,
15740 update: FlashblockUpdate,
15741 ) -> Result<(), SubscriberError> {
15742 self.ingest_flashblock_update_with_ingress(
15743 update,
15744 FlashblockIngressTiming::new(Instant::now()),
15745 )
15746 }
15747
15748 #[cfg(feature = "raw-flashblocks-json")]
15758 pub fn ingest_flashblock_update_with_ingress(
15759 &mut self,
15760 update: FlashblockUpdate,
15761 timing: FlashblockIngressTiming,
15762 ) -> Result<(), SubscriberError> {
15763 validate_subscriber_config(&self.config)?;
15764 self.validate_flashblocks_setup()?;
15765 let configured =
15766 self.external_flashblocks_provider
15767 .as_ref()
15768 .ok_or(SubscriberError::InvalidConfig(
15769 "standardized Flashblock updates require configure_external_flashblock_updates",
15770 ))?;
15771
15772 match update {
15773 FlashblockUpdate::Snapshot(batch) => {
15774 if batch.flashblock.provider.endpoint != configured.endpoint {
15775 return Err(SubscriberError::Provider(
15776 "external Flashblock update came from an unexpected provider endpoint"
15777 .into(),
15778 ));
15779 }
15780 if batch.flashblock.provider.generation < configured.generation
15781 || self.latest_preconfirmation.as_ref().is_some_and(|latest| {
15782 latest.provider.endpoint == batch.flashblock.provider.endpoint
15783 && latest.provider.generation > batch.flashblock.provider.generation
15784 })
15785 {
15786 return Ok(());
15787 }
15788 if self
15789 .rejected_external_flashblock_generation
15790 .is_some_and(|rejected| batch.flashblock.provider.generation <= rejected)
15791 {
15792 return Err(SubscriberError::Provider(
15793 "external Flashblock provider generation was previously rejected".into(),
15794 ));
15795 }
15796 validate_standard_flashblock_snapshot(&batch)?;
15797 if self.validate_external_flashblock_sequence(&batch)? {
15798 return Ok(());
15799 }
15800 let required = self.pending_record_count().saturating_add(batch.logs.len());
15801 if required > self.config.max_pending_records {
15802 self.invalidate_preconfirmation_snapshot();
15803 self.last_external_flashblock_snapshot = None;
15804 return Err(SubscriberError::ResourceExhausted(format!(
15805 "external preconfirmation records require {required} pending records, above the configured limit of {}",
15806 self.config.max_pending_records
15807 )));
15808 }
15809 let accepted_snapshot = (*batch).clone();
15810 let FlashblockSnapshot { flashblock, logs } = *batch;
15811 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
15812 self.last_external_flashblock_snapshot = Some(accepted_snapshot);
15813 if let Some(provider) = self.external_flashblocks_provider.as_mut() {
15814 provider.generation = provider.generation.max(flashblock.provider.generation);
15815 }
15816 if !logs.is_empty() {
15817 self.enqueue_event(SubscriberEvent::PreconfirmedLogs {
15818 flashblock,
15819 logs,
15820 timing,
15821 });
15822 }
15823 }
15824 FlashblockUpdate::Invalidated(invalidation) => {
15825 if invalidation.provider.endpoint != configured.endpoint {
15826 return Err(SubscriberError::Provider(
15827 "external Flashblock invalidation came from an unexpected provider endpoint"
15828 .into(),
15829 ));
15830 }
15831 if self.latest_preconfirmation.as_ref().is_some_and(|latest| {
15832 latest.provider == invalidation.provider
15833 && latest.payload_id == Some(invalidation.payload_id)
15834 }) {
15835 self.invalidate_preconfirmation_snapshot();
15836 self.last_external_flashblock_snapshot = None;
15837 }
15838 }
15839 }
15840 Ok(())
15841 }
15842
15843 #[cfg(feature = "raw-flashblocks-json")]
15844 fn validate_external_flashblock_sequence(
15845 &self,
15846 snapshot: &FlashblockSnapshot,
15847 ) -> Result<bool, SubscriberError> {
15848 let Some(previous) = self.last_external_flashblock_snapshot.as_ref() else {
15849 if snapshot.flashblock.index != Some(0) {
15850 return Err(SubscriberError::Provider(
15851 "external Flashblock payload generation must begin at index zero".into(),
15852 ));
15853 }
15854 return Ok(false);
15855 };
15856
15857 if previous.flashblock.provider == snapshot.flashblock.provider
15858 && previous.flashblock.payload_id == snapshot.flashblock.payload_id
15859 {
15860 let previous_index = previous
15861 .flashblock
15862 .index
15863 .expect("validated indexed snapshot");
15864 let current_index = snapshot
15865 .flashblock
15866 .index
15867 .expect("validated indexed snapshot");
15868 if current_index == previous_index {
15869 if previous == snapshot {
15870 return Ok(true);
15871 }
15872 return Err(SubscriberError::Provider(
15873 "external Flashblock repeated the same index with conflicting content".into(),
15874 ));
15875 }
15876 if current_index < previous_index {
15877 return Err(SubscriberError::Provider(format!(
15878 "external Flashblock index regressed from {previous_index} to {current_index}"
15879 )));
15880 }
15881 if current_index > previous_index.saturating_add(1) {
15882 return Err(SubscriberError::Provider(format!(
15883 "external Flashblock index skipped from {previous_index} to {current_index}"
15884 )));
15885 }
15886 if current_index == previous_index.saturating_add(1)
15887 && !previous.flashblock.same_base_identity(&snapshot.flashblock)
15888 {
15889 return Err(SubscriberError::Provider(
15890 "external Flashblock base identity changed within one payload generation"
15891 .into(),
15892 ));
15893 }
15894 if current_index == previous_index.saturating_add(1)
15895 && !snapshot
15896 .flashblock
15897 .transaction_hashes
15898 .starts_with(&previous.flashblock.transaction_hashes)
15899 {
15900 return Err(SubscriberError::Provider(
15901 "external Flashblock cumulative transaction membership changed its prior prefix"
15902 .into(),
15903 ));
15904 }
15905 let prior_transaction_count =
15906 u64::try_from(previous.flashblock.transaction_hashes.len()).unwrap_or(u64::MAX);
15907 if current_index == previous_index.saturating_add(1)
15908 && snapshot.logs.iter().any(|log| {
15909 log.transaction_index
15910 .is_some_and(|index| index < prior_transaction_count)
15911 })
15912 {
15913 return Err(SubscriberError::Provider(
15914 "external Flashblock delta log does not belong to a newly appended transaction"
15915 .into(),
15916 ));
15917 }
15918 } else if snapshot.flashblock.index != Some(0) {
15919 return Err(SubscriberError::Provider(
15920 "external Flashblock payload generation must begin at index zero".into(),
15921 ));
15922 }
15923 Ok(false)
15924 }
15925
15926 async fn probe_pending_state(&mut self, filters: &[Filter]) -> Result<(), SubscriberError> {
15927 self.flashblocks_rpc_metrics.pending_block_requests = self
15928 .flashblocks_rpc_metrics
15929 .pending_block_requests
15930 .saturating_add(1);
15931 let pending = self
15932 .fetch_op_pending_block()
15933 .await
15934 .map_err(PendingFlashblockPollError::into_subscriber)?
15935 .ok_or_else(|| {
15936 SubscriberError::Provider(
15937 "provider returned no pending block during Flashblocks preflight".into(),
15938 )
15939 })?;
15940 self.certify_op_pending_parent(&pending)
15941 .await
15942 .map_err(PendingFlashblockPollError::into_subscriber)?;
15943 for filter in filters {
15944 self.record_rpc(
15945 SubscriberRpcCause::PendingStateSample,
15946 SubscriberRpcMethod::EthGetLogs,
15947 );
15948 self.flashblocks_rpc_metrics.pending_log_requests = self
15949 .flashblocks_rpc_metrics
15950 .pending_log_requests
15951 .saturating_add(1);
15952 self.flashblocks_state_provider
15953 .as_ref()
15954 .unwrap_or(&self.provider)
15955 .get_logs(
15956 &filter
15957 .clone()
15958 .from_block(BlockNumberOrTag::Latest)
15959 .to_block(BlockNumberOrTag::Pending),
15960 )
15961 .await
15962 .map_err(provider_error)?;
15963 }
15964 self.record_rpc(
15965 SubscriberRpcCause::PendingStateSample,
15966 SubscriberRpcMethod::EthGetTransactionReceipt,
15967 );
15968 self.flashblocks_rpc_metrics.pending_receipt_requests = self
15969 .flashblocks_rpc_metrics
15970 .pending_receipt_requests
15971 .saturating_add(1);
15972 let _: serde_json::Value = self
15973 .flashblocks_state_provider
15974 .as_ref()
15975 .unwrap_or(&self.provider)
15976 .raw_request(Cow::Borrowed("eth_getTransactionReceipt"), (B256::ZERO,))
15977 .await
15978 .map_err(provider_error)?;
15979 Ok(())
15980 }
15981
15982 async fn certify_op_pending_parent(
15983 &mut self,
15984 pending: &N::BlockResponse,
15985 ) -> Result<N::HeaderResponse, PendingFlashblockPollError> {
15986 let pending_header = pending.header();
15987 let pending_number = pending_header.number();
15988 let parent_hash = pending_header.parent_hash();
15989 if pending_number == 0 || parent_hash.is_zero() {
15990 return Err(PendingFlashblockPollError::Integrity(
15991 SubscriberError::Provider(
15992 "OP pending block omitted a certifiable canonical parent".into(),
15993 ),
15994 ));
15995 }
15996 self.record_rpc(
15997 SubscriberRpcCause::CanonicalHeadCertification,
15998 SubscriberRpcMethod::EthGetBlockByHash,
15999 );
16000 self.flashblocks_rpc_metrics.canonical_head_requests = self
16001 .flashblocks_rpc_metrics
16002 .canonical_head_requests
16003 .saturating_add(1);
16004 let parent = self
16005 .flashblocks_state_provider
16006 .as_ref()
16007 .unwrap_or(&self.provider)
16008 .get_block_by_hash(parent_hash)
16009 .await
16010 .map_err(pending_flashblock_request_error)?
16011 .ok_or_else(|| {
16012 PendingFlashblockPollError::Request(SubscriberError::Provider(
16013 "Flashblocks provider returned no exact OP pending parent block".into(),
16014 ))
16015 })?;
16016 let parent_header = parent.header();
16017 if parent_header.hash() != parent_hash
16018 || parent_header.number().checked_add(1) != Some(pending_number)
16019 {
16020 return Err(PendingFlashblockPollError::Integrity(
16021 SubscriberError::Provider(
16022 "OP pending block does not extend its exact certified parent".into(),
16023 ),
16024 ));
16025 }
16026 Ok(parent_header.clone())
16027 }
16028
16029 async fn fetch_op_pending_block(
16030 &mut self,
16031 ) -> Result<Option<N::BlockResponse>, PendingFlashblockPollError> {
16032 self.record_rpc(
16033 SubscriberRpcCause::PendingStateSample,
16034 SubscriberRpcMethod::EthGetBlockByNumber,
16035 );
16036 let state_provider = self
16037 .flashblocks_state_provider
16038 .as_ref()
16039 .unwrap_or(&self.provider);
16040 let value: Option<serde_json::Value> = state_provider
16041 .raw_request(
16042 Cow::Borrowed("eth_getBlockByNumber"),
16043 (BlockNumberOrTag::Pending, true),
16044 )
16045 .await
16046 .map_err(pending_flashblock_request_error)?;
16047 value
16048 .map(normalize_op_pending_block::<N>)
16049 .transpose()
16050 .map_err(PendingFlashblockPollError::Integrity)
16051 }
16052
16053 async fn ensure_chain_id(&mut self) -> Result<u64, SubscriberError> {
16057 if let Some(chain_id) = self.chain_id {
16058 return Ok(chain_id);
16059 }
16060 self.record_rpc(
16061 SubscriberRpcCause::ChainIdentity,
16062 SubscriberRpcMethod::EthChainId,
16063 );
16064 let chain_id = self.provider.get_chain_id().await.map_err(provider_error)?;
16065 self.chain_id = Some(chain_id);
16066 Ok(chain_id)
16067 }
16068
16069 fn validate_flashblocks_setup(&self) -> Result<(), SubscriberError> {
16070 if self.config.preconfirmations == PreconfirmationMode::Disabled {
16071 if self.uses_external_flashblock_updates() {
16072 return Err(SubscriberError::InvalidConfig(
16073 "external Flashblock updates require preconfirmations to be preferred or required",
16074 ));
16075 }
16076 return Ok(());
16077 }
16078 if self.uses_external_flashblock_updates() {
16079 return Ok(());
16080 }
16081 if self.provider_ref.is_none() {
16082 return Err(SubscriberError::InvalidConfig(
16083 "Flashblocks require a stable provider ref from a pinned provider lease",
16084 ));
16085 }
16086 let Some(chain_id) = self.chain_id else {
16087 return Ok(());
16088 };
16089 match flashblocks_adapter(chain_id) {
16090 Some(FlashblocksAdapter::NativeSubscriptions)
16091 if resolve_subscriber_transport(self.mode)? != SubscriberTransport::PubSub
16092 && self.config.preconfirmations == PreconfirmationMode::Required =>
16093 {
16094 return Err(SubscriberError::Unsupported(
16095 "Base Flashblocks require pubsub for newFlashblocks and pendingLogs",
16096 ));
16097 }
16098 Some(FlashblocksAdapter::NativeSubscriptions) => {}
16099 Some(_) => {}
16100 None if self.config.preconfirmations == PreconfirmationMode::Required => {
16101 return Err(SubscriberError::Unsupported(
16102 "Flashblocks are currently implemented for Base and OP chains",
16103 ));
16104 }
16105 None => {}
16106 }
16107 Ok(())
16108 }
16109
16110 pub async fn reconcile_interest_owner(
16124 &mut self,
16125 epoch: &SubscriberOwnerEpoch,
16126 through: BlockRef,
16127 ) -> Result<SubscriberOwnerProgress, SubscriberOwnerError>
16128 where
16129 P: Clone,
16130 {
16131 self.reconcile_interest_owners(std::slice::from_ref(epoch), through)
16132 .await?
16133 .pop()
16134 .ok_or(SubscriberOwnerError::NotStaged)
16135 }
16136
16137 pub async fn reconcile_interest_owners(
16161 &mut self,
16162 epochs: &[SubscriberOwnerEpoch],
16163 through: BlockRef,
16164 ) -> Result<Vec<SubscriberOwnerProgress>, SubscriberOwnerError>
16165 where
16166 P: Clone,
16167 {
16168 if epochs.is_empty() {
16169 return Ok(Vec::new());
16170 }
16171
16172 self.ensure_chain_id().await?;
16173
16174 let mut seen = HashSet::new();
16175 let mut plans = Vec::with_capacity(epochs.len());
16176 for epoch in epochs {
16177 if !seen.insert(epoch.clone()) {
16178 continue;
16179 }
16180 let entry = self
16181 .owned_interests
16182 .iter()
16183 .find(|entry| {
16184 entry.epoch.as_ref() == Some(epoch)
16185 && entry.state == SubscriberOwnerState::Staged
16186 })
16187 .ok_or(SubscriberOwnerError::NotStaged)?;
16188 let position = entry
16189 .progress
16190 .as_ref()
16191 .map(|progress| &progress.through)
16192 .or(entry.baseline.as_ref())
16193 .ok_or(SubscriberOwnerError::MissingBaseline)?;
16194 let baseline = position.number;
16195 if through.number < baseline {
16196 return Err(SubscriberOwnerError::ProgressRegression {
16197 current: baseline,
16198 target: through.number,
16199 });
16200 }
16201 let from_block = baseline
16202 .checked_add(1)
16203 .ok_or(SubscriberOwnerError::PostBlockOverflow(baseline))?;
16204 if through.number == baseline && through.hash != position.hash {
16205 return Err(SubscriberOwnerError::ProgressConflict {
16206 number: baseline,
16207 current_hash: position.hash,
16208 target_hash: through.hash,
16209 });
16210 }
16211 if through.number == from_block
16212 && through
16213 .parent_hash
16214 .is_some_and(|parent| parent != position.hash)
16215 {
16216 return Err(SubscriberOwnerError::ProgressConflict {
16217 number: baseline,
16218 current_hash: position.hash,
16219 target_hash: through.parent_hash.expect("checked as present above"),
16220 });
16221 }
16222 if entry
16223 .interests
16224 .iter()
16225 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
16226 {
16227 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
16228 }
16229 plans.push(SubscriberOwnerReconcilePlan {
16230 epoch: epoch.clone(),
16231 interests: entry.interests.clone(),
16232 retained: *position,
16233 from_block,
16234 });
16235 }
16236
16237 self.ensure_streams().await?;
16240 let provider = self.provider.clone();
16241 let filters = merged_owner_reconcile_filters(&plans, through.number);
16242 let retained = plans.iter().map(|plan| plan.retained).collect();
16243 let target_epochs: HashSet<_> = plans.iter().map(|plan| plan.epoch.clone()).collect();
16244 let fetch = fetch_owner_catchup::<P, N>(
16245 provider,
16246 filters,
16247 retained,
16248 through,
16249 SubscriberOwnerCatchupOptions {
16250 target_preverified: false,
16251 max_logs: self.config.max_pending_records,
16252 max_log_bytes: self.config.max_backfill_log_bytes,
16253 max_requests_in_flight: self.config.max_reconcile_requests_in_flight,
16254 cause: SubscriberRpcCause::OwnerReconcile,
16255 },
16256 Arc::clone(&self.rpc_counters),
16257 );
16258 let SubscriberOwnerCatchup { logs, certified } =
16259 self.drive_reconcile_fetch(fetch, &target_epochs).await?;
16260
16261 let records = logs
16262 .into_iter()
16263 .map(|log| log_input_record(log, InputSource::Backfill))
16264 .collect();
16265 let mut routed_records = Vec::new();
16266 for record in dedupe_records(sort_records(records)).map_err(|error| {
16267 SubscriberError::InvalidBackfill(format!(
16268 "conflicting duplicate owner catch-up record: {error}"
16269 ))
16270 })? {
16271 let block_number = match &record.input {
16272 ReactiveInput::Log(log) => log
16273 .block_number
16274 .expect("bulk catch-up logs were validated before commit"),
16275 _ => unreachable!("bulk owner catch-up contains log records only"),
16276 };
16277 let owners: Vec<SubscriberOwnerEpoch> = plans
16278 .iter()
16279 .filter(|plan| block_number >= plan.from_block)
16280 .filter(|plan| {
16281 plan.interests
16282 .iter()
16283 .any(|interest| interest_matches(interest, &record.input))
16284 })
16285 .map(|plan| plan.epoch.clone())
16286 .collect();
16287 if !owners.is_empty() {
16288 routed_records.push((record, owners));
16289 }
16290 }
16291 self.ensure_pending_record_capacity(
16292 routed_records.len(),
16293 "owner reconciliation historical records",
16294 )?;
16295
16296 self.pending_backfills.retain(|queued| {
16300 queued
16301 .epoch
16302 .as_ref()
16303 .is_none_or(|epoch| !target_epochs.contains(epoch))
16304 });
16305 for (record, owners) in routed_records {
16306 self.enqueue_owner_record_for_owners_unmerged(record, owners);
16307 }
16308 self.promote_reconcile_owner_records(&target_epochs);
16309 self.seed_reconciled_filter_anchors(&plans, certified.number);
16310
16311 let stream_revision = self.stream_revision;
16312 let mut progress = Vec::with_capacity(plans.len());
16313 for plan in plans {
16314 let item = SubscriberOwnerProgress {
16315 owner: plan.epoch.clone(),
16316 through: certified,
16317 };
16318 let entry = self
16319 .owned_interests
16320 .iter_mut()
16321 .find(|entry| entry.epoch.as_ref() == Some(&plan.epoch))
16322 .expect("bulk reconcile holds exclusive access after epoch preflight");
16323 entry.progress = Some(item.clone());
16324 entry.progress_stream_revision = Some(stream_revision);
16325 progress.push(item);
16326 }
16327 Ok(progress)
16328 }
16329
16330 async fn drive_reconcile_fetch<T, F>(
16331 &mut self,
16332 fetch: F,
16333 target_epochs: &HashSet<SubscriberOwnerEpoch>,
16334 ) -> Result<T, SubscriberOwnerError>
16335 where
16336 F: Future<Output = Result<T, SubscriberOwnerError>>,
16337 {
16338 if !matches!(&self.state, AlloySubscriberState::Active(_)) {
16339 return fetch.await;
16340 }
16341 let mut fetch = Box::pin(fetch);
16342 loop {
16343 let event = {
16344 let live = Box::pin(self.next_event());
16345 match select(fetch, live).await {
16346 Either::Left((result, pending_live)) => {
16347 drop(pending_live);
16348 return result;
16349 }
16350 Either::Right((event, pending_fetch)) => {
16351 fetch = pending_fetch;
16352 event
16353 }
16354 }
16355 };
16356 let event = event?.ok_or_else(|| {
16357 SubscriberError::Provider(
16358 "Alloy subscriber streams ended during owner reconcile".to_owned(),
16359 )
16360 })?;
16361 self.buffer_reconcile_event_for_owners(&event, target_epochs);
16362 self.enqueue_event_excluding_owners(event, target_epochs);
16363 self.check_resource_error()?;
16364 }
16365 }
16366
16367 pub async fn next_scoped_batch_or<C, F>(
16386 &mut self,
16387 control: Pin<&mut F>,
16388 ) -> Result<SubscriberDriverPoll<C, N>, SubscriberError>
16389 where
16390 C: Send,
16391 F: Future<Output = C> + Send,
16392 {
16393 let batch = self.next_scoped_batch();
16394 match select(control, batch).await {
16395 Either::Left((control, pending_batch)) => {
16396 drop(pending_batch);
16397 Ok(SubscriberDriverPoll::Control(control))
16398 }
16399 Either::Right((batch, _pending_control)) => batch.map(SubscriberDriverPoll::Batch),
16400 }
16401 }
16402
16403 pub fn next_scoped_batch(&mut self) -> SubscriberNextScopedBatch<'_, N> {
16414 Box::pin(async {
16415 self.check_resource_error()?;
16416 if self.chain_id.is_none()
16417 && (!self.pending_records.is_empty()
16418 || !self.pending_chain_controls.is_empty()
16419 || !self.pending_backfills.is_empty()
16420 || !self.interests.is_empty())
16421 {
16422 self.ensure_chain_id().await?;
16423 }
16424 if let Some(batch) = self.drain_next_scoped_batch() {
16425 return Ok(Some(batch));
16426 }
16427
16428 self.ensure_streams().await?;
16433 self.check_resource_error()?;
16434 if let Some(batch) = self.drain_next_scoped_batch() {
16435 return Ok(Some(batch));
16436 }
16437
16438 self.drain_pending_backfills().await?;
16439 self.check_resource_error()?;
16440 if let Some(batch) = self.drain_next_scoped_batch() {
16441 return Ok(Some(batch));
16442 }
16443
16444 if self.interests.is_empty() {
16445 return Ok(None);
16446 }
16447
16448 loop {
16449 let Some(event) = self.next_event().await? else {
16450 return Ok(None);
16451 };
16452
16453 self.enqueue_event(event);
16454 self.check_resource_error()?;
16455 if let Some(batch) = self.drain_next_scoped_batch() {
16456 return Ok(Some(batch));
16457 }
16458 }
16459 })
16460 }
16461
16462 async fn ensure_streams(&mut self) -> Result<(), SubscriberError> {
16477 if !self.sources_dirty {
16478 return Ok(());
16479 }
16480 if matches!(self.state, AlloySubscriberState::Uninitialized) && self.interests.is_empty() {
16485 self.bump_stream_revision();
16486 self.sources_dirty = false;
16487 return Ok(());
16488 }
16489
16490 let desired = self.stream_sources()?;
16491 let missing: Vec<SubscriberStreamSource> = match &self.state {
16492 AlloySubscriberState::Active(streams) => desired
16493 .iter()
16494 .filter(|source| !streams.contains_source(source))
16495 .cloned()
16496 .collect(),
16497 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => desired.clone(),
16498 };
16499
16500 for source in missing {
16501 let stream = match self.connect_source_stream(source.clone()).await {
16502 Ok(stream) => stream,
16503 Err(error)
16504 if source.is_flashblocks()
16505 && self.config.preconfirmations == PreconfirmationMode::Preferred =>
16506 {
16507 tracing::warn!(
16508 stream = source.label(),
16509 error = %error,
16510 "Flashblocks source unavailable; canonical delivery remains active"
16511 );
16512 if self.config.reconnect.enabled {
16513 self.schedule_flashblock_reconnect(
16514 source,
16515 self.config.reconnect.retry_delay,
16516 );
16517 }
16518 continue;
16519 }
16520 Err(error) => return Err(error),
16521 };
16522 self.install_source_stream(source.clone(), stream);
16526 if self.source_requires_backfill(&source) {
16527 self.queue_source_backfill(source);
16528 }
16529 }
16530
16531 while let Some(source) = self.pending_source_backfills.front().cloned() {
16532 let desired_and_live = desired.iter().any(|item| item.same_key(&source))
16533 && matches!(
16534 &self.state,
16535 AlloySubscriberState::Active(streams) if streams.contains_source(&source)
16536 );
16537 if !desired_and_live {
16538 self.pending_source_backfills.pop_front();
16539 continue;
16540 }
16541
16542 let event = self.backfill_reconnected_source(&source).await?;
16548 self.pending_source_backfills.pop_front();
16549 if let Some(event) = event {
16550 self.enqueue_event(event);
16551 }
16552 }
16553
16554 if let AlloySubscriberState::Active(streams) = &mut self.state {
16555 streams.retain_sources(&desired);
16556 if streams.is_empty() {
16557 self.state = AlloySubscriberState::Empty;
16558 }
16559 }
16560
16561 self.bump_stream_revision();
16562 self.sources_dirty = false;
16563 self.retire_unreferenced_filters();
16564 Ok(())
16565 }
16566
16567 fn install_source_stream(
16568 &mut self,
16569 source: SubscriberStreamSource,
16570 stream: BoxStream<'static, SubscriberEvent<N>>,
16571 ) {
16572 match &mut self.state {
16573 AlloySubscriberState::Active(streams) => {
16574 if streams.contains_source(&source) {
16575 return;
16576 }
16577 streams.push(source, stream);
16578 }
16579 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
16580 let mut streams = SubscriberStreams::new();
16581 streams.push(source, stream);
16582 self.state = AlloySubscriberState::Active(streams);
16583 }
16584 }
16585 self.bump_stream_revision();
16588 }
16589
16590 fn schedule_flashblock_reconnect(
16591 &mut self,
16592 source: SubscriberStreamSource,
16593 first_delay: Duration,
16594 ) {
16595 if self
16596 .pending_flashblock_reconnect_sources
16597 .iter()
16598 .any(|pending| pending.same_key(&source))
16599 {
16600 return;
16601 }
16602 self.pending_flashblock_reconnect_sources
16603 .push(source.clone());
16604 self.pending_flashblock_reconnects
16605 .push(flashblock_reconnect_future(
16606 self.provider.root().clone(),
16607 source,
16608 self.config.max_batch_size,
16609 self.config.reconnect.clone(),
16610 first_delay,
16611 self.config.flashblock_poll_interval,
16612 Arc::clone(&self.rpc_counters),
16613 ));
16614 }
16615
16616 fn reschedule_preferred_flashblock(&mut self, source: SubscriberStreamSource) {
16617 if !self.config.reconnect.enabled {
16618 return;
16619 }
16620 self.schedule_flashblock_reconnect(source, self.config.reconnect.max_delay);
16621 }
16622
16623 fn source_requires_backfill(&self, source: &SubscriberStreamSource) -> bool {
16624 matches!(source, SubscriberStreamSource::PubSubLog { id, .. }
16625 if self.last_seen_log_blocks.contains_key(id))
16626 }
16627
16628 fn queue_source_backfill(&mut self, source: SubscriberStreamSource) {
16629 if !self
16630 .pending_source_backfills
16631 .iter()
16632 .any(|pending| pending.same_key(&source))
16633 {
16634 self.pending_source_backfills.push_back(source);
16635 }
16636 }
16637
16638 async fn drain_pending_backfills(&mut self) -> Result<(), SubscriberError> {
16650 while let Some(queued) = self.pending_backfills.front() {
16651 let epoch = queued.epoch.clone();
16653 let owner = queued.owner.clone();
16654 let owner_exists = match (&epoch, &owner) {
16655 (Some(epoch), _) => self.interest_owner_state(epoch).is_some(),
16656 (None, Some(owner)) => self.owner_interests(owner).is_some(),
16657 (None, None) => true,
16658 };
16659 if !owner_exists {
16660 self.pending_backfills.pop_front();
16661 continue;
16662 }
16663 let filters = queued.filters.clone();
16664 let backfill = queued.backfill;
16665
16666 let to_block = match backfill.end_block() {
16667 Some(to_block) => to_block,
16668 None => {
16669 self.record_rpc(
16670 SubscriberRpcCause::LazyBackfill,
16671 SubscriberRpcMethod::EthBlockNumber,
16672 );
16673 self.provider
16674 .get_block_number()
16675 .await
16676 .map_err(provider_error)?
16677 }
16678 };
16679 if to_block < backfill.start_block() {
16680 let certified = if let Some(retained) = backfill.retained_anchor() {
16685 let actual = fetch_provider_block_ref::<P, N>(
16686 &self.provider,
16687 retained.number,
16688 &self.rpc_counters,
16689 SubscriberRpcCause::LazyBackfill,
16690 )
16691 .await?;
16692 if !block_ref_satisfies_expected(&actual, retained) {
16693 return Err(SubscriberError::InvalidBackfill(format!(
16694 "retained anchor {}:{:?} conflicts with provider block {}:{:?}",
16695 retained.number, retained.hash, actual.number, actual.hash
16696 )));
16697 }
16698 if to_block < retained.number {
16699 return Err(SubscriberError::InvalidBackfill(format!(
16700 "backfill upper bound {to_block} precedes retained anchor {}",
16701 retained.number
16702 )));
16703 }
16704 Some(actual)
16705 } else {
16706 None
16707 };
16708 self.pending_backfills.pop_front();
16709 for filter in &filters {
16710 let source_id = self.log_source_id(filter);
16711 if let Some(certified) = certified {
16712 self.last_seen_log_blocks
16713 .entry(source_id)
16714 .and_modify(|anchor| *anchor = (*anchor).max(certified.number))
16715 .or_insert(certified.number);
16716 }
16717 }
16718 if owner.is_none()
16719 && let Some(certified) = certified
16720 {
16721 self.pending_chain_controls
16722 .push_back(global_backfill_barrier(backfill, certified));
16723 }
16724 if !self.pending_chain_controls.is_empty() {
16725 break;
16726 }
16727 continue;
16728 }
16729
16730 let through = fetch_provider_block_ref::<P, N>(
16731 &self.provider,
16732 to_block,
16733 &self.rpc_counters,
16734 SubscriberRpcCause::LazyBackfill,
16735 )
16736 .await?;
16737 let request_filters =
16738 merged_lazy_backfill_filters(&filters, backfill.start_block(), through.number);
16739 let retained = backfill.retained_anchor().copied().into_iter().collect();
16740 let SubscriberOwnerCatchup {
16741 mut logs,
16742 certified,
16743 } = fetch_owner_catchup::<&P, N>(
16744 &self.provider,
16745 request_filters,
16746 retained,
16747 through,
16748 SubscriberOwnerCatchupOptions {
16749 target_preverified: true,
16750 max_logs: self.config.max_pending_records,
16751 max_log_bytes: self.config.max_backfill_log_bytes,
16752 max_requests_in_flight: self.config.max_reconcile_requests_in_flight,
16753 cause: SubscriberRpcCause::LazyBackfill,
16754 },
16755 Arc::clone(&self.rpc_counters),
16756 )
16757 .await
16758 .map_err(lazy_backfill_error)?;
16759 logs.sort_by_key(|log| {
16760 (
16761 log.block_number.unwrap_or_default(),
16762 log.transaction_index.unwrap_or_default(),
16763 log.log_index.unwrap_or_default(),
16764 )
16765 });
16766 logs.dedup();
16767 self.ensure_pending_record_capacity(logs.len(), "lazy subscriber backfill records")?;
16768
16769 self.pending_backfills.pop_front();
16772 if let Some(epoch) = epoch.as_ref() {
16773 self.enqueue_backfilled_logs(logs, None, Some(epoch), Some(backfill));
16774 } else if let Some(owner) = owner.as_ref() {
16775 self.enqueue_compat_owner_backfilled_logs(logs, owner, backfill);
16776 } else {
16777 self.enqueue_backfilled_logs(logs, None, None, Some(backfill));
16778 self.pending_chain_controls
16779 .push_back(global_backfill_barrier(backfill, certified));
16780 }
16781 for filter in &filters {
16782 let source_id = self.log_source_id(filter);
16783 let anchor = self
16784 .last_seen_log_blocks
16785 .entry(source_id)
16786 .or_insert(certified.number);
16787 *anchor = (*anchor).max(certified.number);
16788 }
16789
16790 if !self.pending_records.is_empty() || !self.pending_chain_controls.is_empty() {
16791 break;
16792 }
16793 }
16794 Ok(())
16795 }
16796
16797 fn stream_sources(&mut self) -> Result<Vec<SubscriberStreamSource>, SubscriberError> {
16798 let sources = match resolve_subscriber_transport(self.mode)? {
16799 SubscriberTransport::PubSub => self.pubsub_stream_sources(),
16800 SubscriberTransport::Polling => self.polling_stream_sources(),
16801 };
16802 #[cfg(feature = "raw-flashblocks-json")]
16803 let sources = {
16804 let mut sources = sources;
16805 if self.external_flashblock_update_channel_opened {
16806 sources.push(SubscriberStreamSource::ExternalFlashblockUpdates);
16807 }
16808 sources
16809 };
16810 Ok(sources)
16811 }
16812
16813 fn pubsub_stream_sources(&mut self) -> Vec<SubscriberStreamSource> {
16814 let mut sources = Vec::new();
16815 let inherited_anchor = self.last_seen_log_blocks.values().copied().min();
16816
16817 for filter in self.log_stream_filters() {
16818 let id = self.log_source_id(&filter);
16819 if let Some(anchor) = inherited_anchor {
16820 self.last_seen_log_blocks.entry(id).or_insert(anchor);
16821 }
16822 sources.push(SubscriberStreamSource::PubSubLog { id, filter });
16823 }
16824
16825 if needs_pending_hash_stream(&self.interests) {
16826 sources.push(SubscriberStreamSource::PubSubPendingHashes);
16827 }
16828
16829 if needs_header_block_stream(&self.interests) {
16830 if !self.uses_external_flashblock_updates()
16831 && self.config.preconfirmations != PreconfirmationMode::Disabled
16832 && self.chain_id.and_then(flashblocks_adapter).is_some()
16833 {
16834 sources.push(SubscriberStreamSource::CanonicalHeadPolling);
16835 } else {
16836 sources.push(SubscriberStreamSource::PubSubBlockHeaders);
16837 }
16838 }
16839
16840 if self.config.preconfirmations != PreconfirmationMode::Disabled
16841 && !self.uses_external_flashblock_updates()
16842 {
16843 match self.chain_id.and_then(flashblocks_adapter) {
16844 Some(FlashblocksAdapter::NativeSubscriptions) => {
16845 sources.push(SubscriberStreamSource::BaseFlashblocks);
16846 for filter in self.log_stream_filters() {
16847 let id = self.log_source_id(&filter);
16848 sources.push(SubscriberStreamSource::BasePendingLog { id, filter });
16849 }
16850 }
16851 Some(FlashblocksAdapter::PendingStatePolling) => {
16852 sources.push(SubscriberStreamSource::OpPendingFlashblocks);
16853 }
16854 None => {}
16855 }
16856 }
16857
16858 sources
16859 }
16860
16861 fn polling_stream_sources(&self) -> Vec<SubscriberStreamSource> {
16862 let mut sources = Vec::new();
16863
16864 for filter in self.log_stream_filters() {
16865 sources.push(SubscriberStreamSource::PollingLog { filter });
16866 }
16867
16868 if needs_pending_hash_stream(&self.interests) {
16869 sources.push(SubscriberStreamSource::PollingPendingHashes);
16870 }
16871
16872 if self.config.preconfirmations != PreconfirmationMode::Disabled
16873 && !self.uses_external_flashblock_updates()
16874 && self.chain_id.and_then(flashblocks_adapter)
16875 == Some(FlashblocksAdapter::PendingStatePolling)
16876 {
16877 sources.push(SubscriberStreamSource::OpPendingFlashblocks);
16878 }
16879
16880 sources
16881 }
16882
16883 fn log_source_id(&mut self, filter: &Filter) -> usize {
16884 if let Some(id) = self.log_source_ids.get(filter) {
16885 return *id;
16886 }
16887
16888 let id = self.next_log_source_id;
16889 self.next_log_source_id = self.next_log_source_id.saturating_add(1);
16890 self.log_source_ids.insert(filter.clone(), id);
16891 id
16892 }
16893
16894 async fn connect_source_stream(
16895 &mut self,
16896 source: SubscriberStreamSource,
16897 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16898 match source {
16899 SubscriberStreamSource::PubSubLog { id, filter } => {
16900 self.connect_pubsub_log_stream(id, filter).await
16901 }
16902 SubscriberStreamSource::BasePendingLog { id, filter } => {
16903 self.connect_base_pending_log_stream(id, filter).await
16904 }
16905 SubscriberStreamSource::BaseFlashblocks => self.connect_base_flashblock_stream().await,
16906 SubscriberStreamSource::OpPendingFlashblocks => {
16907 self.connect_op_flashblock_tick_stream()
16908 }
16909 SubscriberStreamSource::CanonicalHeadPolling => {
16910 self.connect_canonical_head_tick_stream()
16911 }
16912 SubscriberStreamSource::PubSubPendingHashes => {
16913 self.connect_pubsub_pending_hash_stream().await
16914 }
16915 SubscriberStreamSource::PubSubBlockHeaders => {
16916 self.connect_pubsub_block_header_stream().await
16917 }
16918 SubscriberStreamSource::PollingLog { filter } => {
16919 self.connect_polling_log_stream(filter).await
16920 }
16921 SubscriberStreamSource::PollingPendingHashes => {
16922 self.connect_polling_pending_hash_stream().await
16923 }
16924 #[cfg(feature = "raw-flashblocks-json")]
16925 SubscriberStreamSource::ExternalFlashblockUpdates => {
16926 let receiver = self.external_flashblock_updates.take().ok_or_else(|| {
16927 SubscriberError::Provider(
16928 "external Flashblock update channel receiver is unavailable".into(),
16929 )
16930 })?;
16931 let updates = stream::unfold(receiver, |mut receiver| async move {
16932 receiver
16933 .recv()
16934 .await
16935 .map(|update| (SubscriberEvent::ExternalFlashblockUpdate(update), receiver))
16936 });
16937 Ok(stream_with_termination(
16938 updates,
16939 SubscriberStreamSource::ExternalFlashblockUpdates,
16940 ))
16941 }
16942 }
16943 }
16944
16945 async fn connect_pubsub_log_stream(
16946 &mut self,
16947 id: usize,
16948 filter: Filter,
16949 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16950 #[cfg(feature = "reactive-ws")]
16951 {
16952 let source = SubscriberStreamSource::PubSubLog {
16953 id,
16954 filter: filter.clone(),
16955 };
16956 self.record_rpc(
16957 SubscriberRpcCause::StreamSubscription,
16958 SubscriberRpcMethod::EthSubscribe,
16959 );
16960 let subscription = self
16961 .provider
16962 .subscribe_logs(&filter)
16963 .channel_size(self.log_channel_size())
16964 .await
16965 .map_err(provider_error)?;
16966 Ok(gap_observing_stream(
16967 subscription,
16968 source,
16969 Arc::clone(&self.gap_counters),
16970 move |log| SubscriberEvent::Log { source_id: id, log },
16971 ))
16972 }
16973
16974 #[cfg(not(feature = "reactive-ws"))]
16975 {
16976 let _ = (id, filter);
16977 Err(SubscriberError::Unsupported(
16978 "AlloySubscriber pubsub mode requires the reactive-ws feature",
16979 ))
16980 }
16981 }
16982
16983 async fn connect_base_pending_log_stream(
16984 &mut self,
16985 id: usize,
16986 filter: Filter,
16987 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16988 #[cfg(feature = "reactive-ws")]
16989 {
16990 let source = SubscriberStreamSource::BasePendingLog {
16991 id,
16992 filter: filter.clone(),
16993 };
16994 let params = base_pending_log_filter(&filter)?;
16995 self.record_rpc(
16996 SubscriberRpcCause::StreamSubscription,
16997 SubscriberRpcMethod::EthSubscribe,
16998 );
16999 let subscription = self
17000 .provider
17001 .subscribe::<_, Log>(("pendingLogs", params))
17002 .channel_size(self.log_channel_size())
17003 .await
17004 .map_err(provider_error)?;
17005 Ok(gap_observing_stream(
17006 subscription,
17007 source,
17008 Arc::clone(&self.gap_counters),
17009 move |log| SubscriberEvent::BasePendingLogTimed {
17010 source_id: id,
17011 log,
17012 timing: FlashblockIngressTiming::new(Instant::now()),
17013 },
17014 ))
17015 }
17016
17017 #[cfg(not(feature = "reactive-ws"))]
17018 {
17019 let _ = (id, filter);
17020 Err(SubscriberError::Unsupported(
17021 "Base Flashblocks require the reactive-ws feature",
17022 ))
17023 }
17024 }
17025
17026 async fn connect_base_flashblock_stream(
17027 &mut self,
17028 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17029 #[cfg(feature = "reactive-ws")]
17030 {
17031 self.record_rpc(
17032 SubscriberRpcCause::StreamSubscription,
17033 SubscriberRpcMethod::EthSubscribe,
17034 );
17035 let stream = self
17036 .provider
17037 .subscribe::<_, BaseFlashblockWirePayload>(("newFlashblocks",))
17038 .channel_size(self.config.max_batch_size.max(1))
17039 .await
17040 .map_err(provider_error)?
17041 .into_stream()
17042 .map(|payload| SubscriberEvent::BaseFlashblockTimed {
17043 payload,
17044 timing: FlashblockIngressTiming::new(Instant::now()),
17045 });
17046 Ok(stream_with_termination(
17047 stream,
17048 SubscriberStreamSource::BaseFlashblocks,
17049 ))
17050 }
17051
17052 #[cfg(not(feature = "reactive-ws"))]
17053 {
17054 Err(SubscriberError::Unsupported(
17055 "Base Flashblocks require the reactive-ws feature",
17056 ))
17057 }
17058 }
17059
17060 fn connect_canonical_head_tick_stream(
17061 &self,
17062 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17063 let mut interval = tokio::time::interval(self.config.canonical_head_poll_interval);
17064 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
17065 let stream = stream::unfold(interval, |mut interval| async move {
17066 interval.tick().await;
17067 Some((SubscriberEvent::CanonicalHeadTick, interval))
17068 });
17069 Ok(stream_with_termination(
17070 stream,
17071 SubscriberStreamSource::CanonicalHeadPolling,
17072 ))
17073 }
17074
17075 fn connect_op_flashblock_tick_stream(
17076 &self,
17077 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17078 let first_tick = tokio::time::Instant::now() + self.config.flashblock_poll_interval;
17079 let mut interval =
17080 tokio::time::interval_at(first_tick, self.config.flashblock_poll_interval);
17081 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
17082 let stream = stream::unfold(interval, |mut interval| async move {
17083 interval.tick().await;
17084 Some((
17085 SubscriberEvent::OpFlashblockTickTimed(
17086 FlashblockIngressTiming::new(Instant::now()),
17087 ),
17088 interval,
17089 ))
17090 });
17091 Ok(stream_with_termination(
17092 stream,
17093 SubscriberStreamSource::OpPendingFlashblocks,
17094 ))
17095 }
17096
17097 async fn connect_pubsub_pending_hash_stream(
17098 &mut self,
17099 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17100 #[cfg(feature = "reactive-ws")]
17101 {
17102 self.record_rpc(
17103 SubscriberRpcCause::StreamSubscription,
17104 SubscriberRpcMethod::EthSubscribe,
17105 );
17106 let stream = self
17107 .provider
17108 .subscribe_pending_transactions()
17109 .channel_size(self.config.max_batch_size.max(1))
17110 .await
17111 .map_err(provider_error)?
17112 .into_stream()
17113 .map(SubscriberEvent::PendingHash);
17114 Ok(stream_with_termination(
17115 stream,
17116 SubscriberStreamSource::PubSubPendingHashes,
17117 ))
17118 }
17119
17120 #[cfg(not(feature = "reactive-ws"))]
17121 {
17122 Err(SubscriberError::Unsupported(
17123 "AlloySubscriber pubsub mode requires the reactive-ws feature",
17124 ))
17125 }
17126 }
17127
17128 async fn connect_pubsub_block_header_stream(
17129 &mut self,
17130 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17131 #[cfg(feature = "reactive-ws")]
17132 {
17133 self.record_rpc(
17134 SubscriberRpcCause::StreamSubscription,
17135 SubscriberRpcMethod::EthSubscribe,
17136 );
17137 let subscription = self
17138 .provider
17139 .subscribe_blocks()
17140 .channel_size(self.config.max_batch_size.max(1))
17141 .await
17142 .map_err(provider_error)?;
17143 Ok(gap_observing_stream(
17144 subscription,
17145 SubscriberStreamSource::PubSubBlockHeaders,
17146 Arc::clone(&self.gap_counters),
17147 SubscriberEvent::BlockHeader,
17148 ))
17149 }
17150
17151 #[cfg(not(feature = "reactive-ws"))]
17152 {
17153 Err(SubscriberError::Unsupported(
17154 "AlloySubscriber pubsub mode requires the reactive-ws feature",
17155 ))
17156 }
17157 }
17158
17159 async fn connect_polling_log_stream(
17160 &mut self,
17161 filter: Filter,
17162 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17163 #[cfg(feature = "reactive-polling")]
17164 {
17165 let source = SubscriberStreamSource::PollingLog {
17166 filter: filter.clone(),
17167 };
17168 self.record_rpc(
17169 SubscriberRpcCause::StreamSubscription,
17170 SubscriberRpcMethod::EthSubscribe,
17171 );
17172 let stream = self
17173 .provider
17174 .watch_logs(&filter)
17175 .await
17176 .map_err(provider_error)?
17177 .with_channel_size(self.config.max_batch_size.max(1))
17178 .into_stream()
17179 .map(SubscriberEvent::Logs);
17180 Ok(stream_with_termination(stream, source))
17181 }
17182
17183 #[cfg(not(feature = "reactive-polling"))]
17184 {
17185 let _ = filter;
17186 Err(SubscriberError::Unsupported(
17187 "AlloySubscriber polling mode requires the reactive-polling feature",
17188 ))
17189 }
17190 }
17191
17192 async fn connect_polling_pending_hash_stream(
17193 &mut self,
17194 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
17195 #[cfg(feature = "reactive-polling")]
17196 {
17197 self.record_rpc(
17198 SubscriberRpcCause::StreamSubscription,
17199 SubscriberRpcMethod::EthSubscribe,
17200 );
17201 let stream = self
17202 .provider
17203 .watch_pending_transactions()
17204 .await
17205 .map_err(provider_error)?
17206 .with_channel_size(self.config.max_batch_size.max(1))
17207 .into_stream()
17208 .map(SubscriberEvent::PendingHashes);
17209 Ok(stream_with_termination(
17210 stream,
17211 SubscriberStreamSource::PollingPendingHashes,
17212 ))
17213 }
17214
17215 #[cfg(not(feature = "reactive-polling"))]
17216 {
17217 Err(SubscriberError::Unsupported(
17218 "AlloySubscriber polling mode requires the reactive-polling feature",
17219 ))
17220 }
17221 }
17222
17223 async fn next_event(&mut self) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17224 loop {
17225 let ready = match &mut self.state {
17226 AlloySubscriberState::Active(streams)
17227 if !self.pending_flashblock_reconnects.is_empty() =>
17228 {
17229 let stream_event = Box::pin(streams.next());
17230 let reconnect = Box::pin(self.pending_flashblock_reconnects.next());
17231 match select(reconnect, stream_event).await {
17232 Either::Left((reconnect, pending_event)) => {
17233 drop(pending_event);
17234 let Some((source, result)) = reconnect else {
17235 continue;
17236 };
17237 SubscriberReady::FlashblockReconnect(source, result)
17238 }
17239 Either::Right((event, pending_reconnect)) => {
17240 drop(pending_reconnect);
17241 SubscriberReady::Event(event)
17242 }
17243 }
17244 }
17245 AlloySubscriberState::Active(streams) => {
17246 SubscriberReady::Event(streams.next().await)
17247 }
17248 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty
17249 if !self.pending_flashblock_reconnects.is_empty() =>
17250 {
17251 let Some((source, result)) = self.pending_flashblock_reconnects.next().await
17252 else {
17253 continue;
17254 };
17255 SubscriberReady::FlashblockReconnect(source, result)
17256 }
17257 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
17258 return Ok(None);
17259 }
17260 };
17261
17262 let event = match ready {
17263 SubscriberReady::Event(event) => event,
17264 SubscriberReady::FlashblockReconnect(source, result) => {
17265 self.pending_flashblock_reconnect_sources
17266 .retain(|pending| !pending.same_key(&source));
17267 match result {
17268 Ok(stream) => {
17269 self.install_source_stream(source, stream);
17270 }
17271 Err(error)
17272 if self.config.preconfirmations == PreconfirmationMode::Preferred =>
17273 {
17274 tracing::warn!(
17275 stream = source.label(),
17276 error = %error,
17277 "Flashblocks reconnect window exhausted; canonical delivery remains active"
17278 );
17279 self.reschedule_preferred_flashblock(source);
17280 }
17281 Err(error) => return Err(error),
17282 }
17283 continue;
17284 }
17285 };
17286
17287 let Some(event) = event else {
17288 return Err(SubscriberError::Provider(
17289 "Alloy subscriber streams terminated before the subscriber was stopped"
17290 .to_owned(),
17291 ));
17292 };
17293
17294 match event {
17295 SubscriberEvent::StreamTerminated(source) => {
17296 if source.is_external_flashblocks() {
17297 #[cfg(feature = "raw-flashblocks-json")]
17298 {
17299 self.external_flashblock_update_channel_opened = false;
17300 }
17301 if let AlloySubscriberState::Active(streams) = &mut self.state {
17302 streams
17303 .entries
17304 .retain(|entry| !entry.source.is_external_flashblocks());
17305 streams.normalize_next_index();
17306 }
17307 self.invalidate_preconfirmation_snapshot();
17308 if self.config.preconfirmations == PreconfirmationMode::Required {
17309 return Err(SubscriberError::Provider(
17310 "required external Flashblock update channel closed".into(),
17311 ));
17312 }
17313 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
17314 }
17315 if source.is_flashblocks() {
17319 self.invalidate_flashblock_generation();
17320 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
17321 }
17322 self.sources_dirty = true;
17323 self.bump_stream_revision();
17324 if let Some(backfill_event) = self.reconnect_source_stream(source).await? {
17325 self.sources_dirty = false;
17326 if let Some(backfill_event) =
17327 self.normalize_flashblock_event(backfill_event).await?
17328 {
17329 self.verify_event_log_blocks(&backfill_event).await?;
17330 return Ok(Some(backfill_event));
17331 }
17332 }
17333 self.sources_dirty = false;
17334 }
17335 SubscriberEvent::StreamGap { source, gap } => {
17336 if let Some(backfill_event) = self.recover_stream_gap(&source, gap).await? {
17337 self.verify_event_log_blocks(&backfill_event).await?;
17338 return Ok(Some(backfill_event));
17339 }
17340 }
17341 event => {
17342 let Some(event) = self.normalize_flashblock_event(event).await? else {
17343 continue;
17344 };
17345 self.verify_event_log_blocks(&event).await?;
17346 return Ok(Some(event));
17347 }
17348 }
17349 }
17350 }
17351
17352 fn invalidate_flashblock_generation(&mut self) {
17353 self.pending_records
17354 .retain(|record| record.scope != SubscriberInputScope::Preconfirmed);
17355 self.pending_preconfirmation_invalidation = true;
17356 self.reset_flashblock_tracking();
17357 if let Some(provider) = self.provider_ref.as_mut() {
17358 provider.generation = provider.generation.saturating_add(1);
17359 }
17360 if let AlloySubscriberState::Active(streams) = &mut self.state {
17361 streams
17362 .entries
17363 .retain(|entry| !entry.source.is_flashblocks());
17364 streams.normalize_next_index();
17365 }
17366 let reconnect_sources = self
17367 .stream_sources()
17368 .unwrap_or_default()
17369 .into_iter()
17370 .filter(SubscriberStreamSource::is_flashblocks)
17371 .collect::<Vec<_>>();
17372 self.pending_flashblock_reconnects.clear();
17373 self.pending_flashblock_reconnect_sources.clear();
17374 if self.config.preconfirmations == PreconfirmationMode::Required
17375 || self.config.reconnect.enabled
17376 {
17377 for source in reconnect_sources {
17378 self.schedule_flashblock_reconnect(source, self.config.reconnect.initial_delay);
17379 }
17380 }
17381 self.sources_dirty = false;
17382 self.bump_stream_revision();
17383 }
17384
17385 async fn normalize_flashblock_event(
17386 &mut self,
17387 event: SubscriberEvent<N>,
17388 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17389 let event = match event {
17390 SubscriberEvent::BasePendingLog { source_id, log } => {
17391 SubscriberEvent::BasePendingLogTimed {
17392 source_id,
17393 log,
17394 timing: FlashblockIngressTiming::new(Instant::now()),
17395 }
17396 }
17397 SubscriberEvent::BaseFlashblock(payload) => SubscriberEvent::BaseFlashblockTimed {
17398 payload,
17399 timing: FlashblockIngressTiming::new(Instant::now()),
17400 },
17401 SubscriberEvent::OpFlashblockTick => {
17402 SubscriberEvent::OpFlashblockTickTimed(FlashblockIngressTiming::new(Instant::now()))
17403 }
17404 event => event,
17405 };
17406 match event {
17407 #[cfg(feature = "raw-flashblocks-json")]
17408 SubscriberEvent::ExternalFlashblockUpdate(queued) => {
17409 let provider = queued.update.provider().clone();
17410 match self.ingest_flashblock_update_with_ingress(queued.update, queued.timing) {
17411 Ok(()) => {
17412 let _ = queued.acknowledgement.send(Ok(()));
17413 Ok(Some(SubscriberEvent::FlashblockObserved))
17414 }
17415 Err(error)
17416 if self.config.preconfirmations == PreconfirmationMode::Preferred =>
17417 {
17418 let recoverable_capacity =
17419 matches!(error, SubscriberError::ResourceExhausted(_));
17420 if !recoverable_capacity
17421 && let Some(configured) = self.external_flashblocks_provider.as_mut()
17422 && configured.endpoint == provider.endpoint
17423 {
17424 self.rejected_external_flashblock_generation = Some(
17425 self.rejected_external_flashblock_generation
17426 .map_or(provider.generation, |rejected| {
17427 rejected.max(provider.generation)
17428 }),
17429 );
17430 configured.generation = configured
17431 .generation
17432 .max(provider.generation.saturating_add(1));
17433 }
17434 self.invalidate_preconfirmation_snapshot();
17435 self.last_external_flashblock_snapshot = None;
17436 let _ = queued
17437 .acknowledgement
17438 .send(Err(FlashblockUpdateChannelError::Rejected));
17439 tracing::warn!(
17440 provider = %provider.endpoint,
17441 generation = provider.generation,
17442 error = %error,
17443 "external Flashblock update rejected; canonical delivery remains active"
17444 );
17445 Ok(Some(SubscriberEvent::FlashblockInvalidated))
17446 }
17447 Err(error) => {
17448 let _ = queued
17449 .acknowledgement
17450 .send(Err(FlashblockUpdateChannelError::Rejected));
17451 Err(error)
17452 }
17453 }
17454 }
17455 SubscriberEvent::BasePendingLogTimed {
17456 source_id,
17457 log,
17458 timing,
17459 } => {
17460 let block_number = log.block_number.ok_or_else(|| {
17461 SubscriberError::Provider(
17462 "pendingLogs item is missing its pending block number".into(),
17463 )
17464 })?;
17465 let transaction_hash = log.transaction_hash.ok_or_else(|| {
17466 SubscriberError::Provider(
17467 "pendingLogs item is missing its transaction hash".into(),
17468 )
17469 })?;
17470 let matching = self.latest_preconfirmation.as_ref().filter(|flashblock| {
17471 flashblock.block_number == block_number
17472 && flashblock.contains_transaction(&transaction_hash)
17473 });
17474 let Some(flashblock) = matching.cloned() else {
17475 if self
17476 .latest_preconfirmation
17477 .as_ref()
17478 .is_some_and(|latest| block_number < latest.block_number)
17479 {
17480 return Ok(None);
17481 }
17482 if self.unmatched_pending_logs.len() >= self.config.max_pending_records {
17483 return Err(SubscriberError::ResourceExhausted(
17484 "unmatched pendingLogs exceeded max_pending_records".into(),
17485 ));
17486 }
17487 self.unmatched_pending_logs
17488 .push_back((source_id, log, timing));
17489 return Ok(None);
17490 };
17491 let logs = self.filter_preconfirmed_logs(&flashblock, vec![log])?;
17492 Ok(Some(if logs.is_empty() {
17493 SubscriberEvent::FlashblockObserved
17494 } else {
17495 SubscriberEvent::PreconfirmedLogs {
17496 flashblock,
17497 logs,
17498 timing,
17499 }
17500 }))
17501 }
17502 SubscriberEvent::BaseFlashblockTimed {
17503 payload,
17504 timing: source_timing,
17505 } => {
17506 let (flashblock, recover_pending_snapshot) =
17507 self.accept_base_flashblock(payload)?;
17508 if std::mem::take(&mut self.sealed_block_pending_certification)
17509 && let Some(header_event) =
17510 self.certify_canonical_head_on_sealed_block().await?
17511 {
17512 self.enqueue_event(header_event);
17515 }
17516 let mut logs = Vec::new();
17517 let mut retained = VecDeque::new();
17518 let mut timing = source_timing;
17519 while let Some((source_id, log, log_timing)) =
17520 self.unmatched_pending_logs.pop_front()
17521 {
17522 let transaction_hash = log.transaction_hash;
17523 if log.block_number == Some(flashblock.block_number)
17524 && transaction_hash
17525 .as_ref()
17526 .is_some_and(|hash| flashblock.contains_transaction(hash))
17527 {
17528 let _ = source_id;
17529 timing = timing.earliest(log_timing);
17530 logs.push(log);
17531 } else if log
17532 .block_number
17533 .is_some_and(|number| number >= flashblock.block_number)
17534 {
17535 retained.push_back((source_id, log, log_timing));
17536 } else {
17537 }
17540 }
17541 self.unmatched_pending_logs = retained;
17542
17543 let indexed_recovery = recover_pending_snapshot.then(|| {
17544 let payload_id = flashblock
17545 .payload_id
17546 .expect("indexed recovery carries a payload id");
17547 let index = flashblock.index.expect("indexed recovery carries an index");
17548 let last_diff = self
17549 .base_flashblock_transactions
17550 .as_ref()
17551 .filter(|(known_payload, known_index, _, _)| {
17552 *known_payload == payload_id && *known_index == index
17553 })
17554 .map(|(_, _, _, last_diff)| last_diff.clone())
17555 .unwrap_or_default();
17556 (payload_id, index, last_diff)
17557 });
17558 if recover_pending_snapshot {
17559 if let Some(event) = self
17560 .fetch_pending_flashblock_with_timing(indexed_recovery, timing)
17561 .await
17562 .map_err(PendingFlashblockPollError::into_subscriber)?
17563 {
17564 return Ok(Some(event));
17565 }
17566 self.invalidate_flashblock_generation();
17567 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
17568 }
17569 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
17570 Ok(Some(if logs.is_empty() {
17571 SubscriberEvent::FlashblockObserved
17572 } else {
17573 SubscriberEvent::PreconfirmedLogs {
17574 flashblock,
17575 logs,
17576 timing,
17577 }
17578 }))
17579 }
17580 SubscriberEvent::OpFlashblockTickTimed(timing) => {
17581 self.poll_op_pending_flashblock(timing).await
17582 }
17583 SubscriberEvent::CanonicalHeadTick => {
17584 if self.canonical_head_certification_is_current() {
17585 self.flashblocks_rpc_metrics.suppressed_canonical_head_polls = self
17588 .flashblocks_rpc_metrics
17589 .suppressed_canonical_head_polls
17590 .saturating_add(1);
17591 Ok(None)
17592 } else {
17593 self.fetch_certified_canonical_head().await
17594 }
17595 }
17596 SubscriberEvent::PreconfirmedLogs {
17597 flashblock,
17598 logs,
17599 timing,
17600 } => {
17601 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
17602 Ok(Some(if logs.is_empty() {
17603 SubscriberEvent::FlashblockObserved
17604 } else {
17605 SubscriberEvent::PreconfirmedLogs {
17606 flashblock,
17607 logs,
17608 timing,
17609 }
17610 }))
17611 }
17612 SubscriberEvent::FlashblockObserved => Ok(None),
17613 SubscriberEvent::BasePendingLog { .. }
17614 | SubscriberEvent::BaseFlashblock(_)
17615 | SubscriberEvent::OpFlashblockTick => unreachable!("normalized above"),
17616 event => Ok(Some(event)),
17617 }
17618 }
17619
17620 fn canonical_head_certification_is_current(&self) -> bool {
17622 self.last_canonical_head_certification
17623 .is_some_and(|at| at.elapsed() < self.config.canonical_head_poll_interval)
17624 }
17625
17626 async fn certify_canonical_head_on_sealed_block(
17634 &mut self,
17635 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17636 if !needs_header_block_stream(&self.interests) {
17637 return Ok(None);
17638 }
17639 if self.canonical_head_certification_is_current() {
17640 return Ok(None);
17641 }
17642 self.fetch_certified_canonical_head().await
17643 }
17644
17645 async fn fetch_certified_canonical_head(
17646 &mut self,
17647 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17648 self.last_canonical_head_certification = Some(Instant::now());
17649 tokio::time::timeout(
17650 self.config.canonical_head_request_timeout,
17651 self.fetch_certified_canonical_head_inner(),
17652 )
17653 .await
17654 .map_err(|_| {
17655 SubscriberError::Provider(format!(
17656 "canonical head certification timed out after {:?}",
17657 self.config.canonical_head_request_timeout
17658 ))
17659 })?
17660 }
17661
17662 async fn fetch_certified_canonical_head_inner(
17663 &mut self,
17664 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17665 if self.chain_id.and_then(flashblocks_adapter)
17666 == Some(FlashblocksAdapter::PendingStatePolling)
17667 {
17668 if !self.reserve_flashblock_rpc_methods(2) {
17669 return Ok(None);
17670 }
17671 self.flashblocks_rpc_metrics.pending_block_requests = self
17672 .flashblocks_rpc_metrics
17673 .pending_block_requests
17674 .saturating_add(1);
17675 let pending = self
17676 .fetch_op_pending_block()
17677 .await
17678 .map_err(PendingFlashblockPollError::into_subscriber)?
17679 .ok_or_else(|| {
17680 SubscriberError::Provider(
17681 "provider returned no OP pending block while certifying its parent".into(),
17682 )
17683 })?;
17684 let header = self
17685 .certify_op_pending_parent(&pending)
17686 .await
17687 .map_err(PendingFlashblockPollError::into_subscriber)?;
17688 let certified = BlockRef {
17689 number: header.number(),
17690 hash: header.hash(),
17691 parent_hash: Some(header.parent_hash()),
17692 timestamp: Some(header.timestamp()),
17693 };
17694 if self.last_certified_canonical_head.as_ref() == Some(&certified) {
17695 return Ok(None);
17696 }
17697 self.last_certified_canonical_head = Some(certified);
17698 return Ok(Some(SubscriberEvent::BlockHeader(header)));
17699 }
17700 self.record_rpc(
17701 SubscriberRpcCause::CanonicalHeadCertification,
17702 SubscriberRpcMethod::EthGetBlockByNumber,
17703 );
17704 self.flashblocks_rpc_metrics.canonical_head_requests = self
17705 .flashblocks_rpc_metrics
17706 .canonical_head_requests
17707 .saturating_add(1);
17708 let block = self
17709 .provider
17710 .get_block_by_number(BlockNumberOrTag::Latest)
17711 .await
17712 .map_err(provider_error)?
17713 .ok_or_else(|| {
17714 SubscriberError::Provider(
17715 "provider returned no latest block while certifying canonical head".into(),
17716 )
17717 })?;
17718 let header = block.header();
17719 if header.hash().is_zero() {
17720 return Err(SubscriberError::Provider(
17721 "provider returned a placeholder hash for the latest canonical head".into(),
17722 ));
17723 }
17724 let certified = BlockRef {
17725 number: header.number(),
17726 hash: header.hash(),
17727 parent_hash: Some(header.parent_hash()),
17728 timestamp: Some(header.timestamp()),
17729 };
17730 if self.last_certified_canonical_head.as_ref() == Some(&certified) {
17731 return Ok(None);
17732 }
17733 self.last_certified_canonical_head = Some(certified);
17734 Ok(Some(SubscriberEvent::BlockHeader(header.clone())))
17735 }
17736
17737 fn accept_base_flashblock(
17738 &mut self,
17739 payload: BaseFlashblockWirePayload,
17740 ) -> Result<(FlashblockRef, bool), SubscriberError> {
17741 let provider = self.provider_ref.clone().ok_or({
17742 SubscriberError::InvalidConfig(
17743 "Flashblocks require a stable provider ref from a pinned provider lease",
17744 )
17745 })?;
17746
17747 let (flashblock, recover_pending_snapshot) = match payload {
17748 BaseFlashblockWirePayload::Indexed(payload) => {
17749 if payload.index == 0 {
17750 let base = payload.base.clone().ok_or_else(|| {
17751 SubscriberError::Provider(
17752 "indexed newFlashblocks item zero omitted its base header".into(),
17753 )
17754 })?;
17755 if self
17759 .base_flashblock_header
17760 .as_ref()
17761 .is_none_or(|(known, _)| *known != payload.payload_id)
17762 {
17763 self.sealed_block_pending_certification = true;
17764 }
17765 self.base_flashblock_header = Some((payload.payload_id, base));
17766 }
17767
17768 let base = self
17769 .base_flashblock_header
17770 .as_ref()
17771 .filter(|(payload_id, _)| *payload_id == payload.payload_id)
17772 .map(|(_, base)| base);
17773 let block_number = base.map(|base| base.block_number).or_else(|| {
17774 payload
17775 .metadata
17776 .as_ref()
17777 .map(|metadata| metadata.block_number)
17778 });
17779 let block_number = block_number.ok_or_else(|| {
17780 SubscriberError::Provider(
17781 "indexed newFlashblocks payload omitted both base and metadata block number"
17782 .into(),
17783 )
17784 })?;
17785 let diff_transactions = flashblock_transaction_hashes(&payload.diff.transactions)?;
17786 let transaction_hashes = match self.base_flashblock_transactions.as_mut() {
17787 Some((known_payload, known_index, transactions, last_diff))
17788 if *known_payload == payload.payload_id =>
17789 {
17790 if payload.index < *known_index {
17791 return self
17792 .latest_preconfirmation
17793 .clone()
17794 .map(|flashblock| (flashblock, false))
17795 .ok_or_else(|| {
17796 SubscriberError::Provider(
17797 "regressive indexed Flashblock arrived without an active snapshot"
17798 .into(),
17799 )
17800 });
17801 }
17802 if payload.index == *known_index {
17803 if *last_diff != diff_transactions {
17804 return Err(SubscriberError::Provider(
17805 "conflicting duplicate indexed Flashblock payload".into(),
17806 ));
17807 }
17808 } else {
17809 if diff_transactions
17810 .iter()
17811 .any(|hash| transactions.contains(hash))
17812 {
17813 return Err(SubscriberError::Provider(
17814 "indexed Flashblock repeated a transaction from an earlier diff"
17815 .into(),
17816 ));
17817 }
17818 transactions.extend(diff_transactions.iter().copied());
17819 *known_index = payload.index;
17820 *last_diff = diff_transactions;
17821 }
17822 transactions.clone()
17823 }
17824 _ => {
17825 self.base_flashblock_transactions = Some((
17826 payload.payload_id,
17827 payload.index,
17828 diff_transactions.clone(),
17829 diff_transactions.clone(),
17830 ));
17831 diff_transactions
17832 }
17833 };
17834 let partial_block_hash = non_placeholder_hash(payload.diff.block_hash);
17835 let transactions_root = payload
17836 .diff
17837 .transactions_root
17838 .and_then(non_placeholder_hash);
17839 let parent_hash = base.and_then(|base| non_placeholder_hash(base.parent_hash));
17840 let state_root = non_placeholder_hash(payload.diff.state_root);
17841 let timestamp = base.map(|base| base.timestamp);
17842 let base_fee_per_gas = base.and_then(|base| base.base_fee_per_gas);
17843 let beneficiary = base.and_then(|base| base.beneficiary);
17844 let prevrandao = base
17845 .and_then(|base| base.prevrandao)
17846 .and_then(non_placeholder_hash);
17847 let gas_limit = base.and_then(|base| base.gas_limit);
17848 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
17849 provider: &provider,
17850 payload_id: Some(payload.payload_id),
17851 index: Some(payload.index),
17852 block_number,
17853 partial_block_hash,
17854 parent_hash,
17855 state_root,
17856 transactions_root,
17857 transaction_hashes: &transaction_hashes,
17858 timestamp,
17859 base_fee_per_gas,
17860 beneficiary,
17861 prevrandao,
17862 gas_limit,
17863 });
17864 let flashblock = FlashblockRef {
17865 provider,
17866 payload_id: Some(payload.payload_id),
17867 index: Some(payload.index),
17868 block_number,
17869 content_hash,
17870 partial_block_hash,
17871 parent_hash,
17872 state_root,
17873 transactions_root,
17874 transaction_hashes,
17875 timestamp,
17876 base_fee_per_gas,
17877 beneficiary,
17878 prevrandao,
17879 gas_limit,
17880 };
17881 if let Some(previous) = self.latest_preconfirmation.as_ref()
17882 && previous.same_payload(&flashblock)
17883 && previous.index == flashblock.index
17884 && previous.content_hash != flashblock.content_hash
17885 {
17886 return Err(SubscriberError::Provider(
17887 "conflicting duplicate indexed Flashblock content".into(),
17888 ));
17889 }
17890 let recover = match self.latest_preconfirmation.as_ref() {
17891 Some(previous) if previous.same_payload(&flashblock) => {
17892 if let (Some(previous), Some(current)) = (previous.index, flashblock.index)
17893 {
17894 if current < previous {
17895 return Ok((flashblock, false));
17896 }
17897 current > previous.saturating_add(1)
17898 } else {
17899 false
17900 }
17901 }
17902 Some(_) => payload.index != 0,
17903 None => payload.index != 0,
17904 };
17905 (flashblock, recover)
17906 }
17907 BaseFlashblockWirePayload::Block(payload) => {
17908 let transaction_hashes = flashblock_transaction_hashes(&payload.transactions)?;
17909 let parent_hash = non_placeholder_hash(payload.parent_hash);
17910 let state_root = non_placeholder_hash(payload.state_root);
17911 let transactions_root = payload.transactions_root.and_then(non_placeholder_hash);
17912 let partial_block_hash = non_placeholder_hash(payload.hash);
17913 let prevrandao = payload.mix_hash.and_then(non_placeholder_hash);
17914 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
17915 provider: &provider,
17916 payload_id: None,
17917 index: None,
17918 block_number: payload.number,
17919 partial_block_hash,
17920 parent_hash,
17921 state_root,
17922 transactions_root,
17923 transaction_hashes: &transaction_hashes,
17924 timestamp: Some(payload.timestamp),
17925 base_fee_per_gas: payload.base_fee_per_gas,
17926 beneficiary: payload.miner,
17927 prevrandao,
17928 gas_limit: payload.gas_limit,
17929 });
17930 let flashblock = FlashblockRef {
17931 provider,
17932 payload_id: None,
17933 index: None,
17934 block_number: payload.number,
17935 content_hash,
17936 partial_block_hash,
17937 parent_hash,
17938 state_root,
17939 transactions_root,
17940 transaction_hashes,
17941 timestamp: Some(payload.timestamp),
17942 base_fee_per_gas: payload.base_fee_per_gas,
17943 beneficiary: payload.miner,
17944 prevrandao,
17945 gas_limit: payload.gas_limit,
17946 };
17947 if let Some(previous) = self.latest_preconfirmation.as_ref()
17948 && flashblock.same_payload(previous)
17949 && flashblock.content_hash != previous.content_hash
17950 && !flashblock.is_cumulative_successor_of(previous)
17951 {
17952 return Err(SubscriberError::Provider(
17953 "cumulative Flashblock transaction membership is non-monotonic".into(),
17954 ));
17955 }
17956 (flashblock, false)
17957 }
17958 };
17959 Ok((flashblock, recover_pending_snapshot))
17960 }
17961
17962 async fn poll_op_pending_flashblock(
17963 &mut self,
17964 timing: FlashblockIngressTiming,
17965 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17966 match self
17967 .fetch_pending_flashblock_with_timing(None, timing)
17968 .await
17969 {
17970 Ok(event) => {
17971 self.consecutive_flashblock_poll_failures = 0;
17972 Ok(event)
17973 }
17974 Err(PendingFlashblockPollError::Request(error)) => {
17975 self.flashblocks_rpc_metrics.failed_requests = self
17976 .flashblocks_rpc_metrics
17977 .failed_requests
17978 .saturating_add(1);
17979 self.consecutive_flashblock_poll_failures =
17980 self.consecutive_flashblock_poll_failures.saturating_add(1);
17981 if self.consecutive_flashblock_poll_failures
17982 >= self.config.max_consecutive_flashblock_poll_failures
17983 {
17984 return Err(error);
17985 }
17986 tracing::warn!(
17987 consecutive_failures = self.consecutive_flashblock_poll_failures,
17988 failure_limit = self.config.max_consecutive_flashblock_poll_failures,
17989 error = %error,
17990 "Optimism pending-state Flashblocks request failed; retrying on the next tick"
17991 );
17992 Ok(None)
17993 }
17994 Err(PendingFlashblockPollError::Integrity(error)) => Err(error),
17995 }
17996 }
17997
17998 #[cfg(test)]
17999 async fn fetch_pending_flashblock(
18000 &mut self,
18001 indexed_recovery: Option<(FixedBytes<8>, u64, Vec<B256>)>,
18002 ) -> Result<Option<SubscriberEvent<N>>, PendingFlashblockPollError> {
18003 self.fetch_pending_flashblock_with_timing(
18004 indexed_recovery,
18005 FlashblockIngressTiming::new(Instant::now()),
18006 )
18007 .await
18008 }
18009
18010 async fn fetch_pending_flashblock_with_timing(
18011 &mut self,
18012 indexed_recovery: Option<(FixedBytes<8>, u64, Vec<B256>)>,
18013 timing: FlashblockIngressTiming,
18014 ) -> Result<Option<SubscriberEvent<N>>, PendingFlashblockPollError> {
18015 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
18016 == Some(FlashblocksAdapter::PendingStatePolling);
18017 if samples_pending_range {
18018 let fixed_methods = 2_usize.saturating_add(self.log_stream_filters().len());
18019 if !self.reserve_flashblock_rpc_methods(fixed_methods) {
18020 return Ok(None);
18021 }
18022 }
18023 let state_provider = if samples_pending_range {
18024 self.flashblocks_state_provider
18025 .as_ref()
18026 .unwrap_or(&self.provider)
18027 } else {
18028 &self.provider
18029 };
18030 let latest = if samples_pending_range {
18031 None
18032 } else {
18033 self.record_rpc(
18034 SubscriberRpcCause::CanonicalHeadCertification,
18035 SubscriberRpcMethod::EthBlockNumber,
18036 );
18037 self.flashblocks_rpc_metrics.canonical_head_requests = self
18038 .flashblocks_rpc_metrics
18039 .canonical_head_requests
18040 .saturating_add(1);
18041 Some(
18042 state_provider
18043 .get_block_number()
18044 .await
18045 .map_err(pending_flashblock_request_error)?,
18046 )
18047 };
18048 self.flashblocks_rpc_metrics.pending_block_requests = self
18049 .flashblocks_rpc_metrics
18050 .pending_block_requests
18051 .saturating_add(1);
18052 let pending_block = if samples_pending_range {
18053 self.fetch_op_pending_block().await?
18054 } else {
18055 self.record_rpc(
18056 SubscriberRpcCause::PendingStateSample,
18057 SubscriberRpcMethod::EthGetBlockByNumber,
18058 );
18059 self.provider
18060 .get_block_by_number(BlockNumberOrTag::Pending)
18061 .await
18062 .map_err(pending_flashblock_request_error)?
18063 };
18064 let Some(block) = pending_block else {
18065 if self.config.preconfirmations == PreconfirmationMode::Required {
18066 return Err(PendingFlashblockPollError::Request(
18067 SubscriberError::Provider(
18068 "Flashblocks provider returned no pending block".into(),
18069 ),
18070 ));
18071 }
18072 return Ok(None);
18073 };
18074 let latest = if samples_pending_range {
18075 self.certify_op_pending_parent(&block).await?.number()
18076 } else {
18077 latest.expect("non-OP pending recovery fetched a canonical height")
18078 };
18079 let header = block.header();
18080 if header.number() <= latest {
18081 return Ok(None);
18082 }
18083
18084 let provider = self.provider_ref.clone().ok_or({
18085 PendingFlashblockPollError::Integrity(SubscriberError::InvalidConfig(
18086 "Flashblocks require a stable provider ref from a pinned provider lease",
18087 ))
18088 })?;
18089 let parent_hash = Some(header.parent_hash());
18090 let transaction_hashes = if let Some(hashes) = block.transactions().as_hashes() {
18091 hashes.to_vec()
18092 } else if let Some(transactions) = block.transactions().as_transactions() {
18093 transactions
18094 .iter()
18095 .map(|transaction| transaction.tx_hash())
18096 .collect()
18097 } else {
18098 Vec::new()
18099 };
18100 let state_root = non_placeholder_hash(header.state_root());
18101 let transactions_root = non_placeholder_hash(header.transactions_root());
18102 let partial_block_hash = non_placeholder_hash(header.hash());
18103 let prevrandao = header.mix_hash().and_then(non_placeholder_hash);
18104 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
18105 provider: &provider,
18106 payload_id: None,
18107 index: None,
18108 block_number: header.number(),
18109 partial_block_hash,
18110 parent_hash,
18111 state_root,
18112 transactions_root,
18113 transaction_hashes: &transaction_hashes,
18114 timestamp: Some(header.timestamp()),
18115 base_fee_per_gas: header.base_fee_per_gas(),
18116 beneficiary: Some(header.beneficiary()),
18117 prevrandao,
18118 gas_limit: Some(header.gas_limit()),
18119 });
18120 let flashblock = FlashblockRef {
18121 provider,
18122 payload_id: None,
18123 index: None,
18124 block_number: header.number(),
18125 content_hash,
18126 partial_block_hash,
18127 parent_hash,
18128 state_root,
18129 transactions_root,
18130 transaction_hashes,
18131 timestamp: Some(header.timestamp()),
18132 base_fee_per_gas: header.base_fee_per_gas(),
18133 beneficiary: Some(header.beneficiary()),
18134 prevrandao,
18135 gas_limit: Some(header.gas_limit()),
18136 };
18137 if samples_pending_range
18138 && self
18139 .latest_preconfirmation
18140 .as_ref()
18141 .is_some_and(|previous| !previous.same_payload(&flashblock))
18142 {
18143 self.invalidate_preconfirmation_snapshot();
18146 }
18147 if let Some((payload_id, index, last_diff)) = indexed_recovery {
18148 self.base_flashblock_transactions = Some((
18149 payload_id,
18150 index,
18151 flashblock.transaction_hashes.clone(),
18152 last_diff,
18153 ));
18154 }
18155 let repeats_pending_snapshot = self
18156 .latest_preconfirmation
18157 .as_ref()
18158 .is_some_and(|previous| previous == &flashblock);
18159 if repeats_pending_snapshot && !samples_pending_range {
18160 return Ok(None);
18161 }
18162
18163 if let Some(previous) = self.latest_preconfirmation.as_ref()
18164 && flashblock.same_payload(previous)
18165 && !flashblock.is_cumulative_successor_of(previous)
18166 {
18167 if samples_pending_range {
18168 self.invalidate_preconfirmation_snapshot();
18173 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
18174 }
18175 return Err(PendingFlashblockPollError::Integrity(
18176 SubscriberError::Provider(
18177 "sampled cumulative Flashblock transaction membership is non-monotonic".into(),
18178 ),
18179 ));
18180 }
18181
18182 let mut logs = self.fetch_pending_logs(flashblock.block_number).await?;
18183 if samples_pending_range {
18184 let (mut receipt_logs, completed_receipts, unavailable_receipts) =
18185 self.fetch_pending_transaction_receipts(&flashblock).await?;
18186 logs.append(&mut receipt_logs);
18187 logs.retain(|log| log.block_number == Some(flashblock.block_number));
18188 for log in &logs {
18189 let transaction_hash = log.transaction_hash.ok_or_else(|| {
18190 PendingFlashblockPollError::Integrity(SubscriberError::Provider(
18191 "pre-confirmed log is missing its transaction hash".into(),
18192 ))
18193 })?;
18194 if !flashblock.contains_transaction(&transaction_hash) {
18195 self.flashblocks_rpc_metrics.raced_samples =
18196 self.flashblocks_rpc_metrics.raced_samples.saturating_add(1);
18197 return Ok(None);
18198 }
18199 }
18200 let logs = self
18201 .filter_preconfirmed_logs(&flashblock, logs)
18202 .map_err(PendingFlashblockPollError::Integrity)?;
18203 self.preconfirmed_unavailable_receipts
18204 .extend(unavailable_receipts);
18205 for transaction_hash in &completed_receipts {
18206 self.preconfirmed_unavailable_receipts
18207 .remove(transaction_hash);
18208 }
18209 self.preconfirmed_receipted_transactions
18210 .extend(completed_receipts);
18211 if repeats_pending_snapshot && logs.is_empty() {
18212 return Ok(None);
18213 }
18214 return Ok(Some(if logs.is_empty() {
18215 SubscriberEvent::FlashblockObserved
18216 } else {
18217 SubscriberEvent::PreconfirmedLogs {
18218 flashblock,
18219 logs,
18220 timing,
18221 }
18222 }));
18223 }
18224 let logs = self
18225 .filter_preconfirmed_logs(&flashblock, logs)
18226 .map_err(PendingFlashblockPollError::Integrity)?;
18227 Ok(Some(if logs.is_empty() {
18228 SubscriberEvent::FlashblockObserved
18229 } else {
18230 SubscriberEvent::PreconfirmedLogs {
18231 flashblock,
18232 logs,
18233 timing,
18234 }
18235 }))
18236 }
18237
18238 async fn fetch_pending_logs(
18239 &mut self,
18240 pending_block_number: u64,
18241 ) -> Result<Vec<Log>, PendingFlashblockPollError> {
18242 let mut logs = Vec::new();
18243 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
18244 == Some(FlashblocksAdapter::PendingStatePolling);
18245 let state_provider = if samples_pending_range {
18246 self.flashblocks_state_provider
18247 .as_ref()
18248 .unwrap_or(&self.provider)
18249 } else {
18250 &self.provider
18251 };
18252 for filter in self.log_stream_filters() {
18253 self.record_rpc(
18254 SubscriberRpcCause::PendingStateSample,
18255 SubscriberRpcMethod::EthGetLogs,
18256 );
18257 self.flashblocks_rpc_metrics.pending_log_requests = self
18258 .flashblocks_rpc_metrics
18259 .pending_log_requests
18260 .saturating_add(1);
18261 let filter = if samples_pending_range {
18262 filter
18263 .from_block(pending_block_number)
18264 .to_block(BlockNumberOrTag::Pending)
18265 } else {
18266 filter
18267 .from_block(BlockNumberOrTag::Pending)
18268 .to_block(BlockNumberOrTag::Pending)
18269 };
18270 logs.extend(
18271 state_provider
18272 .get_logs(&filter)
18273 .await
18274 .map_err(pending_flashblock_request_error)?,
18275 );
18276 }
18277 if samples_pending_range {
18278 logs.retain(|log| log.block_number == Some(pending_block_number));
18279 }
18280 Ok(logs)
18281 }
18282
18283 async fn fetch_pending_transaction_receipts(
18284 &mut self,
18285 flashblock: &FlashblockRef,
18286 ) -> Result<(Vec<Log>, Vec<B256>, Vec<B256>), PendingFlashblockPollError> {
18287 let receipt_allowance = self.pending_receipt_request_allowance();
18288 let receipt_limit = self
18289 .config
18290 .max_pending_transaction_receipts_per_tick
18291 .min(receipt_allowance);
18292 if receipt_limit == 0 {
18293 return Ok((Vec::new(), Vec::new(), Vec::new()));
18294 }
18295 let mut transaction_hashes = Vec::with_capacity(receipt_limit);
18296 for transaction_hash in &flashblock.transaction_hashes {
18297 if !self
18298 .preconfirmed_receipted_transactions
18299 .contains(transaction_hash)
18300 && !self
18301 .preconfirmed_unavailable_receipts
18302 .contains(transaction_hash)
18303 {
18304 transaction_hashes.push(*transaction_hash);
18305 if transaction_hashes.len() == receipt_limit {
18306 break;
18307 }
18308 }
18309 }
18310 if transaction_hashes.len() < receipt_limit {
18311 for transaction_hash in &flashblock.transaction_hashes {
18312 if self
18313 .preconfirmed_unavailable_receipts
18314 .contains(transaction_hash)
18315 {
18316 transaction_hashes.push(*transaction_hash);
18317 if transaction_hashes.len() == receipt_limit {
18318 break;
18319 }
18320 }
18321 }
18322 }
18323 if transaction_hashes.is_empty() {
18324 return Ok((Vec::new(), Vec::new(), Vec::new()));
18325 }
18326 let reserved = self.reserve_flashblock_rpc_methods(transaction_hashes.len());
18327 debug_assert!(reserved, "receipt allowance must remain reserved until use");
18328 if !reserved {
18329 return Ok((Vec::new(), Vec::new(), Vec::new()));
18330 }
18331 self.rpc_counters.record_many(
18332 SubscriberRpcCause::PendingStateSample,
18333 SubscriberRpcMethod::EthGetTransactionReceipt,
18334 transaction_hashes.len() as u64,
18335 );
18336 self.flashblocks_rpc_metrics.pending_receipt_requests = self
18337 .flashblocks_rpc_metrics
18338 .pending_receipt_requests
18339 .saturating_add(transaction_hashes.len() as u64);
18340 let state_provider = self
18341 .flashblocks_state_provider
18342 .as_ref()
18343 .unwrap_or(&self.provider);
18344 let client = state_provider.client();
18345 let mut batch = BatchRequest::new(client);
18346 let mut waiters = Vec::with_capacity(transaction_hashes.len());
18347 for transaction_hash in transaction_hashes {
18348 let waiter = batch
18349 .add_call::<_, serde_json::Value>("eth_getTransactionReceipt", &(transaction_hash,))
18350 .map_err(pending_flashblock_request_error)?;
18351 waiters.push((transaction_hash, waiter));
18352 }
18353 batch
18354 .send()
18355 .await
18356 .map_err(pending_flashblock_request_error)?;
18357 let mut logs = Vec::new();
18358 let mut completed = Vec::new();
18359 let mut unavailable = Vec::new();
18360 for (transaction_hash, waiter) in waiters {
18361 let value = waiter.await.map_err(pending_flashblock_request_error)?;
18362 if let Some(mut receipt_logs) =
18363 normalize_pending_transaction_receipt(transaction_hash, value)
18364 .map_err(PendingFlashblockPollError::Integrity)?
18365 {
18366 self.flashblocks_rpc_metrics.pending_receipts_completed = self
18367 .flashblocks_rpc_metrics
18368 .pending_receipts_completed
18369 .saturating_add(1);
18370 logs.append(&mut receipt_logs);
18371 completed.push(transaction_hash);
18372 } else {
18373 self.flashblocks_rpc_metrics.pending_receipts_unavailable = self
18374 .flashblocks_rpc_metrics
18375 .pending_receipts_unavailable
18376 .saturating_add(1);
18377 unavailable.push(transaction_hash);
18378 }
18379 }
18380 Ok((logs, completed, unavailable))
18381 }
18382
18383 fn pending_receipt_request_allowance(&mut self) -> usize {
18384 self.prune_flashblock_rpc_request_times();
18385 let rolling_capacity = self
18386 .config
18387 .max_flashblock_rpc_requests_per_second
18388 .saturating_sub(self.flashblock_rpc_request_times.len());
18389 rolling_capacity.min(self.pending_receipt_requests_per_tick_capacity())
18390 }
18391
18392 fn pending_receipt_requests_per_tick_capacity(&self) -> usize {
18393 let interval_nanos = self.config.flashblock_poll_interval.as_nanos().max(1);
18394 let ticks_per_second = Duration::from_secs(1).as_nanos().div_ceil(interval_nanos);
18395 let ticks_per_second = usize::try_from(ticks_per_second).unwrap_or(usize::MAX);
18396 self.pending_receipt_requests_per_second_capacity()
18397 .checked_div(ticks_per_second)
18398 .unwrap_or(0)
18399 }
18400
18401 fn pending_receipt_requests_per_second_capacity(&self) -> usize {
18402 let interval_nanos = self.config.flashblock_poll_interval.as_nanos().max(1);
18403 let ticks_per_second = Duration::from_secs(1).as_nanos().div_ceil(interval_nanos);
18404 let ticks_per_second = usize::try_from(ticks_per_second).unwrap_or(usize::MAX);
18405 let fixed_methods_per_tick = 2_usize.saturating_add(self.log_stream_filters().len());
18406 let mut reserved_methods = ticks_per_second.saturating_mul(fixed_methods_per_tick);
18407 if needs_header_block_stream(&self.interests) {
18408 let canonical_interval_nanos =
18409 self.config.canonical_head_poll_interval.as_nanos().max(1);
18410 let canonical_ticks = Duration::from_secs(1)
18411 .as_nanos()
18412 .div_ceil(canonical_interval_nanos);
18413 let canonical_ticks = usize::try_from(canonical_ticks).unwrap_or(usize::MAX);
18414 reserved_methods = reserved_methods.saturating_add(canonical_ticks.saturating_mul(2));
18415 }
18416 self.config
18417 .max_flashblock_rpc_requests_per_second
18418 .saturating_sub(reserved_methods)
18419 }
18420
18421 fn reserve_flashblock_rpc_methods(&mut self, methods: usize) -> bool {
18422 self.prune_flashblock_rpc_request_times();
18423 if self
18424 .flashblock_rpc_request_times
18425 .len()
18426 .saturating_add(methods)
18427 > self.config.max_flashblock_rpc_requests_per_second
18428 {
18429 return false;
18430 }
18431 let now = Instant::now();
18432 for _ in 0..methods {
18433 self.flashblock_rpc_request_times.push_back(now);
18434 }
18435 true
18436 }
18437
18438 fn prune_flashblock_rpc_request_times(&mut self) {
18439 let now = Instant::now();
18440 while self
18441 .flashblock_rpc_request_times
18442 .front()
18443 .is_some_and(|requested| now.duration_since(*requested) >= Duration::from_secs(1))
18444 {
18445 self.flashblock_rpc_request_times.pop_front();
18446 }
18447 }
18448
18449 fn filter_preconfirmed_logs(
18450 &mut self,
18451 flashblock: &FlashblockRef,
18452 mut logs: Vec<Log>,
18453 ) -> Result<Vec<Log>, SubscriberError> {
18454 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
18455 == Some(FlashblocksAdapter::PendingStatePolling);
18456 if self
18457 .latest_preconfirmation
18458 .as_ref()
18459 .is_some_and(|previous| !previous.same_payload(flashblock))
18460 {
18461 self.invalidate_preconfirmation_snapshot();
18466 }
18467 if self
18468 .latest_preconfirmation
18469 .as_ref()
18470 .is_none_or(|previous| !previous.same_payload(flashblock))
18471 {
18472 self.preconfirmed_seen_logs.clear();
18473 }
18474 if let Some(previous) = self.latest_preconfirmation.as_ref()
18475 && previous.same_payload(flashblock)
18476 && let (Some(previous_index), Some(current_index)) = (previous.index, flashblock.index)
18477 && current_index < previous_index
18478 {
18479 return Ok(Vec::new());
18480 }
18481 self.latest_preconfirmation = Some(flashblock.clone());
18482
18483 logs.sort_by_key(|log| (log.transaction_index.unwrap_or(u64::MAX), log.log_index));
18484 let mut filtered = Vec::new();
18485 for mut log in logs {
18486 if log.removed || log.block_number != Some(flashblock.block_number) {
18487 return Err(SubscriberError::Provider(
18488 "pre-confirmed log disagrees with its Flashblock snapshot".into(),
18489 ));
18490 }
18491 let transaction_hash = log.transaction_hash.ok_or_else(|| {
18492 SubscriberError::Provider(
18493 "pre-confirmed log is missing its transaction hash".into(),
18494 )
18495 })?;
18496 let log_index = log.log_index.ok_or_else(|| {
18497 SubscriberError::Provider("pre-confirmed log is missing its log index".into())
18498 })?;
18499 let transaction_index =
18500 flashblock
18501 .transaction_index(&transaction_hash)
18502 .ok_or_else(|| {
18503 SubscriberError::Provider(
18504 "pre-confirmed log transaction is absent from the cumulative Flashblock"
18505 .into(),
18506 )
18507 })?;
18508 if log
18509 .transaction_index
18510 .is_some_and(|reported| reported != transaction_index)
18511 {
18512 return Err(SubscriberError::Provider(
18513 "pre-confirmed log transaction index disagrees with cumulative membership"
18514 .into(),
18515 ));
18516 }
18517 let reported_hash = log.block_hash.and_then(non_placeholder_hash);
18518 if !samples_pending_range
18519 && let Some(reported) = reported_hash
18520 && reported != flashblock.content_hash
18521 && flashblock
18522 .partial_block_hash
18523 .is_some_and(|expected| reported != expected)
18524 {
18525 return Err(SubscriberError::Provider(
18526 "pre-confirmed log partial block hash disagrees with its Flashblock snapshot"
18527 .into(),
18528 ));
18529 }
18530 log.block_hash = Some(flashblock.content_hash);
18531 log.block_timestamp = flashblock.timestamp.or(log.block_timestamp);
18532 log.transaction_index = Some(transaction_index);
18533 if self
18534 .preconfirmed_seen_logs
18535 .insert((transaction_hash, log_index))
18536 && log_matches_any_interest(&log, &self.interests)
18537 {
18538 filtered.push(log);
18539 }
18540 }
18541 Ok(filtered)
18542 }
18543
18544 async fn verify_event_log_blocks(
18545 &mut self,
18546 event: &SubscriberEvent<N>,
18547 ) -> Result<(), SubscriberError> {
18548 if !self.config.verify_log_block_context {
18549 return Ok(());
18550 }
18551 match event {
18552 SubscriberEvent::Log { log, .. } => self.verify_log_block_context(log).await,
18553 SubscriberEvent::BackfilledLogs { logs, .. } | SubscriberEvent::Logs(logs) => {
18554 for log in logs {
18555 self.verify_log_block_context(log).await?;
18556 }
18557 Ok(())
18558 }
18559 #[cfg(feature = "raw-flashblocks-json")]
18560 SubscriberEvent::ExternalFlashblockUpdate(_) => Ok(()),
18561 SubscriberEvent::BlockHeader(_)
18562 | SubscriberEvent::PendingHash(_)
18563 | SubscriberEvent::PendingHashes(_)
18564 | SubscriberEvent::BasePendingLog { .. }
18565 | SubscriberEvent::BasePendingLogTimed { .. }
18566 | SubscriberEvent::BaseFlashblock { .. }
18567 | SubscriberEvent::BaseFlashblockTimed { .. }
18568 | SubscriberEvent::OpFlashblockTick
18569 | SubscriberEvent::OpFlashblockTickTimed(_)
18570 | SubscriberEvent::CanonicalHeadTick
18571 | SubscriberEvent::PreconfirmedLogs { .. }
18572 | SubscriberEvent::FlashblockInvalidated
18573 | SubscriberEvent::FlashblockObserved
18574 | SubscriberEvent::StreamTerminated(_)
18575 | SubscriberEvent::StreamGap { .. } => Ok(()),
18576 }
18577 }
18578
18579 async fn verify_log_block_context(&mut self, log: &Log) -> Result<(), SubscriberError> {
18580 if log.removed {
18581 return Ok(());
18582 }
18583 let number = log.block_number.ok_or_else(|| {
18584 SubscriberError::Provider(
18585 "canonical log is missing its block number during context verification".into(),
18586 )
18587 })?;
18588 let hash = log.block_hash.ok_or_else(|| {
18589 SubscriberError::Provider(
18590 "canonical log is missing its block hash during context verification".into(),
18591 )
18592 })?;
18593 let key = (number, hash);
18594 if self.verified_log_blocks.contains_key(&key) {
18595 return Ok(());
18596 }
18597 let provider = self
18598 .log_verification_provider
18599 .as_ref()
18600 .unwrap_or(&self.provider);
18601 self.rpc_counters.record(
18602 SubscriberRpcCause::LogBlockVerification,
18603 SubscriberRpcMethod::EthGetBlockByNumber,
18604 );
18605 let block = provider
18606 .get_block_by_number(BlockNumberOrTag::Number(number))
18607 .await
18608 .map_err(provider_error)?
18609 .ok_or_else(|| {
18610 SubscriberError::Provider(format!(
18611 "canonical log block {number} is unavailable during context verification"
18612 ))
18613 })?;
18614 let header = block.header();
18615 let verified = BlockRef {
18616 number: header.number(),
18617 hash: header.hash(),
18618 parent_hash: Some(header.parent_hash()),
18619 timestamp: Some(header.timestamp()),
18620 };
18621 if verified.number != number
18622 || verified.hash != hash
18623 || log
18624 .block_timestamp
18625 .is_some_and(|timestamp| verified.timestamp != Some(timestamp))
18626 {
18627 return Err(SubscriberError::Provider(format!(
18628 "canonical log block {number}:{hash:?} disagrees with the provider's current canonical identity"
18629 )));
18630 }
18631 self.verified_log_blocks.insert(key, verified);
18632 self.verified_log_block_order.push_back(key);
18633 let capacity = self.config.reconnect.dedupe_window.max(1);
18634 while self.verified_log_block_order.len() > capacity {
18635 if let Some(evicted) = self.verified_log_block_order.pop_front() {
18636 self.verified_log_blocks.remove(&evicted);
18637 }
18638 }
18639 Ok(())
18640 }
18641
18642 fn enqueue_event(&mut self, event: SubscriberEvent<N>) {
18643 self.enqueue_event_with_excluded_owners(event, None);
18644 }
18645
18646 fn buffer_reconcile_event_for_owners(
18647 &mut self,
18648 event: &SubscriberEvent<N>,
18649 target_epochs: &HashSet<SubscriberOwnerEpoch>,
18650 ) {
18651 match event {
18652 SubscriberEvent::Log { log, .. } => {
18653 self.buffer_reconcile_log_for_owners(log, InputSource::Subscription, target_epochs)
18654 }
18655 SubscriberEvent::BackfilledLogs { logs, .. } => {
18656 for log in logs {
18657 self.buffer_reconcile_log_for_owners(log, InputSource::Backfill, target_epochs);
18658 }
18659 }
18660 SubscriberEvent::Logs(logs) => {
18661 for log in logs {
18662 self.buffer_reconcile_log_for_owners(log, InputSource::Poll, target_epochs);
18663 }
18664 }
18665 #[cfg(feature = "raw-flashblocks-json")]
18666 SubscriberEvent::ExternalFlashblockUpdate(_) => {}
18667 SubscriberEvent::BlockHeader(_)
18668 | SubscriberEvent::PendingHash(_)
18669 | SubscriberEvent::PendingHashes(_)
18670 | SubscriberEvent::BasePendingLog { .. }
18671 | SubscriberEvent::BasePendingLogTimed { .. }
18672 | SubscriberEvent::BaseFlashblock { .. }
18673 | SubscriberEvent::BaseFlashblockTimed { .. }
18674 | SubscriberEvent::OpFlashblockTick
18675 | SubscriberEvent::OpFlashblockTickTimed(_)
18676 | SubscriberEvent::CanonicalHeadTick
18677 | SubscriberEvent::PreconfirmedLogs { .. }
18678 | SubscriberEvent::FlashblockInvalidated
18679 | SubscriberEvent::FlashblockObserved
18680 | SubscriberEvent::StreamTerminated(_)
18681 | SubscriberEvent::StreamGap { .. } => {}
18682 }
18683 }
18684
18685 fn buffer_reconcile_log_for_owners(
18686 &mut self,
18687 log: &Log,
18688 source: InputSource,
18689 target_epochs: &HashSet<SubscriberOwnerEpoch>,
18690 ) {
18691 let record = self.with_chain_id(log_input_record(log.clone(), source));
18692 let owners = self
18693 .staged_owners_for_record(&record)
18694 .into_iter()
18695 .filter(|owner| target_epochs.contains(owner))
18696 .collect::<Vec<_>>();
18697 if !owners.is_empty() {
18698 self.push_pending_reconcile_record(BufferedSubscriberOwnerRecord { record, owners });
18699 }
18700 }
18701
18702 fn promote_reconcile_owner_records(&mut self, target_epochs: &HashSet<SubscriberOwnerEpoch>) {
18703 let mut retained = VecDeque::new();
18704 while let Some(mut buffered) = self.pending_reconcile_owner_records.pop_front() {
18705 let mut promoted = Vec::new();
18706 buffered.owners.retain(|owner| {
18707 if target_epochs.contains(owner) {
18708 promoted.push(owner.clone());
18709 false
18710 } else {
18711 true
18712 }
18713 });
18714 if promoted.is_empty() {
18715 retained.push_back(buffered);
18716 continue;
18717 }
18718 let promoted_record = if buffered.owners.is_empty() {
18719 buffered.record
18720 } else {
18721 let record = buffered.record.clone();
18722 retained.push_back(buffered);
18723 record
18724 };
18725 self.enqueue_owner_record_for_owners_unmerged(promoted_record, promoted);
18726 }
18727 self.pending_reconcile_owner_records = retained;
18728 }
18729
18730 fn seed_reconciled_filter_anchors(
18731 &mut self,
18732 plans: &[SubscriberOwnerReconcilePlan<N>],
18733 through: u64,
18734 ) {
18735 for filter in plans.iter().flat_map(|plan| log_filters(&plan.interests)) {
18736 let Some(source_id) = self.log_source_ids.get(&filter).copied() else {
18737 continue;
18738 };
18739 let anchor = self
18740 .last_seen_log_blocks
18741 .entry(source_id)
18742 .or_insert(through);
18743 *anchor = (*anchor).max(through);
18744 }
18745 }
18746
18747 fn enqueue_event_excluding_owners(
18748 &mut self,
18749 event: SubscriberEvent<N>,
18750 excluded: &HashSet<SubscriberOwnerEpoch>,
18751 ) {
18752 self.enqueue_event_with_excluded_owners(event, Some(excluded));
18753 }
18754
18755 fn enqueue_event_with_excluded_owners(
18756 &mut self,
18757 event: SubscriberEvent<N>,
18758 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
18759 ) {
18760 match event {
18761 SubscriberEvent::Log { source_id, log } => {
18762 if log_matches_any_interest(&log, &self.interests) {
18763 let record = log_input_record(log, InputSource::Subscription);
18764 self.note_log_block(source_id, &record);
18765 self.enqueue_record_with_excluded_owners(record, excluded);
18766 }
18767 }
18768 SubscriberEvent::BackfilledLogs { source_id, logs } => {
18769 self.enqueue_backfilled_logs_with_excluded_owners(
18770 logs,
18771 Some(source_id),
18772 None,
18773 None,
18774 excluded,
18775 );
18776 }
18777 SubscriberEvent::Logs(logs) => {
18778 for log in logs {
18779 if log_matches_any_interest(&log, &self.interests) {
18780 self.enqueue_record_with_excluded_owners(
18781 log_input_record(log, InputSource::Poll),
18782 excluded,
18783 );
18784 }
18785 }
18786 }
18787 SubscriberEvent::BlockHeader(header) => {
18788 if needs_header_block_stream(&self.interests) {
18789 let record = block_header_input_record::<N>(header);
18790 self.note_attestable_canonical_block(&record);
18791 self.enqueue_record_with_excluded_owners(record, excluded);
18792 }
18793 }
18794 SubscriberEvent::PendingHash(hash) => {
18795 let record = pending_hash_input_record::<N>(hash, InputSource::Subscription);
18796 self.enqueue_record_with_excluded_owners(record, excluded);
18797 }
18798 SubscriberEvent::PendingHashes(hashes) => {
18799 for hash in hashes {
18800 self.enqueue_record_with_excluded_owners(
18801 pending_hash_input_record::<N>(hash, InputSource::Poll),
18802 excluded,
18803 );
18804 }
18805 }
18806 SubscriberEvent::PreconfirmedLogs {
18807 flashblock,
18808 logs,
18809 timing,
18810 } => {
18811 for log in logs {
18812 let record = self
18813 .with_chain_id(preconfirmed_log_input_record::<N>(log, flashblock.clone()));
18814 self.push_pending_record(SubscriberInputRecord {
18815 record,
18816 scope: SubscriberInputScope::Preconfirmed,
18817 preconfirmation_timing: Some(timing),
18818 });
18819 }
18820 }
18821 SubscriberEvent::FlashblockInvalidated => {
18822 self.pending_preconfirmation_invalidation = true;
18823 }
18824 SubscriberEvent::BasePendingLog { .. }
18825 | SubscriberEvent::BasePendingLogTimed { .. }
18826 | SubscriberEvent::BaseFlashblock { .. }
18827 | SubscriberEvent::BaseFlashblockTimed { .. }
18828 | SubscriberEvent::OpFlashblockTick
18829 | SubscriberEvent::OpFlashblockTickTimed(_)
18830 | SubscriberEvent::CanonicalHeadTick
18831 | SubscriberEvent::FlashblockObserved => {}
18832 #[cfg(feature = "raw-flashblocks-json")]
18833 SubscriberEvent::ExternalFlashblockUpdate(_) => {}
18834 SubscriberEvent::StreamTerminated(_) | SubscriberEvent::StreamGap { .. } => {}
18837 }
18838 }
18839
18840 fn enqueue_backfilled_logs(
18841 &mut self,
18842 logs: Vec<Log>,
18843 source_id: Option<usize>,
18844 owner: Option<&SubscriberOwnerEpoch>,
18845 range: Option<SubscriberBackfill>,
18846 ) {
18847 self.enqueue_backfilled_logs_with_excluded_owners(logs, source_id, owner, range, None);
18848 }
18849
18850 fn enqueue_backfilled_logs_with_excluded_owners(
18851 &mut self,
18852 logs: Vec<Log>,
18853 source_id: Option<usize>,
18854 owner: Option<&SubscriberOwnerEpoch>,
18855 range: Option<SubscriberBackfill>,
18856 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
18857 ) {
18858 for log in logs {
18859 if range.as_ref().is_some_and(|range| {
18860 log.block_number.is_some_and(|block| {
18861 block < range.start_block() || range.end_block().is_some_and(|end| block > end)
18862 })
18863 }) {
18864 continue;
18865 }
18866 let matches = match owner {
18867 Some(epoch) => self
18868 .owned_interests
18869 .iter()
18870 .find(|entry| entry.epoch.as_ref() == Some(epoch))
18871 .is_some_and(|entry| log_matches_any_interest(&log, &entry.interests)),
18872 None => log_matches_any_interest(&log, &self.interests),
18873 };
18874 if matches {
18875 let record = log_input_record(log, InputSource::Backfill);
18876 if let Some(epoch) = owner {
18877 self.enqueue_owner_record(record, epoch.clone());
18878 } else {
18879 if let Some(source_id) = source_id {
18880 self.note_log_block(source_id, &record);
18881 }
18882 self.enqueue_record_with_excluded_owners(record, excluded);
18883 }
18884 }
18885 }
18886 }
18887
18888 fn enqueue_compat_owner_backfilled_logs(
18889 &mut self,
18890 logs: Vec<Log>,
18891 owner: &HandlerId,
18892 range: SubscriberBackfill,
18893 ) {
18894 let interests = self
18895 .owned_interests
18896 .iter()
18897 .find(|entry| {
18898 &entry.owner == owner
18899 && entry.epoch.is_none()
18900 && entry.state == SubscriberOwnerState::Active
18901 })
18902 .map(|entry| entry.interests.clone());
18903 let Some(interests) = interests else {
18904 return;
18905 };
18906 for log in logs {
18907 if log.block_number.is_some_and(|block| {
18908 block < range.start_block() || range.end_block().is_some_and(|end| block > end)
18909 }) || !log_matches_any_interest(&log, &interests)
18910 {
18911 continue;
18912 }
18913 let record = log_input_record(log, InputSource::Backfill);
18914 self.enqueue_compat_owner_record(record, owner.clone());
18915 }
18916 }
18917
18918 async fn reconnect_source_stream(
18919 &mut self,
18920 source: SubscriberStreamSource,
18921 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
18922 if !source.is_pubsub() {
18923 return Err(stream_terminated_error(&source));
18924 }
18925
18926 if !self.config.reconnect.enabled {
18927 return Err(SubscriberError::Provider(format!(
18928 "Alloy subscriber {} stream terminated and reconnect is disabled",
18929 source.label()
18930 )));
18931 }
18932
18933 let mut attempts = 0usize;
18934 let mut delay = self.config.reconnect.initial_delay;
18935 let mut retry_delay = self.config.reconnect.retry_delay;
18936
18937 loop {
18938 attempts = attempts.saturating_add(1);
18939 if !delay.is_zero() {
18940 tokio::time::sleep(delay).await;
18941 }
18942
18943 match self.reconnect_source_once(source.clone()).await {
18944 Ok(backfill_event) => return Ok(backfill_event),
18945 Err(error) if reconnect_attempts_exhausted(attempts, &self.config.reconnect) => {
18946 return Err(SubscriberError::Provider(format!(
18947 "Alloy subscriber {} stream terminated and reconnect failed after {attempts} attempt(s): {error}",
18948 source.label()
18949 )));
18950 }
18951 Err(error) => {
18952 tracing::warn!(
18953 stream = source.label(),
18954 attempts,
18955 error = %error,
18956 "Alloy subscriber reconnect attempt failed"
18957 );
18958 delay = retry_delay;
18959 retry_delay =
18960 next_reconnect_delay(retry_delay, self.config.reconnect.max_delay);
18961 }
18962 }
18963 }
18964 }
18965
18966 async fn reconnect_source_once(
18967 &mut self,
18968 source: SubscriberStreamSource,
18969 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
18970 if matches!(
18971 &self.state,
18972 AlloySubscriberState::Active(streams) if streams.contains_source(&source)
18973 ) {
18974 let backfill_event = self.backfill_reconnected_source(&source).await?;
18978 self.pending_source_backfills
18979 .retain(|pending| !pending.same_key(&source));
18980 return Ok(backfill_event);
18981 }
18982 let stream = self.connect_source_stream(source.clone()).await?;
18983 if !matches!(self.state, AlloySubscriberState::Active(_)) {
18984 return Err(SubscriberError::Provider(
18985 "Alloy subscriber state changed before reconnect completed".to_owned(),
18986 ));
18987 }
18988 self.install_source_stream(source.clone(), stream);
18989 if self.source_requires_backfill(&source) {
18990 self.queue_source_backfill(source.clone());
18991 }
18992 let backfill_event = self.backfill_reconnected_source(&source).await?;
18993 self.pending_source_backfills
18994 .retain(|pending| !pending.same_key(&source));
18995
18996 Ok(backfill_event)
18997 }
18998
18999 async fn recover_stream_gap(
19022 &mut self,
19023 source: &SubscriberStreamSource,
19024 gap: SubscriberStreamGap,
19025 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
19026 match source {
19027 SubscriberStreamSource::PubSubLog { id, .. } => {
19028 self.reset_log_attestation();
19029 if !self.last_seen_log_blocks.contains_key(id) {
19030 return Err(SubscriberError::Provider(format!(
19031 "Alloy subscriber {} lost notifications ({gap}) before establishing a \
19032 delivery anchor, so the missed range cannot be bounded",
19033 source.label()
19034 )));
19035 }
19036 let event = self
19037 .backfill_source_window(source, SubscriberRpcCause::GapBackfill)
19038 .await?;
19039 self.gap_counters.record_log_gap_healed();
19040 Ok(event)
19041 }
19042 SubscriberStreamSource::PubSubBlockHeaders => {
19043 self.gap_counters.record_header_gap();
19044 Ok(None)
19045 }
19046 SubscriberStreamSource::BasePendingLog { .. } => {
19047 self.gap_counters.record_preconfirmation_gap();
19048 self.invalidate_preconfirmation_snapshot();
19049 Ok(Some(SubscriberEvent::FlashblockInvalidated))
19050 }
19051 _ => Ok(None),
19052 }
19053 }
19054
19055 async fn backfill_reconnected_source(
19056 &mut self,
19057 source: &SubscriberStreamSource,
19058 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
19059 self.backfill_source_window(source, SubscriberRpcCause::ReconnectBackfill)
19060 .await
19061 }
19062
19063 async fn backfill_source_window(
19073 &mut self,
19074 source: &SubscriberStreamSource,
19075 cause: SubscriberRpcCause,
19076 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
19077 if source.is_flashblocks() {
19078 return Ok(None);
19079 }
19080 let SubscriberStreamSource::PubSubLog { id, filter } = source else {
19081 return Ok(None);
19082 };
19083 let Some(from_block) = self.last_seen_log_blocks.get(id).copied() else {
19084 return Ok(None);
19085 };
19086
19087 self.record_rpc(cause, SubscriberRpcMethod::EthBlockNumber);
19088 let latest = self
19089 .provider
19090 .get_block_number()
19091 .await
19092 .map_err(provider_error)?;
19093 if latest < from_block {
19094 return Ok(None);
19095 }
19096
19097 self.record_rpc(cause, SubscriberRpcMethod::EthGetLogs);
19098 let logs = self
19099 .provider
19100 .get_logs(&filter.clone().from_block(from_block).to_block(latest))
19101 .await
19102 .map_err(provider_error)?;
19103 Ok(Some(SubscriberEvent::BackfilledLogs {
19104 source_id: *id,
19105 logs,
19106 }))
19107 }
19108
19109 fn note_log_block(&mut self, source_id: usize, record: &ReactiveInputRecord<N>) {
19110 if let Some(block) = record.context.block.as_ref() {
19111 self.last_seen_log_blocks.insert(source_id, block.number);
19112 }
19113 }
19114
19115 fn enqueue_record_with_excluded_owners(
19116 &mut self,
19117 record: ReactiveInputRecord<N>,
19118 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
19119 ) {
19120 let record = self.with_chain_id(record);
19121 let mut owners = self.staged_owners_for_record(&record);
19122 if let Some(excluded) = excluded {
19123 owners.retain(|owner| !excluded.contains(owner));
19124 }
19125 let canonical_duplicate = self.should_skip_recent_duplicate(&record);
19126 let owners = self.filter_recent_owner_duplicates(&record, owners);
19127 let compatibility_owners = self.compatibility_owners_for_record(&record);
19128 let (already_served, newly_served): (Vec<_>, Vec<_>) = compatibility_owners
19129 .into_iter()
19130 .partition(|owner| self.compatibility_owner_has_seen(&record, owner));
19131 if canonical_duplicate {
19132 if !owners.is_empty() {
19133 self.push_pending_record(SubscriberInputRecord {
19134 record: record.clone(),
19135 scope: SubscriberInputScope::OwnerOnly { owners },
19136 preconfirmation_timing: None,
19137 });
19138 }
19139 if !newly_served.is_empty() {
19140 for owner in &newly_served {
19141 self.remember_compatibility_owner_record(&record, owner);
19142 }
19143 self.push_pending_record(SubscriberInputRecord {
19144 record,
19145 scope: SubscriberInputScope::OwnerOnlyHandlers {
19146 owners: newly_served,
19147 },
19148 preconfirmation_timing: None,
19149 });
19150 }
19151 return;
19152 }
19153 self.remember_record(&record);
19154 for owner in already_served.iter().chain(&newly_served) {
19155 self.remember_compatibility_owner_record(&record, owner);
19156 }
19157 self.push_pending_record(SubscriberInputRecord {
19158 record,
19159 scope: if already_served.is_empty() {
19160 SubscriberInputScope::Canonical { owners }
19161 } else {
19162 SubscriberInputScope::CanonicalResidual {
19163 owners,
19164 excluded: already_served,
19165 }
19166 },
19167 preconfirmation_timing: None,
19168 });
19169 }
19170
19171 fn enqueue_compat_owner_record(&mut self, record: ReactiveInputRecord<N>, owner: HandlerId) {
19172 let record = self.with_chain_id(record);
19173 if self.compatibility_owner_has_seen(&record, &owner) {
19174 return;
19175 }
19176 self.remember_compatibility_owner_record(&record, &owner);
19177 self.push_pending_record(SubscriberInputRecord {
19178 record,
19179 scope: SubscriberInputScope::OwnerOnlyHandlers {
19180 owners: vec![owner],
19181 },
19182 preconfirmation_timing: None,
19183 });
19184 }
19185
19186 fn compatibility_owners_for_record(&self, record: &ReactiveInputRecord<N>) -> Vec<HandlerId> {
19187 self.owned_interests
19188 .iter()
19189 .filter(|entry| entry.epoch.is_none() && entry.state == SubscriberOwnerState::Active)
19190 .filter(|entry| {
19191 entry
19192 .interests
19193 .iter()
19194 .any(|interest| interest_matches(interest, &record.input))
19195 })
19196 .map(|entry| entry.owner.clone())
19197 .collect()
19198 }
19199
19200 fn compatibility_owner_has_seen(
19201 &self,
19202 record: &ReactiveInputRecord<N>,
19203 owner: &HandlerId,
19204 ) -> bool {
19205 should_dedupe_record(record)
19206 && self
19207 .recent_compat_owner_input_ref_sets
19208 .get(owner)
19209 .is_some_and(|seen| seen.contains(&record.input_ref()))
19210 }
19211
19212 fn remember_compatibility_owner_record(
19213 &mut self,
19214 record: &ReactiveInputRecord<N>,
19215 owner: &HandlerId,
19216 ) {
19217 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
19218 return;
19219 }
19220 let input_ref = record.input_ref();
19221 let seen = self
19222 .recent_compat_owner_input_ref_sets
19223 .entry(owner.clone())
19224 .or_default();
19225 if !seen.insert(input_ref) {
19226 return;
19227 }
19228 let recent = self
19229 .recent_compat_owner_input_refs
19230 .entry(owner.clone())
19231 .or_default();
19232 recent.push_back(input_ref);
19233 while recent.len() > self.config.reconnect.dedupe_window {
19234 if let Some(evicted) = recent.pop_front() {
19235 seen.remove(&evicted);
19236 }
19237 }
19238 }
19239
19240 fn enqueue_owner_record(
19241 &mut self,
19242 record: ReactiveInputRecord<N>,
19243 owner: SubscriberOwnerEpoch,
19244 ) {
19245 self.enqueue_owner_record_for_owners(record, vec![owner]);
19246 }
19247
19248 fn enqueue_owner_record_for_owners(
19249 &mut self,
19250 record: ReactiveInputRecord<N>,
19251 owners: Vec<SubscriberOwnerEpoch>,
19252 ) {
19253 self.enqueue_owner_record_for_owners_inner(record, owners, true);
19254 }
19255
19256 fn enqueue_owner_record_for_owners_unmerged(
19257 &mut self,
19258 record: ReactiveInputRecord<N>,
19259 owners: Vec<SubscriberOwnerEpoch>,
19260 ) {
19261 self.enqueue_owner_record_for_owners_inner(record, owners, false);
19262 }
19263
19264 fn enqueue_owner_record_for_owners_inner(
19265 &mut self,
19266 record: ReactiveInputRecord<N>,
19267 owners: Vec<SubscriberOwnerEpoch>,
19268 merge_pending: bool,
19269 ) {
19270 let record = self.with_chain_id(record);
19271 let owners = self.filter_recent_owner_duplicates(&record, owners);
19272 if owners.is_empty() {
19273 return;
19274 }
19275 if merge_pending
19276 && should_dedupe_record(&record)
19277 && self.config.reconnect.dedupe_window != 0
19278 {
19279 let input_ref = record.input_ref();
19280 if let Some(pending) = self
19281 .pending_records
19282 .iter_mut()
19283 .rev()
19284 .find(|pending| pending.record.input_ref() == input_ref)
19285 {
19286 let pending_owners = match &mut pending.scope {
19287 SubscriberInputScope::Canonical { owners }
19288 | SubscriberInputScope::CanonicalResidual { owners, .. }
19289 | SubscriberInputScope::OwnerOnly { owners } => Some(owners),
19290 SubscriberInputScope::OwnerOnlyHandlers { .. }
19291 | SubscriberInputScope::Preconfirmed => None,
19292 };
19293 if let Some(pending_owners) = pending_owners {
19294 for owner in owners {
19295 if !pending_owners.contains(&owner) {
19296 pending_owners.push(owner);
19297 }
19298 }
19299 return;
19300 }
19301 }
19302 }
19303 self.push_pending_record(SubscriberInputRecord {
19304 record,
19305 scope: SubscriberInputScope::OwnerOnly { owners },
19306 preconfirmation_timing: None,
19307 });
19308 }
19309
19310 fn push_pending_record(&mut self, record: SubscriberInputRecord<N>) {
19311 if self.pending_record_count() >= self.config.max_pending_records {
19312 self.note_resource_error(format!(
19313 "pending record queues reached the configured limit of {}",
19314 self.config.max_pending_records
19315 ));
19316 return;
19317 }
19318 self.pending_records.push_back(record);
19319 }
19320
19321 fn ensure_pending_record_capacity(
19322 &mut self,
19323 additional: usize,
19324 operation: &str,
19325 ) -> Result<(), SubscriberError> {
19326 let required = self.pending_record_count().saturating_add(additional);
19327 if required > self.config.max_pending_records {
19328 self.note_resource_error(format!(
19329 "{operation} require {required} pending records, above the configured limit of {}",
19330 self.config.max_pending_records
19331 ));
19332 return self.check_resource_error();
19333 }
19334 Ok(())
19335 }
19336
19337 fn push_pending_reconcile_record(&mut self, record: BufferedSubscriberOwnerRecord<N>) {
19338 if self.pending_record_count() >= self.config.max_pending_records {
19339 self.note_resource_error(format!(
19340 "pending record queues reached the configured limit of {}",
19341 self.config.max_pending_records
19342 ));
19343 return;
19344 }
19345 self.pending_reconcile_owner_records.push_back(record);
19346 }
19347
19348 fn pending_record_count(&self) -> usize {
19349 self.pending_records
19350 .len()
19351 .saturating_add(self.pending_reconcile_owner_records.len())
19352 }
19353
19354 fn note_resource_error(&mut self, message: String) {
19355 if self.resource_error.is_none() {
19356 self.resource_error = Some(message);
19357 }
19358 }
19359
19360 fn check_resource_error(&self) -> Result<(), SubscriberError> {
19361 match &self.resource_error {
19362 Some(message) => Err(SubscriberError::ResourceExhausted(message.clone())),
19363 None => Ok(()),
19364 }
19365 }
19366
19367 fn with_chain_id(&self, mut record: ReactiveInputRecord<N>) -> ReactiveInputRecord<N> {
19368 record.context.chain_id = self.chain_id;
19369 if self.config.verify_log_block_context
19370 && let ReactiveInput::Log(log) = &record.input
19371 && !log.removed
19372 && let (Some(number), Some(hash)) = (log.block_number, log.block_hash)
19373 && let Some(verified) = self.verified_log_blocks.get(&(number, hash)).copied()
19374 {
19375 record.context.block = Some(verified);
19376 record.context.chain_status = ChainStatus::Included {
19377 block: verified,
19378 confirmations: 0,
19379 };
19380 }
19381 record
19382 }
19383
19384 fn staged_owners_for_record(
19385 &self,
19386 record: &ReactiveInputRecord<N>,
19387 ) -> Vec<SubscriberOwnerEpoch> {
19388 self.owned_interests
19389 .iter()
19390 .filter(|entry| entry.state == SubscriberOwnerState::Staged)
19391 .filter(|entry| {
19392 entry
19393 .interests
19394 .iter()
19395 .any(|interest| interest_matches(interest, &record.input))
19396 })
19397 .filter_map(|entry| entry.epoch.clone())
19398 .collect()
19399 }
19400
19401 fn filter_recent_owner_duplicates(
19402 &mut self,
19403 record: &ReactiveInputRecord<N>,
19404 owners: Vec<SubscriberOwnerEpoch>,
19405 ) -> Vec<SubscriberOwnerEpoch> {
19406 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
19407 return owners;
19408 }
19409 let input_ref = record.input_ref();
19410 let window = self.config.reconnect.dedupe_window;
19411 owners
19412 .into_iter()
19413 .filter(|owner| {
19414 let seen = self
19415 .recent_owner_input_ref_sets
19416 .entry(owner.clone())
19417 .or_default();
19418 if !seen.insert(input_ref) {
19419 return false;
19420 }
19421 let recent = self
19422 .recent_owner_input_refs
19423 .entry(owner.clone())
19424 .or_default();
19425 recent.push_back(input_ref);
19426 while recent.len() > window {
19427 if let Some(evicted) = recent.pop_front() {
19428 seen.remove(&evicted);
19429 }
19430 }
19431 true
19432 })
19433 .collect()
19434 }
19435
19436 fn should_skip_recent_duplicate(&self, record: &ReactiveInputRecord<N>) -> bool {
19437 if !should_dedupe_record(record) {
19438 return false;
19439 }
19440 self.recent_input_ref_set.contains(&record.input_ref())
19441 }
19442
19443 fn remember_record(&mut self, record: &ReactiveInputRecord<N>) {
19444 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
19445 return;
19446 }
19447
19448 let input_ref = record.input_ref();
19449 if !self.recent_input_ref_set.insert(input_ref) {
19450 return;
19451 }
19452 self.recent_input_refs.push_back(input_ref);
19453
19454 while self.recent_input_refs.len() > self.config.reconnect.dedupe_window {
19455 if let Some(evicted) = self.recent_input_refs.pop_front() {
19456 self.recent_input_ref_set.remove(&evicted);
19457 }
19458 }
19459 }
19460}
19461
19462#[cfg(feature = "reactive-ws")]
19474fn gap_observing_stream<N, T, F>(
19475 subscription: alloy_pubsub::Subscription<T>,
19476 source: SubscriberStreamSource,
19477 gap_counters: Arc<SubscriberStreamGapCounters>,
19478 to_event: F,
19479) -> BoxStream<'static, SubscriberEvent<N>>
19480where
19481 N: Network + 'static,
19482 T: serde::de::DeserializeOwned + Send + 'static,
19483 F: FnMut(T) -> SubscriberEvent<N> + Send + 'static,
19484{
19485 struct GapState<T, F> {
19488 raw: alloy_pubsub::RawSubscription,
19489 to_event: F,
19490 source: SubscriberStreamSource,
19491 gap_counters: Arc<SubscriberStreamGapCounters>,
19492 _item: PhantomData<fn() -> T>,
19493 }
19494
19495 let state = GapState {
19496 raw: subscription.into_raw(),
19497 to_event,
19498 source: source.clone(),
19499 gap_counters,
19500 _item: PhantomData::<fn() -> T>,
19501 };
19502
19503 let stream = stream::unfold(state, |mut state| async move {
19504 let gap = match state.raw.recv().await {
19505 Ok(value) => match serde_json::from_str::<T>(value.get()) {
19506 Ok(item) => {
19507 let event = (state.to_event)(item);
19508 return Some((event, state));
19509 }
19510 Err(error) => {
19511 tracing::warn!(
19512 stream = state.source.label(),
19513 error = %error,
19514 "pubsub notification did not decode; treating it as lost data"
19515 );
19516 SubscriberStreamGap::Undecodable
19517 }
19518 },
19519 Err(broadcast::error::RecvError::Lagged(skipped)) => {
19520 tracing::warn!(
19521 stream = state.source.label(),
19522 skipped,
19523 "pubsub notification channel overflowed; the missed window will be recovered"
19524 );
19525 SubscriberStreamGap::Lagged { skipped }
19526 }
19527 Err(broadcast::error::RecvError::Closed) => return None,
19530 };
19531 state.gap_counters.record_gap(gap);
19532 let event = SubscriberEvent::StreamGap {
19533 source: state.source.clone(),
19534 gap,
19535 };
19536 Some((event, state))
19537 });
19538
19539 stream_with_termination(stream, source)
19540}
19541
19542fn stream_with_termination<N, S>(
19543 stream: S,
19544 source: SubscriberStreamSource,
19545) -> BoxStream<'static, SubscriberEvent<N>>
19546where
19547 N: Network + 'static,
19548 S: futures::Stream<Item = SubscriberEvent<N>> + Send + 'static,
19549{
19550 stream
19551 .chain(stream::once(async move {
19552 SubscriberEvent::StreamTerminated(source)
19553 }))
19554 .boxed()
19555}
19556
19557fn flashblock_reconnect_future<N>(
19558 provider: RootProvider<N>,
19559 source: SubscriberStreamSource,
19560 channel_size: usize,
19561 reconnect: SubscriberReconnectConfig,
19562 first_delay: Duration,
19563 flashblock_poll_interval: Duration,
19564 counters: Arc<SubscriberRpcCounters>,
19565) -> FlashblockReconnectFuture<N>
19566where
19567 N: Network + 'static,
19568{
19569 Box::pin(async move {
19570 if !reconnect.enabled {
19571 let error = SubscriberError::Provider(format!(
19572 "Alloy subscriber {} stream terminated and reconnect is disabled",
19573 source.label()
19574 ));
19575 return (source, Err(error));
19576 }
19577
19578 let mut attempts = 0_usize;
19579 let mut delay = first_delay;
19580 let mut retry_delay = reconnect.retry_delay;
19581 loop {
19582 attempts = attempts.saturating_add(1);
19583 if !delay.is_zero() {
19584 tokio::time::sleep(delay).await;
19585 }
19586 match connect_flashblock_source_once(
19587 &provider,
19588 source.clone(),
19589 channel_size,
19590 flashblock_poll_interval,
19591 counters.as_ref(),
19592 )
19593 .await
19594 {
19595 Ok(stream) => return (source, Ok(stream)),
19596 Err(error) if reconnect_attempts_exhausted(attempts, &reconnect) => {
19597 return (
19598 source.clone(),
19599 Err(SubscriberError::Provider(format!(
19600 "Alloy subscriber {} stream reconnect failed after {attempts} attempt(s): {error}",
19601 source.label()
19602 ))),
19603 );
19604 }
19605 Err(error) => {
19606 tracing::warn!(
19607 stream = source.label(),
19608 attempts,
19609 error = %error,
19610 "Flashblocks reconnect attempt failed"
19611 );
19612 delay = retry_delay;
19613 retry_delay = next_reconnect_delay(retry_delay, reconnect.max_delay);
19614 }
19615 }
19616 }
19617 })
19618}
19619
19620async fn connect_flashblock_source_once<N>(
19621 provider: &RootProvider<N>,
19622 source: SubscriberStreamSource,
19623 channel_size: usize,
19624 flashblock_poll_interval: Duration,
19625 counters: &SubscriberRpcCounters,
19626) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>
19627where
19628 N: Network + 'static,
19629{
19630 #[cfg(not(feature = "reactive-ws"))]
19631 let _ = (provider, counters);
19632
19633 match source {
19634 SubscriberStreamSource::BasePendingLog { id, filter } => {
19635 #[cfg(feature = "reactive-ws")]
19636 {
19637 let source = SubscriberStreamSource::BasePendingLog {
19638 id,
19639 filter: filter.clone(),
19640 };
19641 let params = base_pending_log_filter(&filter)?;
19642 counters.record(
19643 SubscriberRpcCause::StreamSubscription,
19644 SubscriberRpcMethod::EthSubscribe,
19645 );
19646 let stream = provider
19647 .subscribe::<_, Log>(("pendingLogs", params))
19648 .channel_size(channel_size.max(1))
19649 .await
19650 .map_err(provider_error)?
19651 .into_stream()
19652 .map(move |log| SubscriberEvent::BasePendingLogTimed {
19653 source_id: id,
19654 log,
19655 timing: FlashblockIngressTiming::new(Instant::now()),
19656 });
19657 Ok(stream_with_termination(stream, source))
19658 }
19659 #[cfg(not(feature = "reactive-ws"))]
19660 {
19661 let _ = (id, filter, channel_size);
19662 Err(SubscriberError::Unsupported(
19663 "Base Flashblocks require the reactive-ws feature",
19664 ))
19665 }
19666 }
19667 SubscriberStreamSource::BaseFlashblocks => {
19668 #[cfg(feature = "reactive-ws")]
19669 {
19670 counters.record(
19671 SubscriberRpcCause::StreamSubscription,
19672 SubscriberRpcMethod::EthSubscribe,
19673 );
19674 let stream = provider
19675 .subscribe::<_, BaseFlashblockWirePayload>(("newFlashblocks",))
19676 .channel_size(channel_size.max(1))
19677 .await
19678 .map_err(provider_error)?
19679 .into_stream()
19680 .map(|payload| SubscriberEvent::BaseFlashblockTimed {
19681 payload,
19682 timing: FlashblockIngressTiming::new(Instant::now()),
19683 });
19684 Ok(stream_with_termination(
19685 stream,
19686 SubscriberStreamSource::BaseFlashblocks,
19687 ))
19688 }
19689 #[cfg(not(feature = "reactive-ws"))]
19690 {
19691 let _ = channel_size;
19692 Err(SubscriberError::Unsupported(
19693 "Base Flashblocks require the reactive-ws feature",
19694 ))
19695 }
19696 }
19697 SubscriberStreamSource::OpPendingFlashblocks => {
19698 let first_tick = tokio::time::Instant::now();
19699 let mut interval = tokio::time::interval_at(first_tick, flashblock_poll_interval);
19700 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
19701 let stream = stream::unfold(interval, |mut interval| async move {
19702 interval.tick().await;
19703 Some((
19704 SubscriberEvent::OpFlashblockTickTimed(FlashblockIngressTiming::new(
19705 Instant::now(),
19706 )),
19707 interval,
19708 ))
19709 });
19710 Ok(stream_with_termination(
19711 stream,
19712 SubscriberStreamSource::OpPendingFlashblocks,
19713 ))
19714 }
19715 source => Err(SubscriberError::InvalidConfig(match source {
19716 SubscriberStreamSource::PubSubLog { .. }
19717 | SubscriberStreamSource::CanonicalHeadPolling
19718 | SubscriberStreamSource::PubSubPendingHashes
19719 | SubscriberStreamSource::PubSubBlockHeaders
19720 | SubscriberStreamSource::PollingLog { .. }
19721 | SubscriberStreamSource::PollingPendingHashes => {
19722 "Flashblocks reconnect received a canonical source"
19723 }
19724 SubscriberStreamSource::BasePendingLog { .. }
19725 | SubscriberStreamSource::BaseFlashblocks
19726 | SubscriberStreamSource::OpPendingFlashblocks => unreachable!(),
19727 #[cfg(feature = "raw-flashblocks-json")]
19728 SubscriberStreamSource::ExternalFlashblockUpdates => {
19729 "Flashblocks reconnect cannot own an application-managed source"
19730 }
19731 })),
19732 }
19733}
19734
19735fn aggregate_interests<N: Network>(
19736 base: &[ReactiveInterest<N>],
19737 owned: &[OwnedSubscriberInterests<N>],
19738) -> Vec<ReactiveInterest<N>> {
19739 base.iter()
19740 .cloned()
19741 .chain(
19742 owned
19743 .iter()
19744 .flat_map(|entry| entry.interests.iter().cloned()),
19745 )
19746 .collect()
19747}
19748
19749fn stream_terminated_error(source: &SubscriberStreamSource) -> SubscriberError {
19750 SubscriberError::Provider(format!(
19751 "Alloy subscriber {} stream terminated before the subscriber was stopped",
19752 source.label()
19753 ))
19754}
19755
19756fn reconnect_attempts_exhausted(attempts: usize, config: &SubscriberReconnectConfig) -> bool {
19757 config
19758 .max_attempts
19759 .is_some_and(|max_attempts| attempts >= max_attempts)
19760}
19761
19762fn next_reconnect_delay(current: Duration, max: Duration) -> Duration {
19763 if current.is_zero() {
19764 return current;
19765 }
19766 current.checked_mul(2).unwrap_or(max).min(max)
19767}
19768
19769fn should_dedupe_record<N: Network>(record: &ReactiveInputRecord<N>) -> bool {
19770 match &record.input {
19771 ReactiveInput::Log(log) => {
19772 is_canonical_status(&record.context.chain_status) && !log.removed
19773 }
19774 ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_) => true,
19775 ReactiveInput::FullBlock(_) | ReactiveInput::PendingTx(_) => false,
19776 }
19777}
19778
19779#[cfg(test)]
19780mod subscriber_helper_tests {
19781 use super::*;
19782 use alloy_json_rpc::{RequestPacket, ResponsePacket};
19783 use alloy_provider::ProviderBuilder;
19784 use alloy_rpc_client::RpcClient;
19785 use alloy_transport::{TransportError, TransportFut, mock::Asserter};
19786 use std::task::{Context, Poll};
19787 use tower::Service;
19788
19789 #[derive(Clone, Debug)]
19790 struct NeverRespondingTransport;
19791
19792 impl Service<RequestPacket> for NeverRespondingTransport {
19793 type Response = ResponsePacket;
19794 type Error = TransportError;
19795 type Future = TransportFut<'static>;
19796
19797 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
19798 Poll::Ready(Ok(()))
19799 }
19800
19801 fn call(&mut self, _request: RequestPacket) -> Self::Future {
19802 Box::pin(futures::future::pending())
19803 }
19804 }
19805
19806 fn indexed_flashblock(transaction_hash: B256, state_root: B256) -> BaseFlashblockWirePayload {
19807 BaseFlashblockWirePayload::Indexed(BaseFlashblockPayload {
19808 payload_id: FixedBytes::repeat_byte(0x11),
19809 index: 0,
19810 base: Some(BaseFlashblockBase {
19811 parent_hash: B256::repeat_byte(100),
19812 block_number: 101,
19813 timestamp: 1_700_000_101,
19814 gas_limit: Some(30_000_000),
19815 base_fee_per_gas: Some(7),
19816 beneficiary: Some(Address::repeat_byte(0xcb)),
19817 prevrandao: Some(B256::repeat_byte(0x77)),
19818 }),
19819 diff: BaseFlashblockDiff {
19820 state_root,
19821 block_hash: B256::ZERO,
19822 transactions: vec![serde_json::Value::String(format!("{transaction_hash:#x}"))],
19823 transactions_root: None,
19824 },
19825 metadata: None,
19826 })
19827 }
19828
19829 fn base_flashblock_event(payload: BaseFlashblockWirePayload) -> SubscriberEvent<Ethereum> {
19830 SubscriberEvent::BaseFlashblockTimed {
19831 payload,
19832 timing: FlashblockIngressTiming::new(Instant::now()),
19833 }
19834 }
19835
19836 #[test]
19837 fn duplicate_flashblock_transaction_membership_is_rejected() {
19838 let transaction = format!("{:#x}", B256::repeat_byte(0x41));
19839 let transactions = vec![
19840 serde_json::Value::String(transaction.clone()),
19841 serde_json::Value::String(transaction),
19842 ];
19843 assert!(matches!(
19844 flashblock_transaction_hashes(&transactions),
19845 Err(SubscriberError::Provider(ref message)) if message.contains("duplicate")
19846 ));
19847 }
19848
19849 #[test]
19850 fn conflicting_duplicate_indexed_flashblock_is_rejected() {
19851 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19852 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19853 provider,
19854 SubscriberMode::PubSub,
19855 SubscriberConfig::default(),
19856 )
19857 .with_provider_ref(ProviderRef::new("base-paid", 7));
19858 subscriber.chain_id = Some(8_453);
19859
19860 subscriber
19861 .accept_base_flashblock(indexed_flashblock(
19862 B256::repeat_byte(0x41),
19863 B256::repeat_byte(0xa1),
19864 ))
19865 .expect("first indexed preview");
19866 assert!(matches!(
19867 subscriber.accept_base_flashblock(indexed_flashblock(
19868 B256::repeat_byte(0x42),
19869 B256::repeat_byte(0xa2),
19870 )),
19871 Err(SubscriberError::Provider(ref message))
19872 if message.contains("conflicting duplicate")
19873 ));
19874 }
19875
19876 #[tokio::test]
19877 async fn duplicate_index_with_changed_commitment_is_rejected() {
19878 let transaction = B256::repeat_byte(0x41);
19879 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19880 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19881 provider,
19882 SubscriberMode::PubSub,
19883 SubscriberConfig::default(),
19884 )
19885 .with_provider_ref(ProviderRef::new("base-paid", 7));
19886 subscriber.chain_id = Some(8_453);
19887
19888 subscriber
19889 .normalize_flashblock_event(base_flashblock_event(indexed_flashblock(
19890 transaction,
19891 B256::repeat_byte(0xa1),
19892 )))
19893 .await
19894 .expect("first indexed preview");
19895
19896 let BaseFlashblockWirePayload::Indexed(mut conflicting) =
19897 indexed_flashblock(transaction, B256::repeat_byte(0xa1))
19898 else {
19899 unreachable!()
19900 };
19901 conflicting.diff.state_root = B256::repeat_byte(0xbb);
19902 assert!(matches!(
19903 subscriber
19904 .normalize_flashblock_event(base_flashblock_event(
19905 BaseFlashblockWirePayload::Indexed(conflicting),
19906 ))
19907 .await,
19908 Err(SubscriberError::Provider(ref message))
19909 if message.contains("conflicting duplicate indexed Flashblock content")
19910 ));
19911 }
19912
19913 #[tokio::test]
19914 async fn indexed_gap_recovery_seeds_later_cumulative_membership() {
19915 let transaction_a = B256::repeat_byte(0x41);
19916 let transaction_b = B256::repeat_byte(0x42);
19917 let transaction_c = B256::repeat_byte(0x43);
19918 let transaction_d = B256::repeat_byte(0x44);
19919 let asserter = Asserter::new();
19920 asserter.push_success(&100_u64);
19921 let pending = rpc_block(101, B256::ZERO).with_transactions(
19922 alloy_network::primitives::BlockTransactions::Hashes(vec![
19923 transaction_a,
19924 transaction_b,
19925 transaction_c,
19926 ]),
19927 );
19928 asserter.push_success(&Some(pending));
19929 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
19930 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19931 provider,
19932 SubscriberMode::PubSub,
19933 SubscriberConfig::default(),
19934 )
19935 .with_provider_ref(ProviderRef::new("base-paid", 7));
19936 subscriber.chain_id = Some(8_453);
19937
19938 subscriber
19939 .normalize_flashblock_event(base_flashblock_event(indexed_flashblock(
19940 transaction_a,
19941 B256::repeat_byte(0xa1),
19942 )))
19943 .await
19944 .expect("index zero preview");
19945 let BaseFlashblockWirePayload::Indexed(mut gap) =
19946 indexed_flashblock(transaction_c, B256::repeat_byte(0xa3))
19947 else {
19948 unreachable!()
19949 };
19950 gap.index = 2;
19951 gap.base = None;
19952 gap.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
19953 subscriber
19954 .normalize_flashblock_event(base_flashblock_event(BaseFlashblockWirePayload::Indexed(
19955 gap,
19956 )))
19957 .await
19958 .expect("the missing index is recovered from pending state");
19959
19960 let BaseFlashblockWirePayload::Indexed(mut next) =
19961 indexed_flashblock(transaction_d, B256::repeat_byte(0xa4))
19962 else {
19963 unreachable!()
19964 };
19965 next.index = 3;
19966 next.base = None;
19967 next.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
19968 let (next, recover) = subscriber
19969 .accept_base_flashblock(BaseFlashblockWirePayload::Indexed(next))
19970 .expect("the next diff extends the recovered cumulative set");
19971 assert!(!recover);
19972 assert_eq!(
19973 next.transaction_hashes,
19974 vec![transaction_a, transaction_b, transaction_c, transaction_d]
19975 );
19976 }
19977
19978 #[tokio::test]
19979 async fn unrecoverable_indexed_gap_revokes_the_generation() {
19980 let asserter = Asserter::new();
19981 asserter.push_success(&100_u64);
19982 asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(0x64))));
19983 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
19984 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19985 provider,
19986 SubscriberMode::PubSub,
19987 SubscriberConfig {
19988 preconfirmations: PreconfirmationMode::Preferred,
19989 ..SubscriberConfig::default()
19990 },
19991 )
19992 .with_provider_ref(ProviderRef::new("base-paid", 7));
19993 subscriber.chain_id = Some(8_453);
19994
19995 subscriber
19996 .normalize_flashblock_event(base_flashblock_event(indexed_flashblock(
19997 B256::repeat_byte(0x41),
19998 B256::repeat_byte(0xa1),
19999 )))
20000 .await
20001 .expect("index zero preview");
20002 let BaseFlashblockWirePayload::Indexed(mut gap) =
20003 indexed_flashblock(B256::repeat_byte(0x43), B256::repeat_byte(0xa3))
20004 else {
20005 unreachable!()
20006 };
20007 gap.index = 2;
20008 gap.base = None;
20009 gap.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
20010 let event = subscriber
20011 .normalize_flashblock_event(base_flashblock_event(BaseFlashblockWirePayload::Indexed(
20012 gap,
20013 )))
20014 .await
20015 .expect("preferred mode fails closed without pending recovery")
20016 .expect("generation invalidation is observable");
20017 assert!(matches!(event, SubscriberEvent::FlashblockInvalidated));
20018 assert!(subscriber.latest_preconfirmation.is_none());
20019 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 8);
20020 }
20021
20022 #[test]
20023 fn base_flashblock_wire_decodes_cumulative_block_shape() {
20024 let payload: BaseFlashblockWirePayload = serde_json::from_str(
20025 r#"{
20026 "hash":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
20027 "number":"0x2ef403b",
20028 "parentHash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
20029 "stateRoot":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
20030 "timestamp":"0x6a68dd59",
20031 "transactions":[]
20032 }"#,
20033 )
20034 .expect("decode current Base newFlashblocks shape");
20035 let BaseFlashblockWirePayload::Block(payload) = payload else {
20036 panic!("expected cumulative block-shaped payload")
20037 };
20038 assert_eq!(payload.number, 49_233_979);
20039 assert_eq!(payload.timestamp, 1_785_257_305);
20040 assert_eq!(payload.hash, B256::repeat_byte(0xaa));
20041 assert_eq!(payload.parent_hash, B256::repeat_byte(0xbb));
20042 assert_eq!(payload.state_root, B256::repeat_byte(0xcc));
20043 }
20044
20045 #[tokio::test]
20046 async fn zero_hash_pending_log_waits_for_the_preview_containing_its_transaction() {
20047 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20048 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20049 provider,
20050 SubscriberMode::PubSub,
20051 SubscriberConfig::default(),
20052 )
20053 .with_provider_ref(ProviderRef::new("base-paid", 7));
20054 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
20055 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
20056 local_matcher: None,
20057 route_key: None,
20058 })];
20059 subscriber.interests = subscriber.base_interests.clone();
20060
20061 let first: BaseFlashblockWirePayload = serde_json::from_str(
20062 r#"{
20063 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
20064 "number":"0x65",
20065 "parentHash":"0x6464646464646464646464646464646464646464646464646464646464646464",
20066 "stateRoot":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
20067 "transactionsRoot":"0x1111111111111111111111111111111111111111111111111111111111111111",
20068 "timestamp":"0x6553f165",
20069 "transactions":["0x4141414141414141414141414141414141414141414141414141414141414141"]
20070 }"#,
20071 )
20072 .expect("decode first cumulative preview");
20073 subscriber
20074 .normalize_flashblock_event(base_flashblock_event(first))
20075 .await
20076 .expect("first preview is accepted");
20077
20078 let mut second_log = rpc_log(false);
20079 second_log.block_hash = Some(B256::ZERO);
20080 second_log.block_number = Some(102);
20081 second_log.block_timestamp = Some(1_700_000_102);
20082 second_log.transaction_hash = Some(B256::repeat_byte(0x42));
20083 second_log.transaction_index = Some(0);
20084 second_log.log_index = Some(0);
20085
20086 let pending_log_ingress = Instant::now() - Duration::from_millis(25);
20087 let before_preview = subscriber
20088 .normalize_flashblock_event(SubscriberEvent::BasePendingLogTimed {
20089 source_id: 0,
20090 log: second_log,
20091 timing: FlashblockIngressTiming::new(pending_log_ingress),
20092 })
20093 .await
20094 .expect("a zero-hash log for the next block must be buffered");
20095 assert!(before_preview.is_none());
20096
20097 let second: BaseFlashblockWirePayload = serde_json::from_str(
20098 r#"{
20099 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
20100 "number":"0x66",
20101 "parentHash":"0x6565656565656565656565656565656565656565656565656565656565656565",
20102 "stateRoot":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
20103 "transactionsRoot":"0x2222222222222222222222222222222222222222222222222222222222222222",
20104 "timestamp":"0x6553f166",
20105 "transactions":["0x4242424242424242424242424242424242424242424242424242424242424242"]
20106 }"#,
20107 )
20108 .expect("decode second cumulative preview");
20109 let event = subscriber
20110 .normalize_flashblock_event(base_flashblock_event(second))
20111 .await
20112 .expect("second preview is accepted")
20113 .expect("the matching buffered log is released");
20114 let SubscriberEvent::PreconfirmedLogs {
20115 flashblock,
20116 logs,
20117 timing,
20118 } = event
20119 else {
20120 panic!("expected a preconfirmed log batch")
20121 };
20122 assert_eq!(timing.source_ingress(), pending_log_ingress);
20123 assert_eq!(flashblock.block_number, 102);
20124 assert_ne!(flashblock.content_hash, B256::ZERO);
20125 assert_eq!(flashblock.partial_block_hash, None);
20126 assert_eq!(logs.len(), 1);
20127 assert_eq!(logs[0].transaction_hash, Some(B256::repeat_byte(0x42)));
20128 assert_eq!(logs[0].block_hash, Some(flashblock.content_hash));
20129 }
20130
20131 #[test]
20132 fn flashblock_endpoints_certify_canonical_heads_instead_of_trusting_newheads() {
20133 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20134 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20135 provider,
20136 SubscriberMode::PubSub,
20137 SubscriberConfig {
20138 preconfirmations: PreconfirmationMode::Required,
20139 ..SubscriberConfig::default()
20140 },
20141 )
20142 .with_provider_ref(ProviderRef::new("base-paid", 7));
20143 subscriber.chain_id = Some(8_453);
20144 subscriber.interests = vec![ReactiveInterest::Blocks(BlockInterest::default())];
20145
20146 let sources = subscriber.pubsub_stream_sources();
20147 assert!(
20148 sources
20149 .iter()
20150 .any(|source| matches!(source, SubscriberStreamSource::CanonicalHeadPolling))
20151 );
20152 assert!(
20153 !sources
20154 .iter()
20155 .any(|source| matches!(source, SubscriberStreamSource::PubSubBlockHeaders))
20156 );
20157 }
20158
20159 #[test]
20160 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
20161 fn external_flashblocks_keep_normal_canonical_pubsub_sources_on_any_chain() {
20162 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20163 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20164 provider,
20165 SubscriberMode::PubSub,
20166 SubscriberConfig {
20167 preconfirmations: PreconfirmationMode::Required,
20168 ..SubscriberConfig::default()
20169 },
20170 );
20171 subscriber
20172 .configure_external_flashblock_updates(ProviderRef::new("raw-json", 4))
20173 .expect("configure external source");
20174 subscriber.chain_id = Some(1);
20175 subscriber.base_interests = vec![
20176 ReactiveInterest::Blocks(BlockInterest::default()),
20177 log_interest_matching_rpc_log(),
20178 ];
20179 subscriber.interests = subscriber.base_interests.clone();
20180
20181 let pubsub = subscriber.pubsub_stream_sources();
20182 assert!(
20183 pubsub
20184 .iter()
20185 .any(|source| matches!(source, SubscriberStreamSource::PubSubBlockHeaders))
20186 );
20187 assert!(
20188 pubsub
20189 .iter()
20190 .any(|source| matches!(source, SubscriberStreamSource::PubSubLog { .. }))
20191 );
20192 assert!(pubsub.iter().all(|source| !matches!(
20193 source,
20194 SubscriberStreamSource::BaseFlashblocks
20195 | SubscriberStreamSource::BasePendingLog { .. }
20196 | SubscriberStreamSource::OpPendingFlashblocks
20197 | SubscriberStreamSource::CanonicalHeadPolling
20198 )));
20199 assert!(
20200 subscriber
20201 .polling_stream_sources()
20202 .iter()
20203 .all(|source| { !matches!(source, SubscriberStreamSource::OpPendingFlashblocks) })
20204 );
20205 assert!(
20206 subscriber
20207 .capabilities()
20208 .supports(SubscriberCapability::Preconfirmations)
20209 );
20210 }
20211
20212 #[test]
20213 #[cfg(feature = "raw-flashblocks-json")]
20214 fn external_flashblocks_are_rejected_when_preconfirmations_are_disabled() {
20215 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20216 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20217 provider,
20218 SubscriberMode::PubSub,
20219 SubscriberConfig::default(),
20220 );
20221 subscriber
20222 .configure_external_flashblock_updates(ProviderRef::new("raw-json", 4))
20223 .expect("configure external source");
20224 subscriber.chain_id = Some(1);
20225
20226 assert!(matches!(
20227 subscriber.validate_flashblocks_setup(),
20228 Err(SubscriberError::InvalidConfig(message))
20229 if message.contains("require preconfirmations")
20230 ));
20231 }
20232
20233 #[tokio::test]
20234 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
20235 async fn external_flashblocks_configuration_is_rejected_after_registration_starts() {
20236 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20237 let mut fresh = AlloySubscriber::<_, Ethereum>::new(
20238 provider,
20239 SubscriberMode::PubSub,
20240 SubscriberConfig {
20241 preconfirmations: PreconfirmationMode::Preferred,
20242 ..SubscriberConfig::default()
20243 },
20244 );
20245 fresh
20246 .configure_external_flashblock_updates(ProviderRef::new("raw-json", 4))
20247 .expect("construction-time external source");
20248
20249 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20250 let mut started = AlloySubscriber::<_, Ethereum>::new(
20251 provider,
20252 SubscriberMode::PubSub,
20253 SubscriberConfig {
20254 preconfirmations: PreconfirmationMode::Preferred,
20255 ..SubscriberConfig::default()
20256 },
20257 )
20258 .with_provider_ref(ProviderRef::new("canonical", 3));
20259 started.chain_id = Some(8_453);
20260 started
20261 .register_interests(&[log_interest_matching_rpc_log()])
20262 .await
20263 .expect("register canonical topology");
20264
20265 assert!(matches!(
20266 started.configure_external_flashblock_updates(ProviderRef::new("raw-json", 4)),
20267 Err(SubscriberError::InvalidConfig(message))
20268 if message.contains("before subscriber registration")
20269 ));
20270 }
20271
20272 #[tokio::test]
20273 #[cfg(feature = "raw-flashblocks-json")]
20274 async fn external_flashblocks_preflight_performs_no_flashblocks_rpc() {
20275 let asserter = Asserter::new();
20276 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20277 let source = ProviderRef::new("raw-json", 4);
20278 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20279 provider,
20280 SubscriberMode::PubSub,
20281 SubscriberConfig {
20282 preconfirmations: PreconfirmationMode::Required,
20283 ..SubscriberConfig::default()
20284 },
20285 );
20286 subscriber
20287 .configure_external_flashblock_updates(source.clone())
20288 .expect("configure external source");
20289 subscriber.chain_id = Some(1);
20290 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20291 subscriber.interests = subscriber.base_interests.clone();
20292 let desired = subscriber.pubsub_stream_sources();
20293 let mut streams = SubscriberStreams::new();
20294 for source in desired {
20295 streams.push(source, stream::pending().boxed());
20296 }
20297 subscriber.state = AlloySubscriberState::Active(streams);
20298 subscriber.sources_dirty = false;
20299
20300 let preflight = subscriber
20301 .establish_flashblocks_preflight(1)
20302 .await
20303 .expect("external source preflight");
20304 assert_eq!(preflight.provider(), &source);
20305 assert_eq!(preflight.delivery(), FlashblocksDelivery::ExternalUpdates);
20306 assert_eq!(preflight.pending_log_subscriptions(), 0);
20307 assert_eq!(subscriber.flashblocks_rpc_metrics().total_requests(), 0);
20308 assert!(asserter.read_q().is_empty());
20309 }
20310
20311 #[tokio::test]
20312 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
20313 async fn bounded_external_channel_survives_subscriber_move_and_closure_keeps_canonical_stream()
20314 {
20315 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20316 let source = ProviderRef::new("raw-json", 4);
20317 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20318 provider,
20319 SubscriberMode::PubSub,
20320 SubscriberConfig {
20321 preconfirmations: PreconfirmationMode::Preferred,
20322 ..SubscriberConfig::default()
20323 },
20324 );
20325 subscriber
20326 .configure_external_flashblock_updates(source.clone())
20327 .expect("configure external source");
20328 subscriber.chain_id = Some(1);
20329 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20330 subscriber.interests = subscriber.base_interests.clone();
20331 let filter = subscriber.log_stream_filters().remove(0);
20332 let source_id = subscriber.log_source_id(&filter);
20333 let mut streams = SubscriberStreams::new();
20334 streams.push(
20335 SubscriberStreamSource::PubSubLog {
20336 id: source_id,
20337 filter,
20338 },
20339 stream::pending().boxed(),
20340 );
20341 subscriber.state = AlloySubscriberState::Active(streams);
20342 subscriber.sources_dirty = false;
20343
20344 let sender = subscriber
20345 .open_external_flashblock_update_channel(2)
20346 .expect("bounded external queue");
20347 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
20348 let update_stream = subscriber
20349 .connect_source_stream(external.clone())
20350 .await
20351 .expect("attach receiver as subscriber source");
20352 subscriber.install_source_stream(external, update_stream);
20353 subscriber.sources_dirty = false;
20354
20355 let mut adapter = RawJsonFlashblocksAdapter::new(source);
20356 let frame = br#"{
20357 "payload_id":"0x1111111111111111",
20358 "index":0,
20359 "base":{
20360 "parent_hash":"0x0606060606060606060606060606060606060606060606060606060606060606",
20361 "block_number":"0x7",
20362 "timestamp":"0x6553f107"
20363 },
20364 "diff":{
20365 "state_root":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
20366 "block_hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
20367 "transactions":["0x01"]
20368 },
20369 "metadata":{
20370 "block_number":7,
20371 "receipts":{
20372 "0x5fe7f977e71dba2ea1a68e21057beebb9be2ac30c6410aa38d4f3fbe41dcffd2":{
20373 "logs":[{
20374 "address":"0x4242424242424242424242424242424242424242",
20375 "topics":["0x0101010101010101010101010101010101010101010101010101010101010101"],
20376 "data":"0x"
20377 }]
20378 }
20379 }
20380 }
20381 }"#;
20382 let update = adapter
20383 .ingest_json(frame)
20384 .expect("valid raw update")
20385 .expect("snapshot update");
20386 let valid_update = update.clone();
20387 let sending = {
20388 let sender = sender.clone();
20389 tokio::spawn(async move { sender.send(update).await })
20390 };
20391
20392 let preview = subscriber
20393 .next_scoped_batch()
20394 .await
20395 .expect("poll preview")
20396 .expect("preview batch");
20397 assert_eq!(preview.records().len(), 1);
20398 assert!(preview.records()[0].scope().is_preconfirmed());
20399 assert_eq!(
20400 preview.records()[0].context.source,
20401 InputSource::Flashblocks
20402 );
20403 assert!(subscriber.latest_preconfirmation.is_some());
20404 assert_eq!(sending.await.expect("sender task"), Ok(()));
20405
20406 let mut invalid_update = valid_update.clone();
20407 let FlashblockUpdate::Snapshot(snapshot) = &mut invalid_update else {
20408 unreachable!("fixture is a snapshot")
20409 };
20410 snapshot.logs[0].block_hash = Some(B256::repeat_byte(0xee));
20411 let rejecting = {
20412 let sender = sender.clone();
20413 tokio::spawn(async move { sender.send(invalid_update).await })
20414 };
20415 let rejected = subscriber
20416 .next_scoped_batch()
20417 .await
20418 .expect("preferred mode keeps polling")
20419 .expect("rejected update invalidation");
20420 assert!(rejected.preconfirmation_invalidated());
20421 assert!(rejected.records().is_empty());
20422 assert!(subscriber.latest_preconfirmation.is_none());
20423 assert_eq!(
20424 rejecting.await.expect("sender task"),
20425 Err(FlashblockUpdateChannelError::Rejected)
20426 );
20427 subscriber
20428 .ingest_flashblock_update(valid_update)
20429 .expect("rejected generation is ignored thereafter");
20430 assert!(subscriber.latest_preconfirmation.is_none());
20431
20432 let _reset = adapter
20433 .reset(ProviderRef::new("raw-json", 5))
20434 .expect("advance rejected source generation");
20435 let recovered_update = adapter
20436 .ingest_json(frame)
20437 .expect("valid replacement generation")
20438 .expect("replacement snapshot update");
20439 let recovering = {
20440 let sender = sender.clone();
20441 tokio::spawn(async move { sender.send(recovered_update).await })
20442 };
20443 let recovered = subscriber
20444 .next_scoped_batch()
20445 .await
20446 .expect("poll replacement generation")
20447 .expect("replacement preview batch");
20448 assert_eq!(recovered.records().len(), 1);
20449 assert!(matches!(
20450 &recovered.records()[0].context.chain_status,
20451 ChainStatus::Preconfirmed { flashblock }
20452 if flashblock.provider == ProviderRef::new("raw-json", 5)
20453 ));
20454 assert_eq!(recovering.await.expect("sender task"), Ok(()));
20455
20456 drop(sender);
20457 let invalidation = subscriber
20458 .next_scoped_batch()
20459 .await
20460 .expect("poll channel closure")
20461 .expect("closure invalidation");
20462 assert!(invalidation.preconfirmation_invalidated());
20463 assert!(invalidation.records().is_empty());
20464 assert!(subscriber.latest_preconfirmation.is_none());
20465 assert!(matches!(
20466 &subscriber.state,
20467 AlloySubscriberState::Active(streams)
20468 if streams.entries.iter().any(|entry| matches!(
20469 entry.source,
20470 SubscriberStreamSource::PubSubLog { id, .. } if id == source_id
20471 ))
20472 ));
20473 }
20474
20475 #[tokio::test]
20476 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
20477 async fn required_external_channel_closure_fails_the_subscriber_closed() {
20478 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20479 let source = ProviderRef::new("raw-json", 4);
20480 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20481 provider,
20482 SubscriberMode::PubSub,
20483 SubscriberConfig {
20484 preconfirmations: PreconfirmationMode::Required,
20485 ..SubscriberConfig::default()
20486 },
20487 );
20488 subscriber
20489 .configure_external_flashblock_updates(source)
20490 .expect("configure external source");
20491 subscriber.chain_id = Some(1);
20492 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20493 subscriber.interests = subscriber.base_interests.clone();
20494 subscriber.state = AlloySubscriberState::Active(SubscriberStreams::new());
20495 subscriber.sources_dirty = false;
20496
20497 let sender = subscriber
20498 .open_external_flashblock_update_channel(1)
20499 .expect("bounded external queue");
20500 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
20501 let update_stream = subscriber
20502 .connect_source_stream(external.clone())
20503 .await
20504 .expect("attach receiver as subscriber source");
20505 subscriber.install_source_stream(external, update_stream);
20506 subscriber.sources_dirty = false;
20507 drop(sender);
20508
20509 assert!(matches!(
20510 subscriber.next_scoped_batch().await,
20511 Err(SubscriberError::Provider(ref message))
20512 if message.contains("required external Flashblock update channel closed")
20513 ));
20514 }
20515
20516 #[tokio::test]
20517 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
20518 async fn required_external_channel_rejects_a_queued_malformed_update() {
20519 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20520 let source = ProviderRef::new("raw-json", 4);
20521 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20522 provider,
20523 SubscriberMode::PubSub,
20524 SubscriberConfig {
20525 preconfirmations: PreconfirmationMode::Required,
20526 ..SubscriberConfig::default()
20527 },
20528 );
20529 subscriber
20530 .configure_external_flashblock_updates(source.clone())
20531 .expect("configure external source");
20532 subscriber.chain_id = Some(1);
20533 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20534 subscriber.interests = subscriber.base_interests.clone();
20535 subscriber.state = AlloySubscriberState::Active(SubscriberStreams::new());
20536 subscriber.sources_dirty = false;
20537
20538 let sender = subscriber
20539 .open_external_flashblock_update_channel(1)
20540 .expect("bounded external queue");
20541 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
20542 let update_stream = subscriber
20543 .connect_source_stream(external.clone())
20544 .await
20545 .expect("attach receiver as subscriber source");
20546 subscriber.install_source_stream(external, update_stream);
20547 subscriber.sources_dirty = false;
20548
20549 let mut adapter = RawJsonFlashblocksAdapter::new(source);
20550 let mut update = adapter
20551 .ingest_json(
20552 br#"{
20553 "payload_id":"0x1111111111111111",
20554 "index":0,
20555 "base":{
20556 "parent_hash":"0x0606060606060606060606060606060606060606060606060606060606060606",
20557 "block_number":"0x7",
20558 "timestamp":"0x6553f107"
20559 },
20560 "diff":{
20561 "state_root":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
20562 "block_hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
20563 "transactions":[]
20564 },
20565 "metadata":{"block_number":7,"receipts":{}}
20566 }"#,
20567 )
20568 .expect("valid raw frame")
20569 .expect("snapshot update");
20570 let FlashblockUpdate::Snapshot(snapshot) = &mut update else {
20571 unreachable!("fixture is a snapshot")
20572 };
20573 snapshot.flashblock.content_hash = B256::ZERO;
20574 let sending = tokio::spawn(async move { sender.send(update).await });
20575
20576 assert!(matches!(
20577 subscriber.next_scoped_batch().await,
20578 Err(SubscriberError::Provider(ref message))
20579 if message.contains("content commitment is invalid")
20580 ));
20581 assert_eq!(
20582 sending.await.expect("sender task"),
20583 Err(FlashblockUpdateChannelError::Rejected)
20584 );
20585 }
20586
20587 #[tokio::test]
20588 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
20589 async fn bounded_external_channel_reports_capacity_rejection_and_accepts_a_new_generation() {
20590 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20591 let source = ProviderRef::new("raw-json", 4);
20592 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20593 provider,
20594 SubscriberMode::PubSub,
20595 SubscriberConfig {
20596 preconfirmations: PreconfirmationMode::Preferred,
20597 max_pending_records: 1,
20598 ..SubscriberConfig::default()
20599 },
20600 );
20601 subscriber
20602 .configure_external_flashblock_updates(source.clone())
20603 .expect("configure external source");
20604 subscriber.chain_id = Some(1);
20605 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20606 subscriber.interests = subscriber.base_interests.clone();
20607 subscriber.state = AlloySubscriberState::Active(SubscriberStreams::new());
20608 subscriber.sources_dirty = false;
20609
20610 let sender = subscriber
20611 .open_external_flashblock_update_channel(1)
20612 .expect("bounded external queue");
20613 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
20614 let update_stream = subscriber
20615 .connect_source_stream(external.clone())
20616 .await
20617 .expect("attach receiver as subscriber source");
20618 subscriber.install_source_stream(external, update_stream);
20619 subscriber.sources_dirty = false;
20620
20621 let first_frame = br#"{
20622 "payload_id":"0x1111111111111111",
20623 "index":0,
20624 "base":{
20625 "parent_hash":"0x0606060606060606060606060606060606060606060606060606060606060606",
20626 "block_number":"0x7",
20627 "timestamp":"0x6553f107"
20628 },
20629 "diff":{
20630 "state_root":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
20631 "block_hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
20632 "transactions":["0x01"]
20633 },
20634 "metadata":{
20635 "block_number":7,
20636 "receipts":{
20637 "0x5fe7f977e71dba2ea1a68e21057beebb9be2ac30c6410aa38d4f3fbe41dcffd2":{
20638 "logs":[{
20639 "address":"0x4242424242424242424242424242424242424242",
20640 "topics":["0x0101010101010101010101010101010101010101010101010101010101010101"],
20641 "data":"0x"
20642 }]
20643 }
20644 }
20645 }
20646 }"#;
20647 let second_frame = br#"{
20648 "payload_id":"0x1111111111111111",
20649 "index":1,
20650 "diff":{
20651 "state_root":"0xabababababababababababababababababababababababababababababababab",
20652 "block_hash":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
20653 "transactions":["0x02"]
20654 },
20655 "metadata":{
20656 "block_number":7,
20657 "receipts":{
20658 "0xf2ee15ea639b73fa3db9b34a245bdfa015c260c598b211bf05a1ecc4b3e3b4f2":{
20659 "logs":[
20660 {"address":"0x4444444444444444444444444444444444444444","topics":[],"data":"0x"},
20661 {"address":"0x4545454545454545454545454545454545454545","topics":[],"data":"0x"}
20662 ]
20663 }
20664 }
20665 }
20666 }"#;
20667 let mut adapter = RawJsonFlashblocksAdapter::new(source);
20668 let first = adapter
20669 .ingest_json(first_frame)
20670 .expect("valid first frame")
20671 .expect("first snapshot");
20672 let first_send = {
20673 let sender = sender.clone();
20674 tokio::spawn(async move { sender.send(first).await })
20675 };
20676 let first_batch = subscriber
20677 .next_scoped_batch()
20678 .await
20679 .expect("poll first preview")
20680 .expect("first preview batch");
20681 assert_eq!(first_batch.records().len(), 1);
20682 assert_eq!(first_send.await.expect("sender task"), Ok(()));
20683
20684 let oversized = adapter
20685 .ingest_json(second_frame)
20686 .expect("valid oversized delta")
20687 .expect("oversized standardized snapshot");
20688 let rejected_send = {
20689 let sender = sender.clone();
20690 tokio::spawn(async move { sender.send(oversized).await })
20691 };
20692 let invalidation = subscriber
20693 .next_scoped_batch()
20694 .await
20695 .expect("poll capacity rejection")
20696 .expect("capacity invalidation batch");
20697 assert!(invalidation.preconfirmation_invalidated());
20698 assert_eq!(
20699 rejected_send.await.expect("sender task"),
20700 Err(FlashblockUpdateChannelError::Rejected)
20701 );
20702 assert_eq!(subscriber.rejected_external_flashblock_generation, None);
20703
20704 let _ = adapter
20705 .reset(ProviderRef::new("raw-json", 5))
20706 .expect("advance after local capacity rejection");
20707 let recovered = adapter
20708 .ingest_json(first_frame)
20709 .expect("valid recovered frame")
20710 .expect("recovered snapshot");
20711 let recovered_send = {
20712 let sender = sender.clone();
20713 tokio::spawn(async move { sender.send(recovered).await })
20714 };
20715 let recovered_batch = subscriber
20716 .next_scoped_batch()
20717 .await
20718 .expect("poll recovered generation")
20719 .expect("recovered preview batch");
20720 assert_eq!(recovered_batch.records().len(), 1);
20721 assert!(matches!(
20722 &recovered_batch.records()[0].context.chain_status,
20723 ChainStatus::Preconfirmed { flashblock }
20724 if flashblock.provider == ProviderRef::new("raw-json", 5)
20725 ));
20726 assert_eq!(recovered_send.await.expect("sender task"), Ok(()));
20727 }
20728
20729 #[tokio::test]
20730 async fn certified_canonical_heads_are_deduplicated_and_reject_placeholder_hashes() {
20731 let asserter = Asserter::new();
20732 let certified = rpc_block(101, B256::repeat_byte(0x65));
20733 asserter.push_success(&Some(certified.clone()));
20734 asserter.push_success(&Some(certified));
20735 asserter.push_success(&Some(rpc_block(102, B256::ZERO)));
20736 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
20737 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20738 provider,
20739 SubscriberMode::PubSub,
20740 SubscriberConfig::default(),
20741 );
20742
20743 assert!(matches!(
20744 subscriber
20745 .fetch_certified_canonical_head()
20746 .await
20747 .expect("first certified head"),
20748 Some(SubscriberEvent::BlockHeader(_))
20749 ));
20750 assert!(
20751 subscriber
20752 .fetch_certified_canonical_head()
20753 .await
20754 .expect("duplicate certified head")
20755 .is_none()
20756 );
20757 assert!(matches!(
20758 subscriber.fetch_certified_canonical_head().await,
20759 Err(SubscriberError::Provider(ref message))
20760 if message.contains("placeholder hash")
20761 ));
20762 }
20763
20764 #[tokio::test]
20765 async fn canonical_head_certification_times_out_a_silent_provider() {
20766 let provider =
20767 ProviderBuilder::new().connect_client(RpcClient::new(NeverRespondingTransport, true));
20768 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20769 provider,
20770 SubscriberMode::PubSub,
20771 SubscriberConfig {
20772 preconfirmations: PreconfirmationMode::Required,
20773 canonical_head_request_timeout: Duration::from_millis(10),
20774 ..SubscriberConfig::default()
20775 },
20776 );
20777 subscriber.chain_id = Some(8_453);
20778
20779 let result = tokio::time::timeout(
20780 Duration::from_millis(100),
20781 subscriber.fetch_certified_canonical_head(),
20782 )
20783 .await
20784 .expect("subscriber must bound a silent provider request");
20785 assert!(matches!(
20786 result,
20787 Err(SubscriberError::Provider(ref message))
20788 if message.contains("canonical head certification timed out")
20789 ));
20790 }
20791
20792 #[tokio::test]
20793 async fn optimism_canonical_head_is_the_exact_parent_of_pending() {
20794 let asserter = Asserter::new();
20795 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
20796 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20797 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20798 provider,
20799 SubscriberMode::PubSub,
20800 SubscriberConfig {
20801 preconfirmations: PreconfirmationMode::Required,
20802 ..SubscriberConfig::default()
20803 },
20804 );
20805 subscriber.chain_id = Some(10);
20806 subscriber.interests = vec![ReactiveInterest::Blocks(BlockInterest::default())];
20807
20808 let event = subscriber
20809 .fetch_certified_canonical_head()
20810 .await
20811 .expect("OP pending parent can be certified")
20812 .expect("the first certified parent is emitted");
20813 let SubscriberEvent::BlockHeader(header) = event else {
20814 panic!("expected a certified canonical block header")
20815 };
20816 assert_eq!(header.number(), 100);
20817 assert_eq!(header.hash, B256::repeat_byte(0x64));
20818 assert_eq!(
20819 subscriber
20820 .flashblocks_rpc_metrics()
20821 .pending_block_requests(),
20822 1
20823 );
20824 assert_eq!(
20825 subscriber
20826 .flashblocks_rpc_metrics()
20827 .canonical_head_requests(),
20828 1
20829 );
20830 assert!(asserter.read_q().is_empty());
20831 }
20832
20833 #[test]
20834 fn optimism_uses_one_bounded_pending_state_stream() {
20835 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20836 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20837 provider,
20838 SubscriberMode::PubSub,
20839 SubscriberConfig {
20840 preconfirmations: PreconfirmationMode::Required,
20841 ..SubscriberConfig::default()
20842 },
20843 )
20844 .with_provider_ref(ProviderRef::new("op-paid", 11));
20845 subscriber.chain_id = Some(10);
20846 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
20847 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
20848 local_matcher: None,
20849 route_key: None,
20850 })];
20851 subscriber.interests = subscriber.base_interests.clone();
20852
20853 let sources = subscriber.pubsub_stream_sources();
20854 assert_eq!(
20855 sources
20856 .iter()
20857 .filter(|source| matches!(source, SubscriberStreamSource::OpPendingFlashblocks))
20858 .count(),
20859 1
20860 );
20861 assert!(sources.iter().all(|source| !matches!(
20862 source,
20863 SubscriberStreamSource::BaseFlashblocks | SubscriberStreamSource::BasePendingLog { .. }
20864 )));
20865 }
20866
20867 #[test]
20868 fn optimism_default_receipt_budget_reserves_every_fixed_sampler_method() {
20869 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20870 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20871 provider,
20872 SubscriberMode::PubSub,
20873 SubscriberConfig {
20874 preconfirmations: PreconfirmationMode::Required,
20875 ..SubscriberConfig::default()
20876 },
20877 );
20878 subscriber.chain_id = Some(10);
20879 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20880 subscriber.interests = subscriber.base_interests.clone();
20881
20882 assert_eq!(
20886 subscriber.pending_receipt_requests_per_second_capacity(),
20887 28
20888 );
20889 assert_eq!(subscriber.pending_receipt_requests_per_tick_capacity(), 7);
20890
20891 subscriber
20892 .interests
20893 .push(ReactiveInterest::Blocks(BlockInterest::default()));
20894 assert_eq!(
20895 subscriber.pending_receipt_requests_per_second_capacity(),
20896 24
20897 );
20898 assert_eq!(subscriber.pending_receipt_requests_per_tick_capacity(), 6);
20899 subscriber.interests.pop();
20900
20901 for _ in 0..4 {
20902 assert!(subscriber.reserve_flashblock_rpc_methods(3));
20903 assert_eq!(subscriber.pending_receipt_request_allowance(), 7);
20904 assert!(subscriber.reserve_flashblock_rpc_methods(7));
20905 }
20906 assert!(!subscriber.reserve_flashblock_rpc_methods(1));
20907 subscriber.reset_flashblock_tracking();
20908 assert!(
20909 !subscriber.reserve_flashblock_rpc_methods(1),
20910 "a reconnect must not reset an endpoint's rolling quota window"
20911 );
20912 }
20913
20914 #[test]
20915 fn flashblocks_config_rejects_a_zero_rpc_budget() {
20916 let config = SubscriberConfig {
20917 preconfirmations: PreconfirmationMode::Required,
20918 max_flashblock_rpc_requests_per_second: 0,
20919 ..SubscriberConfig::default()
20920 };
20921
20922 assert!(matches!(
20923 validate_subscriber_config(&config),
20924 Err(SubscriberError::InvalidConfig(
20925 "SubscriberConfig::max_flashblock_rpc_requests_per_second must be greater than zero"
20926 ))
20927 ));
20928 }
20929
20930 #[test]
20931 fn flashblocks_config_rejects_a_zero_canonical_head_request_timeout() {
20932 let config = SubscriberConfig {
20933 preconfirmations: PreconfirmationMode::Required,
20934 canonical_head_request_timeout: Duration::ZERO,
20935 ..SubscriberConfig::default()
20936 };
20937
20938 assert!(matches!(
20939 validate_subscriber_config(&config),
20940 Err(SubscriberError::InvalidConfig(
20941 "SubscriberConfig::canonical_head_request_timeout must be greater than zero"
20942 ))
20943 ));
20944 }
20945
20946 #[tokio::test]
20947 async fn optimism_preflight_rejects_a_budget_without_receipt_capacity() {
20948 let asserter = Asserter::new();
20949 asserter.push_success(&serde_json::json!(["flashblocksv1"]));
20950 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20951 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20952 provider,
20953 SubscriberMode::PubSub,
20954 SubscriberConfig {
20955 preconfirmations: PreconfirmationMode::Required,
20956 max_flashblock_rpc_requests_per_second: 15,
20959 ..SubscriberConfig::default()
20960 },
20961 )
20962 .with_provider_ref(ProviderRef::new("op-paid", 12));
20963 subscriber.chain_id = Some(10);
20964 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20965 subscriber.interests = subscriber.base_interests.clone();
20966 let desired = subscriber.pubsub_stream_sources();
20967 let mut streams = SubscriberStreams::new();
20968 for source in desired {
20969 streams.push(source, stream::pending().boxed());
20970 }
20971 subscriber.state = AlloySubscriberState::Active(streams);
20972 subscriber.sources_dirty = false;
20973 assert!(matches!(
20974 subscriber.establish_flashblocks_preflight(10).await,
20975 Err(SubscriberError::InvalidConfig(message))
20976 if message.contains("leaves no capacity for OP transaction receipts")
20977 ));
20978 assert!(asserter.read_q().is_empty());
20979 }
20980
20981 #[cfg(feature = "reactive-ws")]
20982 #[tokio::test]
20983 async fn flashblocks_preflight_proves_chain_and_both_subscription_lanes() {
20984 let asserter = Asserter::new();
20985 asserter.push_success(&serde_json::json!({"flashblocks": true}));
20986 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
20987 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20988 provider,
20989 SubscriberMode::PubSub,
20990 SubscriberConfig {
20991 preconfirmations: PreconfirmationMode::Required,
20992 ..SubscriberConfig::default()
20993 },
20994 )
20995 .with_provider_ref(ProviderRef::new("base-paid", 7));
20996 subscriber.chain_id = Some(8_453);
20997 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20998 subscriber.interests = subscriber.base_interests.clone();
20999 let desired = subscriber.pubsub_stream_sources();
21000 let mut streams = SubscriberStreams::new();
21001 for source in desired {
21002 streams.push(source, stream::pending().boxed());
21003 }
21004 subscriber.state = AlloySubscriberState::Active(streams);
21005 subscriber.sources_dirty = false;
21006
21007 let preflight = subscriber
21008 .establish_flashblocks_preflight(8_453)
21009 .await
21010 .expect("preflight succeeds");
21011
21012 assert_eq!(preflight.chain_id(), 8_453);
21013 assert_eq!(preflight.provider(), &ProviderRef::new("base-paid", 7));
21014 assert_eq!(
21015 preflight.delivery(),
21016 FlashblocksDelivery::NativeSubscriptions
21017 );
21018 assert_eq!(preflight.pending_log_subscriptions(), 1);
21019 assert_eq!(preflight.pending_log_filters(), 1);
21020 assert_eq!(
21021 preflight.advertised_capabilities(),
21022 Some(&serde_json::json!({"flashblocks": true}))
21023 );
21024 }
21025
21026 #[tokio::test]
21027 async fn optimism_preflight_probes_pending_state_without_native_subscriptions() {
21028 let asserter = Asserter::new();
21029 asserter.push_success(&serde_json::json!(["flashblocksv1"]));
21030 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
21031 asserter.push_success(&Vec::<Log>::new());
21032 asserter.push_success(&serde_json::json!([]));
21033 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21034 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21035 provider,
21036 SubscriberMode::PubSub,
21037 SubscriberConfig {
21038 preconfirmations: PreconfirmationMode::Required,
21039 ..SubscriberConfig::default()
21040 },
21041 )
21042 .with_provider_ref(ProviderRef::new("op-paid", 12));
21043 subscriber.chain_id = Some(10);
21044 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21045 subscriber.interests = subscriber.base_interests.clone();
21046 let desired = subscriber.pubsub_stream_sources();
21047 let mut streams = SubscriberStreams::new();
21048 for source in desired {
21049 streams.push(source, stream::pending().boxed());
21050 }
21051 subscriber.state = AlloySubscriberState::Active(streams);
21052 subscriber.sources_dirty = false;
21053
21054 let preflight = subscriber
21055 .establish_flashblocks_preflight(10)
21056 .await
21057 .expect("Optimism pending-state preflight succeeds");
21058
21059 assert_eq!(preflight.chain_id(), 10);
21060 assert_eq!(preflight.provider(), &ProviderRef::new("op-paid", 12));
21061 assert_eq!(
21062 preflight.delivery(),
21063 FlashblocksDelivery::PendingStatePolling
21064 );
21065 assert_eq!(preflight.pending_log_subscriptions(), 0);
21066 assert_eq!(preflight.pending_log_filters(), 1);
21067 assert_eq!(
21068 preflight.advertised_capabilities(),
21069 Some(&serde_json::json!(["flashblocksv1"]))
21070 );
21071 assert!(asserter.read_q().is_empty());
21072 }
21073
21074 #[test]
21075 fn optimism_full_pending_block_normalizes_op_transaction_types_to_hashes() {
21076 let transaction_hash = B256::repeat_byte(0x7e);
21077 let mut value = serde_json::to_value(rpc_block(101, B256::ZERO))
21078 .expect("serialize pending block fixture");
21079 value["transactions"] = serde_json::json!([{
21080 "type": "0x7e",
21081 "hash": transaction_hash,
21082 "sourceHash": B256::repeat_byte(0x11),
21083 "from": Address::repeat_byte(0x22),
21084 "to": Address::repeat_byte(0x33)
21085 }]);
21086
21087 let block = normalize_op_pending_block::<Ethereum>(value)
21088 .expect("OP-specific transaction bodies are reduced to hashes");
21089
21090 assert_eq!(
21091 block.transactions().as_hashes(),
21092 Some(&[transaction_hash][..])
21093 );
21094 }
21095
21096 #[tokio::test]
21097 async fn optimism_sampler_does_not_retry_malformed_pending_content() {
21098 let asserter = Asserter::new();
21099 let mut pending = serde_json::to_value(rpc_block(101, B256::ZERO))
21100 .expect("serialize pending block fixture");
21101 pending["transactions"] = serde_json::json!([{"type": "0x7e"}]);
21102 asserter.push_success(&Some(pending));
21103 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21104 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21105 provider,
21106 SubscriberMode::PubSub,
21107 SubscriberConfig {
21108 preconfirmations: PreconfirmationMode::Required,
21109 ..SubscriberConfig::default()
21110 },
21111 )
21112 .with_provider_ref(ProviderRef::new("op-paid", 12));
21113 subscriber.chain_id = Some(10);
21114
21115 let error = match subscriber
21116 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21117 .await
21118 {
21119 Err(error) => error,
21120 Ok(_) => panic!("malformed provider content must fail immediately"),
21121 };
21122 assert!(
21123 error
21124 .to_string()
21125 .contains("transaction is missing its hash")
21126 );
21127 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
21128 assert!(asserter.read_q().is_empty());
21129 }
21130
21131 #[tokio::test]
21132 async fn optimism_sampler_certifies_the_pending_block_by_exact_parent_hash() {
21133 let asserter = Asserter::new();
21134 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
21135 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
21136 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21137 provider,
21138 SubscriberMode::PubSub,
21139 SubscriberConfig {
21140 preconfirmations: PreconfirmationMode::Required,
21141 ..SubscriberConfig::default()
21142 },
21143 )
21144 .with_provider_ref(ProviderRef::new("op-paid", 12));
21145 subscriber.chain_id = Some(10);
21146
21147 assert!(
21148 subscriber
21149 .fetch_pending_flashblock(None)
21150 .await
21151 .expect("the exact parent certifies the pending payload")
21152 .is_some()
21153 );
21154 }
21155
21156 #[tokio::test]
21157 async fn optimism_sampler_rejects_a_nonconsecutive_pending_parent() {
21158 let asserter = Asserter::new();
21159 asserter.push_success(&Some(rpc_block(101, B256::ZERO)));
21160 asserter.push_success(&Some(rpc_block(99, B256::repeat_byte(0x64))));
21161 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21162 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21163 provider,
21164 SubscriberMode::PubSub,
21165 SubscriberConfig {
21166 preconfirmations: PreconfirmationMode::Required,
21167 ..SubscriberConfig::default()
21168 },
21169 )
21170 .with_provider_ref(ProviderRef::new("op-paid", 12));
21171 subscriber.chain_id = Some(10);
21172
21173 assert!(matches!(
21174 subscriber.fetch_pending_flashblock(None).await,
21175 Err(PendingFlashblockPollError::Integrity(SubscriberError::Provider(
21176 ref message
21177 ))) if message.contains("does not extend its exact certified parent")
21178 ));
21179 assert!(asserter.read_q().is_empty());
21180 }
21181
21182 #[tokio::test]
21183 async fn optimism_sampler_rechecks_unchanged_content_without_republishing_logs() {
21184 let asserter = Asserter::new();
21185 let pending = rpc_block(101, B256::ZERO);
21186 queue_op_pending(&asserter, pending.clone());
21187 asserter.push_success(&Vec::<Log>::new());
21188 queue_op_pending(&asserter, pending);
21189 asserter.push_success(&Vec::<Log>::new());
21190 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21191 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21192 provider,
21193 SubscriberMode::PubSub,
21194 SubscriberConfig {
21195 preconfirmations: PreconfirmationMode::Required,
21196 ..SubscriberConfig::default()
21197 },
21198 )
21199 .with_provider_ref(ProviderRef::new("op-paid", 12));
21200 subscriber.chain_id = Some(10);
21201 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21202 subscriber.interests = subscriber.base_interests.clone();
21203
21204 assert!(
21205 subscriber
21206 .fetch_pending_flashblock(None)
21207 .await
21208 .expect("first cumulative pending view")
21209 .is_some()
21210 );
21211 assert!(
21212 subscriber
21213 .fetch_pending_flashblock(None)
21214 .await
21215 .expect("duplicate cumulative pending view")
21216 .is_none()
21217 );
21218
21219 assert_eq!(
21220 subscriber.flashblocks_rpc_metrics(),
21221 FlashblocksRpcMetrics {
21222 capability_requests: 0,
21223 provider_pair_chain_requests: 0,
21224 canonical_head_requests: 2,
21225 pending_block_requests: 2,
21226 pending_log_requests: 2,
21227 pending_receipt_requests: 0,
21228 pending_receipts_completed: 0,
21229 pending_receipts_unavailable: 0,
21230 failed_requests: 0,
21231 raced_samples: 0,
21232 suppressed_canonical_head_polls: 0,
21233 }
21234 );
21235 assert!(asserter.read_q().is_empty());
21236 }
21237
21238 #[tokio::test]
21239 async fn optimism_sampler_rechecks_logs_for_an_unchanged_pending_view() {
21240 let asserter = Asserter::new();
21241 let transaction = B256::repeat_byte(0x42);
21242 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21243 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
21244 );
21245 let mut log = rpc_log(false);
21246 log.block_number = Some(101);
21247 log.block_hash = Some(B256::repeat_byte(0xa2));
21248 log.transaction_hash = Some(transaction);
21249 log.transaction_index = Some(0);
21250 log.log_index = Some(0);
21251 queue_op_pending(&asserter, pending.clone());
21252 asserter.push_success(&Vec::<Log>::new());
21253 asserter.push_success(&serde_json::Value::Null);
21254 queue_op_pending(&asserter, pending);
21255 asserter.push_success(&vec![log]);
21256 asserter.push_success(&serde_json::Value::Null);
21257 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21258 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21259 provider,
21260 SubscriberMode::PubSub,
21261 SubscriberConfig {
21262 preconfirmations: PreconfirmationMode::Required,
21263 ..SubscriberConfig::default()
21264 },
21265 )
21266 .with_provider_ref(ProviderRef::new("op-paid", 12));
21267 subscriber.chain_id = Some(10);
21268 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21269 subscriber.interests = subscriber.base_interests.clone();
21270
21271 assert!(matches!(
21272 subscriber
21273 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21274 .await
21275 .expect("first pending view is coherent"),
21276 Some(SubscriberEvent::FlashblockObserved)
21277 ));
21278 assert!(matches!(
21279 subscriber
21280 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21281 .await
21282 .expect("the unchanged view is checked again for lagging logs"),
21283 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
21284 ));
21285 assert!(asserter.read_q().is_empty());
21286 }
21287
21288 #[tokio::test]
21289 async fn optimism_sampler_hydrates_exact_receipts_when_filtered_logs_are_empty() {
21290 let asserter = Asserter::new();
21291 let transaction = B256::repeat_byte(0x42);
21292 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21293 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
21294 );
21295 let mut log = rpc_log(false);
21296 log.block_number = Some(101);
21297 log.block_hash = Some(B256::repeat_byte(0xa2));
21298 log.transaction_hash = Some(transaction);
21299 log.transaction_index = Some(0);
21300 log.log_index = Some(0);
21301 queue_op_pending(&asserter, pending);
21302 asserter.push_success(&Vec::<Log>::new());
21303 asserter.push_success(&serde_json::json!({
21304 "transactionHash": transaction,
21305 "logs": [log]
21306 }));
21307 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21308 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21309 provider,
21310 SubscriberMode::PubSub,
21311 SubscriberConfig {
21312 preconfirmations: PreconfirmationMode::Required,
21313 ..SubscriberConfig::default()
21314 },
21315 )
21316 .with_provider_ref(ProviderRef::new("op-paid", 12));
21317 subscriber.chain_id = Some(10);
21318 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21319 subscriber.interests = subscriber.base_interests.clone();
21320
21321 let event = subscriber
21322 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21323 .await
21324 .expect("pending receipt fallback succeeds");
21325 assert!(matches!(
21326 event,
21327 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
21328 ));
21329 assert_eq!(
21330 subscriber
21331 .flashblocks_rpc_metrics()
21332 .pending_receipt_requests(),
21333 1
21334 );
21335 assert!(asserter.read_q().is_empty());
21336 }
21337
21338 #[tokio::test]
21339 async fn optimism_receipt_hydration_is_bounded_and_resumes_on_the_next_tick() {
21340 let asserter = Asserter::new();
21341 let transaction_a = B256::repeat_byte(0x41);
21342 let transaction_b = B256::repeat_byte(0x42);
21343 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21344 alloy_network::primitives::BlockTransactions::Hashes(vec![
21345 transaction_a,
21346 transaction_b,
21347 ]),
21348 );
21349 let mut log = rpc_log(false);
21350 log.block_number = Some(101);
21351 log.transaction_hash = Some(transaction_b);
21352 log.transaction_index = Some(1);
21353 queue_op_pending(&asserter, pending.clone());
21354 asserter.push_success(&Vec::<Log>::new());
21355 asserter.push_success(&serde_json::json!({
21356 "transactionHash": transaction_a,
21357 "logs": []
21358 }));
21359 queue_op_pending(&asserter, pending);
21360 asserter.push_success(&Vec::<Log>::new());
21361 asserter.push_success(&serde_json::json!({
21362 "transactionHash": transaction_b,
21363 "logs": [log]
21364 }));
21365 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21366 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21367 provider,
21368 SubscriberMode::PubSub,
21369 SubscriberConfig {
21370 preconfirmations: PreconfirmationMode::Required,
21371 max_pending_transaction_receipts_per_tick: 1,
21372 ..SubscriberConfig::default()
21373 },
21374 )
21375 .with_provider_ref(ProviderRef::new("op-paid", 12));
21376 subscriber.chain_id = Some(10);
21377 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21378 subscriber.interests = subscriber.base_interests.clone();
21379
21380 assert!(matches!(
21381 subscriber
21382 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21383 .await
21384 .expect("the first bounded receipt is hydrated"),
21385 Some(SubscriberEvent::FlashblockObserved)
21386 ));
21387 assert!(matches!(
21388 subscriber
21389 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21390 .await
21391 .expect("the remaining receipt is hydrated on the next tick"),
21392 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
21393 ));
21394 assert_eq!(
21395 subscriber
21396 .flashblocks_rpc_metrics()
21397 .pending_receipt_requests(),
21398 2
21399 );
21400 assert_eq!(subscriber.preconfirmed_receipted_transactions.len(), 2);
21401 assert!(asserter.read_q().is_empty());
21402 }
21403
21404 #[tokio::test]
21405 async fn optimism_receipt_hydration_prioritizes_unattempted_hashes_over_null_retries() {
21406 let asserter = Asserter::new();
21407 let transaction_a = B256::repeat_byte(0x41);
21408 let transaction_b = B256::repeat_byte(0x42);
21409 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21410 alloy_network::primitives::BlockTransactions::Hashes(vec![
21411 transaction_a,
21412 transaction_b,
21413 ]),
21414 );
21415 let mut log = rpc_log(false);
21416 log.block_number = Some(101);
21417 log.transaction_hash = Some(transaction_b);
21418 log.transaction_index = Some(1);
21419 queue_op_pending(&asserter, pending.clone());
21420 asserter.push_success(&Vec::<Log>::new());
21421 asserter.push_success(&serde_json::Value::Null);
21422 queue_op_pending(&asserter, pending);
21423 asserter.push_success(&Vec::<Log>::new());
21424 asserter.push_success(&serde_json::json!({
21425 "transactionHash": transaction_b,
21426 "logs": [log]
21427 }));
21428 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21429 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21430 provider,
21431 SubscriberMode::PubSub,
21432 SubscriberConfig {
21433 preconfirmations: PreconfirmationMode::Required,
21434 max_pending_transaction_receipts_per_tick: 1,
21435 ..SubscriberConfig::default()
21436 },
21437 )
21438 .with_provider_ref(ProviderRef::new("op-paid", 12));
21439 subscriber.chain_id = Some(10);
21440 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21441 subscriber.interests = subscriber.base_interests.clone();
21442
21443 assert!(matches!(
21444 subscriber
21445 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21446 .await
21447 .expect("the first null receipt remains retryable"),
21448 Some(SubscriberEvent::FlashblockObserved)
21449 ));
21450 assert!(matches!(
21451 subscriber
21452 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21453 .await
21454 .expect("the next unattempted receipt is not starved"),
21455 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
21456 ));
21457 assert!(
21458 subscriber
21459 .preconfirmed_unavailable_receipts
21460 .contains(&transaction_a)
21461 );
21462 assert!(
21463 subscriber
21464 .preconfirmed_receipted_transactions
21465 .contains(&transaction_b)
21466 );
21467 assert!(asserter.read_q().is_empty());
21468 }
21469
21470 #[tokio::test]
21471 async fn optimism_receipt_batch_commits_dedupe_only_after_every_response_succeeds() {
21472 let asserter = Asserter::new();
21473 let transaction_a = B256::repeat_byte(0x41);
21474 let transaction_b = B256::repeat_byte(0x42);
21475 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21476 alloy_network::primitives::BlockTransactions::Hashes(vec![
21477 transaction_a,
21478 transaction_b,
21479 ]),
21480 );
21481 let mut log = rpc_log(false);
21482 log.block_number = Some(101);
21483 log.transaction_hash = Some(transaction_b);
21484 log.transaction_index = Some(1);
21485 queue_op_pending(&asserter, pending.clone());
21486 asserter.push_success(&Vec::<Log>::new());
21487 asserter.push_success(&serde_json::json!({
21488 "transactionHash": transaction_a,
21489 "logs": []
21490 }));
21491 asserter.push_failure_msg("receipt temporarily unavailable");
21492 queue_op_pending(&asserter, pending);
21493 asserter.push_success(&Vec::<Log>::new());
21494 asserter.push_success(&serde_json::json!({
21495 "transactionHash": transaction_a,
21496 "logs": []
21497 }));
21498 asserter.push_success(&serde_json::json!({
21499 "transactionHash": transaction_b,
21500 "logs": [log]
21501 }));
21502 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21503 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21504 provider,
21505 SubscriberMode::PubSub,
21506 SubscriberConfig {
21507 preconfirmations: PreconfirmationMode::Required,
21508 max_pending_transaction_receipts_per_tick: 2,
21509 max_consecutive_flashblock_poll_failures: 2,
21510 ..SubscriberConfig::default()
21511 },
21512 )
21513 .with_provider_ref(ProviderRef::new("op-paid", 12));
21514 subscriber.chain_id = Some(10);
21515 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21516 subscriber.interests = subscriber.base_interests.clone();
21517
21518 assert!(
21519 subscriber
21520 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21521 .await
21522 .expect("one failed receipt response remains retryable")
21523 .is_none()
21524 );
21525 assert!(subscriber.preconfirmed_receipted_transactions.is_empty());
21526 assert!(matches!(
21527 subscriber
21528 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21529 .await
21530 .expect("the complete batch is retried transactionally"),
21531 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
21532 ));
21533 assert_eq!(subscriber.preconfirmed_receipted_transactions.len(), 2);
21534 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 1);
21535 assert_eq!(
21536 subscriber
21537 .flashblocks_rpc_metrics()
21538 .pending_receipt_requests(),
21539 4
21540 );
21541 assert!(asserter.read_q().is_empty());
21542 }
21543
21544 #[tokio::test]
21545 async fn optimism_sampler_rejects_a_receipt_for_a_different_transaction() {
21546 let asserter = Asserter::new();
21547 let sampled_transaction = B256::repeat_byte(0x41);
21548 let advanced_transaction = B256::repeat_byte(0x42);
21549 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21550 alloy_network::primitives::BlockTransactions::Hashes(vec![sampled_transaction]),
21551 );
21552 let mut log = rpc_log(false);
21553 log.block_number = Some(101);
21554 log.transaction_hash = Some(advanced_transaction);
21555 queue_op_pending(&asserter, pending);
21556 asserter.push_success(&Vec::<Log>::new());
21557 asserter.push_success(&serde_json::json!({
21558 "transactionHash": advanced_transaction,
21559 "logs": [log]
21560 }));
21561 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21562 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21563 provider,
21564 SubscriberMode::PubSub,
21565 SubscriberConfig {
21566 preconfirmations: PreconfirmationMode::Required,
21567 ..SubscriberConfig::default()
21568 },
21569 )
21570 .with_provider_ref(ProviderRef::new("op-paid", 12));
21571 subscriber.chain_id = Some(10);
21572 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21573 subscriber.interests = subscriber.base_interests.clone();
21574
21575 let error = match subscriber
21576 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21577 .await
21578 {
21579 Err(error) => error,
21580 Ok(_) => panic!("a receipt for another transaction must fail closed"),
21581 };
21582 assert!(
21583 error
21584 .to_string()
21585 .contains("hash disagrees with its request")
21586 );
21587 assert!(asserter.read_q().is_empty());
21588 }
21589
21590 #[tokio::test]
21591 async fn optimism_sampler_revokes_then_recovers_from_a_regressive_pending_view() {
21592 let asserter = Asserter::new();
21593 let transaction_a = B256::repeat_byte(0x41);
21594 let transaction_b = B256::repeat_byte(0x42);
21595 let first = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21596 alloy_network::primitives::BlockTransactions::Hashes(vec![
21597 transaction_a,
21598 transaction_b,
21599 ]),
21600 );
21601 let regressive = rpc_block(101, B256::repeat_byte(0xa2)).with_transactions(
21602 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction_a]),
21603 );
21604 queue_op_pending(&asserter, first);
21605 asserter.push_success(&Vec::<Log>::new());
21606 asserter.push_success(&serde_json::Value::Null);
21607 asserter.push_success(&serde_json::Value::Null);
21608 queue_op_pending(&asserter, regressive.clone());
21609 queue_op_pending(&asserter, regressive);
21610 asserter.push_success(&Vec::<Log>::new());
21611 asserter.push_success(&serde_json::Value::Null);
21612 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21613 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21614 provider,
21615 SubscriberMode::PubSub,
21616 SubscriberConfig {
21617 preconfirmations: PreconfirmationMode::Required,
21618 ..SubscriberConfig::default()
21619 },
21620 )
21621 .with_provider_ref(ProviderRef::new("op-paid", 12));
21622 subscriber.chain_id = Some(10);
21623 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21624 subscriber.interests = subscriber.base_interests.clone();
21625
21626 assert!(
21627 subscriber
21628 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21629 .await
21630 .expect("first pending view is coherent")
21631 .is_some()
21632 );
21633 assert!(matches!(
21634 subscriber
21635 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21636 .await
21637 .expect("regression revokes instead of terminating the stream"),
21638 Some(SubscriberEvent::FlashblockInvalidated)
21639 ));
21640 assert!(subscriber.latest_preconfirmation.is_none());
21641 assert!(subscriber.pending_preconfirmation_invalidation);
21642 assert!(
21643 subscriber
21644 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21645 .await
21646 .expect("a later coherent view establishes a fresh snapshot")
21647 .is_some()
21648 );
21649 assert!(subscriber.latest_preconfirmation.is_some());
21650 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 12);
21651 assert!(asserter.read_q().is_empty());
21652 }
21653
21654 #[tokio::test]
21655 async fn optimism_new_quiet_payload_revokes_the_previous_snapshot() {
21656 let asserter = Asserter::new();
21657 let first = rpc_block(101, B256::repeat_byte(0xa1));
21658 let second = rpc_block(102, B256::repeat_byte(0xa2));
21659 queue_op_pending(&asserter, first);
21660 asserter.push_success(&Vec::<Log>::new());
21661 queue_op_pending(&asserter, second);
21662 asserter.push_success(&Vec::<Log>::new());
21663 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21664 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21665 provider,
21666 SubscriberMode::PubSub,
21667 SubscriberConfig {
21668 preconfirmations: PreconfirmationMode::Required,
21669 ..SubscriberConfig::default()
21670 },
21671 )
21672 .with_provider_ref(ProviderRef::new("op-paid", 12));
21673 subscriber.chain_id = Some(10);
21674 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21675 subscriber.interests = subscriber.base_interests.clone();
21676
21677 subscriber
21678 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21679 .await
21680 .expect("first quiet payload is observed");
21681 assert!(!subscriber.pending_preconfirmation_invalidation);
21682 subscriber
21683 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21684 .await
21685 .expect("replacement quiet payload is observed");
21686 assert!(subscriber.pending_preconfirmation_invalidation);
21687 assert_eq!(
21688 subscriber
21689 .latest_preconfirmation
21690 .as_ref()
21691 .map(|flashblock| flashblock.block_number),
21692 Some(102)
21693 );
21694 assert!(asserter.read_q().is_empty());
21695 }
21696
21697 #[tokio::test]
21698 async fn optimism_sampler_rejects_malformed_pending_receipts() {
21699 let asserter = Asserter::new();
21700 let transaction = B256::repeat_byte(0x42);
21701 let pending = rpc_block(101, B256::ZERO).with_transactions(
21702 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
21703 );
21704 queue_op_pending(&asserter, pending);
21705 asserter.push_success(&Vec::<Log>::new());
21706 asserter.push_success(&serde_json::json!({"transactionHash": transaction}));
21707 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21708 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21709 provider,
21710 SubscriberMode::PubSub,
21711 SubscriberConfig {
21712 preconfirmations: PreconfirmationMode::Required,
21713 ..SubscriberConfig::default()
21714 },
21715 )
21716 .with_provider_ref(ProviderRef::new("op-paid", 12));
21717 subscriber.chain_id = Some(10);
21718 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21719 subscriber.interests = subscriber.base_interests.clone();
21720
21721 let error = match subscriber
21722 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21723 .await
21724 {
21725 Err(error) => error,
21726 Ok(_) => panic!("malformed receipt content must fail closed"),
21727 };
21728 assert!(error.to_string().contains("missing its log array"));
21729 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
21730 assert!(asserter.read_q().is_empty());
21731 }
21732
21733 #[tokio::test]
21734 async fn optimism_sampler_retries_when_logs_advance_past_the_sampled_block() {
21735 let asserter = Asserter::new();
21736 let transaction_a = B256::repeat_byte(0x41);
21737 let transaction_b = B256::repeat_byte(0x42);
21738 let first = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
21739 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction_a]),
21740 );
21741 let second = rpc_block(101, B256::repeat_byte(0xa2)).with_transactions(
21742 alloy_network::primitives::BlockTransactions::Hashes(vec![
21743 transaction_a,
21744 transaction_b,
21745 ]),
21746 );
21747 let mut log = rpc_log(false);
21748 log.block_number = Some(101);
21749 log.block_hash = Some(B256::repeat_byte(0xa2));
21750 log.transaction_hash = Some(transaction_b);
21751 log.transaction_index = Some(1);
21752 log.log_index = Some(0);
21753 queue_op_pending(&asserter, first);
21754 asserter.push_success(&vec![log.clone()]);
21755 asserter.push_success(&serde_json::Value::Null);
21756 queue_op_pending(&asserter, second);
21757 asserter.push_success(&vec![log.clone()]);
21758 asserter.push_success(&serde_json::Value::Null);
21759 asserter.push_success(&serde_json::json!({
21760 "transactionHash": transaction_b,
21761 "logs": [log]
21762 }));
21763 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21764 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21765 provider,
21766 SubscriberMode::PubSub,
21767 SubscriberConfig {
21768 preconfirmations: PreconfirmationMode::Required,
21769 ..SubscriberConfig::default()
21770 },
21771 )
21772 .with_provider_ref(ProviderRef::new("op-paid", 12));
21773 subscriber.chain_id = Some(10);
21774 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21775 subscriber.interests = subscriber.base_interests.clone();
21776
21777 assert!(
21778 subscriber
21779 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21780 .await
21781 .expect("a cross-request race remains retryable")
21782 .is_none()
21783 );
21784 assert!(
21785 subscriber
21786 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21787 .await
21788 .expect("the next coherent cumulative view is delivered")
21789 .is_some()
21790 );
21791 assert_eq!(subscriber.flashblocks_rpc_metrics().raced_samples(), 1);
21792 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
21793 assert!(asserter.read_q().is_empty());
21794 }
21795
21796 #[tokio::test]
21797 async fn optimism_sampler_uses_the_paired_pending_state_provider() {
21798 let stream_asserter = Asserter::new();
21799 let stream_provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
21800 let state_asserter = Asserter::new();
21801 queue_op_pending(&state_asserter, rpc_block(101, B256::ZERO));
21802 state_asserter.push_success(&Vec::<Log>::new());
21803 let state_provider = ProviderBuilder::new().connect_mocked_client(state_asserter.clone());
21804 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21805 stream_provider,
21806 SubscriberMode::PubSub,
21807 SubscriberConfig {
21808 preconfirmations: PreconfirmationMode::Required,
21809 ..SubscriberConfig::default()
21810 },
21811 )
21812 .with_provider_ref(ProviderRef::new("op-paid", 12))
21813 .with_flashblocks_state_provider(state_provider);
21814 subscriber.chain_id = Some(10);
21815 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21816 subscriber.interests = subscriber.base_interests.clone();
21817
21818 assert!(
21819 subscriber
21820 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21821 .await
21822 .expect("paired pending-state reads succeed")
21823 .is_some()
21824 );
21825 assert!(state_asserter.read_q().is_empty());
21826 assert!(stream_asserter.read_q().is_empty());
21827 }
21828
21829 #[tokio::test]
21830 async fn optimism_sampler_retries_an_isolated_provider_request_failure() {
21831 let asserter = Asserter::new();
21832 let pending = rpc_block(101, B256::ZERO);
21833 queue_op_pending(&asserter, pending.clone());
21834 asserter.push_failure_msg("temporarily unavailable");
21835 queue_op_pending(&asserter, pending);
21836 asserter.push_success(&Vec::<Log>::new());
21837 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21838 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21839 provider,
21840 SubscriberMode::PubSub,
21841 SubscriberConfig {
21842 preconfirmations: PreconfirmationMode::Required,
21843 max_consecutive_flashblock_poll_failures: 2,
21844 ..SubscriberConfig::default()
21845 },
21846 )
21847 .with_provider_ref(ProviderRef::new("op-paid", 12));
21848 subscriber.chain_id = Some(10);
21849 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21850 subscriber.interests = subscriber.base_interests.clone();
21851
21852 assert!(
21853 subscriber
21854 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21855 .await
21856 .expect("one request failure stays retryable")
21857 .is_none()
21858 );
21859 assert!(
21860 subscriber
21861 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21862 .await
21863 .expect("the next cumulative view retries the missing logs")
21864 .is_some()
21865 );
21866 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 1);
21867 assert!(asserter.read_q().is_empty());
21868 }
21869
21870 #[tokio::test]
21871 async fn optimism_sampler_surfaces_sustained_provider_request_failures() {
21872 let asserter = Asserter::new();
21873 asserter.push_failure_msg("temporarily unavailable");
21874 asserter.push_failure_msg("still unavailable");
21875 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21876 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21877 provider,
21878 SubscriberMode::PubSub,
21879 SubscriberConfig {
21880 preconfirmations: PreconfirmationMode::Required,
21881 max_consecutive_flashblock_poll_failures: 2,
21882 ..SubscriberConfig::default()
21883 },
21884 )
21885 .with_provider_ref(ProviderRef::new("op-paid", 12));
21886 subscriber.chain_id = Some(10);
21887
21888 assert!(
21889 subscriber
21890 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21891 .await
21892 .expect("the first request failure stays retryable")
21893 .is_none()
21894 );
21895 let error = match subscriber
21896 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
21897 .await
21898 {
21899 Err(error) => error,
21900 Ok(_) => panic!("the configured consecutive-failure limit must fail closed"),
21901 };
21902 assert!(error.to_string().contains("still unavailable"));
21903 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 2);
21904 assert!(asserter.read_q().is_empty());
21905 }
21906
21907 #[tokio::test]
21908 async fn flashblocks_preflight_rejects_a_mismatched_chain_before_subscribing() {
21909 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21910 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21911 provider,
21912 SubscriberMode::PubSub,
21913 SubscriberConfig {
21914 preconfirmations: PreconfirmationMode::Required,
21915 ..SubscriberConfig::default()
21916 },
21917 )
21918 .with_provider_ref(ProviderRef::new("wrong-chain", 1));
21919 subscriber.chain_id = Some(10);
21920 subscriber.interests = vec![log_interest_matching_rpc_log()];
21921
21922 assert!(matches!(
21923 subscriber.establish_flashblocks_preflight(8_453).await,
21924 Err(SubscriberError::ChainMismatch {
21925 expected: 8_453,
21926 actual: 10
21927 })
21928 ));
21929 }
21930
21931 #[tokio::test]
21932 async fn optimism_preflight_rejects_a_mismatched_paired_provider() {
21933 let stream_asserter = Asserter::new();
21934 let stream_provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
21935 let state_asserter = Asserter::new();
21936 state_asserter.push_success(&serde_json::json!(["flashblocksv1"]));
21937 state_asserter.push_success(&8_453_u64);
21938 let state_provider = ProviderBuilder::new().connect_mocked_client(state_asserter.clone());
21939 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21940 stream_provider,
21941 SubscriberMode::PubSub,
21942 SubscriberConfig {
21943 preconfirmations: PreconfirmationMode::Required,
21944 ..SubscriberConfig::default()
21945 },
21946 )
21947 .with_provider_ref(ProviderRef::new("op-paid", 12))
21948 .with_flashblocks_state_provider(state_provider);
21949 subscriber.chain_id = Some(10);
21950 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
21951 subscriber.interests = subscriber.base_interests.clone();
21952 let desired = subscriber.pubsub_stream_sources();
21953 let mut streams = SubscriberStreams::new();
21954 for source in desired {
21955 streams.push(source, stream::pending().boxed());
21956 }
21957 subscriber.state = AlloySubscriberState::Active(streams);
21958 subscriber.sources_dirty = false;
21959 assert!(matches!(
21960 subscriber.establish_flashblocks_preflight(10).await,
21961 Err(SubscriberError::ChainMismatch {
21962 expected: 10,
21963 actual: 8_453
21964 })
21965 ));
21966 assert!(state_asserter.read_q().is_empty());
21967 assert!(stream_asserter.read_q().is_empty());
21968 }
21969
21970 #[test]
21971 fn unproven_parent_replacement_rewind_discards_every_unauthenticated_identity() {
21972 let parent = BlockRef {
21973 number: 79,
21974 hash: B256::repeat_byte(0x79),
21975 parent_hash: Some(B256::repeat_byte(0x78)),
21976 timestamp: Some(1_700_000_079),
21977 };
21978 let old_tip = BlockRef {
21979 number: 80,
21980 hash: B256::repeat_byte(0x80),
21981 parent_hash: Some(parent.hash),
21982 timestamp: Some(1_700_000_080),
21983 };
21984 let replacement = BlockRef {
21985 hash: B256::repeat_byte(0xe0),
21986 parent_hash: Some(B256::repeat_byte(0xdf)),
21987 ..old_tip
21988 };
21989 let mut state =
21990 CanonicalSequenceState::new(vec![parent, old_tip], Some(old_tip), Some(parent), None);
21991
21992 let rewind = apply_sequence_canonical_block(&mut state, &replacement, false)
21993 .expect("replacement metadata is structurally valid")
21994 .expect("unknown parent is an observable rewind");
21995
21996 assert_eq!(rewind.common_ancestor, None);
21997 assert_eq!(rewind.dropped, vec![parent, old_tip]);
21998 assert_eq!(state.retained_canonical_history(), &[replacement]);
21999 assert_eq!(state.coverage_head(), Some(&replacement));
22000 assert_eq!(state.safe_head(), None);
22001 assert_eq!(state.finalized_head(), None);
22002 }
22003
22004 #[test]
22005 fn handler_ids_are_non_empty_across_construction_and_deserialization() {
22006 assert_eq!(HandlerId::try_new("").unwrap_err(), HandlerIdError);
22007 let valid = HandlerId::try_new("owner-1").expect("non-empty id");
22008 let encoded = serde_json::to_string(&valid).expect("serialize id");
22009 assert_eq!(
22010 serde_json::from_str::<HandlerId>(&encoded).expect("deserialize valid id"),
22011 valid
22012 );
22013 assert!(serde_json::from_str::<HandlerId>(r#"""#).is_err());
22014 }
22015
22016 fn rpc_log(removed: bool) -> Log {
22017 Log {
22018 inner: alloy_primitives::Log::new_unchecked(
22019 Address::repeat_byte(0x42),
22020 vec![B256::repeat_byte(0x01)],
22021 Bytes::new(),
22022 ),
22023 block_hash: Some(B256::repeat_byte(0x02)),
22024 block_number: Some(7),
22025 block_timestamp: Some(1_700_000_000),
22026 transaction_hash: Some(B256::repeat_byte(0x03)),
22027 transaction_index: Some(4),
22028 log_index: Some(5),
22029 removed,
22030 }
22031 }
22032
22033 fn rpc_transaction(chain_id: Option<u64>) -> alloy_rpc_types_eth::Transaction {
22034 use alloy_consensus::SignableTransaction as _;
22035
22036 let envelope: alloy_consensus::TxEnvelope = alloy_consensus::TxLegacy {
22037 chain_id,
22038 ..Default::default()
22039 }
22040 .into_signed(alloy_primitives::Signature::test_signature())
22041 .into();
22042 alloy_rpc_types_eth::Transaction {
22043 inner: alloy_consensus::transaction::Recovered::new_unchecked(envelope, Address::ZERO),
22044 block_hash: None,
22045 block_number: None,
22046 transaction_index: None,
22047 effective_gas_price: None,
22048 }
22049 }
22050
22051 #[cfg(feature = "reactive-ws")]
22052 fn rpc_log_at(block_number: u64, transaction_index: u64, log_index: u64) -> Log {
22053 Log {
22054 inner: alloy_primitives::Log::new_unchecked(
22055 Address::repeat_byte(0x42),
22056 vec![B256::repeat_byte(0x01)],
22057 Bytes::new(),
22058 ),
22059 block_hash: Some(B256::repeat_byte(block_number as u8)),
22060 block_number: Some(block_number),
22061 block_timestamp: Some(1_700_000_000 + block_number),
22062 transaction_hash: Some(B256::repeat_byte(0x20 + transaction_index as u8)),
22063 transaction_index: Some(transaction_index),
22064 log_index: Some(log_index),
22065 removed: false,
22066 }
22067 }
22068
22069 #[cfg(any(
22070 feature = "raw-flashblocks-json",
22071 feature = "reactive-polling",
22072 feature = "reactive-ws"
22073 ))]
22074 fn rpc_block(number: u64, hash: B256) -> alloy_rpc_types_eth::Block {
22075 alloy_rpc_types_eth::Block::empty(alloy_rpc_types_eth::Header {
22076 hash,
22077 inner: alloy_consensus::Header {
22078 number,
22079 parent_hash: B256::repeat_byte(number.saturating_sub(1) as u8),
22080 timestamp: 1_700_000_000 + number,
22081 ..Default::default()
22082 },
22083 total_difficulty: None,
22084 size: None,
22085 })
22086 }
22087
22088 fn queue_op_pending(asserter: &Asserter, pending: alloy_rpc_types_eth::Block) {
22089 let parent = rpc_block(
22090 pending.header().number().saturating_sub(1),
22091 pending.header().parent_hash(),
22092 );
22093 asserter.push_success(&Some(pending));
22094 asserter.push_success(&Some(parent));
22095 }
22096
22097 #[tokio::test(flavor = "multi_thread")]
22098 #[cfg(feature = "reactive-ws")]
22099 async fn verified_log_context_fetches_and_caches_exact_parent_identity() {
22100 let stream_asserter = Asserter::new();
22101 let provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
22102 let verification_asserter = Asserter::new();
22103 verification_asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(7))));
22104 let verification_provider =
22105 ProviderBuilder::new().connect_mocked_client(verification_asserter.clone());
22106 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22107 provider,
22108 SubscriberMode::PubSub,
22109 SubscriberConfig {
22110 verify_log_block_context: true,
22111 ..SubscriberConfig::default()
22112 },
22113 )
22114 .with_log_verification_provider(verification_provider);
22115 let log = rpc_log_at(7, 0, 0);
22116
22117 subscriber
22118 .verify_log_block_context(&log)
22119 .await
22120 .expect("verify live log block");
22121 subscriber
22122 .verify_log_block_context(&log)
22123 .await
22124 .expect("reuse verified block cache");
22125 let record = subscriber.with_chain_id(log_input_record(log, InputSource::Subscription));
22126
22127 assert_eq!(
22128 record.context.block.expect("verified block").parent_hash,
22129 Some(B256::repeat_byte(6))
22130 );
22131 assert!(
22132 verification_asserter.read_q().is_empty(),
22133 "one provider lookup should verify every log in the same block"
22134 );
22135 assert!(
22136 stream_asserter.read_q().is_empty(),
22137 "verification must not use the high-volume stream provider"
22138 );
22139 }
22140
22141 #[tokio::test(flavor = "multi_thread")]
22142 async fn stream_with_termination_yields_terminal_source_marker() {
22143 let mut stream = stream_with_termination::<Ethereum, _>(
22144 stream::iter([SubscriberEvent::<Ethereum>::PendingHash(B256::repeat_byte(
22145 0xaa,
22146 ))]),
22147 SubscriberStreamSource::PubSubPendingHashes,
22148 );
22149
22150 assert!(matches!(
22151 stream.next().await,
22152 Some(SubscriberEvent::PendingHash(hash)) if hash == B256::repeat_byte(0xaa)
22153 ));
22154 assert!(matches!(
22155 stream.next().await,
22156 Some(SubscriberEvent::StreamTerminated(source)) if source.is_pubsub()
22157 ));
22158 assert!(stream.next().await.is_none());
22159 }
22160
22161 #[test]
22162 fn reconnect_delay_doubles_until_capped() {
22163 assert_eq!(
22164 next_reconnect_delay(Duration::from_millis(250), Duration::from_secs(1)),
22165 Duration::from_millis(500)
22166 );
22167 assert_eq!(
22168 next_reconnect_delay(Duration::from_millis(750), Duration::from_secs(1)),
22169 Duration::from_secs(1)
22170 );
22171 assert_eq!(
22172 next_reconnect_delay(Duration::ZERO, Duration::from_secs(1)),
22173 Duration::ZERO
22174 );
22175 }
22176
22177 #[test]
22178 fn canonical_logs_are_deduped_but_removed_logs_are_not() {
22179 let included = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
22180 let removed = log_input_record::<Ethereum>(rpc_log(true), InputSource::Subscription);
22181
22182 assert!(should_dedupe_record(&included));
22183 assert!(!should_dedupe_record(&removed));
22184 }
22185
22186 #[test]
22187 fn owner_reconcile_dedupe_rejects_conflicts_and_preserves_compatible_enrichment() {
22188 let set_context_timestamp = |record: &mut ReactiveInputRecord<Ethereum>,
22189 timestamp: Option<u64>| {
22190 record.context.block.as_mut().expect("block").timestamp = timestamp;
22191 match &mut record.context.chain_status {
22192 ChainStatus::Included { block, .. }
22193 | ChainStatus::Safe { block }
22194 | ChainStatus::Finalized { block }
22195 | ChainStatus::Reorged {
22196 dropped_from: block,
22197 } => block.timestamp = timestamp,
22198 ChainStatus::Pending | ChainStatus::Preconfirmed { .. } => {
22199 panic!("log record is canonical")
22200 }
22201 }
22202 };
22203
22204 let mut payload_only = log_input_record::<Ethereum>(rpc_log(false), InputSource::Backfill);
22205 let payload_timestamp = match &payload_only.input {
22206 ReactiveInput::Log(log) => log.block_timestamp.expect("timestamp"),
22207 _ => unreachable!(),
22208 };
22209 set_context_timestamp(&mut payload_only, None);
22210 let mut context_only = payload_only.clone();
22211 if let ReactiveInput::Log(log) = &mut context_only.input {
22212 log.block_timestamp = None;
22213 }
22214 set_context_timestamp(&mut context_only, Some(payload_timestamp + 1));
22215 assert!(matches!(
22216 dedupe_records(vec![payload_only, context_only]),
22217 Err(ReactiveError::InvalidInputRecord { .. })
22218 ));
22219
22220 let mut partial = log_input_record::<Ethereum>(rpc_log(false), InputSource::Backfill);
22221 if let ReactiveInput::Log(log) = &mut partial.input {
22222 log.block_timestamp = None;
22223 }
22224 set_context_timestamp(&mut partial, None);
22225 let complete = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
22226 let deduped =
22227 dedupe_records(vec![partial, complete]).expect("compatible metadata enriches");
22228 assert_eq!(deduped.len(), 1);
22229 deduped[0]
22230 .validated_identity()
22231 .expect("merged record remains coherent");
22232 let resolved = resolve_record_block_payload_metadata(
22233 &deduped[0],
22234 *canonical_record_block(&deduped[0]).expect("canonical"),
22235 )
22236 .expect("effective block");
22237 assert_eq!(resolved.timestamp, Some(payload_timestamp));
22238 }
22239
22240 #[test]
22241 fn full_block_bodies_are_never_suppressed_from_header_hash_alone() {
22242 use alloy_rpc_types_eth::{Block, Header};
22243
22244 let block_ref = BlockRef {
22245 number: 7,
22246 hash: B256::repeat_byte(0x77),
22247 parent_hash: Some(B256::repeat_byte(0x66)),
22248 timestamp: Some(1_700_000_007),
22249 };
22250 let block = Block::empty(Header {
22251 hash: block_ref.hash,
22252 inner: alloy_consensus::Header {
22253 number: block_ref.number,
22254 parent_hash: block_ref.parent_hash.expect("parent"),
22255 timestamp: block_ref.timestamp.expect("timestamp"),
22256 ..Default::default()
22257 },
22258 total_difficulty: None,
22259 size: None,
22260 });
22261 let record = ReactiveInputRecord::<Ethereum>::new(
22262 ReactiveInput::FullBlock(block),
22263 ReactiveContext {
22264 chain_id: Some(1),
22265 source: InputSource::Subscription,
22266 chain_status: ChainStatus::Included {
22267 block: block_ref,
22268 confirmations: 0,
22269 },
22270 block: Some(block_ref),
22271 transaction_index: None,
22272 log_index: None,
22273 },
22274 );
22275
22276 assert!(!record.is_payload_deduplicable());
22277 assert!(!record.same_deduplicable_payload(&record));
22278 let retained = dedupe_scoped_records(vec![
22279 (
22280 record.clone(),
22281 DeliveryAudience::All,
22282 DeliveryScope::Canonical,
22283 ),
22284 (record, DeliveryAudience::All, DeliveryScope::Canonical),
22285 ])
22286 .expect("non-deduplicable bodies are preserved, not treated as conflicts");
22287 assert_eq!(retained.len(), 2);
22288 }
22289
22290 #[test]
22291 fn hydrated_transaction_wrappers_reject_inclusion_and_chain_identity_conflicts() {
22292 let pending_context = ReactiveContext {
22293 chain_id: Some(1),
22294 source: InputSource::Batch,
22295 chain_status: ChainStatus::Pending,
22296 block: None,
22297 transaction_index: None,
22298 log_index: None,
22299 };
22300 let mut included_pending = rpc_transaction(Some(1));
22301 included_pending.block_hash = Some(B256::repeat_byte(0xaa));
22302 assert!(matches!(
22303 ReactiveInputRecord::<Ethereum>::new(
22304 ReactiveInput::PendingTx(included_pending),
22305 pending_context.clone(),
22306 )
22307 .validated_identity(),
22308 Err(ReactiveError::InvalidInputRecord { .. })
22309 ));
22310 assert!(matches!(
22311 ReactiveInputRecord::<Ethereum>::new(
22312 ReactiveInput::PendingTx(rpc_transaction(Some(2))),
22313 pending_context,
22314 )
22315 .validated_identity(),
22316 Err(ReactiveError::InvalidInputRecord { .. })
22317 ));
22318
22319 let block_ref = BlockRef {
22320 number: 8,
22321 hash: B256::repeat_byte(0x88),
22322 parent_hash: Some(B256::repeat_byte(0x77)),
22323 timestamp: Some(1_700_000_008),
22324 };
22325 let header = alloy_rpc_types_eth::Header {
22326 hash: block_ref.hash,
22327 inner: alloy_consensus::Header {
22328 number: block_ref.number,
22329 parent_hash: block_ref.parent_hash.expect("parent"),
22330 timestamp: block_ref.timestamp.expect("timestamp"),
22331 ..Default::default()
22332 },
22333 total_difficulty: None,
22334 size: None,
22335 };
22336 let context = ReactiveContext {
22337 chain_id: Some(1),
22338 source: InputSource::Batch,
22339 chain_status: ChainStatus::Included {
22340 block: block_ref,
22341 confirmations: 0,
22342 },
22343 block: Some(block_ref),
22344 transaction_index: None,
22345 log_index: None,
22346 };
22347 for transaction in [
22348 alloy_rpc_types_eth::Transaction {
22349 block_hash: Some(B256::repeat_byte(0xff)),
22350 ..rpc_transaction(Some(1))
22351 },
22352 alloy_rpc_types_eth::Transaction {
22353 block_hash: Some(block_ref.hash),
22354 block_number: Some(block_ref.number),
22355 transaction_index: Some(1),
22356 ..rpc_transaction(Some(1))
22357 },
22358 rpc_transaction(Some(2)),
22359 ] {
22360 let block = alloy_rpc_types_eth::Block::new(
22361 header.clone(),
22362 alloy_network::primitives::BlockTransactions::Full(vec![transaction]),
22363 );
22364 assert!(matches!(
22365 ReactiveInputRecord::<Ethereum>::new(
22366 ReactiveInput::FullBlock(block),
22367 context.clone(),
22368 )
22369 .validated_identity(),
22370 Err(ReactiveError::InvalidInputRecord { .. })
22371 ));
22372 }
22373 }
22374
22375 #[test]
22376 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
22377 fn compatibility_owner_backfill_and_live_overlap_split_exact_audiences() {
22378 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22379 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22380 provider,
22381 SubscriberMode::Auto,
22382 SubscriberConfig::default(),
22383 );
22384 let owner = HandlerId::new("compat-owner");
22385 subscriber
22386 .add_interest_owner(
22387 owner.clone(),
22388 &[ReactiveInterest::Logs(LogInterest {
22389 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
22390 local_matcher: None,
22391 route_key: None,
22392 })],
22393 )
22394 .unwrap();
22395 let log = rpc_log(false);
22396
22397 subscriber.enqueue_compat_owner_record(
22398 log_input_record(log.clone(), InputSource::Backfill),
22399 owner.clone(),
22400 );
22401 subscriber.enqueue_event(SubscriberEvent::Log { source_id: 0, log });
22402
22403 let batch = subscriber
22404 .drain_next_scoped_batch()
22405 .expect("owner catch-up and residual live copies");
22406 assert_eq!(batch.records.len(), 2);
22407 assert_eq!(
22408 batch.records[0].scope,
22409 SubscriberInputScope::OwnerOnlyHandlers {
22410 owners: vec![owner.clone()]
22411 }
22412 );
22413 assert_eq!(
22414 batch.records[1].scope,
22415 SubscriberInputScope::CanonicalResidual {
22416 owners: Vec::new(),
22417 excluded: vec![owner.clone()]
22418 }
22419 );
22420
22421 let reactive = batch.into_reactive_batch();
22422 assert_eq!(
22423 reactive.record_audience(0),
22424 Some(&DeliveryAudience::Owners(vec![owner.clone()]))
22425 );
22426 assert_eq!(
22427 reactive.record_delivery_scope(0),
22428 Some(DeliveryScope::OwnerCatchup)
22429 );
22430 assert_eq!(
22431 reactive.record_audience(1),
22432 Some(&DeliveryAudience::AllExcept(vec![owner]))
22433 );
22434 assert_eq!(
22435 reactive.record_delivery_scope(1),
22436 Some(DeliveryScope::Canonical)
22437 );
22438 }
22439
22440 #[test]
22441 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
22442 fn active_owner_replacement_commits_atomically_to_one_new_epoch() {
22443 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22444 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22445 provider,
22446 SubscriberMode::Auto,
22447 SubscriberConfig::default(),
22448 );
22449 let owner = HandlerId::new("replace-owner");
22450 let original = ReactiveInterest::Logs(LogInterest {
22451 provider_filter: Filter::new().address(Address::repeat_byte(0x41)),
22452 local_matcher: None,
22453 route_key: None,
22454 });
22455 let replacement_interest = ReactiveInterest::Logs(LogInterest {
22456 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
22457 local_matcher: None,
22458 route_key: None,
22459 });
22460 let active = subscriber
22461 .stage_interest_owner(owner.clone(), &[original], SubscriberOwnerStart::Live)
22462 .unwrap();
22463 assert!(subscriber.activate_interest_owner(&active));
22464 let replacement = subscriber
22465 .stage_interest_owner_replacement(
22466 owner,
22467 &[replacement_interest],
22468 SubscriberOwnerStart::Live,
22469 )
22470 .unwrap();
22471
22472 assert!(subscriber.commit_interest_owner_replacement(&active, &replacement));
22473 assert_eq!(subscriber.interest_owner_state(&active), None);
22474 assert_eq!(
22475 subscriber.interest_owner_state(&replacement),
22476 Some(SubscriberOwnerState::Active)
22477 );
22478 assert_eq!(subscriber.registered_interests().len(), 1);
22479 }
22480
22481 #[test]
22482 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
22483 fn compatibility_and_epoch_owner_lifecycles_cannot_mix() {
22484 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22485 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22486 provider,
22487 SubscriberMode::Auto,
22488 SubscriberConfig::default(),
22489 );
22490 let owner = HandlerId::new("one-lifecycle");
22491 let interest = ReactiveInterest::Logs(LogInterest {
22492 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
22493 local_matcher: None,
22494 route_key: None,
22495 });
22496 let epoch = subscriber
22497 .stage_interest_owner(
22498 owner.clone(),
22499 std::slice::from_ref(&interest),
22500 SubscriberOwnerStart::Live,
22501 )
22502 .expect("stage epoch owner");
22503
22504 assert!(matches!(
22505 subscriber.add_interest_owner(owner.clone(), std::slice::from_ref(&interest)),
22506 Err(SubscriberError::InvalidConfig(_))
22507 ));
22508 assert_eq!(
22509 subscriber.interest_owner_state(&epoch),
22510 Some(SubscriberOwnerState::Staged)
22511 );
22512 assert!(subscriber.abort_interest_owner(&epoch));
22513 subscriber
22514 .add_interest_owner(owner.clone(), std::slice::from_ref(&interest))
22515 .expect("compatibility owner after epoch abort");
22516 assert!(matches!(
22517 subscriber.stage_interest_owner_replacement(
22518 owner,
22519 std::slice::from_ref(&interest),
22520 SubscriberOwnerStart::Live,
22521 ),
22522 Err(SubscriberOwnerError::AlreadyRegistered(_))
22523 ));
22524 }
22525
22526 #[test]
22527 fn pending_record_overflow_is_sticky_and_fail_closed() {
22528 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22529 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22530 provider,
22531 SubscriberMode::Polling,
22532 SubscriberConfig {
22533 max_pending_records: 1,
22534 ..SubscriberConfig::default()
22535 },
22536 );
22537 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
22538 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
22539 local_matcher: None,
22540 route_key: None,
22541 })];
22542 subscriber.enqueue_event(SubscriberEvent::Log {
22543 source_id: 0,
22544 log: rpc_log(false),
22545 });
22546 let mut second = rpc_log(false);
22547 second.log_index = Some(6);
22548 second.transaction_hash = Some(B256::repeat_byte(0x04));
22549 subscriber.enqueue_event(SubscriberEvent::Log {
22550 source_id: 0,
22551 log: second,
22552 });
22553
22554 assert_eq!(subscriber.pending_records.len(), 1);
22555 assert!(matches!(
22556 subscriber.check_resource_error(),
22557 Err(SubscriberError::ResourceExhausted(_))
22558 ));
22559 subscriber.reset_delivery_state();
22560 assert!(subscriber.check_resource_error().is_ok());
22561 }
22562
22563 #[test]
22564 fn historical_log_payload_bytes_are_bounded_independently_of_log_count() {
22565 let baseline = rpc_log(false);
22566 let fixed_bytes =
22567 validate_backfill_resource_limits(std::slice::from_ref(&baseline), 1, usize::MAX)
22568 .expect("measure fixed log accounting");
22569 let mut large = baseline;
22570 large.inner = alloy_primitives::Log::new_unchecked(
22571 Address::repeat_byte(0x42),
22572 vec![B256::repeat_byte(0x01)],
22573 Bytes::from(vec![0u8; 256]),
22574 );
22575
22576 assert!(matches!(
22577 validate_backfill_resource_limits(&[large], 1, fixed_bytes + 255),
22578 Err(SubscriberError::ResourceExhausted(_))
22579 ));
22580 }
22581
22582 #[tokio::test(flavor = "multi_thread")]
22583 #[cfg(feature = "reactive-polling")]
22584 async fn reconcile_capacity_failure_does_not_publish_progress_or_partial_history() {
22585 use alloy_rpc_types_eth::{Block, Header};
22586
22587 let asserter = Asserter::new();
22588 let baseline = BlockRef {
22589 number: 6,
22590 hash: B256::repeat_byte(6),
22591 parent_hash: Some(B256::repeat_byte(5)),
22592 timestamp: Some(1_700_000_006),
22593 };
22594 let through = BlockRef {
22595 number: 7,
22596 hash: B256::repeat_byte(7),
22597 parent_hash: Some(baseline.hash),
22598 timestamp: Some(1_700_000_007),
22599 };
22600 let rpc_block = || -> Block {
22601 Block::empty(Header {
22602 hash: through.hash,
22603 inner: alloy_consensus::Header {
22604 number: through.number,
22605 parent_hash: through.parent_hash.expect("parent"),
22606 timestamp: through.timestamp.expect("timestamp"),
22607 ..Default::default()
22608 },
22609 total_difficulty: None,
22610 size: None,
22611 })
22612 };
22613 let mut historical = rpc_log(false);
22614 historical.block_hash = Some(through.hash);
22615 historical.block_timestamp = through.timestamp;
22616 asserter.push_success(&Some(rpc_block()));
22617 asserter.push_success(&vec![historical]);
22618 asserter.push_success(&Some(rpc_block()));
22619 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
22620 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22621 provider,
22622 SubscriberMode::Polling,
22623 SubscriberConfig {
22624 max_pending_records: 1,
22625 ..SubscriberConfig::default()
22626 },
22627 );
22628 subscriber.chain_id = Some(1);
22629 let interest = ReactiveInterest::Logs(LogInterest {
22630 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
22631 local_matcher: None,
22632 route_key: None,
22633 });
22634 let epoch = subscriber
22635 .stage_interest_owner(
22636 HandlerId::new("capacity-owner"),
22637 std::slice::from_ref(&interest),
22638 SubscriberOwnerStart::PostBlock(baseline),
22639 )
22640 .expect("stage owner");
22641 subscriber.sources_dirty = false;
22644 subscriber.state = AlloySubscriberState::Empty;
22645 subscriber.push_pending_record(SubscriberInputRecord {
22646 record: log_input_record(rpc_log(false), InputSource::Poll),
22647 scope: SubscriberInputScope::Canonical { owners: Vec::new() },
22648 preconfirmation_timing: None,
22649 });
22650
22651 let error = subscriber
22652 .reconcile_interest_owner(&epoch, through)
22653 .await
22654 .expect_err("historical delivery cannot displace the queued live record");
22655 assert!(matches!(
22656 error,
22657 SubscriberOwnerError::Subscriber(SubscriberError::ResourceExhausted(_))
22658 ));
22659 assert!(subscriber.interest_owner_progress(&epoch).is_none());
22660 assert_eq!(subscriber.pending_records.len(), 1);
22661 assert!(matches!(
22662 subscriber.pending_records[0].scope,
22663 SubscriberInputScope::Canonical { .. }
22664 ));
22665 }
22666
22667 #[test]
22668 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
22669 fn lazy_backfill_queue_capacity_failure_is_atomic() {
22670 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22671 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22672 provider,
22673 SubscriberMode::Auto,
22674 SubscriberConfig {
22675 max_pending_backfills: 1,
22676 ..SubscriberConfig::default()
22677 },
22678 );
22679 let interest = |address| {
22680 ReactiveInterest::Logs(LogInterest {
22681 provider_filter: Filter::new().address(address),
22682 local_matcher: None,
22683 route_key: None,
22684 })
22685 };
22686 subscriber
22687 .add_interest_owner_with_backfill(
22688 HandlerId::new("owner-a"),
22689 &[interest(Address::repeat_byte(0x41))],
22690 SubscriberBackfill::from_block(10),
22691 )
22692 .expect("first queued backfill");
22693
22694 let error = subscriber
22695 .add_interest_owner_with_backfill(
22696 HandlerId::new("owner-b"),
22697 &[interest(Address::repeat_byte(0x42))],
22698 SubscriberBackfill::from_block(10),
22699 )
22700 .expect_err("second backfill must exceed capacity");
22701
22702 assert!(matches!(error, SubscriberError::ResourceExhausted(_)));
22703 assert!(
22704 subscriber
22705 .owner_interests(&HandlerId::new("owner-b"))
22706 .is_none()
22707 );
22708 assert_eq!(subscriber.pending_backfills.len(), 1);
22709 }
22710
22711 #[test]
22712 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
22713 fn exact_owner_replacement_is_atomic_and_removes_crash_stale_owners() {
22714 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22715 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22716 provider,
22717 SubscriberMode::Auto,
22718 SubscriberConfig {
22719 max_pending_backfills: 1,
22720 ..SubscriberConfig::default()
22721 },
22722 );
22723 let interest = |address| {
22724 ReactiveInterest::Logs(LogInterest {
22725 provider_filter: Filter::new().address(address),
22726 local_matcher: None,
22727 route_key: None,
22728 })
22729 };
22730 subscriber
22731 .add_interest_owner(
22732 HandlerId::new("crash-stale"),
22733 &[interest(Address::repeat_byte(0xee))],
22734 )
22735 .expect("seed stale owner");
22736 subscriber.base_interests = vec![interest(Address::repeat_byte(0xdd))];
22737 subscriber.rebuild_registered_interests();
22738 subscriber.push_pending_record(SubscriberInputRecord {
22739 record: log_input_record(rpc_log(false), InputSource::Poll),
22740 scope: SubscriberInputScope::Canonical { owners: Vec::new() },
22741 preconfirmation_timing: None,
22742 });
22743 let baseline = BlockRef {
22744 number: 100,
22745 hash: B256::repeat_byte(100),
22746 parent_hash: Some(B256::repeat_byte(99)),
22747 timestamp: Some(1_700_000_100),
22748 };
22749 let backfill = SubscriberBackfill::after_canonical_block(baseline).expect("C + 1");
22750
22751 let error = subscriber
22752 .replace_interest_owners_with_global_backfill(
22753 vec![
22754 (
22755 HandlerId::new("pool-a"),
22756 vec![interest(Address::repeat_byte(0xa1))],
22757 ),
22758 (
22759 HandlerId::new("pool-b"),
22760 vec![ReactiveInterest::Logs(LogInterest {
22761 provider_filter: Filter::new()
22764 .address(Address::repeat_byte(0xb2))
22765 .from_block(7),
22766 local_matcher: None,
22767 route_key: None,
22768 })],
22769 ),
22770 ],
22771 backfill,
22772 )
22773 .expect_err("two backfills exceed atomic capacity");
22774 assert!(matches!(error, SubscriberError::ResourceExhausted(_)));
22775 assert!(
22776 subscriber
22777 .owner_interests(&HandlerId::new("crash-stale"))
22778 .is_some(),
22779 "failed replacement must preserve the prior topology"
22780 );
22781 assert!(
22782 subscriber
22783 .owner_interests(&HandlerId::new("pool-a"))
22784 .is_none()
22785 );
22786 assert_eq!(subscriber.base_interests.len(), 1);
22787 assert_eq!(subscriber.pending_records.len(), 1);
22788
22789 subscriber
22790 .replace_interest_owners_with_global_backfill(
22791 vec![(
22792 HandlerId::new("pool-a"),
22793 vec![interest(Address::repeat_byte(0xa1))],
22794 )],
22795 backfill,
22796 )
22797 .expect("replacement within capacity");
22798 assert!(
22799 subscriber
22800 .owner_interests(&HandlerId::new("crash-stale"))
22801 .is_none(),
22802 "successful exact replacement removes stale owners"
22803 );
22804 assert!(
22805 subscriber.base_interests.is_empty(),
22806 "successful exact replacement removes stale unowned interests"
22807 );
22808 assert!(
22809 subscriber.drain_next_scoped_batch().is_none(),
22810 "stale canonical delivery must not escape before C + 1 recovery"
22811 );
22812 assert!(
22813 subscriber
22814 .owner_interests(&HandlerId::new("pool-a"))
22815 .is_some()
22816 );
22817 assert_eq!(subscriber.pending_backfills.len(), 1);
22818 assert_eq!(subscriber.pending_backfills[0].backfill, backfill);
22819 assert!(
22820 subscriber.pending_backfills[0].owner.is_none(),
22821 "startup history must be global canonical catch-up, not owner-only"
22822 );
22823 }
22824
22825 #[test]
22826 fn exclusive_canonical_backfill_rejects_block_number_overflow() {
22827 let baseline = BlockRef {
22828 number: u64::MAX,
22829 hash: B256::repeat_byte(0xff),
22830 parent_hash: None,
22831 timestamp: None,
22832 };
22833 assert!(matches!(
22834 SubscriberBackfill::after_canonical_block(baseline),
22835 Err(SubscriberError::InvalidConfig(_))
22836 ));
22837 }
22838
22839 #[tokio::test(flavor = "multi_thread")]
22840 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
22841 async fn exclusive_canonical_backfill_validates_the_retained_baseline_hash() {
22842 let asserter = Asserter::new();
22843 asserter.push_success(&101u64);
22844 asserter.push_success(&Some(rpc_block(101, B256::repeat_byte(101))));
22845 asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(0xee))));
22846 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
22847 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22848 provider,
22849 SubscriberMode::Auto,
22850 SubscriberConfig::default(),
22851 );
22852 let baseline = BlockRef {
22853 number: 100,
22854 hash: B256::repeat_byte(0xaa),
22855 parent_hash: None,
22856 timestamp: None,
22857 };
22858 let backfill = SubscriberBackfill::after_canonical_block(baseline).expect("C + 1");
22859 subscriber
22860 .add_interest_owner_with_backfill(
22861 HandlerId::new("pool"),
22862 &[ReactiveInterest::Logs(LogInterest {
22863 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
22864 local_matcher: None,
22865 route_key: None,
22866 })],
22867 backfill,
22868 )
22869 .expect("queue post-baseline backfill");
22870
22871 let error = subscriber
22872 .drain_pending_backfills()
22873 .await
22874 .expect_err("provider branch differs at retained baseline");
22875 assert!(matches!(error, SubscriberError::InvalidBackfill(_)));
22876 assert_eq!(subscriber.pending_backfills.len(), 1);
22877 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 101);
22878 assert!(subscriber.pending_records.is_empty());
22879 }
22880
22881 #[tokio::test(flavor = "multi_thread")]
22882 #[cfg(feature = "reactive-ws")]
22883 async fn coordinated_multifilter_windows_are_globally_sorted_for_owner_and_canonical_delivery()
22884 {
22885 let asserter = Asserter::new();
22886 let retained = BlockRef {
22887 number: 10,
22888 hash: B256::repeat_byte(10),
22889 parent_hash: Some(B256::repeat_byte(9)),
22890 timestamp: Some(1_700_000_010),
22891 };
22892 let activation = BlockRef {
22893 number: 12,
22894 hash: B256::repeat_byte(12),
22895 parent_hash: Some(B256::repeat_byte(11)),
22896 timestamp: Some(1_700_000_012),
22897 };
22898
22899 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
22903 asserter.push_success(&vec![rpc_log_at(10, 2, 2)]);
22904 asserter.push_success(&vec![rpc_log_at(10, 1, 1)]);
22905 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
22906 asserter.push_success(&activation.number);
22907 asserter.push_success(&Some(rpc_block(activation.number, activation.hash)));
22908 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
22909 asserter.push_success(&vec![rpc_log_at(12, 2, 2)]);
22910 asserter.push_success(&vec![rpc_log_at(11, 1, 1)]);
22911 asserter.push_success(&Some(rpc_block(activation.number, activation.hash)));
22912 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
22913 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22914 provider,
22915 SubscriberMode::Auto,
22916 SubscriberConfig::default(),
22917 );
22918 let interests = (0..257)
22919 .map(|start| {
22920 ReactiveInterest::Logs(LogInterest {
22921 provider_filter: Filter::new()
22922 .address(Address::repeat_byte(0x42))
22923 .event_signature(B256::repeat_byte(0x01))
22924 .from_block(start),
22925 local_matcher: None,
22926 route_key: None,
22927 })
22928 })
22929 .collect::<Vec<_>>();
22930 subscriber
22931 .add_interest_owner_with_canonical_catchup(
22932 HandlerId::new("many-filters"),
22933 &interests,
22934 retained,
22935 )
22936 .expect("queue coordinated windows");
22937 assert_eq!(subscriber.pending_backfills.len(), 2);
22938 assert_eq!(subscriber.pending_backfills[0].filters.len(), 257);
22939 assert_eq!(subscriber.pending_backfills[1].filters.len(), 257);
22940
22941 subscriber
22942 .drain_pending_backfills()
22943 .await
22944 .expect("owner filter group");
22945 let owner = subscriber
22946 .drain_next_scoped_batch()
22947 .expect("owner ordered batch");
22948 assert_eq!(owner.records.len(), 2);
22949 assert_eq!(owner.records[0].record.context.transaction_index, Some(1));
22950 assert_eq!(owner.records[1].record.context.transaction_index, Some(2));
22951 assert!(
22952 owner.records.iter().all(|record| matches!(
22953 record.scope,
22954 SubscriberInputScope::OwnerOnlyHandlers { .. }
22955 ))
22956 );
22957
22958 subscriber
22959 .drain_pending_backfills()
22960 .await
22961 .expect("global filter group");
22962 let global = subscriber
22963 .drain_next_scoped_batch()
22964 .expect("global ordered batch");
22965 assert_eq!(global.records.len(), 2);
22966 assert_eq!(
22967 global.records[0].record.context.block.map(|b| b.number),
22968 Some(11)
22969 );
22970 assert_eq!(
22971 global.records[1].record.context.block.map(|b| b.number),
22972 Some(12)
22973 );
22974 assert!(
22975 global
22976 .records
22977 .iter()
22978 .all(|record| record.scope.is_canonical())
22979 );
22980 assert!(matches!(
22981 global.chain_controls.as_slice(),
22982 [ChainControl::Barrier {
22983 block: Some(block),
22984 ..
22985 }] if block == &activation
22986 ));
22987 }
22988
22989 #[tokio::test(flavor = "multi_thread")]
22990 #[cfg(feature = "reactive-ws")]
22991 async fn aborting_staged_epoch_purges_only_its_buffered_delivery() {
22992 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22993 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22994 provider,
22995 SubscriberMode::PubSub,
22996 SubscriberConfig::default(),
22997 );
22998 subscriber.chain_id = Some(1);
22999 let interest = ReactiveInterest::Logs(LogInterest {
23000 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
23001 local_matcher: None,
23002 route_key: None,
23003 });
23004 let owner_a = subscriber
23005 .stage_interest_owner(
23006 HandlerId::new("owner-a"),
23007 std::slice::from_ref(&interest),
23008 SubscriberOwnerStart::Live,
23009 )
23010 .unwrap();
23011 let owner_b = subscriber
23012 .stage_interest_owner(
23013 HandlerId::new("owner-b"),
23014 &[interest],
23015 SubscriberOwnerStart::Live,
23016 )
23017 .unwrap();
23018
23019 subscriber.enqueue_event(SubscriberEvent::Log {
23020 source_id: 0,
23021 log: rpc_log(false),
23022 });
23023 assert!(subscriber.abort_interest_owner(&owner_a));
23024
23025 let batch = subscriber
23026 .next_scoped_batch()
23027 .await
23028 .unwrap()
23029 .expect("shared canonical delivery remains queued");
23030 assert_eq!(batch.records.len(), 1);
23031 assert_eq!(
23032 batch.records[0].scope,
23033 SubscriberInputScope::Canonical {
23034 owners: vec![owner_b]
23035 }
23036 );
23037 }
23038
23039 #[tokio::test(flavor = "multi_thread")]
23040 #[cfg(feature = "reactive-ws")]
23041 async fn owner_backfill_dedupe_never_suppresses_canonical_delivery() {
23042 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23043 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23044 provider,
23045 SubscriberMode::PubSub,
23046 SubscriberConfig::default(),
23047 );
23048 subscriber.chain_id = Some(1);
23049 let epoch = subscriber
23050 .stage_interest_owner(
23051 HandlerId::new("owner"),
23052 &[ReactiveInterest::Logs(LogInterest {
23053 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
23054 local_matcher: None,
23055 route_key: None,
23056 })],
23057 SubscriberOwnerStart::Live,
23058 )
23059 .unwrap();
23060 let log = rpc_log(false);
23061
23062 subscriber.enqueue_owner_record(
23063 log_input_record(log.clone(), InputSource::Backfill),
23064 epoch.clone(),
23065 );
23066 subscriber.enqueue_event(SubscriberEvent::Log { source_id: 0, log });
23067
23068 let batch = subscriber
23069 .next_scoped_batch()
23070 .await
23071 .unwrap()
23072 .expect("owner backfill and canonical live delivery");
23073 assert_eq!(batch.records.len(), 2);
23074 assert_eq!(
23075 batch.records[0].scope,
23076 SubscriberInputScope::OwnerOnly {
23077 owners: vec![epoch]
23078 }
23079 );
23080 assert_eq!(
23081 batch.records[1].scope,
23082 SubscriberInputScope::Canonical { owners: Vec::new() },
23083 "owner replay dedupe must not suppress the global live record"
23084 );
23085 }
23086
23087 #[tokio::test(flavor = "multi_thread")]
23088 #[cfg(feature = "reactive-polling")]
23089 async fn reconcile_fetch_drains_live_burst_beyond_output_batch_capacity() {
23090 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23091 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23092 provider,
23093 SubscriberMode::Polling,
23094 SubscriberConfig {
23095 max_batch_size: 2,
23096 ..SubscriberConfig::default()
23097 },
23098 );
23099 let interest = ReactiveInterest::Logs(LogInterest {
23100 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
23101 local_matcher: None,
23102 route_key: None,
23103 });
23104 let epoch = subscriber
23105 .stage_interest_owner(
23106 HandlerId::new("owner"),
23107 std::slice::from_ref(&interest),
23108 SubscriberOwnerStart::Live,
23109 )
23110 .unwrap();
23111 subscriber.sources_dirty = false;
23112
23113 let mut duplicate = rpc_log(false);
23114 duplicate.transaction_hash = Some(B256::repeat_byte(1));
23115 duplicate.log_index = Some(0);
23116 let events = (0u8..10).map(|index| {
23117 let mut log = rpc_log(false);
23118 log.transaction_hash = Some(B256::repeat_byte(index.saturating_add(1)));
23119 log.log_index = Some(index as u64);
23120 SubscriberEvent::Log { source_id: 0, log }
23121 });
23122 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
23123 let mut streams = SubscriberStreams::new();
23124 streams.push(
23125 SubscriberStreamSource::PollingLog { filter },
23126 stream::iter(events).boxed(),
23127 );
23128 subscriber.state = AlloySubscriberState::Active(streams);
23129
23130 let mut polls = 0usize;
23131 let fetched_duplicate = duplicate.clone();
23132 let fetch = poll_fn(move |cx| {
23133 polls += 1;
23134 if polls > 10 {
23135 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>(fetched_duplicate.clone()))
23136 } else {
23137 cx.waker().wake_by_ref();
23138 std::task::Poll::Pending
23139 }
23140 });
23141 let target_epochs = HashSet::from([epoch.clone()]);
23142 let fetched_duplicate = subscriber
23143 .drive_reconcile_fetch(fetch, &target_epochs)
23144 .await
23145 .unwrap();
23146 subscriber.enqueue_owner_record_for_owners_unmerged(
23147 log_input_record(fetched_duplicate, InputSource::Backfill),
23148 vec![epoch.clone()],
23149 );
23150 subscriber.promote_reconcile_owner_records(&target_epochs);
23151
23152 assert_eq!(subscriber.pending_records.len(), 20);
23153 assert!(subscriber.pending_records.iter().take(10).all(|record| {
23154 record.scope == SubscriberInputScope::Canonical { owners: Vec::new() }
23155 }));
23156 assert!(subscriber.pending_records.iter().skip(10).all(|record| {
23157 record.scope
23158 == SubscriberInputScope::OwnerOnly {
23159 owners: vec![epoch.clone()],
23160 }
23161 }));
23162 }
23163
23164 #[tokio::test(flavor = "multi_thread")]
23165 #[cfg(feature = "reactive-polling")]
23166 async fn reconcile_fetch_waits_for_provider_when_live_topology_is_empty() {
23167 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23168 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23169 provider,
23170 SubscriberMode::Polling,
23171 SubscriberConfig::default(),
23172 );
23173 subscriber.chain_id = Some(1);
23174 subscriber.sources_dirty = false;
23175 let mut first_poll = true;
23176 let fetch = poll_fn(move |cx| {
23177 if first_poll {
23178 first_poll = false;
23179 cx.waker().wake_by_ref();
23180 std::task::Poll::Pending
23181 } else {
23182 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>("certified"))
23183 }
23184 });
23185
23186 let result = subscriber
23187 .drive_reconcile_fetch(fetch, &HashSet::new())
23188 .await
23189 .expect("an empty live topology must not be mistaken for termination");
23190 assert_eq!(result, "certified");
23191 }
23192
23193 #[tokio::test(flavor = "multi_thread")]
23194 #[cfg(all(feature = "reactive-polling", feature = "reactive-ws"))]
23195 async fn successful_owner_reconcile_seeds_its_live_filter_reconnect_anchor() {
23196 use alloy_rpc_types_eth::{Block, Header};
23197
23198 let asserter = Asserter::new();
23199 let baseline = BlockRef {
23200 number: 100,
23201 hash: B256::repeat_byte(0x64),
23202 parent_hash: Some(B256::repeat_byte(0x63)),
23203 timestamp: Some(1_700_000_100),
23204 };
23205 let through = BlockRef {
23206 number: 101,
23207 hash: B256::repeat_byte(0x65),
23208 parent_hash: Some(baseline.hash),
23209 timestamp: Some(1_700_000_101),
23210 };
23211 let rpc_block = || -> Block {
23212 Block::empty(Header {
23213 hash: through.hash,
23214 inner: alloy_consensus::Header {
23215 number: through.number,
23216 parent_hash: through.parent_hash.unwrap(),
23217 timestamp: through.timestamp.unwrap(),
23218 ..Default::default()
23219 },
23220 total_difficulty: None,
23221 size: None,
23222 })
23223 };
23224 asserter.push_success(&Some(rpc_block()));
23225 asserter.push_success(&Vec::<Log>::new());
23226 asserter.push_success(&Some(rpc_block()));
23227 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
23228 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23229 provider,
23230 SubscriberMode::PubSub,
23231 SubscriberConfig::default(),
23232 );
23233 subscriber.chain_id = Some(1);
23234 let interest = ReactiveInterest::Logs(LogInterest {
23235 provider_filter: Filter::new().address(Address::repeat_byte(0xac)),
23236 local_matcher: None,
23237 route_key: None,
23238 });
23239 let epoch = subscriber
23240 .stage_interest_owner(
23241 HandlerId::new("reconnect-anchor"),
23242 std::slice::from_ref(&interest),
23243 SubscriberOwnerStart::PostBlock(baseline),
23244 )
23245 .unwrap();
23246 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
23247 let source = SubscriberStreamSource::PubSubLog {
23248 id: subscriber.log_source_id(&filter),
23249 filter: filter.clone(),
23250 };
23251 let mut streams = SubscriberStreams::new();
23252 streams.push(source, stream::pending().boxed());
23253 subscriber.state = AlloySubscriberState::Active(streams);
23254 subscriber.sources_dirty = false;
23255
23256 subscriber
23257 .reconcile_interest_owner(&epoch, through)
23258 .await
23259 .unwrap();
23260 assert_eq!(subscriber.log_anchor(&filter), Some(through.number));
23261 assert!(asserter.read_q().is_empty());
23262 }
23263
23264 #[tokio::test(flavor = "multi_thread")]
23265 #[cfg(feature = "reactive-polling")]
23266 async fn cancelled_reconcile_retains_hidden_owner_live_delivery_for_retry() {
23267 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23268 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23269 provider,
23270 SubscriberMode::Polling,
23271 SubscriberConfig::default(),
23272 );
23273 let interest = ReactiveInterest::Logs(LogInterest {
23274 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
23275 local_matcher: None,
23276 route_key: None,
23277 });
23278 let epoch = subscriber
23279 .stage_interest_owner(
23280 HandlerId::new("owner"),
23281 std::slice::from_ref(&interest),
23282 SubscriberOwnerStart::PostBlock(BlockRef {
23283 number: 100,
23284 hash: B256::repeat_byte(0x64),
23285 parent_hash: None,
23286 timestamp: None,
23287 }),
23288 )
23289 .unwrap();
23290 subscriber.sources_dirty = false;
23291
23292 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
23293 let event = SubscriberEvent::Log {
23294 source_id: 0,
23295 log: rpc_log(false),
23296 };
23297 let mut streams = SubscriberStreams::new();
23298 streams.push(
23299 SubscriberStreamSource::PollingLog { filter },
23300 stream::once(async move { event })
23301 .chain(stream::pending())
23302 .boxed(),
23303 );
23304 subscriber.state = AlloySubscriberState::Active(streams);
23305
23306 let targets = HashSet::from([epoch.clone()]);
23307 {
23308 let fetch = futures::future::pending::<Result<(), SubscriberOwnerError>>();
23309 let drive = subscriber.drive_reconcile_fetch(fetch, &targets);
23310 futures::pin_mut!(drive);
23311 poll_fn(|cx| {
23312 assert!(drive.as_mut().poll(cx).is_pending());
23313 std::task::Poll::Ready(())
23314 })
23315 .await;
23316 }
23317
23318 assert_eq!(subscriber.pending_records.len(), 1);
23319 assert_eq!(
23320 subscriber.pending_records[0].scope,
23321 SubscriberInputScope::Canonical { owners: Vec::new() },
23322 "canonical delivery commits immediately at a cancellation-safe boundary"
23323 );
23324 assert_eq!(subscriber.pending_reconcile_owner_records.len(), 1);
23325
23326 subscriber
23327 .drive_reconcile_fetch(futures::future::ready(Ok(())), &targets)
23328 .await
23329 .unwrap();
23330 subscriber.promote_reconcile_owner_records(&targets);
23331 assert!(subscriber.pending_reconcile_owner_records.is_empty());
23332 assert_eq!(subscriber.pending_records.len(), 2);
23333 assert_eq!(
23334 subscriber.pending_records[0].scope,
23335 SubscriberInputScope::Canonical { owners: Vec::new() },
23336 "canonical delivery remains target-excluded"
23337 );
23338 assert_eq!(
23339 subscriber.pending_records[1].scope,
23340 SubscriberInputScope::OwnerOnly {
23341 owners: vec![epoch]
23342 },
23343 "retry commit appends hidden owner delivery after historical catch-up"
23344 );
23345 }
23346
23347 #[tokio::test(flavor = "multi_thread")]
23348 #[cfg(feature = "reactive-ws")]
23349 async fn control_cancellation_preserves_terminated_source_reconcile_intent() {
23350 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23351 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23352 provider,
23353 SubscriberMode::PubSub,
23354 SubscriberConfig {
23355 reconnect: SubscriberReconnectConfig {
23356 initial_delay: Duration::from_secs(60),
23357 ..SubscriberReconnectConfig::default()
23358 },
23359 ..SubscriberConfig::default()
23360 },
23361 );
23362 subscriber.chain_id = Some(1);
23363 let interest = ReactiveInterest::Logs(LogInterest {
23364 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
23365 local_matcher: None,
23366 route_key: None,
23367 });
23368 let epoch = subscriber
23369 .stage_interest_owner(
23370 HandlerId::new("owner"),
23371 std::slice::from_ref(&interest),
23372 SubscriberOwnerStart::PostBlock(BlockRef {
23373 number: 7,
23374 hash: B256::repeat_byte(0x07),
23375 parent_hash: Some(B256::repeat_byte(0x06)),
23376 timestamp: Some(1_700_000_007),
23377 }),
23378 )
23379 .unwrap();
23380 subscriber.sources_dirty = false;
23381 subscriber.stream_revision = 1;
23382 let entry = subscriber
23383 .owned_interests
23384 .iter_mut()
23385 .find(|entry| entry.epoch.as_ref() == Some(&epoch))
23386 .unwrap();
23387 entry.progress = Some(SubscriberOwnerProgress {
23388 owner: epoch.clone(),
23389 through: entry.baseline.unwrap(),
23390 });
23391 entry.progress_stream_revision = Some(1);
23392
23393 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
23394 let source = SubscriberStreamSource::PubSubLog {
23395 id: subscriber.log_source_id(&filter),
23396 filter,
23397 };
23398 let mut streams = SubscriberStreams::new();
23399 streams.push(
23400 source.clone(),
23401 stream::iter([SubscriberEvent::StreamTerminated(source)]).boxed(),
23402 );
23403 subscriber.state = AlloySubscriberState::Active(streams);
23404 let prior_revision = subscriber.stream_revision;
23405
23406 let mut first_poll = true;
23407 let control = poll_fn(move |cx| {
23408 if first_poll {
23409 first_poll = false;
23410 cx.waker().wake_by_ref();
23411 std::task::Poll::Pending
23412 } else {
23413 std::task::Poll::Ready("stop")
23414 }
23415 });
23416 futures::pin_mut!(control);
23417 let outcome = subscriber
23418 .next_scoped_batch_or(control.as_mut())
23419 .await
23420 .unwrap();
23421
23422 assert!(matches!(outcome, SubscriberDriverPoll::Control("stop")));
23423 assert!(subscriber.sources_dirty);
23424 assert!(subscriber.stream_revision > prior_revision);
23425 assert!(
23426 !subscriber.activate_interest_owner(&epoch),
23427 "progress certified against the terminated stream revision is stale"
23428 );
23429 }
23430
23431 #[tokio::test]
23432 #[cfg(feature = "reactive-ws")]
23433 async fn pubsub_sources_assign_stable_log_ids_before_shared_streams() {
23434 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23435 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23436 provider,
23437 SubscriberMode::PubSub,
23438 SubscriberConfig::default(),
23439 );
23440 subscriber.chain_id = Some(1);
23441 subscriber
23442 .register_interests(&[
23443 ReactiveInterest::Logs(LogInterest {
23444 provider_filter: Filter::new().address(Address::repeat_byte(0x01)),
23445 local_matcher: None,
23446 route_key: None,
23447 }),
23448 ReactiveInterest::Logs(LogInterest {
23449 provider_filter: Filter::new().address(Address::repeat_byte(0x02)),
23450 local_matcher: None,
23451 route_key: None,
23452 }),
23453 ReactiveInterest::PendingTransactions(PendingTxInterest::default()),
23454 ])
23455 .await
23456 .expect("register base interests");
23457
23458 let sources = subscriber.stream_sources().expect("stream sources");
23462 assert_eq!(sources.len(), 2);
23463 assert!(matches!(
23464 &sources[0],
23465 SubscriberStreamSource::PubSubLog { id: 0, .. }
23466 ));
23467 assert!(matches!(
23468 sources[1],
23469 SubscriberStreamSource::PubSubPendingHashes
23470 ));
23471
23472 let again = subscriber.stream_sources().expect("stream sources again");
23474 assert!(again[0].same_key(&sources[0]));
23475 }
23476
23477 #[tokio::test(flavor = "multi_thread")]
23478 #[cfg(feature = "reactive-ws")]
23479 async fn pubsub_stream_termination_attempts_reconnect_before_error() {
23480 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23481 let mut subscriber = AlloySubscriber::new(
23482 provider,
23483 SubscriberMode::PubSub,
23484 SubscriberConfig {
23485 reconnect: SubscriberReconnectConfig {
23486 initial_delay: Duration::ZERO,
23487 retry_delay: Duration::ZERO,
23488 max_delay: Duration::ZERO,
23489 max_attempts: Some(1),
23490 ..SubscriberReconnectConfig::default()
23491 },
23492 ..SubscriberConfig::default()
23493 },
23494 );
23495 subscriber.chain_id = Some(1);
23496 subscriber.interests = vec![ReactiveInterest::PendingTransactions(
23497 PendingTxInterest::default(),
23498 )];
23499
23500 let mut streams = SubscriberStreams::new();
23501 let source = SubscriberStreamSource::PubSubPendingHashes;
23502 streams.push(
23503 source,
23504 stream::once(async {
23505 SubscriberEvent::<Ethereum>::StreamTerminated(
23506 SubscriberStreamSource::PubSubPendingHashes,
23507 )
23508 })
23509 .boxed(),
23510 );
23511 subscriber.state = AlloySubscriberState::Active(streams);
23512
23513 let result = subscriber.next_batch().await;
23514 assert!(
23515 matches!(result, Err(SubscriberError::Provider(ref message)) if message.contains("reconnect failed after 1 attempt")),
23516 "terminated pubsub streams should attempt reconnect before surfacing failure: {result:?}"
23517 );
23518 }
23519
23520 #[tokio::test]
23521 #[cfg(feature = "reactive-ws")]
23522 async fn flashblock_stream_termination_invalidates_before_reconnect_io() {
23523 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23524 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23525 provider,
23526 SubscriberMode::PubSub,
23527 SubscriberConfig {
23528 preconfirmations: PreconfirmationMode::Required,
23529 ..SubscriberConfig::default()
23530 },
23531 )
23532 .with_provider_ref(ProviderRef::new("base-paid", 7));
23533 subscriber.chain_id = Some(8_453);
23534 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
23535 subscriber.interests = subscriber.base_interests.clone();
23536 subscriber.sources_dirty = false;
23537
23538 let preview: BaseFlashblockWirePayload = serde_json::from_str(
23539 r#"{
23540 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
23541 "number":"0x65",
23542 "parentHash":"0x6464646464646464646464646464646464646464646464646464646464646464",
23543 "stateRoot":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
23544 "transactionsRoot":"0x1111111111111111111111111111111111111111111111111111111111111111",
23545 "timestamp":"0x6553f165",
23546 "transactions":["0x4141414141414141414141414141414141414141414141414141414141414141"]
23547 }"#,
23548 )
23549 .unwrap();
23550 let (preview, _) = subscriber.accept_base_flashblock(preview).unwrap();
23551 subscriber.latest_preconfirmation = Some(preview);
23552
23553 let mut streams = SubscriberStreams::new();
23554 streams.push(
23555 SubscriberStreamSource::BaseFlashblocks,
23556 stream::once(async {
23557 SubscriberEvent::<Ethereum>::StreamTerminated(
23558 SubscriberStreamSource::BaseFlashblocks,
23559 )
23560 })
23561 .boxed(),
23562 );
23563 subscriber.state = AlloySubscriberState::Active(streams);
23564
23565 let batch = subscriber
23566 .next_scoped_batch()
23567 .await
23568 .expect("termination handling succeeds")
23569 .expect("invalidation is delivered");
23570 assert!(batch.preconfirmation_invalidated());
23571 assert!(subscriber.latest_preconfirmation.is_none());
23572 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 8);
23573 assert_eq!(subscriber.pending_flashblock_reconnects.len(), 2);
23574 assert!(
23575 subscriber
23576 .pending_flashblock_reconnect_sources
23577 .iter()
23578 .any(|source| matches!(source, SubscriberStreamSource::BaseFlashblocks))
23579 );
23580 assert!(
23581 subscriber
23582 .pending_flashblock_reconnect_sources
23583 .iter()
23584 .any(|source| matches!(source, SubscriberStreamSource::BasePendingLog { .. }))
23585 );
23586 let AlloySubscriberState::Active(streams) = &subscriber.state else {
23587 panic!("subscriber remains active while reconnect is pending")
23588 };
23589 assert!(
23590 streams
23591 .entries
23592 .iter()
23593 .all(|entry| !entry.source.is_flashblocks())
23594 );
23595 }
23596
23597 #[tokio::test]
23598 #[cfg(feature = "reactive-ws")]
23599 async fn preferred_initial_flashblock_rejection_retains_canonical_streams() {
23600 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23601 let filter = Filter::new().address(Address::repeat_byte(0x42));
23602 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23603 provider,
23604 SubscriberMode::PubSub,
23605 SubscriberConfig {
23606 preconfirmations: PreconfirmationMode::Preferred,
23607 reconnect: SubscriberReconnectConfig {
23608 enabled: false,
23609 ..SubscriberReconnectConfig::default()
23610 },
23611 ..SubscriberConfig::default()
23612 },
23613 )
23614 .with_provider_ref(ProviderRef::new("base-paid", 1));
23615 subscriber.chain_id = Some(8_453);
23616 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
23617 provider_filter: filter.clone(),
23618 local_matcher: None,
23619 route_key: None,
23620 })];
23621 subscriber.interests = subscriber.base_interests.clone();
23622 subscriber.log_source_ids.insert(filter.clone(), 0);
23623 subscriber.next_log_source_id = 1;
23624
23625 let canonical_source = SubscriberStreamSource::PubSubLog {
23626 id: 0,
23627 filter: filter.clone(),
23628 };
23629 let mut streams = SubscriberStreams::new();
23630 streams.push(
23631 canonical_source.clone(),
23632 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
23633 );
23634 subscriber.state = AlloySubscriberState::Active(streams);
23635 subscriber.sources_dirty = true;
23636
23637 subscriber
23638 .ensure_streams()
23639 .await
23640 .expect("preferred Flashblocks setup degrades to canonical-only");
23641 let AlloySubscriberState::Active(streams) = &subscriber.state else {
23642 panic!("canonical stream remains active")
23643 };
23644 assert!(streams.contains_source(&canonical_source));
23645 assert!(
23646 streams
23647 .entries
23648 .iter()
23649 .all(|entry| !entry.source.is_flashblocks())
23650 );
23651 assert!(subscriber.pending_flashblock_reconnects.is_empty());
23652 assert!(!subscriber.sources_dirty);
23653 }
23654
23655 #[tokio::test]
23656 #[cfg(feature = "reactive-ws")]
23657 async fn required_initial_flashblock_rejection_remains_fail_closed() {
23658 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23659 let filter = Filter::new().address(Address::repeat_byte(0x42));
23660 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23661 provider,
23662 SubscriberMode::PubSub,
23663 SubscriberConfig {
23664 preconfirmations: PreconfirmationMode::Required,
23665 reconnect: SubscriberReconnectConfig {
23666 enabled: false,
23667 ..SubscriberReconnectConfig::default()
23668 },
23669 ..SubscriberConfig::default()
23670 },
23671 )
23672 .with_provider_ref(ProviderRef::new("base-paid", 1));
23673 subscriber.chain_id = Some(8_453);
23674 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
23675 provider_filter: filter.clone(),
23676 local_matcher: None,
23677 route_key: None,
23678 })];
23679 subscriber.interests = subscriber.base_interests.clone();
23680 subscriber.log_source_ids.insert(filter.clone(), 0);
23681 subscriber.next_log_source_id = 1;
23682
23683 let canonical_source = SubscriberStreamSource::PubSubLog {
23684 id: 0,
23685 filter: filter.clone(),
23686 };
23687 let mut streams = SubscriberStreams::new();
23688 streams.push(
23689 canonical_source.clone(),
23690 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
23691 );
23692 subscriber.state = AlloySubscriberState::Active(streams);
23693 subscriber.sources_dirty = true;
23694
23695 let error = subscriber
23696 .ensure_streams()
23697 .await
23698 .expect_err("required Flashblocks setup must fail closed");
23699 assert!(matches!(error, SubscriberError::Provider(_)));
23700 let AlloySubscriberState::Active(streams) = &subscriber.state else {
23701 panic!("the already-connected canonical stream is retained")
23702 };
23703 assert!(streams.contains_source(&canonical_source));
23704 }
23705
23706 #[tokio::test]
23707 #[cfg(feature = "reactive-ws")]
23708 async fn preferred_flashblock_termination_preserves_canonical_delivery() {
23709 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23710 let filter = Filter::new().address(Address::repeat_byte(0x42));
23711 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23712 provider,
23713 SubscriberMode::PubSub,
23714 SubscriberConfig {
23715 preconfirmations: PreconfirmationMode::Preferred,
23716 reconnect: SubscriberReconnectConfig {
23717 enabled: false,
23718 ..SubscriberReconnectConfig::default()
23719 },
23720 ..SubscriberConfig::default()
23721 },
23722 )
23723 .with_provider_ref(ProviderRef::new("base-paid", 1));
23724 subscriber.chain_id = Some(8_453);
23725 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
23726 provider_filter: filter.clone(),
23727 local_matcher: None,
23728 route_key: None,
23729 })];
23730 subscriber.interests = subscriber.base_interests.clone();
23731 subscriber.log_source_ids.insert(filter.clone(), 0);
23732 subscriber.next_log_source_id = 1;
23733 subscriber.sources_dirty = false;
23734
23735 let mut streams = SubscriberStreams::new();
23736 streams.push(
23737 SubscriberStreamSource::BaseFlashblocks,
23738 stream::once(async {
23739 SubscriberEvent::<Ethereum>::StreamTerminated(
23740 SubscriberStreamSource::BaseFlashblocks,
23741 )
23742 })
23743 .boxed(),
23744 );
23745 streams.push(
23746 SubscriberStreamSource::PubSubLog {
23747 id: 0,
23748 filter: filter.clone(),
23749 },
23750 stream::once(async {
23751 SubscriberEvent::<Ethereum>::Log {
23752 source_id: 0,
23753 log: rpc_log(false),
23754 }
23755 })
23756 .boxed(),
23757 );
23758 subscriber.state = AlloySubscriberState::Active(streams);
23759
23760 let invalidation = subscriber
23761 .next_scoped_batch()
23762 .await
23763 .expect("preferred termination does not fail")
23764 .expect("invalidation is delivered");
23765 assert!(invalidation.preconfirmation_invalidated());
23766
23767 let canonical = subscriber
23768 .next_scoped_batch()
23769 .await
23770 .expect("canonical stream remains healthy")
23771 .expect("canonical log is delivered");
23772 assert!(!canonical.preconfirmation_invalidated());
23773 assert_eq!(canonical.records().len(), 1);
23774 assert_eq!(
23775 canonical.records()[0].record.context.source,
23776 InputSource::Subscription
23777 );
23778 }
23779
23780 #[tokio::test]
23781 #[cfg(feature = "reactive-ws")]
23782 async fn preferred_flashblock_reconnect_exhaustion_preserves_canonical_delivery() {
23783 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23784 let filter = Filter::new().address(Address::repeat_byte(0x42));
23785 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23786 provider,
23787 SubscriberMode::PubSub,
23788 SubscriberConfig {
23789 preconfirmations: PreconfirmationMode::Preferred,
23790 reconnect: SubscriberReconnectConfig {
23791 enabled: false,
23792 ..SubscriberReconnectConfig::default()
23793 },
23794 ..SubscriberConfig::default()
23795 },
23796 )
23797 .with_provider_ref(ProviderRef::new("base-paid", 1));
23798 subscriber.chain_id = Some(8_453);
23799 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
23800 provider_filter: filter.clone(),
23801 local_matcher: None,
23802 route_key: None,
23803 })];
23804 subscriber.interests = subscriber.base_interests.clone();
23805 subscriber.log_source_ids.insert(filter.clone(), 0);
23806 subscriber.next_log_source_id = 1;
23807 subscriber.sources_dirty = false;
23808
23809 let canonical_source = SubscriberStreamSource::PubSubLog { id: 0, filter };
23810 let mut streams = SubscriberStreams::new();
23811 streams.push(
23812 canonical_source,
23813 stream::once(async {
23814 tokio::time::sleep(Duration::from_millis(1)).await;
23815 SubscriberEvent::<Ethereum>::Log {
23816 source_id: 0,
23817 log: rpc_log(false),
23818 }
23819 })
23820 .boxed(),
23821 );
23822 subscriber.state = AlloySubscriberState::Active(streams);
23823
23824 let source = SubscriberStreamSource::BaseFlashblocks;
23825 subscriber
23826 .pending_flashblock_reconnect_sources
23827 .push(source.clone());
23828 subscriber
23829 .pending_flashblock_reconnects
23830 .push(Box::pin(async move {
23831 (
23832 source,
23833 Err(SubscriberError::Provider(
23834 "test reconnect window exhausted".to_owned(),
23835 )),
23836 )
23837 }));
23838
23839 let canonical = subscriber
23840 .next_scoped_batch()
23841 .await
23842 .expect("preferred reconnect exhaustion does not fail")
23843 .expect("canonical log is delivered");
23844 assert_eq!(canonical.records().len(), 1);
23845 assert!(subscriber.pending_flashblock_reconnects.is_empty());
23846 }
23847
23848 #[test]
23849 fn backfilled_logs_skip_recent_subscription_duplicates() {
23850 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23851 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23852 provider,
23853 SubscriberMode::PubSub,
23854 SubscriberConfig::default(),
23855 );
23856 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
23857 provider_filter: Filter::new()
23858 .address(Address::repeat_byte(0x42))
23859 .event_signature(B256::repeat_byte(0x01)),
23860 local_matcher: None,
23861 route_key: None,
23862 })];
23863
23864 let log = rpc_log(false);
23865 subscriber.enqueue_event(SubscriberEvent::Log {
23866 source_id: 0,
23867 log: log.clone(),
23868 });
23869 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
23870 source_id: 0,
23871 logs: vec![log],
23872 });
23873
23874 assert_eq!(subscriber.pending_records.len(), 1);
23875 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
23876 assert_eq!(
23877 subscriber.pending_records[0].context.source,
23878 InputSource::Subscription
23879 );
23880 }
23881
23882 #[test]
23883 fn backfilled_logs_surface_with_backfill_source() {
23884 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23889 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23890 provider,
23891 SubscriberMode::PubSub,
23892 SubscriberConfig::default(),
23893 );
23894 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
23895 provider_filter: Filter::new()
23896 .address(Address::repeat_byte(0x42))
23897 .event_signature(B256::repeat_byte(0x01)),
23898 local_matcher: None,
23899 route_key: None,
23900 })];
23901
23902 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
23903 source_id: 0,
23904 logs: vec![rpc_log(false)],
23905 });
23906
23907 assert_eq!(subscriber.pending_records.len(), 1);
23908 assert_eq!(
23909 subscriber.pending_records[0].context.source,
23910 InputSource::Backfill
23911 );
23912 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
23913 }
23914
23915 #[test]
23916 #[cfg(feature = "reactive-ws")]
23917 fn owner_removal_preserves_delivery_and_dedupe_state() {
23918 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23919 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23920 provider,
23921 SubscriberMode::PubSub,
23922 SubscriberConfig::default(),
23923 );
23924 subscriber
23925 .add_interest_owner(
23926 HandlerId::new("pool-a"),
23927 &[ReactiveInterest::Logs(LogInterest {
23928 provider_filter: Filter::new()
23929 .address(Address::repeat_byte(0x42))
23930 .event_signature(B256::repeat_byte(0x01)),
23931 local_matcher: None,
23932 route_key: None,
23933 })],
23934 )
23935 .expect("register pool-a owner");
23936 subscriber
23937 .add_interest_owner(
23938 HandlerId::new("pool-b"),
23939 &[ReactiveInterest::Logs(LogInterest {
23940 provider_filter: Filter::new()
23941 .address(Address::repeat_byte(0x24))
23942 .event_signature(B256::repeat_byte(0x02)),
23943 local_matcher: None,
23944 route_key: None,
23945 })],
23946 )
23947 .expect("register pool-b owner");
23948
23949 let sources = subscriber.stream_sources().expect("stream sources");
23952 subscriber.enqueue_event(SubscriberEvent::Log {
23953 source_id: 0,
23954 log: rpc_log(false),
23955 });
23956 let mut streams = SubscriberStreams::new();
23957 streams.push(
23958 sources[0].clone(),
23959 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
23960 );
23961 subscriber.state = AlloySubscriberState::Active(streams);
23962 assert_eq!(subscriber.pending_records.len(), 1);
23963 assert_eq!(subscriber.recent_input_refs.len(), 1);
23964 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
23965
23966 let removed = subscriber
23967 .remove_interest_owner(&HandlerId::new("pool-b"))
23968 .expect("pool-b should be removed");
23969
23970 assert_eq!(removed.len(), 1);
23971 assert_eq!(subscriber.pending_records.len(), 1);
23972 assert_eq!(subscriber.recent_input_refs.len(), 1);
23973 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
23974 assert!(
23975 subscriber
23976 .owner_interests(&HandlerId::new("pool-a"))
23977 .is_some()
23978 );
23979 assert!(
23980 subscriber
23981 .owner_interests(&HandlerId::new("pool-b"))
23982 .is_none()
23983 );
23984 assert_eq!(subscriber.registered_interests().len(), 1);
23985 }
23986
23987 #[test]
23988 #[cfg(feature = "reactive-ws")]
23989 fn owner_log_sources_fan_in_across_owners() {
23990 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23991 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23992 provider,
23993 SubscriberMode::PubSub,
23994 SubscriberConfig::default(),
23995 );
23996 subscriber
23997 .add_interest_owner(
23998 HandlerId::new("pool-a"),
23999 &[ReactiveInterest::Logs(LogInterest {
24000 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
24001 local_matcher: None,
24002 route_key: None,
24003 })],
24004 )
24005 .expect("register pool-a owner");
24006
24007 let initial_sources = subscriber.stream_sources().expect("initial sources");
24008 assert_eq!(initial_sources.len(), 1);
24009 let pool_a_source = initial_sources[0].clone();
24010 assert!(matches!(
24011 &pool_a_source,
24012 SubscriberStreamSource::PubSubLog { id: 0, .. }
24013 ));
24014
24015 subscriber
24016 .add_interest_owner(
24017 HandlerId::new("pool-b"),
24018 &[ReactiveInterest::Logs(LogInterest {
24019 provider_filter: Filter::new().address(Address::repeat_byte(0xb2)),
24020 local_matcher: None,
24021 route_key: None,
24022 })],
24023 )
24024 .expect("register pool-b owner");
24025
24026 let expanded_sources = subscriber.stream_sources().expect("expanded sources");
24027 assert_eq!(
24028 expanded_sources.len(),
24029 1,
24030 "compatible owner filters should share one provider subscription"
24031 );
24032 assert!(
24033 !expanded_sources[0].same_key(&pool_a_source),
24034 "the provider-facing superset changes while owner routing remains exact"
24035 );
24036
24037 subscriber
24038 .remove_interest_owner(&HandlerId::new("pool-b"))
24039 .expect("pool-b should be removed");
24040 let trimmed_sources = subscriber.stream_sources().expect("trimmed sources");
24041 assert_eq!(trimmed_sources.len(), 1);
24042 assert!(trimmed_sources[0].same_key(&pool_a_source));
24043 }
24044
24045 #[test]
24046 #[cfg(feature = "reactive-ws")]
24047 fn provider_log_fan_in_respects_address_ceiling() {
24048 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24049 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24050 provider,
24051 SubscriberMode::PubSub,
24052 SubscriberConfig {
24053 max_log_addresses_per_subscription: 2,
24054 ..SubscriberConfig::default()
24055 },
24056 );
24057 for index in 0..5 {
24058 subscriber
24059 .add_interest_owner(
24060 HandlerId::new(format!("pool-{index}")),
24061 &[log_interest_for(index + 1)],
24062 )
24063 .expect("register pool owner");
24064 }
24065
24066 let sources = subscriber.stream_sources().expect("stream sources");
24067 assert_eq!(sources.len(), 3);
24068 let mut address_counts: Vec<_> = sources
24069 .iter()
24070 .map(|source| match source {
24071 SubscriberStreamSource::PubSubLog { filter, .. } => filter.address.iter().count(),
24072 _ => panic!("expected log source"),
24073 })
24074 .collect();
24075 address_counts.sort_unstable();
24076 assert_eq!(address_counts, vec![1, 2, 2]);
24077 }
24078
24079 #[tokio::test(flavor = "multi_thread")]
24080 #[cfg(feature = "reactive-ws")]
24081 async fn owner_backfill_seeds_reconnect_anchor_before_live_log() {
24082 let asserter = Asserter::new();
24083 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
24084 asserter.push_success(&vec![rpc_log(false)]);
24085 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
24086 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
24087 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24088 provider,
24089 SubscriberMode::PubSub,
24090 SubscriberConfig::default(),
24091 );
24092 subscriber
24093 .add_interest_owner_with_backfill(
24094 HandlerId::new("pool-a"),
24095 &[ReactiveInterest::Logs(LogInterest {
24096 provider_filter: Filter::new()
24097 .address(Address::repeat_byte(0x42))
24098 .event_signature(B256::repeat_byte(0x01)),
24099 local_matcher: None,
24100 route_key: None,
24101 })],
24102 SubscriberBackfill::range(1, 7),
24103 )
24104 .expect("register pool-a with backfill");
24105
24106 subscriber
24107 .drain_pending_backfills()
24108 .await
24109 .expect("owner backfill should drain");
24110
24111 assert_eq!(subscriber.pending_records.len(), 1);
24112 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
24113 }
24114
24115 #[tokio::test(flavor = "multi_thread")]
24116 async fn subscriber_streams_poll_ready_sources_round_robin() {
24117 let first_hash = B256::repeat_byte(0x01);
24118 let second_hash = B256::repeat_byte(0x02);
24119 let mut streams = SubscriberStreams::new();
24120 streams.push(
24121 SubscriberStreamSource::PubSubPendingHashes,
24122 stream::iter([
24123 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
24124 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
24125 ])
24126 .boxed(),
24127 );
24128 streams.push(
24129 SubscriberStreamSource::PubSubBlockHeaders,
24130 stream::once(async move { SubscriberEvent::<Ethereum>::PendingHash(second_hash) })
24131 .boxed(),
24132 );
24133
24134 assert!(matches!(
24135 streams.next().await,
24136 Some(SubscriberEvent::PendingHash(hash)) if hash == first_hash
24137 ));
24138 assert!(matches!(
24139 streams.next().await,
24140 Some(SubscriberEvent::PendingHash(hash)) if hash == second_hash
24141 ));
24142 }
24143
24144 #[tokio::test(flavor = "multi_thread")]
24145 #[cfg(feature = "reactive-ws")]
24146 async fn owner_updates_ensure_streams_without_full_reset() {
24147 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24148 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24149 provider,
24150 SubscriberMode::PubSub,
24151 SubscriberConfig::default(),
24152 );
24153 subscriber.chain_id = Some(1);
24154 subscriber
24155 .register_interests(&[ReactiveInterest::PendingTransactions(
24156 PendingTxInterest::default(),
24157 )])
24158 .await
24159 .expect("register base pending interest");
24160 subscriber
24161 .add_interest_owner(
24162 HandlerId::new("headers"),
24163 &[ReactiveInterest::Blocks(BlockInterest::default())],
24164 )
24165 .expect("register header owner");
24166
24167 let mut streams = SubscriberStreams::new();
24168 streams.push(
24169 SubscriberStreamSource::PubSubPendingHashes,
24170 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
24171 );
24172 streams.push(
24173 SubscriberStreamSource::PubSubBlockHeaders,
24174 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
24175 );
24176 subscriber.state = AlloySubscriberState::Active(streams);
24177
24178 subscriber
24179 .remove_interest_owner(&HandlerId::new("headers"))
24180 .expect("header owner should be removed");
24181 assert!(matches!(
24182 &subscriber.state,
24183 AlloySubscriberState::Active(streams) if streams.len() == 2
24184 ));
24185
24186 subscriber
24187 .ensure_streams()
24188 .await
24189 .expect("pure removal reconciliation should not touch provider");
24190
24191 assert!(matches!(
24192 &subscriber.state,
24193 AlloySubscriberState::Active(streams)
24194 if streams.len() == 1
24195 && streams.contains_source(&SubscriberStreamSource::PubSubPendingHashes)
24196 && !streams.contains_source(&SubscriberStreamSource::PubSubBlockHeaders)
24197 ));
24198
24199 subscriber
24200 .add_interest_owner(
24201 HandlerId::new("headers"),
24202 &[ReactiveInterest::Blocks(BlockInterest::default())],
24203 )
24204 .expect("re-add header owner");
24205 assert!(matches!(
24206 &subscriber.state,
24207 AlloySubscriberState::Active(streams) if streams.len() == 1
24208 ));
24209 }
24210
24211 #[tokio::test(flavor = "multi_thread")]
24212 #[cfg(feature = "reactive-polling")]
24213 async fn ensure_streams_retains_each_successful_connection_across_later_failure() {
24214 let asserter = Asserter::new();
24215 asserter.push_success(&U256::from(1));
24216 asserter.push_failure_msg("second filter connection failed");
24217 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
24218 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24219 provider,
24220 SubscriberMode::Polling,
24221 SubscriberConfig {
24222 max_log_addresses_per_subscription: 1,
24223 ..SubscriberConfig::default()
24224 },
24225 );
24226 subscriber.chain_id = Some(1);
24227 subscriber
24228 .register_interests(&[log_interest_for(0x41), log_interest_for(0x42)])
24229 .await
24230 .expect("register two independently connected filters");
24231
24232 let error = subscriber
24233 .ensure_streams()
24234 .await
24235 .expect_err("second provider connection is forced to fail");
24236 assert!(matches!(error, SubscriberError::Provider(_)));
24237 assert!(subscriber.sources_dirty);
24238 let retained_streams = match &subscriber.state {
24239 AlloySubscriberState::Active(streams) => Some(streams.len()),
24240 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => None,
24241 };
24242 assert_eq!(
24243 retained_streams,
24244 Some(1),
24245 "first connection must survive later error {error:?}; revision {}",
24246 subscriber.stream_revision
24247 );
24248
24249 asserter.push_success(&U256::from(2));
24250 subscriber
24251 .ensure_streams()
24252 .await
24253 .expect("retry connects only the missing source");
24254 assert!(!subscriber.sources_dirty);
24255 assert!(matches!(
24256 &subscriber.state,
24257 AlloySubscriberState::Active(streams) if streams.len() == 2
24258 ));
24259 assert!(asserter.read_q().is_empty());
24260 }
24261
24262 #[tokio::test(flavor = "multi_thread")]
24263 #[cfg(feature = "reactive-ws")]
24264 async fn cancelled_post_install_backfill_is_retried_without_reconnecting() {
24265 let asserter = Asserter::new();
24266 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
24267 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24268 provider,
24269 SubscriberMode::PubSub,
24270 SubscriberConfig::default(),
24271 );
24272 subscriber.chain_id = Some(1);
24273 subscriber
24274 .register_interests(&[log_interest_for(0x43)])
24275 .await
24276 .expect("register log source");
24277 let source = subscriber
24278 .stream_sources()
24279 .expect("one desired source")
24280 .pop()
24281 .expect("log source");
24282 let SubscriberStreamSource::PubSubLog { id, .. } = source else {
24283 panic!("expected pubsub log source")
24284 };
24285 subscriber.last_seen_log_blocks.insert(id, 6);
24286
24287 {
24288 let source = SubscriberStreamSource::PubSubLog {
24289 id,
24290 filter: subscriber
24291 .log_stream_filters()
24292 .pop()
24293 .expect("provider filter"),
24294 };
24295 let interrupted = async {
24296 subscriber.install_source_stream(
24297 source.clone(),
24298 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
24299 );
24300 subscriber.queue_source_backfill(source);
24301 subscriber.sources_dirty = true;
24302 futures::future::pending::<()>().await;
24303 };
24304 futures::pin_mut!(interrupted);
24305 poll_fn(|cx| {
24306 assert!(interrupted.as_mut().poll(cx).is_pending());
24307 std::task::Poll::Ready(())
24308 })
24309 .await;
24310 }
24311
24312 assert_eq!(subscriber.pending_source_backfills.len(), 1);
24313 assert!(matches!(
24314 &subscriber.state,
24315 AlloySubscriberState::Active(streams) if streams.len() == 1
24316 ));
24317
24318 asserter.push_success(&7u64);
24319 asserter.push_success(&Vec::<Log>::new());
24320 subscriber
24321 .ensure_streams()
24322 .await
24323 .expect("retry completes only the pending historical window");
24324
24325 assert!(subscriber.pending_source_backfills.is_empty());
24326 assert!(!subscriber.sources_dirty);
24327 assert!(matches!(
24328 &subscriber.state,
24329 AlloySubscriberState::Active(streams) if streams.len() == 1
24330 ));
24331 assert!(asserter.read_q().is_empty());
24332 }
24333
24334 #[cfg(any(
24336 feature = "raw-flashblocks-json",
24337 feature = "reactive-polling",
24338 feature = "reactive-ws"
24339 ))]
24340 fn log_interest_matching_rpc_log() -> ReactiveInterest<Ethereum> {
24341 ReactiveInterest::Logs(LogInterest {
24342 provider_filter: Filter::new()
24343 .address(Address::repeat_byte(0x42))
24344 .event_signature(B256::repeat_byte(0x01)),
24345 local_matcher: None,
24346 route_key: None,
24347 })
24348 }
24349
24350 #[cfg(any(feature = "reactive-ws", feature = "reactive-polling"))]
24351 fn log_interest_for(address: u8) -> ReactiveInterest<Ethereum> {
24352 ReactiveInterest::Logs(LogInterest {
24353 provider_filter: Filter::new().address(Address::repeat_byte(address)),
24354 local_matcher: None,
24355 route_key: None,
24356 })
24357 }
24358
24359 #[tokio::test(flavor = "multi_thread")]
24362 #[cfg(feature = "reactive-ws")]
24363 async fn drain_backfill_retains_queue_entry_on_provider_error() {
24364 let asserter = Asserter::new();
24365 asserter.push_failure_msg("rate limited");
24366 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
24367 asserter.push_success(&vec![rpc_log(false)]);
24368 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
24369 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
24370 let mut subscriber = AlloySubscriber::new(
24371 provider,
24372 SubscriberMode::PubSub,
24373 SubscriberConfig::default(),
24374 );
24375 subscriber
24376 .add_interest_owner_with_backfill(
24377 HandlerId::new("pool"),
24378 &[log_interest_matching_rpc_log()],
24379 SubscriberBackfill::range(1, 7),
24380 )
24381 .expect("register owner with backfill");
24382 assert_eq!(subscriber.pending_backfills.len(), 1);
24383
24384 let first = subscriber.drain_pending_backfills().await;
24385 assert!(first.is_err(), "provider failure should surface");
24386 assert_eq!(
24387 subscriber.pending_backfills.len(),
24388 1,
24389 "failed fetch must leave the backfill queued for retry"
24390 );
24391 assert!(subscriber.pending_records.is_empty());
24392
24393 subscriber
24394 .drain_pending_backfills()
24395 .await
24396 .expect("retry should succeed");
24397 assert!(subscriber.pending_backfills.is_empty());
24398 assert_eq!(subscriber.pending_records.len(), 1);
24399 }
24400
24401 #[tokio::test(flavor = "multi_thread")]
24404 #[cfg(feature = "reactive-ws")]
24405 async fn drain_backfill_seeds_anchor_on_empty_window() {
24406 let asserter = Asserter::new();
24407 asserter.push_success(&Some(rpc_block(42, B256::repeat_byte(42))));
24408 asserter.push_success(&Vec::<Log>::new());
24409 asserter.push_success(&Some(rpc_block(42, B256::repeat_byte(42))));
24410 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
24411 let mut subscriber = AlloySubscriber::new(
24412 provider,
24413 SubscriberMode::PubSub,
24414 SubscriberConfig::default(),
24415 );
24416 subscriber
24417 .add_interest_owner_with_backfill(
24418 HandlerId::new("pool"),
24419 &[log_interest_matching_rpc_log()],
24420 SubscriberBackfill::range(1, 42),
24421 )
24422 .expect("register owner with backfill");
24423
24424 subscriber
24425 .drain_pending_backfills()
24426 .await
24427 .expect("empty backfill should drain");
24428
24429 assert!(subscriber.pending_records.is_empty());
24430 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
24431 .pop()
24432 .unwrap();
24433 assert_eq!(
24434 subscriber.log_anchor(&filter),
24435 Some(42),
24436 "empty window must still seed the anchor at its upper bound"
24437 );
24438 }
24439
24440 #[tokio::test(flavor = "multi_thread")]
24443 #[cfg(feature = "reactive-ws")]
24444 async fn drain_backfill_open_ended_resolves_head_and_seeds_anchor() {
24445 let asserter = Asserter::new();
24446 asserter.push_success(&100u64); asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(100))));
24448 asserter.push_success(&Vec::<Log>::new()); asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(100))));
24450 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
24451 let mut subscriber = AlloySubscriber::new(
24452 provider,
24453 SubscriberMode::PubSub,
24454 SubscriberConfig::default(),
24455 );
24456 subscriber
24457 .add_interest_owner_with_backfill(
24458 HandlerId::new("pool"),
24459 &[log_interest_matching_rpc_log()],
24460 SubscriberBackfill::from_block(10),
24461 )
24462 .expect("register owner with open-ended backfill");
24463
24464 subscriber
24465 .drain_pending_backfills()
24466 .await
24467 .expect("open-ended backfill should drain");
24468
24469 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
24470 .pop()
24471 .unwrap();
24472 assert_eq!(subscriber.log_anchor(&filter), Some(100));
24473 }
24474
24475 #[test]
24478 #[cfg(feature = "reactive-ws")]
24479 fn duplicate_filters_across_owners_map_to_single_source() {
24480 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24481 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24482 provider,
24483 SubscriberMode::PubSub,
24484 SubscriberConfig::default(),
24485 );
24486 subscriber
24487 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
24488 .expect("register pool-a");
24489 subscriber
24490 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xaa)])
24491 .expect("register pool-b with identical filter");
24492
24493 assert_eq!(
24494 subscriber.log_stream_filters().len(),
24495 1,
24496 "identical filters across owners must collapse to one"
24497 );
24498 let sources = subscriber.stream_sources().expect("stream sources");
24499 assert_eq!(sources.len(), 1);
24500 }
24501
24502 #[test]
24505 #[cfg(feature = "reactive-ws")]
24506 fn owner_removal_prunes_source_ids_and_anchors() {
24507 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24508 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24509 provider,
24510 SubscriberMode::PubSub,
24511 SubscriberConfig::default(),
24512 );
24513 subscriber
24514 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
24515 .expect("register pool-a");
24516 subscriber
24517 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xbb)])
24518 .expect("register pool-b");
24519
24520 let _ = subscriber.stream_sources().expect("stream sources");
24522 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
24523 let filter_b = log_filters(&[log_interest_for(0xbb)]).pop().unwrap();
24524 let id_a = subscriber.log_source_id(&filter_a);
24525 let id_b = subscriber.log_source_id(&filter_b);
24526 subscriber.last_seen_log_blocks.insert(id_a, 10);
24527 subscriber.last_seen_log_blocks.insert(id_b, 20);
24528 assert_eq!(
24529 subscriber.log_source_ids.len(),
24530 3,
24531 "one provider fan-in id plus two explicitly seeded logical ids"
24532 );
24533
24534 subscriber
24535 .remove_interest_owner(&HandlerId::new("pool-b"))
24536 .expect("remove pool-b");
24537
24538 assert_eq!(
24539 subscriber.log_source_ids.len(),
24540 1,
24541 "pool-b's filter id should be retired"
24542 );
24543 assert!(subscriber.log_source_ids.contains_key(&filter_a));
24544 assert_eq!(subscriber.last_seen_log_blocks.get(&id_a), Some(&10));
24545 assert_eq!(
24546 subscriber.last_seen_log_blocks.get(&id_b),
24547 None,
24548 "pool-b's anchor should be pruned"
24549 );
24550 }
24551
24552 #[test]
24557 #[cfg(feature = "reactive-ws")]
24558 fn owner_filter_growth_queues_continuity_backfill_from_prior_anchor() {
24559 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24560 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24561 provider,
24562 SubscriberMode::PubSub,
24563 SubscriberConfig::default(),
24564 );
24565 subscriber
24566 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
24567 .expect("register amm with pool A");
24568
24569 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
24572 let id_a = subscriber.log_source_id(&filter_a);
24573 subscriber.last_seen_log_blocks.insert(id_a, 50);
24574
24575 subscriber
24578 .add_interest_owner(
24579 HandlerId::new("amm"),
24580 &[log_interest_for(0xaa), log_interest_for(0xbb)],
24581 )
24582 .expect("grow amm to pools A+B");
24583
24584 assert_eq!(
24585 subscriber.pending_backfills.len(),
24586 1,
24587 "the changed merged filter should queue exactly one continuity backfill"
24588 );
24589 let queued = &subscriber.pending_backfills[0];
24590 assert_eq!(queued.owner, Some(HandlerId::new("amm")));
24591 assert_eq!(queued.backfill.start_block(), 50);
24592 assert_eq!(
24593 queued.backfill.end_block(),
24594 None,
24595 "continuity backfill runs open-ended to the current head"
24596 );
24597 }
24598
24599 #[test]
24602 #[cfg(feature = "reactive-ws")]
24603 fn unchanged_owner_filter_does_not_queue_continuity_backfill() {
24604 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24605 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24606 provider,
24607 SubscriberMode::PubSub,
24608 SubscriberConfig::default(),
24609 );
24610 subscriber
24611 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
24612 .expect("register amm");
24613 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
24614 let id_a = subscriber.log_source_id(&filter_a);
24615 subscriber.last_seen_log_blocks.insert(id_a, 50);
24616
24617 subscriber
24618 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
24619 .expect("re-register identical interests");
24620
24621 assert!(
24622 subscriber.pending_backfills.is_empty(),
24623 "an unchanged filter shape must not queue continuity backfill"
24624 );
24625 }
24626
24627 #[test]
24631 #[cfg(feature = "reactive-ws")]
24632 fn explicit_open_ended_backfill_below_anchor_suppresses_continuity() {
24633 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24634 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24635 provider,
24636 SubscriberMode::PubSub,
24637 SubscriberConfig::default(),
24638 );
24639 subscriber
24640 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
24641 .expect("register amm");
24642 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
24643 let id_a = subscriber.log_source_id(&filter_a);
24644 subscriber.last_seen_log_blocks.insert(id_a, 50);
24645
24646 subscriber
24648 .add_interest_owner_with_backfill(
24649 HandlerId::new("amm"),
24650 &[log_interest_for(0xaa), log_interest_for(0xbb)],
24651 SubscriberBackfill::from_block(10),
24652 )
24653 .expect("grow amm with explicit deep backfill");
24654
24655 assert_eq!(
24656 subscriber.pending_backfills.len(),
24657 1,
24658 "only the explicit backfill should be queued; continuity is subsumed"
24659 );
24660 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 10);
24661 }
24662
24663 #[tokio::test(flavor = "multi_thread")]
24666 #[cfg(feature = "reactive-ws")]
24667 async fn ensure_streams_is_noop_when_not_dirty() {
24668 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
24669 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
24670 provider,
24671 SubscriberMode::PubSub,
24672 SubscriberConfig::default(),
24673 );
24674 subscriber
24676 .add_interest_owner(
24677 HandlerId::new("headers"),
24678 &[ReactiveInterest::Blocks(BlockInterest::default())],
24679 )
24680 .expect("register header owner");
24681 subscriber.state = AlloySubscriberState::Empty;
24683 subscriber.sources_dirty = false;
24684
24685 subscriber
24686 .ensure_streams()
24687 .await
24688 .expect("clean reconcile must be a no-op");
24689
24690 assert!(
24691 matches!(subscriber.state, AlloySubscriberState::Empty),
24692 "not-dirty ensure_streams must not connect new sources"
24693 );
24694 }
24695}
24696
24697fn resolve_subscriber_transport(
24698 mode: SubscriberMode,
24699) -> Result<SubscriberTransport, SubscriberError> {
24700 match mode {
24701 SubscriberMode::PubSub => {
24702 #[cfg(feature = "reactive-ws")]
24703 {
24704 Ok(SubscriberTransport::PubSub)
24705 }
24706 #[cfg(not(feature = "reactive-ws"))]
24707 {
24708 Err(SubscriberError::Unsupported(
24709 "AlloySubscriber pubsub mode requires the reactive-ws feature",
24710 ))
24711 }
24712 }
24713 SubscriberMode::Polling => {
24714 #[cfg(feature = "reactive-polling")]
24715 {
24716 Ok(SubscriberTransport::Polling)
24717 }
24718 #[cfg(not(feature = "reactive-polling"))]
24719 {
24720 Err(SubscriberError::Unsupported(
24721 "AlloySubscriber polling mode requires the reactive-polling feature",
24722 ))
24723 }
24724 }
24725 SubscriberMode::Auto => resolve_auto_subscriber_transport(),
24726 }
24727}
24728
24729fn resolve_auto_subscriber_transport() -> Result<SubscriberTransport, SubscriberError> {
24730 #[cfg(feature = "reactive-ws")]
24731 {
24732 Ok(SubscriberTransport::PubSub)
24733 }
24734
24735 #[cfg(all(not(feature = "reactive-ws"), feature = "reactive-polling"))]
24736 {
24737 Ok(SubscriberTransport::Polling)
24738 }
24739
24740 #[cfg(not(any(feature = "reactive-ws", feature = "reactive-polling")))]
24741 {
24742 Err(SubscriberError::Unsupported(
24743 "AlloySubscriber requires either reactive-ws or reactive-polling",
24744 ))
24745 }
24746}
24747
24748fn validate_subscriber_config(config: &SubscriberConfig) -> Result<(), SubscriberError> {
24749 if config.preconfirmations != PreconfirmationMode::Disabled
24750 && config.canonical_head_poll_interval.is_zero()
24751 {
24752 return Err(SubscriberError::InvalidConfig(
24753 "SubscriberConfig::canonical_head_poll_interval must be greater than zero",
24754 ));
24755 }
24756 if config.preconfirmations != PreconfirmationMode::Disabled
24757 && config.canonical_head_request_timeout.is_zero()
24758 {
24759 return Err(SubscriberError::InvalidConfig(
24760 "SubscriberConfig::canonical_head_request_timeout must be greater than zero",
24761 ));
24762 }
24763 if config.preconfirmations != PreconfirmationMode::Disabled
24764 && config.flashblock_poll_interval.is_zero()
24765 {
24766 return Err(SubscriberError::InvalidConfig(
24767 "SubscriberConfig::flashblock_poll_interval must be greater than zero",
24768 ));
24769 }
24770 if config.preconfirmations != PreconfirmationMode::Disabled
24771 && config.max_consecutive_flashblock_poll_failures == 0
24772 {
24773 return Err(SubscriberError::InvalidConfig(
24774 "SubscriberConfig::max_consecutive_flashblock_poll_failures must be greater than zero",
24775 ));
24776 }
24777 if config.preconfirmations != PreconfirmationMode::Disabled
24778 && config.max_pending_transaction_receipts_per_tick == 0
24779 {
24780 return Err(SubscriberError::InvalidConfig(
24781 "SubscriberConfig::max_pending_transaction_receipts_per_tick must be greater than zero",
24782 ));
24783 }
24784 if config.preconfirmations != PreconfirmationMode::Disabled
24785 && config.max_flashblock_rpc_requests_per_second == 0
24786 {
24787 return Err(SubscriberError::InvalidConfig(
24788 "SubscriberConfig::max_flashblock_rpc_requests_per_second must be greater than zero",
24789 ));
24790 }
24791 if config.max_batch_size == 0 {
24792 return Err(SubscriberError::InvalidConfig(
24793 "SubscriberConfig::max_batch_size must be greater than zero",
24794 ));
24795 }
24796 if config.max_log_addresses_per_subscription == 0 {
24797 return Err(SubscriberError::InvalidConfig(
24798 "SubscriberConfig::max_log_addresses_per_subscription must be greater than zero",
24799 ));
24800 }
24801 if config.max_pending_records == 0 {
24802 return Err(SubscriberError::InvalidConfig(
24803 "SubscriberConfig::max_pending_records must be greater than zero",
24804 ));
24805 }
24806 if config.max_pending_backfills == 0 {
24807 return Err(SubscriberError::InvalidConfig(
24808 "SubscriberConfig::max_pending_backfills must be greater than zero",
24809 ));
24810 }
24811 if config.max_backfill_log_bytes == 0 {
24812 return Err(SubscriberError::InvalidConfig(
24813 "SubscriberConfig::max_backfill_log_bytes must be greater than zero",
24814 ));
24815 }
24816 if config.max_reconcile_requests_in_flight == 0 {
24817 return Err(SubscriberError::InvalidConfig(
24818 "SubscriberConfig::max_reconcile_requests_in_flight must be greater than zero",
24819 ));
24820 }
24821 if config.reconnect.enabled {
24822 if config.reconnect.retry_delay > config.reconnect.max_delay {
24823 return Err(SubscriberError::InvalidConfig(
24824 "SubscriberReconnectConfig::retry_delay must be less than or equal to max_delay",
24825 ));
24826 }
24827 if matches!(config.reconnect.max_attempts, Some(0)) {
24828 return Err(SubscriberError::InvalidConfig(
24829 "SubscriberReconnectConfig::max_attempts must be greater than zero when set",
24830 ));
24831 }
24832 }
24833 Ok(())
24834}
24835
24836fn validate_supported_interests<N: Network>(
24837 mode: SubscriberMode,
24838 config: &SubscriberConfig,
24839 interests: &[ReactiveInterest<N>],
24840) -> Result<(), SubscriberError> {
24841 let transport = resolve_subscriber_transport(mode)?;
24842
24843 for interest in interests {
24844 match interest {
24845 ReactiveInterest::Logs(_) => {}
24846 ReactiveInterest::PendingTransactions(interest)
24847 if !config.hydrate_pending_transactions && interest.matches_hash_only() => {}
24848 ReactiveInterest::PendingTransactions(_) => {
24849 return Err(SubscriberError::Unsupported(
24850 "AlloySubscriber currently supports pending transaction hash interests only (full pending-tx hydration is unimplemented)",
24851 ));
24852 }
24853 ReactiveInterest::Blocks(interest) => match (transport, interest.mode) {
24854 (SubscriberTransport::PubSub, BlockInterestMode::Header) => {}
24855 (_, BlockInterestMode::FullBlock) => {
24856 return Err(SubscriberError::Unsupported(
24857 "AlloySubscriber full block streams are not implemented in this transport slice",
24858 ));
24859 }
24860 (SubscriberTransport::Polling, BlockInterestMode::Header) => {
24861 return Err(SubscriberError::Unsupported(
24862 "AlloySubscriber polling block streams are not implemented in this transport slice",
24863 ));
24864 }
24865 },
24866 }
24867 }
24868
24869 Ok(())
24870}
24871
24872fn log_filters<N: Network>(interests: &[ReactiveInterest<N>]) -> Vec<Filter> {
24873 let mut filters = Vec::new();
24874 for interest in interests {
24875 if let ReactiveInterest::Logs(interest) = interest {
24876 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
24877 }
24878 }
24879 filters
24880}
24881
24882fn needs_header_block_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
24883 interests.iter().any(|interest| {
24884 matches!(
24885 interest,
24886 ReactiveInterest::Blocks(BlockInterest {
24887 mode: BlockInterestMode::Header,
24888 })
24889 )
24890 })
24891}
24892
24893fn needs_pending_hash_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
24894 interests.iter().any(|interest| {
24895 matches!(
24896 interest,
24897 ReactiveInterest::PendingTransactions(interest) if interest.matches_hash_only()
24898 )
24899 })
24900}
24901
24902fn log_matches_any_interest<N: Network>(log: &Log, interests: &[ReactiveInterest<N>]) -> bool {
24903 interests.iter().any(|interest| {
24904 matches!(
24905 interest,
24906 ReactiveInterest::Logs(interest) if interest.matches(log)
24907 )
24908 })
24909}
24910
24911fn validate_owner_backfill_logs(
24912 logs: &[Log],
24913 from_block: u64,
24914 through: &BlockRef,
24915) -> Result<(), SubscriberOwnerError> {
24916 for log in logs {
24917 if log.removed {
24918 return Err(SubscriberOwnerError::InvalidBackfillLog(
24919 "removed log in canonical catch-up",
24920 ));
24921 }
24922 let number = log
24923 .block_number
24924 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
24925 "log missing block number",
24926 ))?;
24927 let hash = log
24928 .block_hash
24929 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
24930 "log missing block hash",
24931 ))?;
24932 log.transaction_hash
24933 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
24934 "log missing transaction hash",
24935 ))?;
24936 log.transaction_index
24937 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
24938 "log missing transaction index",
24939 ))?;
24940 log.log_index
24941 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
24942 "log missing log index",
24943 ))?;
24944 if number < from_block || number > through.number {
24945 return Err(SubscriberOwnerError::InvalidBackfillLog(
24946 "log outside requested block range",
24947 ));
24948 }
24949 if number == through.number && hash != through.hash {
24950 return Err(SubscriberOwnerError::InvalidBackfillLog(
24951 "target-block log hash mismatch",
24952 ));
24953 }
24954 }
24955 Ok(())
24956}
24957
24958fn validate_backfill_resource_limits(
24959 logs: &[Log],
24960 max_logs: usize,
24961 max_log_bytes: usize,
24962) -> Result<usize, SubscriberError> {
24963 if logs.len() > max_logs {
24964 return Err(SubscriberError::ResourceExhausted(format!(
24965 "historical response returned {} logs, above the configured limit of {max_logs}",
24966 logs.len()
24967 )));
24968 }
24969 let bytes = logs.iter().fold(0usize, |total, log| {
24970 let fixed = 20usize + (32 * 3) + (8 * 4) + 1;
24974 total
24975 .saturating_add(fixed)
24976 .saturating_add(log.topics().len().saturating_mul(32))
24977 .saturating_add(log.inner.data.data.len())
24978 });
24979 if bytes > max_log_bytes {
24980 return Err(SubscriberError::ResourceExhausted(format!(
24981 "historical response retained approximately {bytes} log bytes, above the configured limit of {max_log_bytes}"
24982 )));
24983 }
24984 Ok(bytes)
24985}
24986
24987async fn fetch_provider_block_ref<P, N>(
24988 provider: &P,
24989 number: u64,
24990 counters: &SubscriberRpcCounters,
24991 cause: SubscriberRpcCause,
24992) -> Result<BlockRef, SubscriberError>
24993where
24994 P: Provider<N> + Send + Sync,
24995 N: Network,
24996{
24997 counters.record(cause, SubscriberRpcMethod::EthGetBlockByNumber);
24998 let block = provider
24999 .get_block_by_number(BlockNumberOrTag::Number(number))
25000 .await
25001 .map_err(provider_error)?
25002 .ok_or_else(|| {
25003 SubscriberError::InvalidBackfill(format!(
25004 "canonical target block {number} is unavailable"
25005 ))
25006 })?;
25007 let header = block.header();
25008 Ok(BlockRef {
25009 number: header.number(),
25010 hash: header.hash(),
25011 parent_hash: Some(header.parent_hash()),
25012 timestamp: Some(header.timestamp()),
25013 })
25014}
25015
25016fn block_ref_satisfies_expected(actual: &BlockRef, expected: &BlockRef) -> bool {
25017 actual.number == expected.number
25018 && actual.hash == expected.hash
25019 && optional_metadata_compatible(actual.parent_hash.as_ref(), expected.parent_hash.as_ref())
25020 && optional_metadata_compatible(actual.timestamp.as_ref(), expected.timestamp.as_ref())
25021}
25022
25023fn validate_owner_backfill_log_set(logs: &[Log]) -> Result<(), SubscriberOwnerError> {
25024 let mut positions = HashMap::new();
25025 let mut block_hashes = HashMap::new();
25026 let mut transaction_hashes = HashMap::new();
25027 let mut transaction_positions = HashMap::new();
25028 let mut ordering = BTreeMap::<u64, Vec<(u64, u64)>>::new();
25029 for log in logs {
25030 let number = log
25031 .block_number
25032 .expect("individual owner catch-up logs are validated before set validation");
25033 let block_hash = log
25034 .block_hash
25035 .expect("individual owner catch-up logs are validated before set validation");
25036 let transaction_hash = log
25037 .transaction_hash
25038 .expect("individual owner catch-up logs are validated before set validation");
25039 let transaction_index = log
25040 .transaction_index
25041 .expect("individual owner catch-up logs are validated before set validation");
25042 let log_index = log
25043 .log_index
25044 .expect("individual owner catch-up logs are validated before set validation");
25045 if block_hashes
25046 .insert(number, block_hash)
25047 .is_some_and(|prior| prior != block_hash)
25048 {
25049 return Err(SubscriberOwnerError::InvalidBackfillLog(
25050 "conflicting block identity in canonical catch-up",
25051 ));
25052 }
25053 if let Some(previous) = positions.insert((number, log_index), log)
25054 && previous != log
25055 {
25056 return Err(SubscriberOwnerError::InvalidBackfillLog(
25057 "conflicting logs at one canonical block position",
25058 ));
25059 }
25060 let conflicting_transaction = transaction_hashes
25061 .insert((number, transaction_index), transaction_hash)
25062 .is_some_and(|prior| prior != transaction_hash)
25063 || transaction_positions
25064 .insert((number, transaction_hash), transaction_index)
25065 .is_some_and(|prior| prior != transaction_index);
25066 if conflicting_transaction {
25067 return Err(SubscriberOwnerError::InvalidBackfillLog(
25068 "conflicting transaction identity at one canonical block position",
25069 ));
25070 }
25071 ordering
25072 .entry(number)
25073 .or_default()
25074 .push((log_index, transaction_index));
25075 }
25076 for positions in ordering.values_mut() {
25077 positions.sort_unstable();
25078 if positions.windows(2).any(|pair| pair[0].1 > pair[1].1) {
25079 return Err(SubscriberOwnerError::InvalidBackfillLog(
25080 "transaction and log positions disagree on canonical order",
25081 ));
25082 }
25083 }
25084 Ok(())
25085}
25086
25087fn merged_owner_reconcile_filters<N: Network>(
25088 plans: &[SubscriberOwnerReconcilePlan<N>],
25089 through: u64,
25090) -> Vec<SubscriberOwnerReconcileFilter> {
25091 let mut by_start = BTreeMap::<u64, Vec<Filter>>::new();
25092 for plan in plans.iter().filter(|plan| plan.from_block <= through) {
25093 let filters = by_start.entry(plan.from_block).or_default();
25094 filters.extend(
25095 log_filters(&plan.interests)
25096 .into_iter()
25097 .map(|filter| filter.from_block(plan.from_block).to_block(through)),
25098 );
25099 }
25100
25101 let mut chunks = Vec::new();
25102 for (from_block, filters) in by_start {
25103 for filters in filters.chunks(OWNER_RECONCILE_FILTERS_PER_CHUNK) {
25104 let mut merged = Vec::new();
25105 for filter in filters {
25106 merge_log_subscription_filter(&mut merged, filter);
25107 }
25108 chunks.extend(
25109 merged
25110 .into_iter()
25111 .map(|filter| SubscriberOwnerReconcileFilter { filter, from_block }),
25112 );
25113 }
25114 }
25115 chunks
25116}
25117
25118fn merged_lazy_backfill_filters(
25119 filters: &[Filter],
25120 from_block: u64,
25121 through: u64,
25122) -> Vec<SubscriberOwnerReconcileFilter> {
25123 let mut requests = Vec::new();
25124 for filters in filters.chunks(OWNER_RECONCILE_FILTERS_PER_CHUNK) {
25125 let mut merged = Vec::new();
25126 for filter in filters {
25127 merge_log_subscription_filter(
25128 &mut merged,
25129 &filter.clone().from_block(from_block).to_block(through),
25130 );
25131 }
25132 requests.extend(
25133 merged
25134 .into_iter()
25135 .map(|filter| SubscriberOwnerReconcileFilter { filter, from_block }),
25136 );
25137 }
25138 requests
25139}
25140
25141fn lazy_backfill_error(error: SubscriberOwnerError) -> SubscriberError {
25142 match error {
25143 SubscriberOwnerError::Subscriber(error) => error,
25144 error => SubscriberError::InvalidBackfill(error.to_string()),
25145 }
25146}
25147
25148fn global_backfill_barrier(backfill: SubscriberBackfill, certified: BlockRef) -> ChainControl {
25149 let mut id = b"alloy-global-backfill-v1".to_vec();
25150 id.extend_from_slice(&backfill.start_block().to_be_bytes());
25151 id.extend_from_slice(&certified.number.to_be_bytes());
25152 id.extend_from_slice(certified.hash.as_slice());
25153 ChainControl::Barrier {
25154 id,
25155 block: Some(certified),
25156 }
25157}
25158
25159async fn fetch_owner_catchup<P, N>(
25160 provider: P,
25161 filters: Vec<SubscriberOwnerReconcileFilter>,
25162 retained: Vec<BlockRef>,
25163 through: BlockRef,
25164 options: SubscriberOwnerCatchupOptions,
25165 counters: Arc<SubscriberRpcCounters>,
25166) -> Result<SubscriberOwnerCatchup, SubscriberOwnerError>
25167where
25168 P: Provider<N> + Send + Sync,
25169 N: Network,
25170{
25171 let counters = counters.as_ref();
25172 if !options.target_preverified {
25173 let _ =
25174 verify_provider_reconcile_target::<P, N>(&provider, &through, counters, options.cause)
25175 .await?;
25176 }
25177 let mut certified_positions = HashSet::new();
25178 for position in retained {
25179 let target_certifies_position = position == through
25180 || (position.number.checked_add(1) == Some(through.number)
25181 && through.parent_hash == Some(position.hash));
25182 if !target_certifies_position && certified_positions.insert(position) {
25183 let _ = verify_provider_reconcile_target::<P, N>(
25184 &provider,
25185 &position,
25186 counters,
25187 options.cause,
25188 )
25189 .await?;
25190 }
25191 }
25192 let mut logs = Vec::new();
25193 let mut total_log_bytes = 0usize;
25194 let requests = stream::iter(filters.into_iter().map(|filter| {
25195 let provider = &provider;
25196 async move {
25197 counters.record(options.cause, SubscriberRpcMethod::EthGetLogs);
25198 let logs = provider
25199 .get_logs(&filter.filter)
25200 .await
25201 .map_err(provider_error)?;
25202 Ok::<_, SubscriberOwnerError>((filter.from_block, logs))
25203 }
25204 }))
25205 .buffer_unordered(options.max_requests_in_flight);
25206 futures::pin_mut!(requests);
25207 while let Some(result) = requests.next().await {
25208 let (from_block, fetched) = result?;
25209 let fetched_bytes =
25210 validate_backfill_resource_limits(&fetched, options.max_logs, options.max_log_bytes)?;
25211 validate_owner_backfill_logs(&fetched, from_block, &through)?;
25212 if logs.len().saturating_add(fetched.len()) > options.max_logs {
25213 return Err(SubscriberError::ResourceExhausted(format!(
25214 "bulk reconcile returned more than {} logs",
25215 options.max_logs
25216 ))
25217 .into());
25218 }
25219 total_log_bytes = total_log_bytes.saturating_add(fetched_bytes);
25220 if total_log_bytes > options.max_log_bytes {
25221 return Err(SubscriberError::ResourceExhausted(format!(
25222 "bulk reconcile retained approximately {total_log_bytes} log bytes, above the configured limit of {}",
25223 options.max_log_bytes
25224 ))
25225 .into());
25226 }
25227 logs.extend(fetched);
25228 }
25229 validate_owner_backfill_log_set(&logs)?;
25230 let certified =
25231 verify_provider_reconcile_target::<P, N>(&provider, &through, counters, options.cause)
25232 .await?;
25233 Ok(SubscriberOwnerCatchup { logs, certified })
25234}
25235
25236async fn verify_provider_reconcile_target<P, N>(
25237 provider: &P,
25238 expected: &BlockRef,
25239 counters: &SubscriberRpcCounters,
25240 cause: SubscriberRpcCause,
25241) -> Result<BlockRef, SubscriberOwnerError>
25242where
25243 P: Provider<N> + Send + Sync,
25244 N: Network,
25245{
25246 counters.record(cause, SubscriberRpcMethod::EthGetBlockByNumber);
25247 let block = provider
25248 .get_block_by_number(BlockNumberOrTag::Number(expected.number))
25249 .await
25250 .map_err(provider_error)?
25251 .ok_or(SubscriberOwnerError::BlockUnavailable(expected.number))?;
25252 let header = block.header();
25253 let actual = BlockRef {
25254 number: header.number(),
25255 hash: header.hash(),
25256 parent_hash: Some(header.parent_hash()),
25257 timestamp: Some(header.timestamp()),
25258 };
25259 let exact_parent = expected
25260 .parent_hash
25261 .is_none_or(|parent| Some(parent) == actual.parent_hash);
25262 let exact_timestamp = expected
25263 .timestamp
25264 .is_none_or(|timestamp| Some(timestamp) == actual.timestamp);
25265 if actual.number != expected.number
25266 || actual.hash != expected.hash
25267 || !exact_parent
25268 || !exact_timestamp
25269 {
25270 return Err(SubscriberOwnerError::BlockMismatch {
25271 expected_number: expected.number,
25272 expected_hash: expected.hash,
25273 actual_number: actual.number,
25274 actual_hash: actual.hash,
25275 });
25276 }
25277 Ok(actual)
25278}
25279
25280fn log_input_record<N: Network>(log: Log, source: InputSource) -> ReactiveInputRecord<N> {
25281 let context = log_reactive_context(&log);
25282 ReactiveInputRecord::new(
25283 ReactiveInput::Log(log),
25284 ReactiveContext { source, ..context },
25285 )
25286}
25287
25288fn preconfirmed_log_input_record<N: Network>(
25289 log: Log,
25290 flashblock: FlashblockRef,
25291) -> ReactiveInputRecord<N> {
25292 let block = flashblock.block_ref();
25293 let provider = flashblock.provider.clone();
25294 ReactiveInputRecord::new(
25295 ReactiveInput::Log(log.clone()),
25296 ReactiveContext {
25297 chain_id: None,
25298 source: InputSource::Flashblocks,
25299 chain_status: ChainStatus::Preconfirmed {
25300 flashblock: Arc::new(flashblock),
25301 },
25302 block: Some(block),
25303 transaction_index: log.transaction_index,
25304 log_index: log.log_index,
25305 },
25306 )
25307 .with_provider(provider)
25308}
25309
25310fn log_reactive_context(log: &Log) -> ReactiveContext {
25311 let block = match (log.block_hash, log.block_number) {
25312 (Some(hash), Some(number)) => Some(BlockRef {
25313 number,
25314 hash,
25315 parent_hash: None,
25316 timestamp: log.block_timestamp,
25317 }),
25318 _ => None,
25319 };
25320
25321 let chain_status = match (&block, log.removed) {
25322 (Some(block), true) => ChainStatus::Reorged {
25323 dropped_from: *block,
25324 },
25325 (Some(block), false) => ChainStatus::Included {
25326 block: *block,
25327 confirmations: 0,
25328 },
25329 (None, _) => ChainStatus::Pending,
25330 };
25331
25332 ReactiveContext {
25333 chain_id: None,
25334 source: InputSource::Poll,
25335 chain_status,
25336 block,
25337 transaction_index: log.transaction_index,
25338 log_index: log.log_index,
25339 }
25340}
25341
25342fn block_header_input_record<N>(header: N::HeaderResponse) -> ReactiveInputRecord<N>
25343where
25344 N: Network,
25345{
25346 let block = BlockRef {
25347 number: header.number(),
25348 hash: HeaderResponseTrait::hash(&header),
25349 parent_hash: Some(header.parent_hash()),
25350 timestamp: Some(header.timestamp()),
25351 };
25352 ReactiveInputRecord::new(
25353 ReactiveInput::BlockHeader(header),
25354 ReactiveContext {
25355 chain_id: None,
25356 source: InputSource::Subscription,
25357 chain_status: ChainStatus::Included {
25358 block,
25359 confirmations: 0,
25360 },
25361 block: Some(block),
25362 transaction_index: None,
25363 log_index: None,
25364 },
25365 )
25366}
25367
25368fn pending_hash_input_record<N: Network>(
25369 hash: B256,
25370 source: InputSource,
25371) -> ReactiveInputRecord<N> {
25372 ReactiveInputRecord::new(
25373 ReactiveInput::PendingTxHash(hash),
25374 ReactiveContext {
25375 chain_id: None,
25376 source,
25377 chain_status: ChainStatus::Pending,
25378 block: None,
25379 transaction_index: None,
25380 log_index: None,
25381 },
25382 )
25383}
25384
25385#[cfg(feature = "reactive-ws")]
25386fn base_pending_log_filter(filter: &Filter) -> Result<serde_json::Value, SubscriberError> {
25387 let encoded = serde_json::to_value(filter)
25388 .map_err(|error| SubscriberError::Provider(error.to_string()))?;
25389 let serde_json::Value::Object(mut fields) = encoded else {
25390 return Err(SubscriberError::Provider(
25391 "Alloy log filter did not serialize as an object".into(),
25392 ));
25393 };
25394 fields.retain(|key, _| key == "address" || key == "topics");
25395 Ok(serde_json::Value::Object(fields))
25396}
25397
25398fn provider_error(error: impl fmt::Display) -> SubscriberError {
25399 SubscriberError::Provider(error.to_string())
25400}
25401
25402#[derive(Debug, thiserror::Error)]
25404#[non_exhaustive]
25405pub enum SubscriberError {
25406 #[error("{0}")]
25408 InvalidConfig(&'static str),
25409 #[error("{0}")]
25411 Unsupported(&'static str),
25412 #[error("subscriber chain mismatch: expected {expected}, got {actual}")]
25414 ChainMismatch {
25415 expected: u64,
25417 actual: u64,
25419 },
25420 #[error("provider error: {0}")]
25422 Provider(String),
25423 #[error("invalid canonical backfill: {0}")]
25426 InvalidBackfill(String),
25427 #[error("subscriber resource limit exceeded: {0}")]
25429 ResourceExhausted(String),
25430}
25431
25432#[cfg(test)]
25437mod canonical_head_rpc_stats_tests {
25438 use super::*;
25439 use alloy_provider::ProviderBuilder;
25440 use alloy_rpc_types_eth::{Block, Header as RpcHeader};
25441 use alloy_transport::mock::Asserter;
25442
25443 fn sealed_head() -> Block {
25444 Block::empty(RpcHeader {
25445 hash: B256::repeat_byte(0x65),
25446 inner: alloy_consensus::Header {
25447 number: 101,
25448 parent_hash: B256::repeat_byte(0x64),
25449 timestamp: 1_700_000_101,
25450 ..alloy_consensus::Header::default()
25451 },
25452 total_difficulty: None,
25453 size: None,
25454 })
25455 }
25456
25457 #[tokio::test]
25462 async fn unchanged_head_still_counts_its_certification_request() {
25463 let asserter = Asserter::new();
25464 asserter.push_success(&Some(sealed_head()));
25465 asserter.push_success(&Some(sealed_head()));
25466 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
25467 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
25468 provider,
25469 SubscriberMode::PubSub,
25470 SubscriberConfig::default(),
25471 );
25472 subscriber.chain_id = Some(8_453);
25473
25474 let first = subscriber
25475 .fetch_certified_canonical_head()
25476 .await
25477 .expect("first certification succeeds");
25478 assert!(
25479 matches!(first, Some(SubscriberEvent::BlockHeader(_))),
25480 "a newly certified head is delivered"
25481 );
25482
25483 let second = subscriber
25484 .fetch_certified_canonical_head()
25485 .await
25486 .expect("second certification succeeds");
25487 assert!(
25488 second.is_none(),
25489 "an unchanged head produces no event to deliver"
25490 );
25491
25492 let stats = subscriber.rpc_stats();
25493 assert_eq!(
25494 stats.get(
25495 SubscriberRpcCause::CanonicalHeadCertification,
25496 SubscriberRpcMethod::EthGetBlockByNumber,
25497 ),
25498 2,
25499 "both polls are billed even though only one advanced the head"
25500 );
25501 assert_eq!(stats.total(), 2, "certification is the only request issued");
25502 assert_eq!(
25503 subscriber
25504 .flashblocks_rpc_metrics()
25505 .canonical_head_requests(),
25506 2,
25507 "the attributed counter agrees with the Flashblocks-scoped one"
25508 );
25509 assert!(asserter.read_q().is_empty());
25510 }
25511}
25512
25513#[cfg(all(test, feature = "reactive-ws"))]
25521mod stream_gap_tests {
25522 use super::*;
25523 use alloy_provider::ProviderBuilder;
25524 use alloy_transport::mock::Asserter;
25525 use serde_json::value::RawValue;
25526
25527 fn raw_json(value: &serde_json::Value) -> Box<RawValue> {
25528 RawValue::from_string(value.to_string()).expect("valid JSON")
25529 }
25530
25531 fn wire_log(block_number: u64, log_index: u64) -> Box<RawValue> {
25532 raw_json(&serde_json::json!({
25533 "address": "0x0000000000000000000000000000000000000077",
25534 "topics": ["0x1111111111111111111111111111111111111111111111111111111111111111"],
25535 "data": "0x",
25536 "blockHash": format!("0x{:064x}", block_number),
25537 "blockNumber": format!("0x{block_number:x}"),
25538 "transactionHash": format!("0x{:064x}", 0x20 + log_index),
25539 "transactionIndex": format!("0x{log_index:x}"),
25540 "logIndex": format!("0x{log_index:x}"),
25541 "removed": false,
25542 }))
25543 }
25544
25545 fn log_source(id: usize) -> SubscriberStreamSource {
25546 SubscriberStreamSource::PubSubLog {
25547 id,
25548 filter: Filter::new().address(Address::repeat_byte(0x77)),
25549 }
25550 }
25551
25552 fn wired_subscription(
25555 capacity: usize,
25556 ) -> (
25557 tokio::sync::broadcast::Sender<Box<RawValue>>,
25558 alloy_pubsub::Subscription<Log>,
25559 ) {
25560 let (tx, rx) = tokio::sync::broadcast::channel(capacity);
25561 let raw = alloy_pubsub::RawSubscription {
25562 rx,
25563 local_id: B256::repeat_byte(0x5b),
25564 };
25565 (tx, raw.into_typed())
25566 }
25567
25568 fn gap_stream(
25569 subscription: alloy_pubsub::Subscription<Log>,
25570 source: SubscriberStreamSource,
25571 counters: Arc<SubscriberStreamGapCounters>,
25572 ) -> BoxStream<'static, SubscriberEvent<Ethereum>> {
25573 gap_observing_stream(subscription, source, counters, |log| SubscriberEvent::Log {
25574 source_id: 0,
25575 log,
25576 })
25577 }
25578
25579 #[tokio::test]
25583 async fn overflowing_channel_reports_the_gap_instead_of_skipping_it() {
25584 let counters = Arc::new(SubscriberStreamGapCounters::default());
25585 let (tx, subscription) = wired_subscription(2);
25586 for index in 0..5 {
25588 tx.send(wire_log(100 + index, index))
25589 .expect("receiver alive");
25590 }
25591 let mut stream = gap_stream(subscription, log_source(0), Arc::clone(&counters));
25592
25593 let first = stream.next().await.expect("an event is produced");
25594 let SubscriberEvent::StreamGap { source, gap } = first else {
25595 panic!("expected the dropped notifications to surface as a gap, got a delivery");
25596 };
25597 assert!(
25598 matches!(source, SubscriberStreamSource::PubSubLog { id: 0, .. }),
25599 "the gap must name the source that lost data"
25600 );
25601 assert_eq!(gap, SubscriberStreamGap::Lagged { skipped: 3 });
25602 assert_eq!(gap.skipped(), Some(3));
25603
25604 assert!(matches!(
25606 stream.next().await,
25607 Some(SubscriberEvent::Log { .. })
25608 ));
25609 assert_eq!(counters.snapshot().lagged_notifications(), 3);
25610 assert_eq!(counters.snapshot().undecodable_notifications(), 0);
25611 }
25612
25613 #[tokio::test]
25616 async fn undecodable_notification_fails_closed_as_a_gap() {
25617 let counters = Arc::new(SubscriberStreamGapCounters::default());
25618 let (tx, subscription) = wired_subscription(8);
25619 tx.send(raw_json(&serde_json::json!({"not": "a log"})))
25620 .expect("receiver alive");
25621 tx.send(wire_log(101, 0)).expect("receiver alive");
25622 let mut stream = gap_stream(subscription, log_source(0), Arc::clone(&counters));
25623
25624 assert_eq!(
25625 stream.next().await.map(|event| matches!(
25626 event,
25627 SubscriberEvent::StreamGap {
25628 gap: SubscriberStreamGap::Undecodable,
25629 ..
25630 }
25631 )),
25632 Some(true),
25633 );
25634 assert!(matches!(
25635 stream.next().await,
25636 Some(SubscriberEvent::Log { .. })
25637 ));
25638 let stats = counters.snapshot();
25639 assert_eq!(stats.undecodable_notifications(), 1);
25640 assert_eq!(stats.lagged_notifications(), 0);
25641 assert_eq!(stats.total_gaps(), 1);
25642 }
25643
25644 #[tokio::test]
25647 async fn closed_channel_terminates_the_stream_for_reconnect() {
25648 let counters = Arc::new(SubscriberStreamGapCounters::default());
25649 let (tx, subscription) = wired_subscription(8);
25650 tx.send(wire_log(101, 0)).expect("receiver alive");
25651 drop(tx);
25652 let mut stream = gap_stream(subscription, log_source(0), Arc::clone(&counters));
25653
25654 assert!(matches!(
25655 stream.next().await,
25656 Some(SubscriberEvent::Log { .. })
25657 ));
25658 assert!(
25659 matches!(
25660 stream.next().await,
25661 Some(SubscriberEvent::StreamTerminated(
25662 SubscriberStreamSource::PubSubLog { id: 0, .. }
25663 ))
25664 ),
25665 "a closed subscription must terminate, not report a gap"
25666 );
25667 assert_eq!(counters.snapshot().total_gaps(), 0);
25668 }
25669
25670 fn mocked_subscriber(
25671 asserter: Asserter,
25672 ) -> AlloySubscriber<impl alloy_provider::Provider<Ethereum> + Clone, Ethereum> {
25673 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
25674 AlloySubscriber::new(
25675 provider,
25676 SubscriberMode::PubSub,
25677 SubscriberConfig::default(),
25678 )
25679 }
25680
25681 #[tokio::test]
25685 async fn log_gap_refetches_the_missed_window_and_attributes_it() {
25686 let asserter = Asserter::new();
25687 asserter.push_success(&U256::from(104)); asserter.push_success(&vec![
25689 serde_json::from_str::<Log>(wire_log(103, 0).get()).expect("log"),
25690 ]);
25691 let mut subscriber = mocked_subscriber(asserter.clone());
25692 subscriber.chain_id = Some(1);
25693 subscriber.last_seen_log_blocks.insert(0, 102);
25695
25696 let event = subscriber
25697 .recover_stream_gap(&log_source(0), SubscriberStreamGap::Lagged { skipped: 2 })
25698 .await
25699 .expect("a bounded window is recoverable");
25700
25701 assert!(
25702 matches!(
25703 event,
25704 Some(SubscriberEvent::BackfilledLogs { source_id: 0, ref logs }) if logs.len() == 1
25705 ),
25706 "the missed window is delivered as backfill"
25707 );
25708 let stats = subscriber.rpc_stats();
25709 assert_eq!(
25710 stats.by_cause(SubscriberRpcCause::GapBackfill),
25711 2,
25712 "one head read plus one bounded eth_getLogs"
25713 );
25714 assert_eq!(
25715 stats.by_cause(SubscriberRpcCause::ReconnectBackfill),
25716 0,
25717 "a live-stream gap is not reconnect churn"
25718 );
25719 assert_eq!(subscriber.stream_gap_stats().log_gaps_healed(), 1);
25720 assert!(asserter.read_q().is_empty());
25721 }
25722
25723 #[tokio::test]
25726 async fn log_gap_without_a_delivery_anchor_fails_closed() {
25727 let mut subscriber = mocked_subscriber(Asserter::new());
25728 subscriber.chain_id = Some(1);
25729
25730 let Err(error) = subscriber
25731 .recover_stream_gap(&log_source(0), SubscriberStreamGap::Lagged { skipped: 9 })
25732 .await
25733 else {
25734 panic!("an unbounded gap must not be silently ignored");
25735 };
25736
25737 assert!(
25738 matches!(&error, SubscriberError::Provider(message)
25739 if message.contains("delivery anchor") && message.contains("lagged(9)")),
25740 "the error must name the cause and the loss: {error}"
25741 );
25742 assert_eq!(subscriber.stream_gap_stats().log_gaps_healed(), 0);
25743 }
25744
25745 #[tokio::test]
25748 async fn header_gap_is_counted_without_spending_a_request() {
25749 let asserter = Asserter::new();
25750 let mut subscriber = mocked_subscriber(asserter.clone());
25751 subscriber.chain_id = Some(1);
25752
25753 let event = subscriber
25754 .recover_stream_gap(
25755 &SubscriberStreamSource::PubSubBlockHeaders,
25756 SubscriberStreamGap::Lagged { skipped: 1 },
25757 )
25758 .await
25759 .expect("a header gap is not fatal");
25760
25761 assert!(event.is_none(), "no synthetic header is fabricated");
25762 assert_eq!(subscriber.stream_gap_stats().header_gaps(), 1);
25763 assert_eq!(
25764 subscriber.rpc_stats().total(),
25765 0,
25766 "the lineage walk already covers this; refetching would duplicate it"
25767 );
25768 assert!(asserter.read_q().is_empty());
25769 }
25770
25771 #[tokio::test]
25774 async fn preconfirmation_gap_discards_the_speculative_snapshot() {
25775 let mut subscriber = mocked_subscriber(Asserter::new());
25776 subscriber.chain_id = Some(8_453);
25777
25778 let event = subscriber
25779 .recover_stream_gap(
25780 &SubscriberStreamSource::BasePendingLog {
25781 id: 0,
25782 filter: Filter::new(),
25783 },
25784 SubscriberStreamGap::Undecodable,
25785 )
25786 .await
25787 .expect("a preview gap is recoverable by discarding it");
25788
25789 assert!(
25790 matches!(event, Some(SubscriberEvent::FlashblockInvalidated)),
25791 "the incomplete preview must be invalidated, not delivered"
25792 );
25793 assert_eq!(subscriber.stream_gap_stats().preconfirmation_gaps(), 1);
25794 assert_eq!(subscriber.rpc_stats().total(), 0);
25795 }
25796
25797 #[tokio::test]
25799 async fn resetting_gap_stats_opens_a_new_window() {
25800 let counters = Arc::new(SubscriberStreamGapCounters::default());
25801 counters.record_gap(SubscriberStreamGap::Lagged { skipped: 4 });
25802 counters.record_gap(SubscriberStreamGap::Undecodable);
25803 counters.record_header_gap();
25804 assert_eq!(counters.snapshot().total_gaps(), 5);
25805
25806 counters.reset();
25807
25808 assert_eq!(counters.snapshot(), SubscriberStreamGapStats::default());
25809 }
25810
25811 #[test]
25813 fn gap_labels_are_stable() {
25814 assert_eq!(
25815 SubscriberStreamGap::Lagged { skipped: 7 }.as_str(),
25816 "lagged"
25817 );
25818 assert_eq!(SubscriberStreamGap::Undecodable.as_str(), "undecodable");
25819 assert_eq!(
25820 SubscriberStreamGap::Lagged { skipped: 7 }.to_string(),
25821 "lagged(7)"
25822 );
25823 assert_eq!(SubscriberRpcCause::GapBackfill.as_str(), "gap_backfill");
25824 assert_eq!(SubscriberStreamGap::Undecodable.skipped(), None);
25825 }
25826}
25827
25828#[cfg(all(test, feature = "reactive-ws"))]
25832mod log_coverage_attestation_tests {
25833 use super::*;
25834 use alloy_provider::ProviderBuilder;
25835 use alloy_rpc_types_eth::Header as RpcHeader;
25836 use alloy_transport::mock::Asserter;
25837
25838 fn header_record(number: u64) -> ReactiveInputRecord<Ethereum> {
25839 block_header_input_record::<Ethereum>(RpcHeader {
25840 hash: B256::repeat_byte(number as u8),
25841 inner: alloy_consensus::Header {
25842 number,
25843 parent_hash: B256::repeat_byte((number - 1) as u8),
25844 timestamp: 1_700_000_000 + number,
25845 ..alloy_consensus::Header::default()
25846 },
25847 total_difficulty: None,
25848 size: None,
25849 })
25850 }
25851
25852 fn subscriber(
25853 mode: SubscriberMode,
25854 with_log_interest: bool,
25855 ) -> AlloySubscriber<impl alloy_provider::Provider<Ethereum> + Clone, Ethereum> {
25856 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
25857 let mut subscriber = AlloySubscriber::new(provider, mode, SubscriberConfig::default());
25858 if with_log_interest {
25859 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
25860 provider_filter: Filter::new().address(Address::repeat_byte(0x77)),
25861 local_matcher: None,
25862 route_key: None,
25863 })];
25864 }
25865 subscriber
25866 }
25867
25868 fn queued_coverage(
25869 subscriber: &mut AlloySubscriber<impl alloy_provider::Provider<Ethereum> + Clone, Ethereum>,
25870 ) -> Vec<u64> {
25871 subscriber.queue_log_coverage_attestation();
25872 subscriber
25873 .pending_chain_controls
25874 .drain(..)
25875 .filter_map(|control| match control {
25876 ChainControl::LogCoverage(block) => Some(block.number),
25877 _ => None,
25878 })
25879 .collect()
25880 }
25881
25882 #[test]
25883 fn attestation_advances_with_observed_canonical_headers() {
25884 let mut subscriber = subscriber(SubscriberMode::PubSub, true);
25885
25886 subscriber.note_attestable_canonical_block(&header_record(101));
25887 assert_eq!(queued_coverage(&mut subscriber), vec![101]);
25888
25889 assert!(queued_coverage(&mut subscriber).is_empty());
25891
25892 subscriber.note_attestable_canonical_block(&header_record(102));
25893 assert_eq!(queued_coverage(&mut subscriber), vec![102]);
25894 }
25895
25896 #[test]
25901 fn a_detected_gap_withdraws_the_pending_attestation() {
25902 let mut subscriber = subscriber(SubscriberMode::PubSub, true);
25903 subscriber.note_attestable_canonical_block(&header_record(101));
25904
25905 subscriber.reset_log_attestation();
25906
25907 assert!(
25908 queued_coverage(&mut subscriber).is_empty(),
25909 "a withdrawn candidate must not be attested"
25910 );
25911
25912 subscriber.note_attestable_canonical_block(&header_record(102));
25914 assert_eq!(queued_coverage(&mut subscriber), vec![102]);
25915 }
25916
25917 #[test]
25920 fn attestation_never_regresses_after_a_gap() {
25921 let mut subscriber = subscriber(SubscriberMode::PubSub, true);
25922 subscriber.note_attestable_canonical_block(&header_record(105));
25923 assert_eq!(queued_coverage(&mut subscriber), vec![105]);
25924
25925 subscriber.reset_log_attestation();
25926 subscriber.note_attestable_canonical_block(&header_record(103));
25927
25928 assert!(
25929 queued_coverage(&mut subscriber).is_empty(),
25930 "an older block must never be attested after a newer one"
25931 );
25932 }
25933
25934 #[test]
25937 #[cfg(feature = "reactive-polling")]
25938 fn polling_transport_neither_claims_nor_emits_the_attestation() {
25939 let mut subscriber = subscriber(SubscriberMode::Polling, true);
25940 assert!(!subscriber.attests_log_coverage());
25941
25942 subscriber.note_attestable_canonical_block(&header_record(101));
25943
25944 assert!(queued_coverage(&mut subscriber).is_empty());
25945 assert!(
25946 !EventSubscriber::capabilities(&subscriber)
25947 .supports(SubscriberCapability::LogCoverageAttestation)
25948 );
25949 }
25950
25951 #[test]
25952 fn pubsub_transport_claims_the_capability() {
25953 let subscriber = subscriber(SubscriberMode::PubSub, true);
25954 assert!(
25955 EventSubscriber::capabilities(&subscriber)
25956 .supports(SubscriberCapability::LogCoverageAttestation)
25957 );
25958 }
25959
25960 #[test]
25963 fn subscriber_without_log_interests_stays_silent() {
25964 let mut subscriber = subscriber(SubscriberMode::PubSub, false);
25965 assert!(!subscriber.attests_log_coverage());
25966
25967 subscriber.note_attestable_canonical_block(&header_record(101));
25968
25969 assert!(queued_coverage(&mut subscriber).is_empty());
25970 }
25971}
25972
25973#[cfg(all(test, feature = "reactive-ws"))]
25979mod canonical_head_suppression_tests {
25980 use super::*;
25981 use alloy_provider::ProviderBuilder;
25982 use alloy_rpc_types_eth::{Block, Header as RpcHeader};
25983 use alloy_transport::mock::Asserter;
25984
25985 fn sealed_head(number: u64) -> Block {
25986 Block::empty(RpcHeader {
25987 hash: B256::repeat_byte(number as u8),
25988 inner: alloy_consensus::Header {
25989 number,
25990 parent_hash: B256::repeat_byte((number - 1) as u8),
25991 timestamp: 1_700_000_000 + number,
25992 ..alloy_consensus::Header::default()
25993 },
25994 total_difficulty: None,
25995 size: None,
25996 })
25997 }
25998
25999 fn subscriber(
26000 asserter: Asserter,
26001 ) -> AlloySubscriber<impl alloy_provider::Provider<Ethereum> + Clone, Ethereum> {
26002 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
26003 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
26004 provider,
26005 SubscriberMode::PubSub,
26006 SubscriberConfig::default(),
26007 );
26008 subscriber.chain_id = Some(8_453);
26009 subscriber.interests = vec![ReactiveInterest::Blocks(BlockInterest::default())];
26010 subscriber
26011 }
26012
26013 #[tokio::test]
26017 async fn a_tick_inside_the_window_after_a_certification_costs_nothing() {
26018 let asserter = Asserter::new();
26019 asserter.push_success(&Some(sealed_head(101)));
26020 let mut subscriber = subscriber(asserter.clone());
26021
26022 let certified = subscriber
26024 .certify_canonical_head_on_sealed_block()
26025 .await
26026 .expect("certification succeeds");
26027 assert!(matches!(certified, Some(SubscriberEvent::BlockHeader(_))));
26028
26029 assert!(subscriber.canonical_head_certification_is_current());
26031 let suppressed = subscriber
26032 .certify_canonical_head_on_sealed_block()
26033 .await
26034 .expect("a suppressed certification is not an error");
26035
26036 assert!(suppressed.is_none());
26037 assert_eq!(
26038 subscriber.rpc_stats().get(
26039 SubscriberRpcCause::CanonicalHeadCertification,
26040 SubscriberRpcMethod::EthGetBlockByNumber,
26041 ),
26042 1,
26043 "only the first certification may spend a request"
26044 );
26045 assert!(
26046 asserter.read_q().is_empty(),
26047 "the suppressed call must not consume a queued response"
26048 );
26049 }
26050
26051 #[tokio::test]
26055 async fn certification_resumes_once_the_window_lapses() {
26056 let asserter = Asserter::new();
26057 asserter.push_success(&Some(sealed_head(101)));
26058 asserter.push_success(&Some(sealed_head(102)));
26059 let mut subscriber = subscriber(asserter.clone());
26060
26061 let _ = subscriber
26062 .certify_canonical_head_on_sealed_block()
26063 .await
26064 .expect("first certification");
26065
26066 subscriber.last_canonical_head_certification = Some(
26068 Instant::now()
26069 - subscriber.config.canonical_head_poll_interval
26070 - Duration::from_millis(1),
26071 );
26072 assert!(!subscriber.canonical_head_certification_is_current());
26073
26074 let second = subscriber
26075 .certify_canonical_head_on_sealed_block()
26076 .await
26077 .expect("second certification");
26078
26079 assert!(matches!(second, Some(SubscriberEvent::BlockHeader(_))));
26080 assert_eq!(
26081 subscriber
26082 .rpc_stats()
26083 .by_cause(SubscriberRpcCause::CanonicalHeadCertification),
26084 2
26085 );
26086 assert!(asserter.read_q().is_empty());
26087 }
26088
26089 #[tokio::test]
26092 async fn without_a_header_interest_nothing_is_certified() {
26093 let asserter = Asserter::new();
26094 let mut subscriber = subscriber(asserter.clone());
26095 subscriber.interests = Vec::new();
26096
26097 let certified = subscriber
26098 .certify_canonical_head_on_sealed_block()
26099 .await
26100 .expect("no interest is not an error");
26101
26102 assert!(certified.is_none());
26103 assert_eq!(subscriber.rpc_stats().total(), 0);
26104 assert!(asserter.read_q().is_empty());
26105 }
26106}