1use std::{
15 any::Any,
16 borrow::Cow,
17 collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
18 fmt,
19 future::Future,
20 hash::Hash,
21 marker::PhantomData,
22 num::NonZeroU64,
23 path::PathBuf,
24 pin::Pin,
25 sync::{
26 Arc,
27 atomic::{AtomicU64, Ordering},
28 },
29 time::{Duration, Instant},
30};
31
32use alloy_consensus::{BlockHeader as _, Transaction as _};
33use alloy_eips::{BlockId, BlockNumberOrTag};
34use alloy_network::{
35 Ethereum, Network,
36 primitives::{
37 BlockResponse as _, HeaderResponse as HeaderResponseTrait,
38 TransactionResponse as TransactionResponseTrait,
39 },
40};
41use alloy_primitives::{Address, B256, Bytes, FixedBytes, Keccak256, U256};
42use alloy_provider::{Provider, RootProvider};
43use alloy_rpc_client::BatchRequest;
44use alloy_rpc_types_eth::{Filter, FilterSet, Log};
45pub use alloy_transport_balancer::EndpointId;
46use bincode::Options;
47use futures::{StreamExt, stream};
48use futures::{
49 future::{Either, poll_fn, select},
50 stream::{BoxStream, FuturesUnordered},
51};
52
53use crate::{
54 cache::{
55 AccountProof, BlockStateDiff, DurableCheckpointBlock, DurableCheckpointError,
56 DurableCheckpointIdentity, DurableCheckpointMetadata, DurableCheckpointStore, EvmCache,
57 EvmCacheStateSnapshot, LoadedDurableCheckpoint,
58 },
59 errors::{BlockContextError, StorageFetchResult},
60 events::{EventDecoder, StateView},
61 freshness::FreshnessRegistry,
62 state_update::{AccountPatch, PurgeScope, StateDiff, StateUpdate},
63};
64
65#[cfg(feature = "raw-flashblocks-json")]
66mod raw_json_flashblocks;
67#[cfg(feature = "raw-flashblocks-json")]
68pub use raw_json_flashblocks::{
69 FlashblockInvalidation, FlashblockInvalidationReason, FlashblockSnapshot, FlashblockUpdate,
70 FlashblockUpdateAcknowledgement, FlashblockUpdateChannelError, FlashblockUpdateSender,
71 RawJsonFlashblocksAdapter, RawJsonFlashblocksError, RawJsonFlashblocksLimits,
72};
73
74#[derive(Clone, Debug, PartialEq, Eq)]
76pub enum ReactiveInput<N: Network = Ethereum> {
77 Log(Log),
79 BlockHeader(N::HeaderResponse),
81 FullBlock(N::BlockResponse),
83 PendingTxHash(B256),
85 PendingTx(N::TransactionResponse),
87}
88
89#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
91pub struct ReactiveContext {
92 pub chain_id: Option<u64>,
94 pub source: InputSource,
96 pub chain_status: ChainStatus,
98 pub block: Option<BlockRef>,
100 pub transaction_index: Option<u64>,
102 pub log_index: Option<u64>,
104}
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
108pub struct BlockRef {
109 pub number: u64,
111 pub hash: B256,
113 pub parent_hash: Option<B256>,
115 pub timestamp: Option<u64>,
117}
118
119#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
126pub struct ProviderRef {
127 pub endpoint: EndpointId,
129 pub generation: u64,
131}
132
133impl ProviderRef {
134 pub fn new(endpoint: impl Into<EndpointId>, generation: u64) -> Self {
136 Self {
137 endpoint: endpoint.into(),
138 generation,
139 }
140 }
141}
142
143#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
145pub struct FlashblockRef {
146 pub provider: ProviderRef,
148 pub payload_id: Option<FixedBytes<8>>,
152 pub index: Option<u64>,
154 pub block_number: u64,
156 pub content_hash: B256,
162 pub partial_block_hash: Option<B256>,
164 pub parent_hash: Option<B256>,
166 pub state_root: Option<B256>,
168 pub transactions_root: Option<B256>,
170 pub transaction_hashes: Vec<B256>,
172 pub timestamp: Option<u64>,
174 pub base_fee_per_gas: Option<u64>,
176 pub beneficiary: Option<Address>,
178 pub prevrandao: Option<B256>,
180 pub gas_limit: Option<u64>,
182}
183
184impl FlashblockRef {
185 pub const fn block_ref(&self) -> BlockRef {
190 BlockRef {
191 number: self.block_number,
192 hash: self.content_hash,
193 parent_hash: self.parent_hash,
194 timestamp: self.timestamp,
195 }
196 }
197
198 pub fn contains_transaction(&self, transaction_hash: &B256) -> bool {
200 self.transaction_hashes.contains(transaction_hash)
201 }
202
203 fn transaction_index(&self, transaction_hash: &B256) -> Option<u64> {
204 self.transaction_hashes
205 .iter()
206 .position(|candidate| candidate == transaction_hash)
207 .and_then(|index| u64::try_from(index).ok())
208 }
209
210 fn same_payload(&self, other: &Self) -> bool {
211 self.provider == other.provider
212 && match (self.payload_id, other.payload_id) {
213 (Some(left), Some(right)) => left == right,
214 _ => {
215 self.block_number == other.block_number && self.parent_hash == other.parent_hash
216 }
217 }
218 }
219
220 #[cfg(feature = "raw-flashblocks-json")]
221 fn same_base_identity(&self, other: &Self) -> bool {
222 self.block_number == other.block_number
223 && self.parent_hash == other.parent_hash
224 && self.timestamp == other.timestamp
225 && self.base_fee_per_gas == other.base_fee_per_gas
226 && self.beneficiary == other.beneficiary
227 && self.prevrandao == other.prevrandao
228 && self.gas_limit == other.gas_limit
229 }
230
231 fn is_cumulative_successor_of(&self, previous: &Self) -> bool {
232 self.same_payload(previous)
233 && self.transaction_hashes.len() >= previous.transaction_hashes.len()
234 && self
235 .transaction_hashes
236 .starts_with(&previous.transaction_hashes)
237 && match (previous.index, self.index) {
238 (Some(previous), Some(current)) => current >= previous,
239 _ => true,
240 }
241 }
242}
243
244#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
246pub enum PreconfirmationMode {
247 #[default]
249 Disabled,
250 Preferred,
253 Required,
255}
256
257#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
259pub struct BaseFlashblockPayload {
260 pub payload_id: FixedBytes<8>,
262 pub index: u64,
264 pub base: Option<BaseFlashblockBase>,
266 pub diff: BaseFlashblockDiff,
268 #[serde(default)]
270 pub metadata: Option<BaseFlashblockMetadata>,
271}
272
273#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
275pub struct BaseFlashblockBase {
276 pub parent_hash: B256,
278 #[serde(deserialize_with = "deserialize_rpc_u64")]
280 pub block_number: u64,
281 #[serde(deserialize_with = "deserialize_rpc_u64")]
283 pub timestamp: u64,
284 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
286 pub gas_limit: Option<u64>,
287 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
289 pub base_fee_per_gas: Option<u64>,
290 #[serde(default, alias = "fee_recipient", alias = "feeRecipient")]
292 pub beneficiary: Option<Address>,
293 #[serde(
295 default,
296 alias = "prev_randao",
297 alias = "prevRandao",
298 alias = "mixHash"
299 )]
300 pub prevrandao: Option<B256>,
301}
302
303#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
305pub struct BaseFlashblockDiff {
306 pub state_root: B256,
308 pub block_hash: B256,
310 #[serde(default)]
312 pub transactions: Vec<serde_json::Value>,
313 #[serde(default)]
315 pub transactions_root: Option<B256>,
316}
317
318#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
321pub struct BaseFlashblockMetadata {
322 #[serde(deserialize_with = "deserialize_rpc_u64")]
324 pub block_number: u64,
325}
326
327#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
330#[serde(rename_all = "camelCase")]
331struct BaseFlashblockBlockPayload {
332 hash: B256,
333 #[serde(deserialize_with = "deserialize_rpc_u64")]
334 number: u64,
335 parent_hash: B256,
336 state_root: B256,
337 #[serde(default)]
338 transactions_root: Option<B256>,
339 #[serde(default)]
340 transactions: Vec<serde_json::Value>,
341 #[serde(deserialize_with = "deserialize_rpc_u64")]
342 timestamp: u64,
343 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
344 base_fee_per_gas: Option<u64>,
345 #[serde(default, alias = "beneficiary", alias = "feeRecipient")]
346 miner: Option<Address>,
347 #[serde(default, alias = "prevRandao")]
348 mix_hash: Option<B256>,
349 #[serde(default, deserialize_with = "deserialize_optional_rpc_u64")]
350 gas_limit: Option<u64>,
351}
352
353#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
357#[serde(untagged)]
358enum BaseFlashblockWirePayload {
359 Indexed(BaseFlashblockPayload),
360 Block(BaseFlashblockBlockPayload),
361}
362
363fn deserialize_rpc_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
364where
365 D: serde::Deserializer<'de>,
366{
367 #[derive(serde::Deserialize)]
368 #[serde(untagged)]
369 enum RpcU64 {
370 Number(u64),
371 String(String),
372 }
373
374 match <RpcU64 as serde::Deserialize>::deserialize(deserializer)? {
375 RpcU64::Number(number) => Ok(number),
376 RpcU64::String(value) => {
377 let value = value.strip_prefix("0x").unwrap_or(&value);
378 u64::from_str_radix(value, 16).map_err(serde::de::Error::custom)
379 }
380 }
381}
382
383fn deserialize_optional_rpc_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
384where
385 D: serde::Deserializer<'de>,
386{
387 #[derive(serde::Deserialize)]
388 #[serde(untagged)]
389 enum RpcU64 {
390 Number(u64),
391 String(String),
392 }
393
394 let Some(value) = <Option<RpcU64> as serde::Deserialize>::deserialize(deserializer)? else {
395 return Ok(None);
396 };
397 match value {
398 RpcU64::Number(number) => Ok(Some(number)),
399 RpcU64::String(value) => {
400 let value = value.strip_prefix("0x").unwrap_or(&value);
401 u64::from_str_radix(value, 16)
402 .map(Some)
403 .map_err(serde::de::Error::custom)
404 }
405 }
406}
407
408fn non_placeholder_hash(hash: B256) -> Option<B256> {
409 (!hash.is_zero()).then_some(hash)
410}
411
412fn flashblock_transaction_hashes(
413 transactions: &[serde_json::Value],
414) -> Result<Vec<B256>, SubscriberError> {
415 let hashes: Vec<B256> = transactions
416 .iter()
417 .map(|transaction| {
418 let value = match transaction {
419 serde_json::Value::String(value) => value.as_str(),
420 serde_json::Value::Object(object) => object
421 .get("hash")
422 .or_else(|| object.get("transactionHash"))
423 .and_then(serde_json::Value::as_str)
424 .ok_or_else(|| {
425 SubscriberError::Provider(
426 "Flashblock transaction object is missing its hash".into(),
427 )
428 })?,
429 _ => {
430 return Err(SubscriberError::Provider(
431 "Flashblock transaction must be a hash, raw transaction, or object".into(),
432 ));
433 }
434 };
435 if value.len() == 66 {
436 return value.parse::<B256>().map_err(|error| {
437 SubscriberError::Provider(format!(
438 "Flashblock transaction hash is invalid: {error}"
439 ))
440 });
441 }
442 let encoded = value.strip_prefix("0x").unwrap_or(value);
443 let raw = alloy_primitives::hex::decode(encoded).map_err(|error| {
444 SubscriberError::Provider(format!(
445 "Flashblock raw transaction is invalid hex: {error}"
446 ))
447 })?;
448 Ok(alloy_primitives::keccak256(raw))
449 })
450 .collect::<Result<_, _>>()?;
451 let mut unique = HashSet::with_capacity(hashes.len());
452 if hashes.iter().any(|hash| !unique.insert(*hash)) {
453 return Err(SubscriberError::Provider(
454 "Flashblock cumulative transaction membership contains a duplicate hash".into(),
455 ));
456 }
457 Ok(hashes)
458}
459
460struct FlashblockContentCommitment<'a> {
461 provider: &'a ProviderRef,
462 payload_id: Option<FixedBytes<8>>,
463 index: Option<u64>,
464 block_number: u64,
465 partial_block_hash: Option<B256>,
466 parent_hash: Option<B256>,
467 state_root: Option<B256>,
468 transactions_root: Option<B256>,
469 transaction_hashes: &'a [B256],
470 timestamp: Option<u64>,
471 base_fee_per_gas: Option<u64>,
472 beneficiary: Option<Address>,
473 prevrandao: Option<B256>,
474 gas_limit: Option<u64>,
475}
476
477fn flashblock_content_hash(content: FlashblockContentCommitment<'_>) -> B256 {
478 let mut commitment = Keccak256::new();
479 commitment.update(b"evm-fork-cache/flashblock-content/v1");
480 let endpoint = content.provider.endpoint.as_str().as_bytes();
481 commitment.update((endpoint.len() as u64).to_be_bytes());
482 commitment.update(endpoint);
483 commitment.update(content.provider.generation.to_be_bytes());
484 commitment.update(content.block_number.to_be_bytes());
485 commit_optional_bytes(
486 &mut commitment,
487 content.payload_id.as_ref().map(FixedBytes::as_slice),
488 );
489 commit_optional_u64(&mut commitment, content.index);
490 commit_optional_bytes(
491 &mut commitment,
492 content
493 .partial_block_hash
494 .as_ref()
495 .map(FixedBytes::as_slice),
496 );
497 commit_optional_bytes(
498 &mut commitment,
499 content.parent_hash.as_ref().map(FixedBytes::as_slice),
500 );
501 commit_optional_bytes(
502 &mut commitment,
503 content.state_root.as_ref().map(FixedBytes::as_slice),
504 );
505 commit_optional_bytes(
506 &mut commitment,
507 content.transactions_root.as_ref().map(FixedBytes::as_slice),
508 );
509 commitment.update((content.transaction_hashes.len() as u64).to_be_bytes());
510 for transaction_hash in content.transaction_hashes {
511 commitment.update(transaction_hash);
512 }
513 commit_optional_u64(&mut commitment, content.timestamp);
514 commit_optional_u64(&mut commitment, content.base_fee_per_gas);
515 commit_optional_bytes(
516 &mut commitment,
517 content
518 .beneficiary
519 .as_ref()
520 .map(|address| address.as_slice()),
521 );
522 commit_optional_bytes(
523 &mut commitment,
524 content.prevrandao.as_ref().map(FixedBytes::as_slice),
525 );
526 commit_optional_u64(&mut commitment, content.gas_limit);
527 let hash = commitment.finalize();
528 if hash.is_zero() {
529 B256::with_last_byte(1)
530 } else {
531 hash
532 }
533}
534
535#[cfg(feature = "raw-flashblocks-json")]
536fn validate_standard_flashblock_snapshot(
537 snapshot: &FlashblockSnapshot,
538) -> Result<(), SubscriberError> {
539 let flashblock = &snapshot.flashblock;
540 if flashblock.payload_id.is_none() || flashblock.index.is_none() {
541 return Err(SubscriberError::Provider(
542 "external Flashblock snapshot is missing its indexed payload identity".into(),
543 ));
544 }
545 let expected_content_hash = flashblock_content_hash(FlashblockContentCommitment {
546 provider: &flashblock.provider,
547 payload_id: flashblock.payload_id,
548 index: flashblock.index,
549 block_number: flashblock.block_number,
550 partial_block_hash: flashblock.partial_block_hash,
551 parent_hash: flashblock.parent_hash,
552 state_root: flashblock.state_root,
553 transactions_root: flashblock.transactions_root,
554 transaction_hashes: &flashblock.transaction_hashes,
555 timestamp: flashblock.timestamp,
556 base_fee_per_gas: flashblock.base_fee_per_gas,
557 beneficiary: flashblock.beneficiary,
558 prevrandao: flashblock.prevrandao,
559 gas_limit: flashblock.gas_limit,
560 });
561 if flashblock.content_hash != expected_content_hash {
562 return Err(SubscriberError::Provider(
563 "external Flashblock content commitment is invalid".into(),
564 ));
565 }
566
567 let mut transactions = HashSet::with_capacity(flashblock.transaction_hashes.len());
568 if flashblock
569 .transaction_hashes
570 .iter()
571 .any(|hash| !transactions.insert(*hash))
572 {
573 return Err(SubscriberError::Provider(
574 "external Flashblock cumulative transaction membership contains a duplicate hash"
575 .into(),
576 ));
577 }
578
579 let mut log_ids = HashSet::with_capacity(snapshot.logs.len());
580 for log in &snapshot.logs {
581 if log.removed || log.block_number != Some(flashblock.block_number) {
582 return Err(SubscriberError::Provider(
583 "external pre-confirmed log disagrees with its Flashblock block identity".into(),
584 ));
585 }
586 if log.block_hash != Some(flashblock.content_hash) {
587 return Err(SubscriberError::Provider(
588 "external pre-confirmed log is not bound to its Flashblock content commitment"
589 .into(),
590 ));
591 }
592 let transaction_hash = log.transaction_hash.ok_or_else(|| {
593 SubscriberError::Provider(
594 "external pre-confirmed log is missing its transaction hash".into(),
595 )
596 })?;
597 let expected_transaction_index = flashblock
598 .transaction_index(&transaction_hash)
599 .ok_or_else(|| {
600 SubscriberError::Provider(
601 "external pre-confirmed log transaction is absent from the cumulative Flashblock"
602 .into(),
603 )
604 })?;
605 if log.transaction_index != Some(expected_transaction_index) {
606 return Err(SubscriberError::Provider(
607 "external pre-confirmed log transaction index disagrees with cumulative membership"
608 .into(),
609 ));
610 }
611 let log_index = log.log_index.ok_or_else(|| {
612 SubscriberError::Provider("external pre-confirmed log is missing its log index".into())
613 })?;
614 if !log_ids.insert((transaction_hash, log_index)) {
615 return Err(SubscriberError::Provider(
616 "external Flashblock snapshot contains a duplicate log identity".into(),
617 ));
618 }
619 }
620 Ok(())
621}
622
623fn commit_optional_bytes(commitment: &mut Keccak256, value: Option<&[u8]>) {
624 match value {
625 Some(value) => {
626 commitment.update([1]);
627 commitment.update((value.len() as u64).to_be_bytes());
628 commitment.update(value);
629 }
630 None => commitment.update([0]),
631 }
632}
633
634fn commit_optional_u64(commitment: &mut Keccak256, value: Option<u64>) {
635 match value {
636 Some(value) => {
637 commitment.update([1]);
638 commitment.update(value.to_be_bytes());
639 }
640 None => commitment.update([0]),
641 }
642}
643
644#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
647pub struct ReactiveCanonicalBaseline {
648 pub chain_id: u64,
650 pub block: BlockRef,
652}
653
654impl ReactiveCanonicalBaseline {
655 pub const fn new(chain_id: u64, block: BlockRef) -> Self {
657 Self { chain_id, block }
658 }
659}
660
661#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
670#[non_exhaustive]
671pub enum ChainControl {
672 Reorg {
674 common_ancestor: BlockRef,
676 old_tip: BlockRef,
678 new_tip: BlockRef,
680 },
681 Safe(BlockRef),
683 Finalized(BlockRef),
685 CanonicalProgress(BlockRef),
692 Barrier {
694 id: Vec<u8>,
696 block: Option<BlockRef>,
698 },
699}
700
701#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
720pub struct CanonicalSequenceState {
721 retained_canonical_history: Vec<BlockRef>,
722 coverage_head: Option<BlockRef>,
723 safe_head: Option<BlockRef>,
724 finalized_head: Option<BlockRef>,
725}
726
727impl CanonicalSequenceState {
728 pub fn new(
734 retained_canonical_history: Vec<BlockRef>,
735 coverage_head: Option<BlockRef>,
736 safe_head: Option<BlockRef>,
737 finalized_head: Option<BlockRef>,
738 ) -> Self {
739 Self {
740 retained_canonical_history,
741 coverage_head,
742 safe_head,
743 finalized_head,
744 }
745 }
746
747 pub fn retained_canonical_history(&self) -> &[BlockRef] {
749 &self.retained_canonical_history
750 }
751
752 pub const fn coverage_head(&self) -> Option<&BlockRef> {
754 self.coverage_head.as_ref()
755 }
756
757 pub const fn safe_head(&self) -> Option<&BlockRef> {
759 self.safe_head.as_ref()
760 }
761
762 pub const fn finalized_head(&self) -> Option<&BlockRef> {
764 self.finalized_head.as_ref()
765 }
766
767 pub fn retain_recent_history(&mut self, max_entries: usize) {
776 let remove = self
777 .retained_canonical_history
778 .len()
779 .saturating_sub(max_entries);
780 self.retained_canonical_history.drain(..remove);
781 }
782
783 pub fn validate(&self) -> Result<(), ReactiveError> {
797 validate_canonical_sequence_snapshot(self)
798 }
799}
800
801#[derive(Clone, Debug, PartialEq, Eq)]
803#[non_exhaustive]
804pub enum CanonicalSequenceMutation {
805 Rewind {
807 common_ancestor: Option<BlockRef>,
813 dropped: Vec<BlockRef>,
815 },
816 Canonical(BlockRef),
818 Safe(BlockRef),
820 Finalized(BlockRef),
822}
823
824#[derive(Clone, Debug, PartialEq, Eq)]
826pub struct CanonicalSequenceValidation {
827 pre_record_state: CanonicalSequenceState,
828 next_state: CanonicalSequenceState,
829 mutations: Vec<CanonicalSequenceMutation>,
830 normalized_chain_controls: Vec<ChainControl>,
831}
832
833impl CanonicalSequenceValidation {
834 pub const fn pre_record_state(&self) -> &CanonicalSequenceState {
836 &self.pre_record_state
837 }
838
839 pub const fn next_state(&self) -> &CanonicalSequenceState {
841 &self.next_state
842 }
843
844 pub fn mutations(&self) -> &[CanonicalSequenceMutation] {
846 &self.mutations
847 }
848
849 pub fn normalized_chain_controls(&self) -> &[ChainControl] {
859 &self.normalized_chain_controls
860 }
861}
862
863#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
865#[non_exhaustive]
866pub enum ChainStatus {
867 Pending,
869 Preconfirmed {
874 flashblock: Arc<FlashblockRef>,
878 },
879 Included {
881 block: BlockRef,
883 confirmations: u64,
885 },
886 Safe {
888 block: BlockRef,
890 },
891 Finalized {
893 block: BlockRef,
895 },
896 Reorged {
898 dropped_from: BlockRef,
900 },
901}
902
903#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
905#[non_exhaustive]
906pub enum InputSource {
907 Batch,
909 Subscription,
911 Poll,
913 Backfill,
915 Flashblocks,
917 Synthetic,
919}
920
921#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
923pub enum InputRef {
924 Log {
926 chain_id: Option<u64>,
928 block_hash: B256,
930 transaction_hash: B256,
932 log_index: u64,
934 },
935 PendingTx {
937 chain_id: Option<u64>,
939 hash: B256,
941 },
942 Block {
944 chain_id: Option<u64>,
946 hash: B256,
948 number: u64,
950 },
951}
952
953#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
960#[non_exhaustive]
961pub enum ReactiveInputKind {
962 CanonicalLog,
964 ReorgSignalLog,
966 BlockHeader,
968 FullBlock,
970 PendingTxHash,
972 PendingTx,
974}
975
976#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
983pub struct ReactiveInputIdentity {
984 input_ref: InputRef,
985 kind: ReactiveInputKind,
986}
987
988impl ReactiveInputIdentity {
989 pub fn try_from_parts(
1003 input_ref: InputRef,
1004 kind: ReactiveInputKind,
1005 ) -> Result<Self, ReactiveInputIdentityError> {
1006 let compatible = matches!(
1007 (input_ref, kind),
1008 (
1009 InputRef::Log { .. },
1010 ReactiveInputKind::CanonicalLog | ReactiveInputKind::ReorgSignalLog
1011 ) | (
1012 InputRef::Block { .. },
1013 ReactiveInputKind::BlockHeader | ReactiveInputKind::FullBlock
1014 ) | (
1015 InputRef::PendingTx { .. },
1016 ReactiveInputKind::PendingTxHash | ReactiveInputKind::PendingTx
1017 )
1018 );
1019 if !compatible {
1020 return Err(ReactiveInputIdentityError { input_ref, kind });
1021 }
1022 Ok(Self { input_ref, kind })
1023 }
1024
1025 pub const fn input_ref(&self) -> InputRef {
1027 self.input_ref
1028 }
1029
1030 pub const fn kind(&self) -> ReactiveInputKind {
1032 self.kind
1033 }
1034}
1035
1036#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
1039#[error("reactive input kind {kind:?} is incompatible with input reference {input_ref:?}")]
1040pub struct ReactiveInputIdentityError {
1041 input_ref: InputRef,
1042 kind: ReactiveInputKind,
1043}
1044
1045impl ReactiveInputIdentityError {
1046 pub const fn input_ref(&self) -> InputRef {
1048 self.input_ref
1049 }
1050
1051 pub const fn kind(&self) -> ReactiveInputKind {
1053 self.kind
1054 }
1055}
1056
1057#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1059pub enum StateEffectQuality {
1060 ExactFromInput,
1062 AppliedWithPendingResync,
1064 ResyncedAuthoritatively,
1066 RequiresRepair,
1068 NoStateEffect,
1070}
1071
1072#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize)]
1074pub struct HandlerId(String);
1075
1076impl HandlerId {
1077 pub fn new(id: impl Into<String>) -> Self {
1084 Self::try_new(id).expect("handler id must not be empty")
1085 }
1086
1087 pub fn try_new(id: impl Into<String>) -> Result<Self, HandlerIdError> {
1094 let id = id.into();
1095 if id.is_empty() {
1096 return Err(HandlerIdError);
1097 }
1098 Ok(Self(id))
1099 }
1100
1101 pub fn as_str(&self) -> &str {
1103 &self.0
1104 }
1105}
1106
1107impl<'de> serde::Deserialize<'de> for HandlerId {
1108 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1109 where
1110 D: serde::Deserializer<'de>,
1111 {
1112 let id = <String as serde::Deserialize>::deserialize(deserializer)?;
1113 Self::try_new(id).map_err(serde::de::Error::custom)
1114 }
1115}
1116
1117#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
1120#[error("handler id must not be empty")]
1121pub struct HandlerIdError;
1122
1123impl fmt::Display for HandlerId {
1124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1125 self.0.fmt(f)
1126 }
1127}
1128
1129#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1131pub struct ReportTag {
1132 pub key: String,
1134 pub value: String,
1136}
1137
1138impl ReportTag {
1139 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
1141 Self {
1142 key: key.into(),
1143 value: value.into(),
1144 }
1145 }
1146}
1147
1148#[derive(Clone)]
1150pub struct HookSignal {
1151 pub namespace: Cow<'static, str>,
1153 pub kind: Cow<'static, str>,
1155 pub labels: Vec<ReportTag>,
1157 pub payload: Option<Arc<dyn Any + Send + Sync>>,
1159}
1160
1161impl fmt::Debug for HookSignal {
1162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1163 f.debug_struct("HookSignal")
1164 .field("namespace", &self.namespace)
1165 .field("kind", &self.kind)
1166 .field("labels", &self.labels)
1167 .field("payload", &self.payload.as_ref().map(|_| "<payload>"))
1168 .finish()
1169 }
1170}
1171
1172#[derive(Clone, Debug)]
1174pub enum ReactiveEffect {
1175 StateUpdate(StateUpdate),
1177 Resync(ResyncRequest),
1179 Invalidate(InvalidationRequest),
1181 Hook(HookSignal),
1183 Speculative(SpeculativeRequest),
1185}
1186
1187#[derive(Clone, Debug)]
1189pub struct HandlerOutcome {
1190 pub effects: Vec<ReactiveEffect>,
1192 pub quality: StateEffectQuality,
1194 pub tags: Vec<ReportTag>,
1196}
1197
1198impl HandlerOutcome {
1199 pub fn empty(quality: StateEffectQuality) -> Self {
1201 Self {
1202 effects: Vec::new(),
1203 quality,
1204 tags: Vec::new(),
1205 }
1206 }
1207}
1208
1209#[derive(Clone, Debug)]
1211pub struct ReactiveInputRecord<N: Network = Ethereum> {
1212 pub input: ReactiveInput<N>,
1214 pub context: ReactiveContext,
1216 pub provider: Option<ProviderRef>,
1219}
1220
1221impl<N: Network> ReactiveInputRecord<N> {
1222 pub fn new(input: ReactiveInput<N>, context: ReactiveContext) -> Self {
1224 Self {
1225 input,
1226 context,
1227 provider: None,
1228 }
1229 }
1230
1231 #[must_use]
1233 pub fn with_provider(mut self, provider: ProviderRef) -> Self {
1234 self.provider = Some(provider);
1235 self
1236 }
1237
1238 pub fn input_ref(&self) -> InputRef {
1240 input_ref(&self.input, &self.context)
1241 }
1242
1243 pub fn validated_identity(&self) -> Result<ReactiveInputIdentity, ReactiveError> {
1260 validate_input_record(self)?;
1261 let kind = match &self.input {
1262 ReactiveInput::Log(log)
1263 if log.removed
1264 || matches!(self.context.chain_status, ChainStatus::Reorged { .. }) =>
1265 {
1266 ReactiveInputKind::ReorgSignalLog
1267 }
1268 ReactiveInput::Log(_) => ReactiveInputKind::CanonicalLog,
1269 ReactiveInput::BlockHeader(_) => ReactiveInputKind::BlockHeader,
1270 ReactiveInput::FullBlock(_) => ReactiveInputKind::FullBlock,
1271 ReactiveInput::PendingTxHash(_) => ReactiveInputKind::PendingTxHash,
1272 ReactiveInput::PendingTx(_) => ReactiveInputKind::PendingTx,
1273 };
1274 ReactiveInputIdentity::try_from_parts(self.input_ref(), kind).map_err(|error| {
1275 ReactiveError::InvalidInputRecord {
1276 message: error.to_string(),
1277 }
1278 })
1279 }
1280
1281 pub fn same_deduplicable_payload(&self, other: &Self) -> bool {
1295 match (&self.input, &other.input) {
1296 (ReactiveInput::Log(left), ReactiveInput::Log(right)) => {
1297 left.inner == right.inner
1298 && left.block_hash == right.block_hash
1299 && left.block_number == right.block_number
1300 && optional_metadata_compatible(
1301 left.block_timestamp.as_ref(),
1302 right.block_timestamp.as_ref(),
1303 )
1304 && left.transaction_hash == right.transaction_hash
1305 && left.transaction_index == right.transaction_index
1306 && left.log_index == right.log_index
1307 && left.removed == right.removed
1308 }
1309 (ReactiveInput::BlockHeader(left), ReactiveInput::BlockHeader(right)) => {
1310 left.hash() == right.hash()
1311 }
1312 (ReactiveInput::FullBlock(_), ReactiveInput::FullBlock(_)) => false,
1313 (ReactiveInput::PendingTxHash(left), ReactiveInput::PendingTxHash(right)) => {
1314 left == right
1315 }
1316 (ReactiveInput::PendingTx(_), ReactiveInput::PendingTx(_)) => false,
1317 _ => false,
1318 }
1319 }
1320
1321 pub fn is_payload_deduplicable(&self) -> bool {
1327 matches!(
1328 &self.input,
1329 ReactiveInput::Log(_) | ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_)
1330 )
1331 }
1332
1333 pub fn merge_compatible_duplicate(&mut self, other: &Self) -> Result<bool, ReactiveError> {
1348 let identity = self.validated_identity()?;
1349 let other_identity = other.validated_identity()?;
1350 if identity != other_identity
1351 || !self.is_payload_deduplicable()
1352 || !other.is_payload_deduplicable()
1353 {
1354 return Ok(false);
1355 }
1356 if !self.same_deduplicable_payload(other) || !self.dedupe_context_is_compatible(other) {
1357 return Err(ReactiveError::InvalidInputRecord {
1358 message: format!(
1359 "conflicting payload or semantic context for identity {identity:?}"
1360 ),
1361 });
1362 }
1363 let mut merged = self.clone();
1364 merge_deduplicable_record(&mut merged, other);
1365 merged.validated_identity()?;
1366 *self = merged;
1367 Ok(true)
1368 }
1369
1370 pub fn dedupe_context_is_compatible(&self, other: &Self) -> bool {
1378 let left = &self.context;
1379 let right = &other.context;
1380 left.chain_id == right.chain_id
1381 && optional_block_refs_are_compatible(left.block.as_ref(), right.block.as_ref())
1382 && left.transaction_index == right.transaction_index
1383 && left.log_index == right.log_index
1384 && chain_statuses_are_dedupe_compatible(&left.chain_status, &right.chain_status)
1385 }
1386}
1387
1388fn chain_statuses_are_dedupe_compatible(left: &ChainStatus, right: &ChainStatus) -> bool {
1389 match (left, right) {
1390 (ChainStatus::Pending, ChainStatus::Pending)
1391 | (ChainStatus::Reorged { .. }, ChainStatus::Reorged { .. }) => true,
1392 (
1393 ChainStatus::Preconfirmed { flashblock: left },
1394 ChainStatus::Preconfirmed { flashblock: right },
1395 ) => left == right,
1396 (
1397 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. },
1398 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. },
1399 ) => true,
1400 _ => false,
1401 }
1402}
1403
1404fn optional_metadata_compatible<T: PartialEq>(left: Option<&T>, right: Option<&T>) -> bool {
1405 left.zip(right).is_none_or(|(left, right)| left == right)
1406}
1407
1408fn optional_block_refs_are_compatible(left: Option<&BlockRef>, right: Option<&BlockRef>) -> bool {
1409 match (left, right) {
1410 (None, None) => true,
1411 (Some(left), Some(right)) => {
1412 left.number == right.number
1413 && left.hash == right.hash
1414 && optional_metadata_compatible(
1415 left.parent_hash.as_ref(),
1416 right.parent_hash.as_ref(),
1417 )
1418 && optional_metadata_compatible(left.timestamp.as_ref(), right.timestamp.as_ref())
1419 }
1420 _ => false,
1421 }
1422}
1423
1424fn merge_deduplicable_record<N: Network>(
1425 retained: &mut ReactiveInputRecord<N>,
1426 incoming: &ReactiveInputRecord<N>,
1427) {
1428 if let (ReactiveInput::Log(retained), ReactiveInput::Log(incoming)) =
1429 (&mut retained.input, &incoming.input)
1430 && retained.block_timestamp.is_none()
1431 {
1432 retained.block_timestamp = incoming.block_timestamp;
1433 }
1434 if let (Some(retained), Some(incoming)) =
1435 (&mut retained.context.block, incoming.context.block.as_ref())
1436 {
1437 enrich_block_ref(retained, incoming);
1438 }
1439 retained.context.chain_status = merged_chain_status(
1440 &retained.context.chain_status,
1441 &incoming.context.chain_status,
1442 );
1443 if input_source_rank(incoming.context.source) > input_source_rank(retained.context.source) {
1444 retained.context.source = incoming.context.source;
1445 }
1446 if retained.provider.is_none() {
1447 retained.provider = incoming.provider.clone();
1448 }
1449}
1450
1451fn enrich_block_ref(retained: &mut BlockRef, incoming: &BlockRef) {
1452 if retained.parent_hash.is_none() {
1453 retained.parent_hash = incoming.parent_hash;
1454 }
1455 if retained.timestamp.is_none() {
1456 retained.timestamp = incoming.timestamp;
1457 }
1458}
1459
1460fn merged_chain_status(retained: &ChainStatus, incoming: &ChainStatus) -> ChainStatus {
1461 let merged_block = |left: &BlockRef, right: &BlockRef| {
1462 let mut block = *left;
1463 enrich_block_ref(&mut block, right);
1464 block
1465 };
1466 match (retained, incoming) {
1467 (ChainStatus::Pending, ChainStatus::Pending) => ChainStatus::Pending,
1468 (
1469 ChainStatus::Preconfirmed { flashblock: left },
1470 ChainStatus::Preconfirmed { flashblock: right },
1471 ) => {
1472 debug_assert_eq!(left, right, "compatible pre-confirmed records agree");
1473 ChainStatus::Preconfirmed {
1474 flashblock: left.clone(),
1475 }
1476 }
1477 (
1478 ChainStatus::Reorged { dropped_from: left },
1479 ChainStatus::Reorged {
1480 dropped_from: right,
1481 },
1482 ) => ChainStatus::Reorged {
1483 dropped_from: merged_block(left, right),
1484 },
1485 (left, right) => {
1486 let (left_block, left_rank, left_confirmations) = canonical_status_parts(left)
1487 .expect("compatible duplicate has a canonical lifecycle");
1488 let (right_block, right_rank, right_confirmations) = canonical_status_parts(right)
1489 .expect("compatible duplicate has a canonical lifecycle");
1490 let block = merged_block(left_block, right_block);
1491 let rank = left_rank.max(right_rank);
1492 match rank {
1493 3 => ChainStatus::Finalized { block },
1494 2 => ChainStatus::Safe { block },
1495 _ => ChainStatus::Included {
1496 block,
1497 confirmations: left_confirmations.max(right_confirmations),
1498 },
1499 }
1500 }
1501 }
1502}
1503
1504fn canonical_status_parts(status: &ChainStatus) -> Option<(&BlockRef, u8, u64)> {
1505 match status {
1506 ChainStatus::Included {
1507 block,
1508 confirmations,
1509 } => Some((block, 1, *confirmations)),
1510 ChainStatus::Safe { block } => Some((block, 2, 0)),
1511 ChainStatus::Finalized { block } => Some((block, 3, 0)),
1512 ChainStatus::Pending | ChainStatus::Preconfirmed { .. } | ChainStatus::Reorged { .. } => {
1513 None
1514 }
1515 }
1516}
1517
1518fn input_source_rank(source: InputSource) -> u8 {
1519 match source {
1520 InputSource::Backfill => 0,
1521 InputSource::Poll => 1,
1522 InputSource::Subscription => 2,
1523 InputSource::Flashblocks => 3,
1524 InputSource::Batch => 4,
1525 InputSource::Synthetic => 5,
1526 }
1527}
1528
1529#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1539pub struct SubscriberDeliveryToken(Vec<u8>);
1540
1541impl SubscriberDeliveryToken {
1542 pub fn new(bytes: Vec<u8>) -> Self {
1544 Self(bytes)
1545 }
1546
1547 pub fn as_bytes(&self) -> &[u8] {
1549 &self.0
1550 }
1551
1552 pub fn into_bytes(self) -> Vec<u8> {
1554 self.0
1555 }
1556}
1557
1558#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1564pub struct SubscriberCheckpoint(Vec<u8>);
1565
1566impl SubscriberCheckpoint {
1567 pub fn new(bytes: Vec<u8>) -> Self {
1569 Self(bytes)
1570 }
1571
1572 pub fn as_bytes(&self) -> &[u8] {
1574 &self.0
1575 }
1576
1577 pub fn into_bytes(self) -> Vec<u8> {
1579 self.0
1580 }
1581}
1582
1583#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
1593pub struct SubscriberPayloadCommitment(B256);
1594
1595impl SubscriberPayloadCommitment {
1596 pub const fn new(commitment: B256) -> Self {
1598 Self(commitment)
1599 }
1600
1601 pub const fn digest(&self) -> B256 {
1603 self.0
1604 }
1605}
1606
1607#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1613#[non_exhaustive]
1614pub struct SubscriberResumePosition {
1615 pub chain_id: u64,
1617 pub coverage_head: BlockRef,
1619 pub canonical_history: Vec<BlockRef>,
1621 pub delivery_token: Option<SubscriberDeliveryToken>,
1625 pub subscriber_checkpoint: Option<SubscriberCheckpoint>,
1627}
1628
1629impl SubscriberResumePosition {
1630 pub fn new(
1632 chain_id: u64,
1633 coverage_head: BlockRef,
1634 canonical_history: Vec<BlockRef>,
1635 delivery_token: Option<SubscriberDeliveryToken>,
1636 subscriber_checkpoint: Option<SubscriberCheckpoint>,
1637 ) -> Self {
1638 Self {
1639 chain_id,
1640 coverage_head,
1641 canonical_history,
1642 delivery_token,
1643 subscriber_checkpoint,
1644 }
1645 }
1646}
1647
1648#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1656#[non_exhaustive]
1657pub enum DeliveryAudience {
1658 #[default]
1660 All,
1661 Owners(Vec<HandlerId>),
1663 AllExcept(Vec<HandlerId>),
1668}
1669
1670#[derive(
1678 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
1679)]
1680#[non_exhaustive]
1681pub enum DeliveryScope {
1682 #[default]
1684 Canonical,
1685 CanonicalProgress,
1687 OwnerCatchup,
1689 Preconfirmed,
1692}
1693
1694impl DeliveryScope {
1695 const fn advances_canonical_state(self) -> bool {
1696 matches!(self, Self::Canonical | Self::CanonicalProgress)
1697 }
1698}
1699
1700#[derive(Clone, Debug)]
1702pub struct ReactiveInputDelivery<N: Network = Ethereum> {
1703 record: ReactiveInputRecord<N>,
1704 audience: DeliveryAudience,
1705 scope: DeliveryScope,
1706}
1707
1708impl<N: Network> ReactiveInputDelivery<N> {
1709 pub fn new(
1711 record: ReactiveInputRecord<N>,
1712 audience: DeliveryAudience,
1713 scope: DeliveryScope,
1714 ) -> Self {
1715 Self {
1716 record,
1717 audience,
1718 scope,
1719 }
1720 }
1721
1722 pub const fn record(&self) -> &ReactiveInputRecord<N> {
1724 &self.record
1725 }
1726
1727 pub const fn audience(&self) -> &DeliveryAudience {
1729 &self.audience
1730 }
1731
1732 pub const fn scope(&self) -> DeliveryScope {
1734 self.scope
1735 }
1736
1737 pub fn into_parts(self) -> (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope) {
1739 (self.record, self.audience, self.scope)
1740 }
1741}
1742
1743#[derive(Clone, Debug)]
1748#[non_exhaustive]
1749pub struct ReactiveInputBatchParts<N: Network = Ethereum> {
1750 pub chain_id: Option<u64>,
1752 pub deliveries: Vec<ReactiveInputDelivery<N>>,
1754 pub delivery_token: Option<SubscriberDeliveryToken>,
1756 pub subscriber_checkpoint: Option<SubscriberCheckpoint>,
1758 pub payload_commitment: Option<SubscriberPayloadCommitment>,
1760 pub chain_controls: Vec<ChainControl>,
1762}
1763
1764#[derive(Clone, Debug)]
1766pub struct ReactiveInputBatch<N: Network = Ethereum> {
1767 records: Vec<ReactiveInputRecord<N>>,
1768 chain_id: Option<u64>,
1769 delivery_token: Option<SubscriberDeliveryToken>,
1770 subscriber_checkpoint: Option<SubscriberCheckpoint>,
1771 payload_commitment: Option<SubscriberPayloadCommitment>,
1772 audience: DeliveryAudience,
1773 record_audiences: Option<Vec<DeliveryAudience>>,
1774 delivery_scope: DeliveryScope,
1775 record_delivery_scopes: Option<Vec<DeliveryScope>>,
1776 chain_controls: Vec<ChainControl>,
1777}
1778
1779type RuntimeInputDelivery<N> = (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope);
1780
1781impl<N: Network> ReactiveInputBatch<N> {
1782 pub fn new(records: Vec<ReactiveInputRecord<N>>) -> Self {
1784 let chain_id = common_record_chain_id(&records);
1785 Self {
1786 records,
1787 chain_id,
1788 delivery_token: None,
1789 subscriber_checkpoint: None,
1790 payload_commitment: None,
1791 audience: DeliveryAudience::All,
1792 record_audiences: None,
1793 delivery_scope: DeliveryScope::Canonical,
1794 record_delivery_scopes: None,
1795 chain_controls: Vec::new(),
1796 }
1797 }
1798
1799 pub fn with_chain_id(mut self, chain_id: u64) -> Self {
1802 self.chain_id = Some(chain_id);
1803 self
1804 }
1805
1806 pub const fn chain_id(&self) -> Option<u64> {
1809 self.chain_id
1810 }
1811
1812 pub fn with_delivery_token(mut self, token: SubscriberDeliveryToken) -> Self {
1814 self.delivery_token = Some(token);
1815 self
1816 }
1817
1818 pub fn delivery_token(&self) -> Option<&SubscriberDeliveryToken> {
1820 self.delivery_token.as_ref()
1821 }
1822
1823 pub fn with_subscriber_checkpoint(mut self, checkpoint: SubscriberCheckpoint) -> Self {
1825 self.subscriber_checkpoint = Some(checkpoint);
1826 self
1827 }
1828
1829 pub fn subscriber_checkpoint(&self) -> Option<&SubscriberCheckpoint> {
1831 self.subscriber_checkpoint.as_ref()
1832 }
1833
1834 pub fn with_payload_commitment(mut self, commitment: SubscriberPayloadCommitment) -> Self {
1837 self.payload_commitment = Some(commitment);
1838 self
1839 }
1840
1841 pub const fn payload_commitment(&self) -> Option<&SubscriberPayloadCommitment> {
1843 self.payload_commitment.as_ref()
1844 }
1845
1846 pub fn with_audience(mut self, audience: DeliveryAudience) -> Self {
1848 self.audience = audience;
1849 self.record_audiences = None;
1850 self
1851 }
1852
1853 pub const fn audience(&self) -> &DeliveryAudience {
1855 &self.audience
1856 }
1857
1858 pub fn from_scoped_records(
1860 records: impl IntoIterator<Item = (ReactiveInputRecord<N>, DeliveryAudience)>,
1861 ) -> Self {
1862 let (records, record_audiences): (Vec<_>, Vec<_>) = records.into_iter().unzip();
1863 let chain_id = common_record_chain_id(&records);
1864 Self {
1865 records,
1866 chain_id,
1867 delivery_token: None,
1868 subscriber_checkpoint: None,
1869 payload_commitment: None,
1870 audience: DeliveryAudience::All,
1871 record_audiences: Some(record_audiences),
1872 delivery_scope: DeliveryScope::Canonical,
1873 record_delivery_scopes: None,
1874 chain_controls: Vec::new(),
1875 }
1876 }
1877
1878 pub fn from_deliveries(deliveries: impl IntoIterator<Item = ReactiveInputDelivery<N>>) -> Self {
1880 Self::from_scoped_records_with_delivery_scope(
1881 deliveries
1882 .into_iter()
1883 .map(ReactiveInputDelivery::into_parts),
1884 )
1885 }
1886
1887 pub fn record_audience(&self, index: usize) -> Option<&DeliveryAudience> {
1889 if index >= self.records.len() {
1890 return None;
1891 }
1892 Some(
1893 self.record_audiences
1894 .as_ref()
1895 .and_then(|audiences| audiences.get(index))
1896 .unwrap_or(&self.audience),
1897 )
1898 }
1899
1900 pub fn with_delivery_scope(mut self, scope: DeliveryScope) -> Self {
1902 self.delivery_scope = scope;
1903 self.record_delivery_scopes = None;
1904 self
1905 }
1906
1907 pub fn record_delivery_scope(&self, index: usize) -> Option<DeliveryScope> {
1909 if index >= self.records.len() {
1910 return None;
1911 }
1912 Some(
1913 self.record_delivery_scopes
1914 .as_ref()
1915 .and_then(|scopes| scopes.get(index))
1916 .copied()
1917 .unwrap_or(self.delivery_scope),
1918 )
1919 }
1920
1921 fn from_scoped_records_with_delivery_scope(
1922 records: impl IntoIterator<Item = (ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
1923 ) -> Self {
1924 let mut input_records = Vec::new();
1925 let mut audiences = Vec::new();
1926 let mut scopes = Vec::new();
1927 for (record, audience, scope) in records {
1928 input_records.push(record);
1929 audiences.push(audience);
1930 scopes.push(scope);
1931 }
1932 let chain_id = common_record_chain_id(&input_records);
1933 Self {
1934 records: input_records,
1935 chain_id,
1936 delivery_token: None,
1937 subscriber_checkpoint: None,
1938 payload_commitment: None,
1939 audience: DeliveryAudience::All,
1940 record_audiences: Some(audiences),
1941 delivery_scope: DeliveryScope::Canonical,
1942 record_delivery_scopes: Some(scopes),
1943 chain_controls: Vec::new(),
1944 }
1945 }
1946
1947 pub fn with_chain_controls(mut self, controls: impl IntoIterator<Item = ChainControl>) -> Self {
1954 self.chain_controls = controls.into_iter().collect();
1955 self
1956 }
1957
1958 pub fn chain_controls(&self) -> &[ChainControl] {
1960 &self.chain_controls
1961 }
1962
1963 pub fn records(&self) -> &[ReactiveInputRecord<N>] {
1965 &self.records
1966 }
1967
1968 pub fn into_records(self) -> Vec<ReactiveInputRecord<N>> {
1975 self.records
1976 }
1977
1978 pub fn into_parts(self) -> ReactiveInputBatchParts<N> {
1980 let chain_id = self.chain_id;
1981 let delivery_token = self.delivery_token;
1982 let subscriber_checkpoint = self.subscriber_checkpoint;
1983 let payload_commitment = self.payload_commitment;
1984 let chain_controls = self.chain_controls;
1985 let audiences = self
1986 .record_audiences
1987 .unwrap_or_else(|| vec![self.audience; self.records.len()]);
1988 let scopes = self
1989 .record_delivery_scopes
1990 .unwrap_or_else(|| vec![self.delivery_scope; self.records.len()]);
1991 let deliveries = self
1992 .records
1993 .into_iter()
1994 .zip(audiences)
1995 .zip(scopes)
1996 .map(|((record, audience), scope)| ReactiveInputDelivery::new(record, audience, scope))
1997 .collect();
1998 ReactiveInputBatchParts {
1999 chain_id,
2000 deliveries,
2001 delivery_token,
2002 subscriber_checkpoint,
2003 payload_commitment,
2004 chain_controls,
2005 }
2006 }
2007
2008 fn into_runtime_parts(self) -> (Vec<RuntimeInputDelivery<N>>, Vec<ChainControl>, Option<u64>) {
2009 let audiences = self
2010 .record_audiences
2011 .unwrap_or_else(|| vec![self.audience; self.records.len()]);
2012 let scopes = self
2013 .record_delivery_scopes
2014 .unwrap_or_else(|| vec![self.delivery_scope; self.records.len()]);
2015 let records = self
2016 .records
2017 .into_iter()
2018 .zip(audiences)
2019 .zip(scopes)
2020 .map(|((record, audience), scope)| (record, audience, scope))
2021 .collect();
2022 (records, self.chain_controls, self.chain_id)
2023 }
2024
2025 fn take_delivery_token(&mut self) -> Option<SubscriberDeliveryToken> {
2026 self.delivery_token.take()
2027 }
2028
2029 fn take_subscriber_checkpoint(&mut self) -> Option<SubscriberCheckpoint> {
2030 self.subscriber_checkpoint.take()
2031 }
2032}
2033
2034fn common_record_chain_id<N: Network>(records: &[ReactiveInputRecord<N>]) -> Option<u64> {
2035 let chain_id = records.first()?.context.chain_id?;
2036 records
2037 .iter()
2038 .all(|record| record.context.chain_id == Some(chain_id))
2039 .then_some(chain_id)
2040}
2041
2042pub trait ReactiveHandler<N: Network = Ethereum>: Send + Sync {
2044 fn id(&self) -> HandlerId;
2046
2047 fn interests(&self) -> Vec<ReactiveInterest<N>>;
2049
2050 fn log_route_index(&self) -> Option<LogRouteIndex> {
2057 None
2058 }
2059
2060 fn handle(
2062 &self,
2063 ctx: &ReactiveContext,
2064 input: &ReactiveInput<N>,
2065 state: &dyn StateView,
2066 ) -> Result<HandlerOutcome, HandlerError>;
2067}
2068
2069pub trait ReactiveHook<N: Network = Ethereum>: Send + Sync {
2079 fn on_report(&self, report: Arc<ReactiveReport<N>>);
2081}
2082
2083#[allow(clippy::large_enum_variant)]
2085#[derive(Clone)]
2086pub enum ReactiveInterest<N: Network = Ethereum> {
2087 Logs(LogInterest),
2089 Blocks(BlockInterest),
2091 PendingTransactions(PendingTxInterest<N>),
2093}
2094
2095impl<N: Network> fmt::Debug for ReactiveInterest<N> {
2096 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2097 match self {
2098 Self::Logs(interest) => f.debug_tuple("Logs").field(interest).finish(),
2099 Self::Blocks(interest) => f.debug_tuple("Blocks").field(interest).finish(),
2100 Self::PendingTransactions(interest) => f
2101 .debug_tuple("PendingTransactions")
2102 .field(interest)
2103 .finish(),
2104 }
2105 }
2106}
2107
2108#[derive(Clone)]
2110pub struct LogInterest {
2111 pub provider_filter: Filter,
2113 pub local_matcher: Option<Arc<dyn LogMatcher>>,
2115 pub route_key: Option<RouteKeySpec>,
2117}
2118
2119impl LogInterest {
2120 pub fn matches(&self, log: &Log) -> bool {
2122 self.provider_filter.rpc_matches(log)
2123 && self
2124 .local_matcher
2125 .as_ref()
2126 .is_none_or(|matcher| matcher.matches(log))
2127 }
2128
2129 pub fn route_key(&self, log: &Log) -> Option<RouteKey> {
2131 self.route_key.as_ref().and_then(|spec| spec.extract(log))
2132 }
2133}
2134
2135impl fmt::Debug for LogInterest {
2136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2137 f.debug_struct("LogInterest")
2138 .field("provider_filter", &self.provider_filter)
2139 .field(
2140 "local_matcher",
2141 &self.local_matcher.as_ref().map(|_| "<matcher>"),
2142 )
2143 .field("route_key", &self.route_key)
2144 .finish()
2145 }
2146}
2147
2148pub trait LogMatcher: Send + Sync {
2150 fn matches(&self, log: &Log) -> bool;
2152}
2153
2154#[derive(Clone)]
2156pub enum RouteKeySpec {
2157 EmitterAddress,
2159 Topic {
2161 index: usize,
2163 },
2164 DataSlice {
2166 offset: usize,
2168 len: usize,
2170 },
2171 Custom(Arc<dyn RouteKeyExtractor>),
2173}
2174
2175impl RouteKeySpec {
2176 pub fn extract(&self, log: &Log) -> Option<RouteKey> {
2178 match self {
2179 Self::EmitterAddress => Some(RouteKey::Address(log.address())),
2180 Self::Topic { index } => log.topics().get(*index).copied().map(RouteKey::Bytes32),
2181 Self::DataSlice { offset, len } => {
2182 let data = log.inner.data.data.as_ref();
2183 let end = offset.checked_add(*len)?;
2184 data.get(*offset..end)
2185 .map(|bytes| RouteKey::Bytes(bytes.to_vec()))
2186 }
2187 Self::Custom(extractor) => extractor.extract(log),
2188 }
2189 }
2190}
2191
2192impl fmt::Debug for RouteKeySpec {
2193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2194 match self {
2195 Self::EmitterAddress => f.write_str("EmitterAddress"),
2196 Self::Topic { index } => f.debug_struct("Topic").field("index", index).finish(),
2197 Self::DataSlice { offset, len } => f
2198 .debug_struct("DataSlice")
2199 .field("offset", offset)
2200 .field("len", len)
2201 .finish(),
2202 Self::Custom(_) => f.write_str("Custom(<extractor>)"),
2203 }
2204 }
2205}
2206
2207pub trait RouteKeyExtractor: Send + Sync {
2209 fn extract(&self, log: &Log) -> Option<RouteKey>;
2211}
2212
2213#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2215pub enum RouteKey {
2216 Address(Address),
2218 Bytes32(B256),
2220 Bytes(Vec<u8>),
2222}
2223
2224#[non_exhaustive]
2226#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2227pub enum LogRouteKey {
2228 Emitter(Address),
2230 Topic {
2232 index: usize,
2234 value: B256,
2236 },
2237 DataSlice {
2239 offset: usize,
2241 value: Vec<u8>,
2243 },
2244}
2245
2246#[derive(Clone, Debug, PartialEq, Eq)]
2248pub struct LogRouteIndex {
2249 keys: Vec<LogRouteKey>,
2250}
2251
2252impl LogRouteIndex {
2253 pub fn new(primary: LogRouteKey, additional: impl IntoIterator<Item = LogRouteKey>) -> Self {
2255 let mut keys = vec![primary];
2256 for key in additional {
2257 if !keys.contains(&key) {
2258 keys.push(key);
2259 }
2260 }
2261 Self { keys }
2262 }
2263
2264 pub fn single(key: LogRouteKey) -> Self {
2266 Self { keys: vec![key] }
2267 }
2268
2269 pub fn keys(&self) -> &[LogRouteKey] {
2271 &self.keys
2272 }
2273}
2274
2275#[derive(Clone, Debug, PartialEq, Eq)]
2277pub struct ReactiveLogRoute {
2278 pub handler_id: HandlerId,
2280 pub route_key: Option<RouteKey>,
2282}
2283
2284#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2286pub struct BlockInterest {
2287 pub mode: BlockInterestMode,
2289}
2290
2291impl Default for BlockInterest {
2292 fn default() -> Self {
2293 Self {
2294 mode: BlockInterestMode::Header,
2295 }
2296 }
2297}
2298
2299#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2301pub enum BlockInterestMode {
2302 Header,
2304 FullBlock,
2306}
2307
2308#[derive(Clone)]
2310pub struct PendingTxInterest<N: Network = Ethereum> {
2311 pub full_transactions: bool,
2313 pub from: AddressMatcher,
2315 pub to: AddressMatcher,
2317 pub selectors: SelectorMatcher,
2319 pub local_matcher: Option<Arc<dyn PendingTxMatcher<N>>>,
2321}
2322
2323impl<N: Network> Default for PendingTxInterest<N> {
2324 fn default() -> Self {
2325 Self {
2326 full_transactions: false,
2327 from: AddressMatcher::Any,
2328 to: AddressMatcher::Any,
2329 selectors: SelectorMatcher::Any,
2330 local_matcher: None,
2331 }
2332 }
2333}
2334
2335impl<N: Network> PendingTxInterest<N> {
2336 fn matches_hash_only(&self) -> bool {
2337 !self.full_transactions
2338 && self.from.is_any()
2339 && self.to.is_any()
2340 && self.selectors.is_any()
2341 && self.local_matcher.is_none()
2342 }
2343
2344 fn matches_tx(&self, tx: &N::TransactionResponse) -> bool {
2345 self.from.matches(tx.from())
2346 && self.to.matches_option(tx.to())
2347 && self.selectors.matches(tx.input())
2348 && self
2349 .local_matcher
2350 .as_ref()
2351 .is_none_or(|matcher| matcher.matches(tx))
2352 }
2353}
2354
2355impl<N: Network> fmt::Debug for PendingTxInterest<N> {
2356 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2357 f.debug_struct("PendingTxInterest")
2358 .field("full_transactions", &self.full_transactions)
2359 .field("from", &self.from)
2360 .field("to", &self.to)
2361 .field("selectors", &self.selectors)
2362 .field(
2363 "local_matcher",
2364 &self.local_matcher.as_ref().map(|_| "<matcher>"),
2365 )
2366 .finish()
2367 }
2368}
2369
2370#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2372pub enum AddressMatcher {
2373 Any,
2375 Exact(Address),
2377 AnyOf(Vec<Address>),
2379}
2380
2381impl AddressMatcher {
2382 pub fn is_any(&self) -> bool {
2384 matches!(self, Self::Any)
2385 }
2386
2387 pub fn matches(&self, address: Address) -> bool {
2389 match self {
2390 Self::Any => true,
2391 Self::Exact(expected) => *expected == address,
2392 Self::AnyOf(addresses) => addresses.contains(&address),
2393 }
2394 }
2395
2396 pub fn matches_option(&self, address: Option<Address>) -> bool {
2398 match (self, address) {
2399 (Self::Any, _) => true,
2400 (_, Some(address)) => self.matches(address),
2401 _ => false,
2402 }
2403 }
2404}
2405
2406#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2408pub enum SelectorMatcher {
2409 Any,
2411 AnyOf(Vec<[u8; 4]>),
2413}
2414
2415impl SelectorMatcher {
2416 pub fn is_any(&self) -> bool {
2418 matches!(self, Self::Any)
2419 }
2420
2421 pub fn matches(&self, input: &Bytes) -> bool {
2423 match self {
2424 Self::Any => true,
2425 Self::AnyOf(selectors) => input
2426 .get(..4)
2427 .and_then(|bytes| bytes.try_into().ok())
2428 .is_some_and(|selector| selectors.contains(&selector)),
2429 }
2430 }
2431}
2432
2433pub trait PendingTxMatcher<N: Network = Ethereum>: Send + Sync {
2435 fn matches(&self, tx: &N::TransactionResponse) -> bool;
2437}
2438
2439#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
2457#[non_exhaustive]
2458pub enum TrackingPolicy {
2459 Slots {
2464 slots: Vec<U256>,
2466 },
2467 WholeAccount,
2472 Scalars,
2477}
2478
2479#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2492pub enum RootGateCadence {
2493 EveryNBlocks(NonZeroU64),
2497 Disabled,
2499}
2500
2501impl RootGateCadence {
2502 pub fn every_n_blocks(n: u64) -> Self {
2504 Self::EveryNBlocks(NonZeroU64::new(n.max(1)).expect("clamped to at least 1"))
2505 }
2506}
2507
2508impl Default for RootGateCadence {
2509 fn default() -> Self {
2513 Self::every_n_blocks(16)
2514 }
2515}
2516
2517#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
2524struct TrackedRoot {
2525 last_root: B256,
2526 last_block: u64,
2527 balance: U256,
2528 nonce: u64,
2529 code_hash: B256,
2530}
2531
2532#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2534pub struct ResyncRequest {
2535 pub id: ResyncId,
2537 pub reason: ResyncReason,
2539 pub block: ResyncBlock,
2541 pub targets: Vec<ResyncTarget>,
2543 pub priority: ResyncPriority,
2545}
2546
2547#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2549pub struct ResyncId(String);
2550
2551impl ResyncId {
2552 pub fn new(id: impl Into<String>) -> Self {
2554 Self(id.into())
2555 }
2556}
2557
2558#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2560#[non_exhaustive]
2561pub enum ResyncReason {
2562 HandlerRequested,
2564 SkippedStateEffect,
2566 MissedBlockRange,
2573 RootMoved,
2583 Custom(String),
2585}
2586
2587#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2589pub enum ResyncBlock {
2590 Latest,
2592 Pending,
2594 Safe,
2596 Finalized,
2598 Number(u64),
2600 Hash {
2602 number: u64,
2604 hash: B256,
2606 require_canonical: bool,
2608 },
2609}
2610
2611#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2613pub enum ResyncTarget {
2614 StorageSlot {
2616 address: Address,
2618 slot: U256,
2620 },
2621 StorageSlots {
2623 address: Address,
2625 slots: Vec<U256>,
2627 },
2628 Account {
2630 address: Address,
2632 fields: AccountFieldMask,
2634 },
2635}
2636
2637#[derive(
2639 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize,
2640)]
2641pub struct AccountFieldMask {
2642 pub balance: bool,
2644 pub nonce: bool,
2646 pub code: bool,
2648}
2649
2650#[derive(
2652 Clone,
2653 Copy,
2654 Debug,
2655 Default,
2656 PartialEq,
2657 Eq,
2658 Hash,
2659 PartialOrd,
2660 Ord,
2661 serde::Serialize,
2662 serde::Deserialize,
2663)]
2664pub enum ResyncPriority {
2665 Low,
2667 #[default]
2669 Normal,
2670 High,
2672}
2673
2674#[derive(Clone, Debug, PartialEq, Eq)]
2676pub struct InvalidationRequest {
2677 pub scope: PurgeScope,
2679 pub address: Address,
2681 pub reason: InvalidationReason,
2683}
2684
2685#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2687pub enum InvalidationReason {
2688 HandlerRequested,
2690 Reorg,
2692 Custom(String),
2694}
2695
2696#[derive(Clone, Debug, PartialEq, Eq)]
2698pub struct SpeculativeRequest {
2699 pub id: SpeculativeId,
2701 pub input_ref: InputRef,
2703 pub labels: Vec<ReportTag>,
2705}
2706
2707#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2709pub struct SpeculativeId(String);
2710
2711impl SpeculativeId {
2712 pub fn new(id: impl Into<String>) -> Self {
2714 Self(id.into())
2715 }
2716}
2717
2718#[derive(Clone, Debug, PartialEq, Eq)]
2720pub struct ReactiveConfig {
2721 pub hook_backpressure: HookBackpressure,
2726 pub journal_depth: usize,
2743}
2744
2745impl Default for ReactiveConfig {
2746 fn default() -> Self {
2747 Self {
2748 hook_backpressure: HookBackpressure::Block,
2749 journal_depth: 64,
2750 }
2751 }
2752}
2753
2754#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2756pub enum HookBackpressure {
2757 Block,
2759 DropNewest,
2761 DropOldest,
2763 Error,
2765}
2766
2767#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2775#[non_exhaustive]
2776pub enum CacheHealth {
2777 #[default]
2779 Healthy,
2780 Degraded {
2784 since_block: u64,
2786 },
2787 Unhealthy {
2790 since_block: u64,
2792 },
2793}
2794
2795#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2802#[non_exhaustive]
2803pub struct CacheMetricsSnapshot {
2804 pub deep_reorgs: u64,
2807 pub reorgs_recovered: u64,
2809 pub resync_requests: u64,
2811 pub resync_failures: u64,
2813 pub missed_ranges: u64,
2815 pub coverage_gaps: u64,
2817 pub pending_contamination: u64,
2819 pub stale_verdicts: u64,
2821}
2822
2823#[derive(Debug, Default)]
2829struct CacheMetrics {
2830 deep_reorgs: AtomicU64,
2831 reorgs_recovered: AtomicU64,
2832 resync_requests: AtomicU64,
2833 resync_failures: AtomicU64,
2834 missed_ranges: AtomicU64,
2835 coverage_gaps: AtomicU64,
2836 pending_contamination: AtomicU64,
2837 stale_verdicts: AtomicU64,
2838}
2839
2840impl CacheMetrics {
2841 fn snapshot(&self) -> CacheMetricsSnapshot {
2842 CacheMetricsSnapshot {
2843 deep_reorgs: self.deep_reorgs.load(Ordering::Relaxed),
2844 reorgs_recovered: self.reorgs_recovered.load(Ordering::Relaxed),
2845 resync_requests: self.resync_requests.load(Ordering::Relaxed),
2846 resync_failures: self.resync_failures.load(Ordering::Relaxed),
2847 missed_ranges: self.missed_ranges.load(Ordering::Relaxed),
2848 coverage_gaps: self.coverage_gaps.load(Ordering::Relaxed),
2849 pending_contamination: self.pending_contamination.load(Ordering::Relaxed),
2850 stale_verdicts: self.stale_verdicts.load(Ordering::Relaxed),
2851 }
2852 }
2853
2854 fn restore(&self, snapshot: CacheMetricsSnapshot) {
2855 self.deep_reorgs
2856 .store(snapshot.deep_reorgs, Ordering::Relaxed);
2857 self.reorgs_recovered
2858 .store(snapshot.reorgs_recovered, Ordering::Relaxed);
2859 self.resync_requests
2860 .store(snapshot.resync_requests, Ordering::Relaxed);
2861 self.resync_failures
2862 .store(snapshot.resync_failures, Ordering::Relaxed);
2863 self.missed_ranges
2864 .store(snapshot.missed_ranges, Ordering::Relaxed);
2865 self.coverage_gaps
2866 .store(snapshot.coverage_gaps, Ordering::Relaxed);
2867 self.pending_contamination
2868 .store(snapshot.pending_contamination, Ordering::Relaxed);
2869 self.stale_verdicts
2870 .store(snapshot.stale_verdicts, Ordering::Relaxed);
2871 }
2872}
2873
2874#[derive(Clone, Debug)]
2876#[non_exhaustive]
2877pub enum ReactiveReport<N: Network = Ethereum> {
2878 Input(InputReport<N>),
2880 Decoded(DecodedReport<N>),
2882 Applied(AppliedReport<N>),
2884 Resynced(ResyncReport),
2886 BlockCommitted(BlockReport<N>),
2888 Reorg(ReorgReport<N>),
2890 ChainControl(ChainControlReport),
2892 MissedBlockRange(MissedRangeReport<N>),
2895 Health(HealthReport<N>),
2897 CoverageGap(CoverageGapReport<N>),
2900 Error(ReactiveErrorReport<N>),
2902}
2903
2904#[derive(Clone, Debug, PartialEq, Eq)]
2906pub struct ChainControlReport {
2907 pub control: ChainControl,
2909}
2910
2911#[derive(Clone, Debug)]
2913pub struct InputReport<N: Network = Ethereum> {
2914 pub input_ref: InputRef,
2916 pub context: ReactiveContext,
2918 pub provider: Option<ProviderRef>,
2920 pub _network: PhantomData<N>,
2922}
2923
2924#[derive(Clone, Debug)]
2926pub struct DecodedReport<N: Network = Ethereum> {
2927 pub input_ref: InputRef,
2929 pub handler_ids: Vec<HandlerId>,
2931 pub _network: PhantomData<N>,
2933}
2934
2935#[derive(Clone, Debug)]
2937pub struct AppliedReport<N: Network = Ethereum> {
2938 pub input_ref: InputRef,
2940 pub handler_id: HandlerId,
2942 pub quality: StateEffectQuality,
2944 pub tags: Vec<ReportTag>,
2946 pub diff: StateDiff,
2948 pub state_updates: Vec<StateUpdate>,
2950 pub invalidations: Vec<InvalidationRequest>,
2952 pub resyncs: Vec<ResyncRequest>,
2954 pub speculative: Vec<SpeculativeRequest>,
2956 pub hook_signals: Vec<HookSignal>,
2958 pub _network: PhantomData<N>,
2960}
2961
2962#[derive(Clone, Debug, Default, PartialEq, Eq)]
2966pub struct ResyncReport {
2967 pub requested: Vec<ResyncRequest>,
2969 pub state_updates: Vec<StateUpdate>,
2971 pub diff: StateDiff,
2973 pub failed: Vec<ResyncFailure>,
2975}
2976
2977#[derive(Clone, Debug, PartialEq, Eq)]
2979pub struct ResyncFailure {
2980 pub request_id: ResyncId,
2982 pub block: ResyncBlock,
2984 pub target: ResyncTarget,
2986 pub kind: ResyncFailureKind,
2988 pub message: String,
2990}
2991
2992#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2994#[non_exhaustive]
2995pub enum ResyncFailureKind {
2996 MissingStorageFetcher,
2998 StorageFetchFailed,
3000 StorageFetchOmitted,
3002 MissingAccountFetcher,
3004 AccountFetchFailed,
3006 AccountFetchOmitted,
3008}
3009
3010#[derive(Clone, Debug)]
3012pub struct BlockReport<N: Network = Ethereum> {
3013 pub block: Option<BlockRef>,
3015 pub inputs: Vec<InputRef>,
3017 pub _network: PhantomData<N>,
3019}
3020
3021#[derive(Clone, Debug)]
3037pub struct ReorgReport<N: Network = Ethereum> {
3038 pub dropped: Option<BlockRef>,
3040 pub dropped_blocks: Vec<BlockRef>,
3042 pub dropped_inputs: Vec<InputRef>,
3044 pub rollback_updates: Vec<StateUpdate>,
3046 pub rollback_diff: StateDiff,
3048 pub purge_updates: Vec<StateUpdate>,
3050 pub purge_diff: StateDiff,
3052 pub canceled_resyncs: Vec<ResyncRequest>,
3054 pub reason: ReorgReason,
3056 pub _network: PhantomData<N>,
3058}
3059
3060#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
3062pub enum ReorgReason {
3063 RemovedLog,
3065 ReorgedInput,
3067 ParentMismatch,
3069 Explicit,
3071}
3072
3073#[derive(Clone, Debug)]
3081pub struct MissedRangeReport<N: Network = Ethereum> {
3082 pub from: u64,
3084 pub to: u64,
3086 pub block: u64,
3088 pub _network: PhantomData<N>,
3090}
3091
3092#[derive(Clone, Debug)]
3095pub struct HealthReport<N: Network = Ethereum> {
3096 pub from: CacheHealth,
3098 pub to: CacheHealth,
3100 pub block: Option<u64>,
3102 pub _network: PhantomData<N>,
3104}
3105
3106#[derive(Clone, Debug)]
3119pub struct CoverageGapReport<N: Network = Ethereum> {
3120 pub address: Address,
3122 pub block: u64,
3124 pub _network: PhantomData<N>,
3126}
3127
3128#[derive(Clone, Debug)]
3131pub struct ReactiveErrorReport<N: Network = Ethereum> {
3132 pub input_ref: Option<InputRef>,
3134 pub message: String,
3136 pub _network: PhantomData<N>,
3138}
3139
3140#[derive(Clone, Debug)]
3143pub struct ReactiveBatchReport<N: Network = Ethereum> {
3144 pub applied: Vec<AppliedReport<N>>,
3146 pub resyncs: Vec<ResyncRequest>,
3148 pub speculative: Vec<SpeculativeRequest>,
3150 pub reports: Vec<Arc<ReactiveReport<N>>>,
3152}
3153
3154impl<N: Network> Default for ReactiveBatchReport<N> {
3155 fn default() -> Self {
3156 Self {
3157 applied: Vec::new(),
3158 resyncs: Vec::new(),
3159 speculative: Vec::new(),
3160 reports: Vec::new(),
3161 }
3162 }
3163}
3164
3165#[derive(Clone, Debug, PartialEq, Eq)]
3167pub struct HandlerError {
3168 message: String,
3169}
3170
3171impl HandlerError {
3172 pub fn new(message: impl Into<String>) -> Self {
3174 Self {
3175 message: message.into(),
3176 }
3177 }
3178}
3179
3180impl fmt::Display for HandlerError {
3181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3182 self.message.fmt(f)
3183 }
3184}
3185
3186impl std::error::Error for HandlerError {}
3187
3188impl From<String> for HandlerError {
3189 fn from(message: String) -> Self {
3190 Self::new(message)
3191 }
3192}
3193
3194impl From<&str> for HandlerError {
3195 fn from(message: &str) -> Self {
3196 Self::new(message)
3197 }
3198}
3199
3200#[derive(Debug, thiserror::Error)]
3202#[non_exhaustive]
3203pub enum ReactiveError {
3204 #[error("handler `{handler_id}` failed: {source}")]
3206 HandlerFailed {
3207 handler_id: HandlerId,
3209 source: HandlerError,
3211 },
3212 #[error(
3214 "conflicting effects for input {input_ref:?} on target {target:?}: `{first}` vs `{second}`"
3215 )]
3216 ConflictingEffects {
3217 input_ref: Box<InputRef>,
3219 target: Box<EffectTarget>,
3221 first: HandlerId,
3223 second: HandlerId,
3225 },
3226 #[error(
3228 "pending input {input_ref:?} emitted invalid canonical effect `{effect_kind}` from `{handler_id}`"
3229 )]
3230 InvalidPendingEffect {
3231 input_ref: Box<InputRef>,
3233 handler_id: HandlerId,
3235 effect_kind: &'static str,
3237 },
3238 #[error("invalid reactive input record: {message}")]
3241 InvalidInputRecord {
3242 message: String,
3244 },
3245 #[error("invalid chain control: {message}")]
3247 InvalidChainControl {
3248 message: String,
3250 },
3251 #[error(
3254 "owner catch-up block {number} {hash} is outside the retained canonical rollback journal"
3255 )]
3256 OwnerCatchupOutsideJournal {
3257 number: u64,
3259 hash: B256,
3261 },
3262 #[error(transparent)]
3264 Register(#[from] RegisterError),
3265}
3266
3267#[derive(Debug, thiserror::Error)]
3269#[non_exhaustive]
3270pub enum RegisterError {
3271 #[error("handler id `{0}` is already registered")]
3273 DuplicateHandler(HandlerId),
3274}
3275
3276#[derive(Debug, thiserror::Error)]
3279#[non_exhaustive]
3280pub enum ReactiveEngineRegisterError {
3281 #[error(transparent)]
3283 Register(#[from] RegisterError),
3284 #[error(transparent)]
3286 Subscriber(#[from] SubscriberError),
3287 #[error(
3290 "owner backfill {start_block}..={end_block:?} must target exactly one hash-certified block in the retained rollback journal"
3291 )]
3292 BackfillOutsideJournal {
3293 start_block: u64,
3295 end_block: Option<u64>,
3297 retained_anchor: Option<BlockRef>,
3299 },
3300}
3301
3302#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
3305#[non_exhaustive]
3306pub enum ReactiveBaselineError {
3307 #[error("cannot adopt a canonical baseline after reactive processing has started")]
3309 ActiveRuntime,
3310 #[error(
3313 "canonical baseline conflicts with existing block {existing_number} {existing_hash} (requested {requested_number} {requested_hash})"
3314 )]
3315 ConflictingBaseline {
3316 existing_number: u64,
3318 existing_hash: B256,
3320 requested_number: u64,
3322 requested_hash: B256,
3324 },
3325 #[error("baseline chain id {baseline_chain_id} does not match cache chain id {cache_chain_id}")]
3327 CacheChainMismatch {
3328 baseline_chain_id: u64,
3330 cache_chain_id: u64,
3332 },
3333 #[error("cache block selector is not canonically hash-pinned to baseline {number} {hash}")]
3335 CacheBlockMismatch {
3336 number: u64,
3338 hash: B256,
3340 },
3341}
3342
3343#[derive(Debug, thiserror::Error)]
3346#[non_exhaustive]
3347pub enum ReactiveEngineError {
3348 #[error(transparent)]
3350 Subscriber(#[from] SubscriberError),
3351 #[error(transparent)]
3353 Runtime(ReactiveError),
3354 #[error(transparent)]
3356 Baseline(#[from] ReactiveBaselineError),
3357 #[error("runtime ingestion succeeded but subscriber acknowledgement failed: {0}")]
3360 Acknowledgement(#[source] SubscriberError),
3361 #[error("runtime ingestion succeeded but durable checkpoint commit failed: {0}")]
3365 Checkpoint(#[source] DurableCheckpointError),
3366 #[error("cannot durably checkpoint reactive state before observing a canonical block")]
3368 MissingCheckpointBlock,
3369 #[error("pre-confirmed Flashblock batches cannot be durably checkpointed")]
3372 PreconfirmationNotCheckpointable,
3373 #[error("failed to encode durable reactive runtime state: {0}")]
3375 RuntimeCheckpoint(String),
3376 #[error("cannot use ordinary ingestion while a durable checkpoint commit is pending")]
3379 PendingCheckpointCommit,
3380 #[error("cannot use checkpointed ingestion while an ordinary acknowledgement is pending")]
3383 PendingAcknowledgementCommit,
3384 #[error(
3388 "raw engine ingestion cannot consume delivery tokens or subscriber checkpoints; use a combined next_ingest helper"
3389 )]
3390 UncommittedDeliveryMetadata,
3391 #[error(
3393 "subscriber chain id {subscriber_chain_id} does not match cache chain id {cache_chain_id}"
3394 )]
3395 SubscriberChainMismatch {
3396 subscriber_chain_id: u64,
3398 cache_chain_id: u64,
3400 },
3401 #[error("subscriber does not advertise durable replay support")]
3403 SubscriberNotDurable,
3404 #[error(
3407 "committed delivery token has no delivery witness; replay cannot be acknowledged safely"
3408 )]
3409 MissingReplayWitness,
3410 #[error("replayed delivery token does not match its committed delivery witness")]
3413 ReplayDeliveryMismatch,
3414 #[error("failed to encode durable delivery witness: {0}")]
3416 DeliveryWitness(String),
3417 #[error(
3420 "tokened block-header, full-block, or hydrated-transaction delivery requires an exact payload commitment"
3421 )]
3422 MissingPayloadCommitment,
3423 #[error(
3426 "cache changed while durable checkpoint commit was pending (staged generation {staged_generation}, current generation {current_generation})"
3427 )]
3428 PendingCheckpointCacheChanged {
3429 staged_generation: u64,
3431 current_generation: u64,
3433 },
3434 #[error(
3437 "reorg after block {common_ancestor} exceeds the retained rollback journal (oldest retained block {oldest_journaled:?}, configured depth {journal_depth})"
3438 )]
3439 CheckpointReorgOutsideJournal {
3440 common_ancestor: u64,
3442 oldest_journaled: Option<u64>,
3444 journal_depth: usize,
3446 },
3447 #[error(
3450 "owner catch-up block {number} {hash} is outside the retained canonical rollback journal"
3451 )]
3452 OwnerCatchupOutsideJournal {
3453 number: u64,
3455 hash: B256,
3457 },
3458}
3459
3460impl From<ReactiveError> for ReactiveEngineError {
3461 fn from(error: ReactiveError) -> Self {
3462 match error {
3463 ReactiveError::OwnerCatchupOutsideJournal { number, hash } => {
3464 Self::OwnerCatchupOutsideJournal { number, hash }
3465 }
3466 error => Self::Runtime(error),
3467 }
3468 }
3469}
3470
3471#[derive(Debug, thiserror::Error)]
3473#[non_exhaustive]
3474pub enum ReactiveCheckpointRestoreError {
3475 #[error("cannot restore a durable checkpoint into a runtime with canonical journal state")]
3477 ActiveRuntime,
3478 #[error("invalid durable reactive runtime state: {0}")]
3480 InvalidRuntimeCheckpoint(String),
3481 #[error(transparent)]
3483 Checkpoint(#[from] DurableCheckpointError),
3484 #[error("subscriber rejected durable resume position: {0}")]
3486 Subscriber(#[source] SubscriberError),
3487 #[error(
3489 "subscriber chain id {subscriber_chain_id} does not match checkpoint chain id {checkpoint_chain_id}"
3490 )]
3491 SubscriberChainMismatch {
3492 subscriber_chain_id: u64,
3494 checkpoint_chain_id: u64,
3496 },
3497 #[error("subscriber does not advertise durable replay support")]
3499 SubscriberNotDurable,
3500}
3501
3502#[derive(Clone, Debug)]
3504#[non_exhaustive]
3505pub enum CheckpointedIngest<N: Network = Ethereum> {
3506 Applied(ReactiveBatchReport<N>),
3508 ReplayAcknowledged,
3511}
3512
3513#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3515pub enum EffectTarget {
3516 StorageSlot {
3518 address: Address,
3520 slot: U256,
3522 },
3523 AccountBalance {
3525 address: Address,
3527 },
3528 AccountNonce {
3530 address: Address,
3532 },
3533 AccountCode {
3535 address: Address,
3537 },
3538 MaskedStorageSlot {
3540 address: Address,
3542 slot: U256,
3544 mask: U256,
3546 },
3547}
3548
3549#[derive(Clone, Debug, PartialEq, Eq)]
3550enum AbsoluteValue {
3551 U256(U256),
3552 U64(u64),
3553 Bytes(Bytes),
3554}
3555
3556pub struct ReactiveRuntime<N: Network = Ethereum> {
3558 registry: ReactiveRegistry<N>,
3559 hooks: Vec<Arc<dyn ReactiveHook<N>>>,
3560 config: ReactiveConfig,
3561 journal: VecDeque<BlockJournal<N>>,
3562 coverage_head: Option<BlockRef>,
3563 pending_resyncs: Vec<ResyncRequest>,
3564 health: CacheHealth,
3565 safe_head: Option<BlockRef>,
3566 finalized_head: Option<BlockRef>,
3567 metrics: CacheMetrics,
3568 freshness: Option<FreshnessRegistry>,
3577 tracking: HashMap<Address, TrackingPolicy>,
3581 tracked_roots: HashMap<Address, TrackedRoot>,
3584 root_gate_cadence: RootGateCadence,
3586 last_gate_block: Option<u64>,
3590 touched_since_gate: HashSet<Address>,
3596 preconfirmed_branch: Option<PreconfirmedBranch>,
3599}
3600
3601#[derive(Clone)]
3602struct PreconfirmedBranch {
3603 flashblock: FlashblockRef,
3604 canonical_cache: EvmCacheStateSnapshot,
3605}
3606
3607#[derive(Clone, Debug)]
3608struct BlockJournal<N: Network = Ethereum> {
3609 block: BlockRef,
3610 inputs: Vec<InputRef>,
3611 applied: Vec<AppliedReport<N>>,
3612 handler_ids: Vec<HandlerId>,
3613 resynced: Vec<ResyncReport>,
3614 rollback_diffs: Vec<StateDiff>,
3615}
3616
3617const DURABLE_RUNTIME_CHECKPOINT_VERSION: u32 = 3;
3618
3619#[derive(serde::Serialize, serde::Deserialize)]
3620struct DurableRuntimeCheckpoint {
3621 version: u32,
3622 safe_head: Option<BlockRef>,
3623 finalized_head: Option<BlockRef>,
3624 health: CacheHealth,
3625 pending_resyncs: Vec<ResyncRequest>,
3626 coverage_head: Option<BlockRef>,
3627 journal: Vec<DurableBlockJournal>,
3628 freshness: Option<FreshnessRegistry>,
3629 tracking: HashMap<Address, TrackingPolicy>,
3630 tracked_roots: HashMap<Address, TrackedRoot>,
3631 root_gate_cadence: RootGateCadence,
3632 last_gate_block: Option<u64>,
3633 touched_since_gate: HashSet<Address>,
3634 metrics: CacheMetricsSnapshot,
3635}
3636
3637#[derive(serde::Serialize, serde::Deserialize)]
3638struct DurableBlockJournal {
3639 block: BlockRef,
3640 handler_ids: Vec<HandlerId>,
3641 rollback_diffs: Vec<StateDiff>,
3642}
3643
3644struct DurableRuntimeRestorePlan {
3645 checkpoint: Option<DurableRuntimeCheckpoint>,
3646 fallback_history: Vec<BlockRef>,
3647}
3648
3649impl DurableRuntimeRestorePlan {
3650 fn canonical_history(&self) -> Vec<BlockRef> {
3651 self.checkpoint.as_ref().map_or_else(
3652 || self.fallback_history.clone(),
3653 |checkpoint| checkpoint.journal.iter().map(|entry| entry.block).collect(),
3654 )
3655 }
3656}
3657
3658#[derive(Clone)]
3659struct ReactiveRuntimeState<N: Network> {
3660 journal: VecDeque<BlockJournal<N>>,
3661 coverage_head: Option<BlockRef>,
3662 pending_resyncs: Vec<ResyncRequest>,
3663 health: CacheHealth,
3664 safe_head: Option<BlockRef>,
3665 finalized_head: Option<BlockRef>,
3666 freshness: Option<FreshnessRegistry>,
3667 tracking: HashMap<Address, TrackingPolicy>,
3668 tracked_roots: HashMap<Address, TrackedRoot>,
3669 root_gate_cadence: RootGateCadence,
3670 last_gate_block: Option<u64>,
3671 touched_since_gate: HashSet<Address>,
3672 metrics: CacheMetricsSnapshot,
3673}
3674
3675#[derive(Clone)]
3676struct ChainControlState {
3677 journal_invalidated_from: Option<u64>,
3678 resolved_canonical_blocks: HashMap<(u64, B256), BlockRef>,
3679}
3680
3681#[derive(Default)]
3690struct BatchDroppedCanonical {
3691 identities: HashSet<(u64, B256)>,
3692 implicit_spans: Vec<(u64, u64)>,
3693}
3694
3695impl BatchDroppedCanonical {
3696 fn covers_implicit_number(&self, number: u64) -> bool {
3697 self.implicit_spans
3698 .iter()
3699 .any(|(from, through)| number >= *from && number <= *through)
3700 }
3701
3702 fn contains(&self, block: &BlockRef) -> bool {
3703 self.identities.contains(&(block.number, block.hash))
3704 }
3705
3706 fn record_identity(&mut self, block: &BlockRef) {
3707 self.identities.insert((block.number, block.hash));
3708 }
3709
3710 fn record_explicit(&mut self, _common_ancestor: &BlockRef, old_tip: &BlockRef) {
3711 self.identities.insert((old_tip.number, old_tip.hash));
3712 }
3713
3714 fn record_drained(&mut self, blocks: &[BlockRef]) {
3715 let Some(from) = blocks.iter().map(|block| block.number).min() else {
3716 return;
3717 };
3718 let through = blocks
3719 .iter()
3720 .map(|block| block.number)
3721 .max()
3722 .expect("a non-empty drained set has a maximum");
3723 self.implicit_spans.push((from, through));
3724 self.identities
3725 .extend(blocks.iter().map(|block| (block.number, block.hash)));
3726 }
3727}
3728
3729pub struct ReactiveRegistry<N: Network = Ethereum> {
3737 handlers: BTreeMap<u128, RegisteredHandler<N>>,
3738 handler_positions: HashMap<HandlerId, u128>,
3739 next_handler_position: u128,
3740 indexed_log_handlers: HashMap<LogRouteKey, BTreeSet<u128>>,
3741 fallback_log_handlers: BTreeSet<u128>,
3742 data_slice_shapes: HashMap<(usize, usize), usize>,
3743}
3744
3745struct RegisteredHandler<N: Network = Ethereum> {
3746 id: HandlerId,
3747 handler: Arc<dyn ReactiveHandler<N>>,
3748 interests: Vec<ReactiveInterest<N>>,
3749 has_log_interests: bool,
3750 log_route_index: Option<LogRouteIndex>,
3751}
3752
3753impl<N: Network> Default for ReactiveRegistry<N> {
3754 fn default() -> Self {
3755 Self::new()
3756 }
3757}
3758
3759impl<N: Network> ReactiveRegistry<N> {
3760 pub fn new() -> Self {
3762 Self {
3763 handlers: BTreeMap::new(),
3764 handler_positions: HashMap::new(),
3765 next_handler_position: 0,
3766 indexed_log_handlers: HashMap::new(),
3767 fallback_log_handlers: BTreeSet::new(),
3768 data_slice_shapes: HashMap::new(),
3769 }
3770 }
3771
3772 pub fn register_handler(
3782 &mut self,
3783 handler: Arc<dyn ReactiveHandler<N>>,
3784 ) -> Result<(), RegisterError> {
3785 let id = handler.id();
3786 if self.handler_positions.contains_key(&id) {
3787 return Err(RegisterError::DuplicateHandler(id));
3788 }
3789 let interests = handler.interests();
3790 self.insert_handler_prepared(id, handler, interests);
3791 Ok(())
3792 }
3793
3794 fn insert_handler_prepared(
3795 &mut self,
3796 id: HandlerId,
3797 handler: Arc<dyn ReactiveHandler<N>>,
3798 interests: Vec<ReactiveInterest<N>>,
3799 ) {
3800 debug_assert!(!self.handler_positions.contains_key(&id));
3801 let has_log_interests = interests
3802 .iter()
3803 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)));
3804 let log_route_index = handler.log_route_index();
3805 if self.next_handler_position == u128::MAX {
3806 self.compact_handler_positions();
3807 }
3808 let position = self.next_handler_position;
3809 self.next_handler_position += 1;
3810 self.handler_positions.insert(id.clone(), position);
3811 if let Some(index) = &log_route_index {
3812 for key in index.keys() {
3813 if let LogRouteKey::DataSlice { offset, value } = key {
3814 *self
3815 .data_slice_shapes
3816 .entry((*offset, value.len()))
3817 .or_default() += 1;
3818 }
3819 self.indexed_log_handlers
3820 .entry(key.clone())
3821 .or_default()
3822 .insert(position);
3823 }
3824 } else if has_log_interests {
3825 self.fallback_log_handlers.insert(position);
3826 }
3827 self.handlers.insert(
3828 position,
3829 RegisteredHandler {
3830 id,
3831 handler,
3832 interests,
3833 has_log_interests,
3834 log_route_index,
3835 },
3836 );
3837 }
3838
3839 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
3845 let position = self.handler_positions.remove(id)?;
3846 let registered = self.handlers.remove(&position)?;
3847 if let Some(index) = ®istered.log_route_index {
3848 for key in index.keys() {
3849 let remove_bucket = self
3850 .indexed_log_handlers
3851 .get_mut(key)
3852 .is_some_and(|owners| {
3853 owners.remove(&position);
3854 owners.is_empty()
3855 });
3856 if remove_bucket {
3857 self.indexed_log_handlers.remove(key);
3858 }
3859 if let LogRouteKey::DataSlice { offset, value } = key {
3860 let shape = (*offset, value.len());
3861 let remove_shape =
3862 self.data_slice_shapes.get_mut(&shape).is_some_and(|count| {
3863 *count -= 1;
3864 *count == 0
3865 });
3866 if remove_shape {
3867 self.data_slice_shapes.remove(&shape);
3868 }
3869 }
3870 }
3871 } else {
3872 self.fallback_log_handlers.remove(&position);
3873 }
3874 Some(registered.handler)
3875 }
3876
3877 pub fn contains_handler(&self, id: &HandlerId) -> bool {
3879 self.handler_positions.contains_key(id)
3880 }
3881
3882 pub fn handler_ids(&self) -> Vec<HandlerId> {
3884 self.handlers
3885 .values()
3886 .map(|handler| handler.id.clone())
3887 .collect()
3888 }
3889
3890 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
3892 self.handler_positions
3893 .get(id)
3894 .and_then(|position| self.handlers.get(position))
3895 .map(|registered| registered.interests.as_slice())
3896 }
3897
3898 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
3900 self.handlers
3901 .values()
3902 .flat_map(|handler| handler.interests.clone())
3903 .collect()
3904 }
3905
3906 pub fn log_subscription_filters(&self) -> Vec<Filter> {
3913 let mut filters = Vec::new();
3914 for interest in self.log_interests() {
3915 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
3916 }
3917 filters
3918 }
3919
3920 pub fn route_log(&self, log: &Log) -> Vec<ReactiveLogRoute> {
3926 self.log_handler_candidates(log)
3927 .into_iter()
3928 .filter_map(|handler| handler.route_log(log))
3929 .collect()
3930 }
3931
3932 fn log_handler_candidates(&self, log: &Log) -> Vec<&RegisteredHandler<N>> {
3933 let mut indexed_positions = Vec::new();
3934 if let Some(indexed) = self
3935 .indexed_log_handlers
3936 .get(&LogRouteKey::Emitter(log.address()))
3937 {
3938 indexed_positions.extend(indexed.iter().copied());
3939 }
3940 for (index, value) in log.topics().iter().copied().enumerate() {
3941 if let Some(indexed) = self
3942 .indexed_log_handlers
3943 .get(&LogRouteKey::Topic { index, value })
3944 {
3945 indexed_positions.extend(indexed.iter().copied());
3946 }
3947 }
3948 let data = log.inner.data.data.as_ref();
3949 for &(offset, len) in self.data_slice_shapes.keys() {
3950 let Some(end) = offset.checked_add(len) else {
3951 continue;
3952 };
3953 let Some(value) = data.get(offset..end) else {
3954 continue;
3955 };
3956 if let Some(indexed) = self.indexed_log_handlers.get(&LogRouteKey::DataSlice {
3957 offset,
3958 value: value.to_vec(),
3959 }) {
3960 indexed_positions.extend(indexed.iter().copied());
3961 }
3962 }
3963 if indexed_positions.is_empty() {
3964 if self.fallback_log_handlers.is_empty() {
3965 return Vec::new();
3966 }
3967 if !self.indexed_log_handlers.is_empty() {
3968 return self
3969 .fallback_log_handlers
3970 .iter()
3971 .filter_map(|position| self.handlers.get(position))
3972 .collect();
3973 }
3974 return self
3975 .handlers
3976 .values()
3977 .filter(|handler| handler.has_log_interests && handler.log_route_index.is_none())
3978 .collect();
3979 }
3980
3981 indexed_positions.extend(self.fallback_log_handlers.iter().copied());
3982 indexed_positions.sort_unstable();
3983 indexed_positions.dedup();
3984 indexed_positions
3985 .into_iter()
3986 .filter_map(|position| self.handlers.get(&position))
3987 .collect()
3988 }
3989
3990 fn handlers(&self) -> impl Iterator<Item = &RegisteredHandler<N>> {
3991 self.handlers.values()
3992 }
3993
3994 fn log_interests(&self) -> impl Iterator<Item = &LogInterest> {
3995 self.handlers.values().flat_map(|handler| {
3996 handler
3997 .interests
3998 .iter()
3999 .filter_map(|interest| match interest {
4000 ReactiveInterest::Logs(interest) => Some(interest),
4001 ReactiveInterest::Blocks(_) | ReactiveInterest::PendingTransactions(_) => None,
4002 })
4003 })
4004 }
4005
4006 fn compact_handler_positions(&mut self) {
4007 let handlers = std::mem::take(&mut self.handlers);
4008 self.handler_positions.clear();
4009 self.indexed_log_handlers.clear();
4010 self.fallback_log_handlers.clear();
4011 self.data_slice_shapes.clear();
4012
4013 for (position, (_, handler)) in handlers.into_iter().enumerate() {
4014 let position = position as u128;
4015 self.handler_positions.insert(handler.id.clone(), position);
4016 if let Some(index) = &handler.log_route_index {
4017 for key in index.keys() {
4018 if let LogRouteKey::DataSlice { offset, value } = key {
4019 *self
4020 .data_slice_shapes
4021 .entry((*offset, value.len()))
4022 .or_default() += 1;
4023 }
4024 self.indexed_log_handlers
4025 .entry(key.clone())
4026 .or_default()
4027 .insert(position);
4028 }
4029 } else if handler.has_log_interests {
4030 self.fallback_log_handlers.insert(position);
4031 }
4032 self.handlers.insert(position, handler);
4033 }
4034 self.next_handler_position = self.handlers.len() as u128;
4035 }
4036}
4037
4038impl<N: Network> ReactiveRuntime<N> {
4039 pub fn new(config: ReactiveConfig) -> Self {
4041 Self {
4042 registry: ReactiveRegistry::new(),
4043 hooks: Vec::new(),
4044 config,
4045 journal: VecDeque::new(),
4046 coverage_head: None,
4047 pending_resyncs: Vec::new(),
4048 health: CacheHealth::Healthy,
4049 safe_head: None,
4050 finalized_head: None,
4051 metrics: CacheMetrics::default(),
4052 freshness: None,
4053 tracking: HashMap::new(),
4054 tracked_roots: HashMap::new(),
4055 root_gate_cadence: RootGateCadence::default(),
4056 last_gate_block: None,
4057 touched_since_gate: HashSet::new(),
4058 preconfirmed_branch: None,
4059 }
4060 }
4061
4062 fn checkpoint_state(&self) -> ReactiveRuntimeState<N> {
4063 ReactiveRuntimeState {
4064 journal: self.journal.clone(),
4065 coverage_head: self.coverage_head,
4066 pending_resyncs: self.pending_resyncs.clone(),
4067 health: self.health,
4068 safe_head: self.safe_head,
4069 finalized_head: self.finalized_head,
4070 freshness: self.freshness.clone(),
4071 tracking: self.tracking.clone(),
4072 tracked_roots: self.tracked_roots.clone(),
4073 root_gate_cadence: self.root_gate_cadence,
4074 last_gate_block: self.last_gate_block,
4075 touched_since_gate: self.touched_since_gate.clone(),
4076 metrics: self.metrics.snapshot(),
4077 }
4078 }
4079
4080 fn is_pristine_for_checkpoint_restore(&self) -> bool {
4081 self.preconfirmed_branch.is_none()
4082 && self.journal.is_empty()
4083 && self.coverage_head.is_none()
4084 && self.pending_resyncs.is_empty()
4085 && self.health == CacheHealth::Healthy
4086 && self.safe_head.is_none()
4087 && self.finalized_head.is_none()
4088 && self.tracked_roots.is_empty()
4089 && self.last_gate_block.is_none()
4090 && self.touched_since_gate.is_empty()
4091 && self.metrics.snapshot() == CacheMetricsSnapshot::default()
4092 }
4093
4094 fn adopted_baseline_only(&self) -> Option<BlockRef> {
4095 let baseline = self.coverage_head?;
4096 let journal_is_baseline_only = if self.config.journal_depth == 0 {
4097 self.journal.is_empty()
4098 } else {
4099 self.journal.len() == 1
4100 && self.journal.front().is_some_and(|entry| {
4101 entry.block == baseline
4102 && entry.inputs.is_empty()
4103 && entry.applied.is_empty()
4104 && entry.handler_ids.is_empty()
4105 && entry.resynced.is_empty()
4106 && entry.rollback_diffs.is_empty()
4107 })
4108 };
4109 (self.preconfirmed_branch.is_none()
4110 && journal_is_baseline_only
4111 && self.pending_resyncs.is_empty()
4112 && self.health == CacheHealth::Healthy
4113 && self.safe_head.is_none()
4114 && self.finalized_head.is_none()
4115 && self.tracked_roots.is_empty()
4116 && self.last_gate_block.is_none()
4117 && self.touched_since_gate.is_empty()
4118 && self.metrics.snapshot() == CacheMetricsSnapshot::default())
4119 .then_some(baseline)
4120 }
4121
4122 fn restore_state(&mut self, state: ReactiveRuntimeState<N>) {
4123 self.journal = state.journal;
4124 self.coverage_head = state.coverage_head;
4125 self.pending_resyncs = state.pending_resyncs;
4126 self.health = state.health;
4127 self.safe_head = state.safe_head;
4128 self.finalized_head = state.finalized_head;
4129 self.freshness = state.freshness;
4130 self.tracking = state.tracking;
4131 self.tracked_roots = state.tracked_roots;
4132 self.root_gate_cadence = state.root_gate_cadence;
4133 self.last_gate_block = state.last_gate_block;
4134 self.touched_since_gate = state.touched_since_gate;
4135 self.metrics.restore(state.metrics);
4136 }
4137
4138 fn restore_transaction_state(&mut self, state: ReactiveRuntimeState<N>) {
4139 let metrics = self.metrics.snapshot();
4144 self.restore_state(state);
4145 self.metrics.restore(metrics);
4146 }
4147
4148 fn durable_checkpoint_bytes(&self) -> Result<Vec<u8>, ReactiveEngineError> {
4149 let checkpoint = DurableRuntimeCheckpoint {
4150 version: DURABLE_RUNTIME_CHECKPOINT_VERSION,
4151 safe_head: self.safe_head,
4152 finalized_head: self.finalized_head,
4153 health: self.health,
4154 pending_resyncs: self.pending_resyncs.clone(),
4155 coverage_head: self.coverage_head,
4156 journal: self
4157 .journal
4158 .iter()
4159 .map(|entry| DurableBlockJournal {
4160 block: entry.block,
4161 handler_ids: entry.handler_ids.clone(),
4162 rollback_diffs: entry.rollback_diffs.clone(),
4163 })
4164 .collect(),
4165 freshness: self.freshness.clone(),
4166 tracking: self.tracking.clone(),
4167 tracked_roots: self.tracked_roots.clone(),
4168 root_gate_cadence: self.root_gate_cadence,
4169 last_gate_block: self.last_gate_block,
4170 touched_since_gate: self.touched_since_gate.clone(),
4171 metrics: self.metrics.snapshot(),
4172 };
4173 bincode::serialize(&checkpoint)
4174 .map_err(|error| ReactiveEngineError::RuntimeCheckpoint(error.to_string()))
4175 }
4176
4177 fn plan_durable_checkpoint_restore(
4178 &self,
4179 bytes: &[u8],
4180 expected_coverage: &BlockRef,
4181 ) -> Result<DurableRuntimeRestorePlan, ReactiveCheckpointRestoreError> {
4182 let mut cursor = std::io::Cursor::new(bytes);
4183 let mut checkpoint: DurableRuntimeCheckpoint = bincode::DefaultOptions::new()
4184 .with_fixint_encoding()
4185 .with_limit(bytes.len() as u64)
4186 .deserialize_from(&mut cursor)
4187 .map_err(|error| {
4188 ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(error.to_string())
4189 })?;
4190 if cursor.position() != bytes.len() as u64 {
4191 return Err(ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(
4192 "runtime checkpoint has trailing bytes".to_owned(),
4193 ));
4194 }
4195 if checkpoint.version != DURABLE_RUNTIME_CHECKPOINT_VERSION {
4196 return Err(ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(
4197 format!(
4198 "unsupported runtime checkpoint version {}",
4199 checkpoint.version
4200 ),
4201 ));
4202 }
4203 self.validate_durable_runtime_checkpoint(&checkpoint, expected_coverage)?;
4204
4205 let retained = self.config.journal_depth.min(checkpoint.journal.len());
4206 let discard = checkpoint.journal.len() - retained;
4207 checkpoint.journal.drain(..discard);
4208 Ok(DurableRuntimeRestorePlan {
4209 checkpoint: Some(checkpoint),
4210 fallback_history: Vec::new(),
4211 })
4212 }
4213
4214 fn apply_durable_checkpoint_restore(&mut self, plan: DurableRuntimeRestorePlan) {
4215 let Some(checkpoint) = plan.checkpoint else {
4216 self.journal = plan
4217 .fallback_history
4218 .into_iter()
4219 .map(|block| BlockJournal {
4220 block,
4221 inputs: Vec::new(),
4222 applied: Vec::new(),
4223 handler_ids: Vec::new(),
4224 resynced: Vec::new(),
4225 rollback_diffs: Vec::new(),
4226 })
4227 .collect();
4228 return;
4229 };
4230 self.safe_head = checkpoint.safe_head;
4231 self.finalized_head = checkpoint.finalized_head;
4232 self.health = checkpoint.health;
4233 self.pending_resyncs = checkpoint.pending_resyncs;
4234 self.coverage_head = checkpoint.coverage_head;
4235 self.journal = checkpoint
4236 .journal
4237 .into_iter()
4238 .map(|entry| BlockJournal {
4239 block: entry.block,
4240 inputs: Vec::new(),
4241 applied: Vec::new(),
4242 handler_ids: entry.handler_ids,
4243 resynced: Vec::new(),
4244 rollback_diffs: entry.rollback_diffs,
4245 })
4246 .collect();
4247 self.freshness = checkpoint.freshness;
4248 self.tracking = checkpoint.tracking;
4249 self.tracked_roots = checkpoint.tracked_roots;
4250 self.root_gate_cadence = checkpoint.root_gate_cadence;
4251 self.last_gate_block = checkpoint.last_gate_block;
4252 self.touched_since_gate = checkpoint.touched_since_gate;
4253 self.metrics.restore(checkpoint.metrics);
4254 }
4255
4256 fn validate_durable_runtime_checkpoint(
4257 &self,
4258 checkpoint: &DurableRuntimeCheckpoint,
4259 expected_coverage: &BlockRef,
4260 ) -> Result<(), ReactiveCheckpointRestoreError> {
4261 let invalid =
4262 |message: String| ReactiveCheckpointRestoreError::InvalidRuntimeCheckpoint(message);
4263 let Some(coverage) = checkpoint.coverage_head.as_ref() else {
4264 return Err(invalid(
4265 "runtime checkpoint is missing its canonical coverage head".into(),
4266 ));
4267 };
4268 if !optional_block_refs_are_compatible(Some(coverage), Some(expected_coverage)) {
4269 return Err(invalid(format!(
4270 "runtime coverage {}:{:?} conflicts with checkpoint metadata {}:{:?}",
4271 coverage.number, coverage.hash, expected_coverage.number, expected_coverage.hash
4272 )));
4273 }
4274 for (label, head) in [
4275 ("safe", checkpoint.safe_head.as_ref()),
4276 ("finalized", checkpoint.finalized_head.as_ref()),
4277 ] {
4278 let Some(head) = head else { continue };
4279 if head.number > coverage.number
4280 || (head.number == coverage.number && head.hash != coverage.hash)
4281 {
4282 return Err(invalid(format!(
4283 "{label} head {}:{:?} lies beyond or conflicts with canonical coverage {}:{:?}",
4284 head.number, head.hash, coverage.number, coverage.hash
4285 )));
4286 }
4287 if head.number.checked_add(1) == Some(coverage.number)
4288 && coverage
4289 .parent_hash
4290 .is_some_and(|parent| parent != head.hash)
4291 {
4292 return Err(invalid(format!(
4293 "canonical coverage does not descend from adjacent {label} head"
4294 )));
4295 }
4296 }
4297 if let (Some(finalized), Some(safe)) = (
4298 checkpoint.finalized_head.as_ref(),
4299 checkpoint.safe_head.as_ref(),
4300 ) {
4301 if finalized.number > safe.number
4302 || (finalized.number == safe.number && finalized.hash != safe.hash)
4303 {
4304 return Err(invalid(
4305 "finalized head is above or conflicts with the safe head".into(),
4306 ));
4307 }
4308 if finalized.number.checked_add(1) == Some(safe.number)
4309 && safe.parent_hash != Some(finalized.hash)
4310 {
4311 return Err(invalid(
4312 "adjacent safe head does not descend from finalized head".into(),
4313 ));
4314 }
4315 }
4316
4317 let mut previous: Option<&DurableBlockJournal> = None;
4318 for entry in &checkpoint.journal {
4319 if entry.block.number > coverage.number
4320 || (entry.block.number == coverage.number && entry.block.hash != coverage.hash)
4321 {
4322 return Err(invalid(format!(
4323 "journal block {}:{:?} lies beyond or conflicts with canonical coverage",
4324 entry.block.number, entry.block.hash
4325 )));
4326 }
4327 if let Some(previous) = previous {
4328 if entry.block.number <= previous.block.number {
4329 return Err(invalid(
4330 "runtime journal block numbers are not strictly increasing".into(),
4331 ));
4332 }
4333 if previous.block.number.checked_add(1) == Some(entry.block.number)
4334 && entry.block.parent_hash.is_some()
4335 && entry.block.parent_hash != Some(previous.block.hash)
4336 {
4337 return Err(invalid(
4338 "adjacent runtime journal blocks are not parent-linked".into(),
4339 ));
4340 }
4341 }
4342 for (label, head) in [
4343 ("safe", checkpoint.safe_head.as_ref()),
4344 ("finalized", checkpoint.finalized_head.as_ref()),
4345 ] {
4346 if let Some(head) = head
4347 && head.number == entry.block.number
4348 && !optional_block_refs_are_compatible(Some(head), Some(&entry.block))
4349 {
4350 return Err(invalid(format!(
4351 "{label} head conflicts with the retained journal at block {}",
4352 head.number
4353 )));
4354 }
4355 }
4356 let mut handler_ids = HashSet::new();
4357 if entry
4358 .handler_ids
4359 .iter()
4360 .any(|handler_id| !handler_ids.insert(handler_id))
4361 {
4362 return Err(invalid(
4363 "runtime journal contains duplicate handler generation ids".into(),
4364 ));
4365 }
4366 previous = Some(entry);
4367 }
4368 if let Some(tail) = checkpoint.journal.last()
4369 && tail.block.number == coverage.number
4370 && !optional_block_refs_are_compatible(Some(&tail.block), Some(coverage))
4371 {
4372 return Err(invalid(format!(
4373 "runtime journal tail conflicts with canonical coverage at block {}",
4374 coverage.number
4375 )));
4376 }
4377 if let Some(tail) = checkpoint.journal.last()
4378 && tail.block.number.checked_add(1) == Some(coverage.number)
4379 && coverage
4380 .parent_hash
4381 .is_some_and(|parent_hash| parent_hash != tail.block.hash)
4382 {
4383 return Err(invalid(format!(
4384 "canonical coverage does not descend from adjacent runtime journal tail at block {}",
4385 tail.block.number
4386 )));
4387 }
4388
4389 if let Some(last_gate_block) = checkpoint.last_gate_block {
4390 if last_gate_block > coverage.number {
4391 return Err(invalid(
4392 "root-gate cursor lies beyond canonical coverage".into(),
4393 ));
4394 }
4395 } else if !checkpoint.tracked_roots.is_empty() {
4396 return Err(invalid(
4397 "root-gate baselines exist without a completed gate cursor".into(),
4398 ));
4399 }
4400 for (address, baseline) in &checkpoint.tracked_roots {
4401 let Some(policy) = checkpoint.tracking.get(address) else {
4402 return Err(invalid(
4403 "root-gate baseline has no corresponding tracking policy".into(),
4404 ));
4405 };
4406 if matches!(policy, TrackingPolicy::Slots { .. }) {
4407 return Err(invalid(
4408 "slot-only tracking cannot carry an account root baseline".into(),
4409 ));
4410 }
4411 if baseline.last_block > coverage.number
4412 || checkpoint
4413 .last_gate_block
4414 .is_some_and(|last_gate| baseline.last_block > last_gate)
4415 {
4416 return Err(invalid(
4417 "root-gate baseline lies beyond the committed gate window".into(),
4418 ));
4419 }
4420 }
4421 Ok(())
4422 }
4423
4424 pub fn track_account(&mut self, address: Address, policy: TrackingPolicy) {
4438 self.tracking.insert(address, policy);
4439 self.tracked_roots.remove(&address);
4440 }
4441
4442 pub fn untrack_account(&mut self, address: Address) -> bool {
4446 self.tracked_roots.remove(&address);
4447 self.tracking.remove(&address).is_some()
4448 }
4449
4450 pub fn set_root_gate_cadence(&mut self, cadence: RootGateCadence) {
4458 self.root_gate_cadence = cadence;
4459 self.last_gate_block = None;
4460 self.touched_since_gate.clear();
4461 }
4462
4463 pub fn root_gate_cadence(&self) -> RootGateCadence {
4465 self.root_gate_cadence
4466 }
4467
4468 pub fn enable_freshness_stamping(&mut self) {
4480 if self.freshness.is_none() {
4481 self.freshness = Some(FreshnessRegistry::new());
4482 }
4483 }
4484
4485 pub fn freshness(&self) -> Option<&FreshnessRegistry> {
4490 self.freshness.as_ref()
4491 }
4492
4493 pub fn freshness_mut(&mut self) -> Option<&mut FreshnessRegistry> {
4498 self.freshness.as_mut()
4499 }
4500
4501 pub fn health(&self) -> CacheHealth {
4503 self.health
4504 }
4505
4506 pub fn metrics(&self) -> CacheMetricsSnapshot {
4508 self.metrics.snapshot()
4509 }
4510
4511 pub fn reset_health(&mut self) {
4521 self.health = CacheHealth::Healthy;
4522 }
4523
4524 fn escalate_trust(&mut self, block: u64) -> Option<Arc<ReactiveReport<N>>> {
4538 let to = match self.health {
4539 CacheHealth::Healthy => CacheHealth::Degraded { since_block: block },
4540 CacheHealth::Degraded { .. } => CacheHealth::Unhealthy { since_block: block },
4541 CacheHealth::Unhealthy { .. } => return None,
4542 };
4543 self.transition_health(to, Some(block))
4544 }
4545
4546 fn transition_health(
4554 &mut self,
4555 to: CacheHealth,
4556 block: Option<u64>,
4557 ) -> Option<Arc<ReactiveReport<N>>> {
4558 if to == self.health {
4559 return None;
4560 }
4561 let from = self.health;
4562 self.health = to;
4563 Some(Arc::new(ReactiveReport::Health(HealthReport {
4564 from,
4565 to,
4566 block,
4567 _network: PhantomData,
4568 })))
4569 }
4570
4571 pub fn register_handler(
4578 &mut self,
4579 handler: Arc<dyn ReactiveHandler<N>>,
4580 ) -> Result<(), RegisterError> {
4581 self.registry.register_handler(handler)
4582 }
4583
4584 pub fn unregister_handler(&mut self, id: &HandlerId) -> Option<Arc<dyn ReactiveHandler<N>>> {
4592 self.registry.unregister_handler(id)
4593 }
4594
4595 pub fn contains_handler(&self, id: &HandlerId) -> bool {
4597 self.registry.contains_handler(id)
4598 }
4599
4600 pub fn handler_ids(&self) -> Vec<HandlerId> {
4602 self.registry.handler_ids()
4603 }
4604
4605 pub fn handler_interests(&self, id: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
4607 self.registry.handler_interests(id)
4608 }
4609
4610 pub fn last_canonical_block(&self) -> Option<BlockRef> {
4620 self.coverage_head
4621 }
4622
4623 pub fn adopt_canonical_baseline(
4640 &mut self,
4641 baseline: BlockRef,
4642 ) -> Result<(), ReactiveBaselineError> {
4643 self.validate_canonical_baseline_adoption(baseline)?;
4644 if self.adopted_baseline_only().is_some() {
4645 return Ok(());
4646 }
4647
4648 self.coverage_head = Some(baseline);
4649 if self.config.journal_depth > 0 {
4650 self.journal.push_back(BlockJournal {
4651 block: baseline,
4652 inputs: Vec::new(),
4653 applied: Vec::new(),
4654 handler_ids: Vec::new(),
4655 resynced: Vec::new(),
4656 rollback_diffs: Vec::new(),
4657 });
4658 }
4659 Ok(())
4660 }
4661
4662 fn validate_canonical_baseline_adoption(
4663 &self,
4664 baseline: BlockRef,
4665 ) -> Result<(), ReactiveBaselineError> {
4666 if let Some(existing) = self.adopted_baseline_only() {
4667 return if existing == baseline {
4668 Ok(())
4669 } else {
4670 Err(ReactiveBaselineError::ConflictingBaseline {
4671 existing_number: existing.number,
4672 existing_hash: existing.hash,
4673 requested_number: baseline.number,
4674 requested_hash: baseline.hash,
4675 })
4676 };
4677 }
4678 if !self.is_pristine_for_checkpoint_restore() {
4679 return Err(ReactiveBaselineError::ActiveRuntime);
4680 }
4681 Ok(())
4682 }
4683
4684 pub const fn safe_head(&self) -> Option<&BlockRef> {
4686 self.safe_head.as_ref()
4687 }
4688
4689 pub const fn finalized_head(&self) -> Option<&BlockRef> {
4691 self.finalized_head.as_ref()
4692 }
4693
4694 pub fn has_journaled_handler_effects(&self, handler_id: &HandlerId) -> bool {
4702 self.journal
4703 .iter()
4704 .any(|entry| entry.handler_ids.contains(handler_id))
4705 }
4706
4707 pub fn journaled_handler_ids(&self) -> HashSet<HandlerId> {
4714 self.journal
4715 .iter()
4716 .flat_map(|entry| entry.handler_ids.iter().cloned())
4717 .collect()
4718 }
4719
4720 pub fn pending_resyncs(&self) -> &[ResyncRequest] {
4731 &self.pending_resyncs
4732 }
4733
4734 pub fn cancel_pending_resync(&mut self, id: &ResyncId) -> Vec<ResyncRequest> {
4743 self.cancel_pending_resyncs_by_id(std::slice::from_ref(id))
4744 }
4745
4746 pub fn cancel_pending_resyncs_by_id(&mut self, ids: &[ResyncId]) -> Vec<ResyncRequest> {
4753 if ids.is_empty() {
4754 return Vec::new();
4755 }
4756 let ids: HashSet<&ResyncId> = ids.iter().collect();
4757 let mut cancelled = Vec::new();
4758 self.pending_resyncs.retain(|request| {
4759 if ids.contains(&request.id) {
4760 cancelled.push(request.clone());
4761 false
4762 } else {
4763 true
4764 }
4765 });
4766 cancelled
4767 }
4768
4769 pub fn cancel_pending_resyncs(&mut self, address: Address) -> Vec<ResyncRequest> {
4784 let mut cancelled = Vec::new();
4785 self.pending_resyncs.retain_mut(|request| {
4786 let (matching, remaining): (Vec<_>, Vec<_>) = request
4787 .targets
4788 .drain(..)
4789 .partition(|target| resync_target_address(target) == address);
4790 request.targets = remaining;
4791 if !matching.is_empty() {
4792 cancelled.push(ResyncRequest {
4793 id: request.id.clone(),
4794 reason: request.reason.clone(),
4795 block: request.block.clone(),
4796 targets: matching,
4797 priority: request.priority,
4798 });
4799 }
4800 !request.targets.is_empty()
4801 });
4802 cancelled
4803 }
4804
4805 pub fn register_hook(&mut self, hook: Arc<dyn ReactiveHook<N>>) -> Result<(), RegisterError> {
4812 self.hooks.push(hook);
4813 Ok(())
4814 }
4815
4816 pub fn interests(&self) -> Vec<ReactiveInterest<N>> {
4818 self.registry.interests()
4819 }
4820
4821 pub fn ingest_batch(
4839 &mut self,
4840 cache: &mut EvmCache,
4841 batch: ReactiveInputBatch<N>,
4842 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4843 let preconfirmation = batch_preconfirmation(&batch)?;
4844 if let Some(flashblock) = preconfirmation.as_ref() {
4845 self.prepare_preconfirmed_branch(cache, flashblock)?;
4846 } else {
4847 self.discard_preconfirmed_branch(cache);
4848 }
4849 let cache_state = EvmCacheStateSnapshot::capture(cache);
4850 let runtime_state = self.checkpoint_state();
4851 let batch_report = match self.ingest_batch_direct(cache, batch) {
4852 Ok(report) => report,
4853 Err(error) => {
4854 cache_state.restore(cache);
4855 self.restore_transaction_state(runtime_state);
4856 return Err(error);
4857 }
4858 };
4859 if let Some(flashblock) = preconfirmation {
4860 self.restore_transaction_state(runtime_state);
4861 if let Some(branch) = self.preconfirmed_branch.as_mut() {
4862 branch.flashblock = flashblock;
4863 }
4864 }
4865 self.dispatch_reports(&batch_report.reports);
4866 let _ = &self.config;
4867 Ok(batch_report)
4868 }
4869
4870 pub fn ingest_batch_with_resync(
4887 &mut self,
4888 cache: &mut EvmCache,
4889 batch: ReactiveInputBatch<N>,
4890 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
4891 let preconfirmation = batch_preconfirmation(&batch)?;
4892 if let Some(flashblock) = preconfirmation.as_ref() {
4893 self.prepare_preconfirmed_branch(cache, flashblock)?;
4894 } else {
4895 self.discard_preconfirmed_branch(cache);
4896 }
4897 let cache_state = EvmCacheStateSnapshot::capture(cache);
4898 let runtime_state = self.checkpoint_state();
4899 let batch_report = match self.ingest_batch_with_resync_direct(cache, batch) {
4900 Ok(report) => report,
4901 Err(error) => {
4902 cache_state.restore(cache);
4903 self.restore_transaction_state(runtime_state);
4904 return Err(error);
4905 }
4906 };
4907
4908 if let Some(flashblock) = preconfirmation {
4909 self.restore_transaction_state(runtime_state);
4910 if let Some(branch) = self.preconfirmed_branch.as_mut() {
4911 branch.flashblock = flashblock;
4912 }
4913 }
4914
4915 self.dispatch_reports(&batch_report.reports);
4916 let _ = &self.config;
4917 Ok(batch_report)
4918 }
4919
4920 pub fn active_preconfirmation(&self) -> Option<&FlashblockRef> {
4923 self.preconfirmed_branch
4924 .as_ref()
4925 .map(|branch| &branch.flashblock)
4926 }
4927
4928 pub fn discard_preconfirmation(&mut self, cache: &mut EvmCache) {
4931 self.discard_preconfirmed_branch(cache);
4932 }
4933
4934 fn discard_preconfirmed_branch(&mut self, cache: &mut EvmCache) {
4935 if let Some(branch) = self.preconfirmed_branch.take() {
4936 branch.canonical_cache.restore(cache);
4937 }
4938 }
4939
4940 fn prepare_preconfirmed_branch(
4941 &mut self,
4942 cache: &mut EvmCache,
4943 incoming: &FlashblockRef,
4944 ) -> Result<(), ReactiveError> {
4945 let Some(canonical) = self.coverage_head else {
4946 self.discard_preconfirmed_branch(cache);
4947 return Err(ReactiveError::InvalidInputRecord {
4948 message: "pre-confirmed state requires an exact canonical coverage baseline".into(),
4949 });
4950 };
4951 if canonical.number.checked_add(1) != Some(incoming.block_number) {
4952 self.discard_preconfirmed_branch(cache);
4953 return Err(ReactiveError::InvalidInputRecord {
4954 message: format!(
4955 "pre-confirmed block {} is not the exact successor of canonical block {}",
4956 incoming.block_number, canonical.number
4957 ),
4958 });
4959 }
4960 if incoming.parent_hash != Some(canonical.hash) {
4961 self.discard_preconfirmed_branch(cache);
4962 return Err(ReactiveError::InvalidInputRecord {
4963 message: "pre-confirmed block parent does not match the canonical coverage hash"
4964 .into(),
4965 });
4966 }
4967 if let Some(active) = self.preconfirmed_branch.as_ref()
4968 && active.flashblock.same_payload(incoming)
4969 {
4970 if let (Some(active_index), Some(incoming_index)) =
4971 (active.flashblock.index, incoming.index)
4972 && incoming_index < active_index
4973 {
4974 return Err(ReactiveError::InvalidInputRecord {
4975 message: format!(
4976 "Flashblock index regressed from {active_index} to {incoming_index}"
4977 ),
4978 });
4979 }
4980 if active.flashblock.index.is_some()
4981 && active.flashblock.index == incoming.index
4982 && active.flashblock.content_hash != incoming.content_hash
4983 {
4984 self.discard_preconfirmed_branch(cache);
4985 return Err(ReactiveError::InvalidInputRecord {
4986 message: "same Flashblock payload/index carried conflicting cumulative content"
4987 .into(),
4988 });
4989 }
4990 install_preconfirmed_cache_context(cache, incoming);
4991 return Ok(());
4992 }
4993
4994 self.discard_preconfirmed_branch(cache);
4995 self.preconfirmed_branch = Some(PreconfirmedBranch {
4996 flashblock: incoming.clone(),
4997 canonical_cache: EvmCacheStateSnapshot::capture(cache),
4998 });
4999 install_preconfirmed_cache_context(cache, incoming);
5000 Ok(())
5001 }
5002
5003 fn ingest_batch_with_resync_direct(
5004 &mut self,
5005 cache: &mut EvmCache,
5006 batch: ReactiveInputBatch<N>,
5007 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
5008 let mut batch_report = self.ingest_batch_direct(cache, batch)?;
5009 if !batch_report.resyncs.is_empty() {
5010 let resync_report = execute_resync_requests(cache, &batch_report.resyncs);
5011 let unique_requests = resync_report
5015 .requested
5016 .iter()
5017 .map(|request| &request.id)
5018 .collect::<HashSet<_>>()
5019 .len();
5020 self.metrics
5021 .resync_requests
5022 .fetch_add(unique_requests as u64, Ordering::Relaxed);
5023 self.metrics
5024 .resync_failures
5025 .fetch_add(resync_report.failed.len() as u64, Ordering::Relaxed);
5026 self.remove_pending_resyncs(batch_report.resyncs.iter().map(|request| &request.id));
5027 self.record_journal_resync(&resync_report);
5028 batch_report
5029 .reports
5030 .push(Arc::new(ReactiveReport::Resynced(resync_report)));
5031 }
5032 Ok(batch_report)
5033 }
5034
5035 fn ingest_batch_direct(
5036 &mut self,
5037 cache: &mut EvmCache,
5038 batch: ReactiveInputBatch<N>,
5039 ) -> Result<ReactiveBatchReport<N>, ReactiveError> {
5040 let (records, chain_controls, batch_chain_id) = batch.into_runtime_parts();
5041 if let Some(chain_id) = batch_chain_id
5042 && chain_id != cache.chain_id()
5043 {
5044 return Err(ReactiveError::InvalidInputRecord {
5045 message: format!(
5046 "batch chain id {chain_id} does not match cache chain id {}",
5047 cache.chain_id()
5048 ),
5049 });
5050 }
5051 if !chain_controls.is_empty() && batch_chain_id.is_none() {
5052 return Err(ReactiveError::InvalidChainControl {
5053 message: "chain-control batches require an authoritative batch chain id".into(),
5054 });
5055 }
5056 for (record, _, _) in &records {
5057 record.validated_identity()?;
5058 if let Some(chain_id) = record.context.chain_id
5059 && chain_id != cache.chain_id()
5060 {
5061 return Err(ReactiveError::InvalidInputRecord {
5062 message: format!(
5063 "input chain id {chain_id} does not match cache chain id {}",
5064 cache.chain_id()
5065 ),
5066 });
5067 }
5068 }
5069 let records = sort_scoped_records(dedupe_scoped_records(records)?);
5070
5071 let mut batch_report = ReactiveBatchReport::default();
5072 let mut reports_to_dispatch = Vec::new();
5073 let control_split = validate_control_phase_order(&chain_controls)?;
5074 let (pre_record_controls, post_record_controls) = chain_controls.split_at(control_split);
5075 let pre_record_state =
5076 self.validate_ingest_sequence(pre_record_controls, post_record_controls, &records)?;
5077 self.validate_owner_catchup_against_journal(&pre_record_state, &records)?;
5078 let mut batch_dropped = BatchDroppedCanonical::default();
5079 for control in pre_record_controls {
5080 if let ChainControl::Reorg {
5081 common_ancestor,
5082 old_tip,
5083 ..
5084 } = control
5085 {
5086 batch_dropped.record_explicit(common_ancestor, old_tip);
5087 let drained = self
5088 .journal
5089 .iter()
5090 .filter(|entry| entry.block.number > common_ancestor.number)
5091 .map(|entry| entry.block)
5092 .collect::<Vec<_>>();
5093 batch_dropped.record_drained(&drained);
5094 }
5095 }
5096 let certified_progress_through = post_record_controls
5097 .iter()
5098 .filter_map(canonical_coverage_control_block)
5099 .map(|block| block.number)
5100 .max();
5101 for control in pre_record_controls.iter().cloned() {
5102 self.apply_chain_control(cache, control, &mut batch_report, &mut reports_to_dispatch);
5103 }
5104 let mut touched_addrs: HashSet<Address> = HashSet::new();
5109 let mut canonical_batch_block: Option<u64> = None;
5110
5111 for (record, audience, delivery_scope) in records {
5112 let raw_canonical_block = canonical_record_block(&record).copied();
5113 let canonical_block = raw_canonical_block.map(|block| {
5114 pre_record_state
5115 .resolved_canonical_blocks
5116 .get(&(block.number, block.hash))
5117 .copied()
5118 .unwrap_or(block)
5119 });
5120 let input_ref = record.input_ref();
5121 reports_to_dispatch.push(Arc::new(ReactiveReport::Input(InputReport {
5122 input_ref,
5123 context: record.context.clone(),
5124 provider: record.provider.clone(),
5125 _network: PhantomData,
5126 })));
5127
5128 let recovered_reorg = if delivery_scope.advances_canonical_state() {
5129 if let Some(block) = canonical_block.as_ref() {
5130 let gap_is_certified = delivery_scope == DeliveryScope::CanonicalProgress
5131 && certified_progress_through
5132 .is_some_and(|through| block.number <= through);
5133 let parentless_replacement_is_proven = raw_canonical_block.is_some_and(|raw| {
5134 raw.parent_hash.is_none()
5135 && batch_dropped.covers_implicit_number(raw.number)
5136 });
5137 self.recover_for_canonical_input(
5138 cache,
5139 block,
5140 gap_is_certified,
5141 parentless_replacement_is_proven,
5142 &mut reports_to_dispatch,
5143 )
5144 } else {
5145 None
5146 }
5147 } else {
5148 None
5149 };
5150 let recovered_reorg_for_input = recovered_reorg.is_some();
5151 if let Some(reorg_report) = recovered_reorg {
5152 self.metrics
5153 .reorgs_recovered
5154 .fetch_add(1, Ordering::Relaxed);
5155 remove_canceled_resyncs_from_batch(
5156 &mut batch_report.resyncs,
5157 &reorg_report.canceled_resyncs,
5158 );
5159 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5160 }
5161
5162 if reorg_signal_block(&record).is_some() {
5168 if delivery_scope.advances_canonical_state()
5169 && let Some(reorg_report) = self.recover_for_reorged_input(
5170 cache,
5171 &record,
5172 &mut batch_dropped,
5173 &mut reports_to_dispatch,
5174 )
5175 {
5176 self.metrics
5177 .reorgs_recovered
5178 .fetch_add(1, Ordering::Relaxed);
5179 remove_canceled_resyncs_from_batch(
5180 &mut batch_report.resyncs,
5181 &reorg_report.canceled_resyncs,
5182 );
5183 reports_to_dispatch.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5184 }
5185 continue;
5186 }
5187
5188 if delivery_scope == DeliveryScope::OwnerCatchup {
5195 self.validate_owner_catchup_record_against_current_journal(&record)?;
5196 }
5197
5198 if delivery_scope.advances_canonical_state()
5199 && let Some(block) = canonical_block.as_ref()
5200 {
5201 canonical_batch_block = Some(block.number);
5204 self.record_journal_input(block, input_ref);
5205 }
5206
5207 if delivery_scope.advances_canonical_state()
5213 && let Some(block) = canonical_block.as_ref()
5214 {
5215 match advance_block_for_canonical_record(cache, &record) {
5216 Some(Ok(())) => {
5217 cache.advance_compact_block(block.number, block.hash, block.timestamp, true)
5218 }
5219 Some(Err(err)) => {
5220 cache.advance_compact_block(
5221 block.number,
5222 block.hash,
5223 block.timestamp,
5224 false,
5225 );
5226 reports_to_dispatch.push(Arc::new(ReactiveReport::Error(
5227 ReactiveErrorReport {
5228 input_ref: Some(input_ref),
5229 message: err.to_string(),
5230 _network: PhantomData,
5231 },
5232 )));
5233 }
5234 None => cache.advance_compact_block(
5235 block.number,
5236 block.hash,
5237 block.timestamp,
5238 !recovered_reorg_for_input,
5239 ),
5240 }
5241 }
5242
5243 let executions = self.execute_handlers(cache, &record, input_ref, &audience)?;
5244 if executions.is_empty() {
5245 continue;
5246 }
5247
5248 reports_to_dispatch.push(Arc::new(ReactiveReport::Decoded(DecodedReport {
5249 input_ref,
5250 handler_ids: executions
5251 .iter()
5252 .map(|execution| execution.handler_id.clone())
5253 .collect(),
5254 _network: PhantomData,
5255 })));
5256
5257 detect_conflicts(input_ref, &executions)?;
5258
5259 let canonical_block_number = delivery_scope
5265 .advances_canonical_state()
5266 .then_some(canonical_block)
5267 .flatten()
5268 .map(|block| block.number);
5269
5270 for execution in executions {
5271 let diff = if execution.state_updates.is_empty() {
5272 StateDiff::default()
5273 } else {
5274 cache.apply_updates(&execution.state_updates)
5275 };
5276
5277 batch_report
5278 .resyncs
5279 .extend(execution.resyncs.iter().cloned());
5280 self.pending_resyncs
5281 .extend(execution.resyncs.iter().cloned());
5282 batch_report
5283 .speculative
5284 .extend(execution.speculative.iter().cloned());
5285
5286 let applied = AppliedReport {
5287 input_ref,
5288 handler_id: execution.handler_id,
5289 quality: execution.quality,
5290 tags: execution.tags,
5291 diff,
5292 state_updates: execution.state_updates,
5293 invalidations: execution.invalidations,
5294 resyncs: execution.resyncs,
5295 speculative: execution.speculative,
5296 hook_signals: execution.hook_signals,
5297 _network: PhantomData,
5298 };
5299 if let (Some(number), Some(registry)) =
5308 (canonical_block_number, self.freshness.as_mut())
5309 {
5310 for change in &applied.diff.slots {
5311 registry.valid_through_slot(change.address, change.slot, number);
5312 }
5313 }
5314
5315 if delivery_scope.advances_canonical_state() {
5323 collect_diff_addresses(&applied.diff, &mut touched_addrs);
5324 }
5325
5326 let report = Arc::new(ReactiveReport::Applied(applied.clone()));
5327 reports_to_dispatch.push(report);
5328 if let Some(block) = canonical_block.as_ref() {
5329 if delivery_scope.advances_canonical_state() {
5330 self.record_journal_applied(block, applied.clone());
5331 } else {
5332 self.record_journal_applied_if_present(block, applied.clone());
5333 }
5334 }
5335 batch_report.applied.push(applied);
5336 }
5337 }
5338
5339 for control in post_record_controls.iter().cloned() {
5344 if let Some(block) = canonical_coverage_control_block(&control) {
5345 canonical_batch_block = Some(
5346 canonical_batch_block.map_or(block.number, |current| current.max(block.number)),
5347 );
5348 }
5349 self.apply_chain_control(cache, control, &mut batch_report, &mut reports_to_dispatch);
5350 }
5351
5352 if self.root_gate_runnable(cache) {
5362 self.touched_since_gate
5363 .extend(touched_addrs.iter().copied());
5364 if self.root_gate_due(canonical_batch_block) {
5365 let accumulated = std::mem::take(&mut self.touched_since_gate);
5366 self.run_root_gate(
5367 cache,
5368 canonical_batch_block,
5369 &accumulated,
5370 &mut batch_report.resyncs,
5371 &mut reports_to_dispatch,
5372 );
5373 self.last_gate_block = canonical_batch_block;
5374 }
5375 } else {
5376 self.touched_since_gate.clear();
5383 }
5384
5385 batch_report.reports = reports_to_dispatch;
5386 Ok(batch_report)
5387 }
5388
5389 fn validate_owner_catchup_against_journal(
5403 &self,
5404 control_state: &ChainControlState,
5405 records: &[(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)],
5406 ) -> Result<(), ReactiveError> {
5407 for (record, _, delivery_scope) in records {
5408 if *delivery_scope != DeliveryScope::OwnerCatchup {
5409 continue;
5410 }
5411 if reorg_signal_block(record).is_some() {
5416 continue;
5417 }
5418 let context_block = canonical_record_block(record).ok_or_else(|| {
5419 ReactiveError::InvalidChainControl {
5420 message: "owner catch-up input has no canonical block identity".into(),
5421 }
5422 })?;
5423 let block = resolve_record_block_payload_metadata(record, *context_block)?;
5424 let invalidated_by_control = control_state
5425 .journal_invalidated_from
5426 .is_some_and(|from| block.number >= from);
5427 let rollbackable = !invalidated_by_control
5428 && self.journal.iter().any(|entry| {
5429 optional_block_refs_are_compatible(Some(&entry.block), Some(&block))
5430 });
5431 if !rollbackable {
5432 return Err(ReactiveError::OwnerCatchupOutsideJournal {
5433 number: block.number,
5434 hash: block.hash,
5435 });
5436 }
5437 }
5438 Ok(())
5439 }
5440
5441 fn validate_owner_catchup_record_against_current_journal(
5442 &self,
5443 record: &ReactiveInputRecord<N>,
5444 ) -> Result<(), ReactiveError> {
5445 let context_block =
5446 canonical_record_block(record).ok_or_else(|| ReactiveError::InvalidChainControl {
5447 message: "owner catch-up input has no canonical block identity".into(),
5448 })?;
5449 let block = resolve_record_block_payload_metadata(record, *context_block)?;
5450 if self
5451 .journal
5452 .iter()
5453 .any(|entry| optional_block_refs_are_compatible(Some(&entry.block), Some(&block)))
5454 {
5455 return Ok(());
5456 }
5457 Err(ReactiveError::OwnerCatchupOutsideJournal {
5458 number: block.number,
5459 hash: block.hash,
5460 })
5461 }
5462
5463 fn root_gate_runnable(&self, cache: &EvmCache) -> bool {
5468 if matches!(self.root_gate_cadence, RootGateCadence::Disabled) {
5469 return false;
5470 }
5471 let has_gated_targets = self
5472 .tracking
5473 .values()
5474 .any(|policy| !matches!(policy, TrackingPolicy::Slots { .. }));
5475 has_gated_targets && cache.account_proof_fetcher().is_some()
5476 }
5477
5478 fn root_gate_due(&self, canonical_block: Option<u64>) -> bool {
5482 let Some(block) = canonical_block else {
5483 return false;
5484 };
5485 match self.root_gate_cadence {
5486 RootGateCadence::Disabled => false,
5487 RootGateCadence::EveryNBlocks(n) => match self.last_gate_block {
5488 None => true,
5489 Some(last) => block >= last.saturating_add(n.get()),
5490 },
5491 }
5492 }
5493
5494 fn run_root_gate(
5520 &mut self,
5521 cache: &EvmCache,
5522 canonical_block: Option<u64>,
5523 touched: &HashSet<Address>,
5524 resyncs: &mut Vec<ResyncRequest>,
5525 reports: &mut Vec<Arc<ReactiveReport<N>>>,
5526 ) {
5527 if self.tracking.is_empty() {
5528 return;
5529 }
5530 let Some(block) = canonical_block else {
5531 return;
5532 };
5533 let Some(fetcher) = cache.account_proof_fetcher().cloned() else {
5534 return;
5535 };
5536
5537 let mut targets: Vec<(Address, bool)> = self
5540 .tracking
5541 .iter()
5542 .filter_map(|(address, policy)| match policy {
5543 TrackingPolicy::Slots { .. } => None,
5544 TrackingPolicy::WholeAccount => Some((*address, true)),
5545 TrackingPolicy::Scalars => Some((*address, false)),
5546 })
5547 .collect();
5548 if targets.is_empty() {
5549 return;
5550 }
5551 targets.sort_by_key(|(address, _)| *address);
5552
5553 let block_id = BlockId::number(block);
5554 let mut probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
5559 targets
5560 .iter()
5561 .map(|&(address, _)| (address, vec![]))
5562 .collect(),
5563 block_id,
5564 )
5565 .into_iter()
5566 .collect();
5567 for (address, whole_account) in targets {
5568 let Some(Ok(proof)) = probes.remove(&address) else {
5569 continue;
5572 };
5573
5574 let baseline = self.tracked_roots.get(&address).cloned();
5575 let Some(baseline) = baseline else {
5576 self.adopt_root(address, block, &proof);
5578 continue;
5579 };
5580
5581 if block <= baseline.last_block {
5586 continue;
5587 }
5588
5589 if whole_account {
5590 if proof.storage_hash == baseline.last_root {
5591 continue;
5593 }
5594 if !touched.contains(&address) {
5596 reports.push(Arc::new(ReactiveReport::CoverageGap(CoverageGapReport {
5598 address,
5599 block,
5600 _network: PhantomData,
5601 })));
5602 self.metrics.coverage_gaps.fetch_add(1, Ordering::Relaxed);
5603 resyncs.push(root_moved_account_resync(
5604 address,
5605 block,
5606 AccountFieldMask {
5607 balance: true,
5608 nonce: true,
5609 code: true,
5610 },
5611 ));
5612 }
5613 self.adopt_root(address, block, &proof);
5615 } else {
5616 let balance_moved = proof.balance != baseline.balance;
5619 let nonce_moved = proof.nonce != baseline.nonce;
5620 let code_moved = proof.code_hash != baseline.code_hash;
5621 if (balance_moved || nonce_moved || code_moved) && !touched.contains(&address) {
5622 resyncs.push(root_moved_account_resync(
5623 address,
5624 block,
5625 AccountFieldMask {
5626 balance: balance_moved,
5627 nonce: nonce_moved,
5628 code: code_moved,
5629 },
5630 ));
5631 }
5632 self.adopt_root(address, block, &proof);
5633 }
5634 }
5635 }
5636
5637 fn adopt_root(&mut self, address: Address, block: u64, proof: &AccountProof) {
5639 self.tracked_roots.insert(
5640 address,
5641 TrackedRoot {
5642 last_root: proof.storage_hash,
5643 last_block: block,
5644 balance: proof.balance,
5645 nonce: proof.nonce,
5646 code_hash: proof.code_hash,
5647 },
5648 );
5649 }
5650
5651 fn execute_handlers(
5652 &self,
5653 cache: &EvmCache,
5654 record: &ReactiveInputRecord<N>,
5655 input_ref: InputRef,
5656 audience: &DeliveryAudience,
5657 ) -> Result<Vec<HandlerExecution>, ReactiveError> {
5658 let mut executions = Vec::new();
5659 let candidates: Vec<_> = match &record.input {
5660 ReactiveInput::Log(log) => self.registry.log_handler_candidates(log),
5661 ReactiveInput::BlockHeader(_)
5662 | ReactiveInput::FullBlock(_)
5663 | ReactiveInput::PendingTxHash(_)
5664 | ReactiveInput::PendingTx(_) => self.registry.handlers().collect(),
5665 };
5666 for registered in candidates {
5667 match audience {
5668 DeliveryAudience::Owners(owners) if !owners.contains(®istered.id) => continue,
5669 DeliveryAudience::AllExcept(excluded) if excluded.contains(®istered.id) => {
5670 continue;
5671 }
5672 DeliveryAudience::All
5673 | DeliveryAudience::Owners(_)
5674 | DeliveryAudience::AllExcept(_) => {}
5675 }
5676 if !registered.matches(&record.input) {
5677 continue;
5678 }
5679
5680 let outcome = registered
5681 .handler
5682 .handle(&record.context, &record.input, cache)
5683 .map_err(|source| ReactiveError::HandlerFailed {
5684 handler_id: registered.id.clone(),
5685 source,
5686 })?;
5687
5688 if let Err(error) =
5689 validate_effects(input_ref, &record.context, ®istered.id, &outcome.effects)
5690 {
5691 if matches!(error, ReactiveError::InvalidPendingEffect { .. }) {
5692 self.metrics
5693 .pending_contamination
5694 .fetch_add(1, Ordering::Relaxed);
5695 }
5696 return Err(error);
5697 }
5698 executions.push(HandlerExecution::from_outcome(
5699 registered.id.clone(),
5700 input_ref,
5701 outcome,
5702 matches!(
5703 record.context.chain_status,
5704 ChainStatus::Preconfirmed { .. }
5705 ),
5706 ));
5707 }
5708 Ok(executions)
5709 }
5710
5711 fn dispatch_reports(&self, reports: &[Arc<ReactiveReport<N>>]) {
5712 for report in reports {
5713 for hook in &self.hooks {
5714 hook.on_report(report.clone());
5715 }
5716 }
5717 }
5718
5719 fn apply_chain_control(
5720 &mut self,
5721 cache: &mut EvmCache,
5722 control: ChainControl,
5723 batch_report: &mut ReactiveBatchReport<N>,
5724 reports: &mut Vec<Arc<ReactiveReport<N>>>,
5725 ) {
5726 match &control {
5727 ChainControl::Safe(block) => set_or_enrich_block_ref(&mut self.safe_head, block),
5728 ChainControl::Finalized(block) => {
5729 set_or_enrich_block_ref(&mut self.finalized_head, block);
5730 }
5731 ChainControl::CanonicalProgress(block)
5732 | ChainControl::Barrier {
5733 block: Some(block), ..
5734 } => {
5735 let preserve_env = self.coverage_head.as_ref().is_some_and(|current| {
5736 optional_block_refs_are_compatible(Some(current), Some(block))
5737 });
5738 cache.advance_compact_block(
5739 block.number,
5740 block.hash,
5741 block.timestamp,
5742 preserve_env,
5743 );
5744 advance_or_enrich_coverage(&mut self.coverage_head, block);
5745 let enriched = self.journal_entry_mut(block).block;
5746 advance_or_enrich_coverage(&mut self.coverage_head, &enriched);
5747 self.trim_journal();
5748 }
5749 ChainControl::Barrier { block: None, .. } => {}
5750 ChainControl::Reorg {
5751 common_ancestor,
5752 old_tip,
5753 ..
5754 } => {
5755 cache.invalidate_cached_block_hashes_from(common_ancestor.number.saturating_add(1));
5756 self.rebase_validation_state_from(common_ancestor.number.saturating_add(1));
5757 let dropped = if let Some(ancestor_index) = self.journal.iter().rposition(|entry| {
5758 entry.block.number == common_ancestor.number
5759 && entry.block.hash == common_ancestor.hash
5760 }) {
5761 self.drain_journal_after(ancestor_index)
5762 } else {
5763 if self
5768 .journal
5769 .front()
5770 .is_none_or(|entry| entry.block.number > common_ancestor.number)
5771 {
5772 reports.extend(
5773 self.warn_under_recovery(common_ancestor.number.saturating_add(1)),
5774 );
5775 }
5776 self.drain_journal_from_number(common_ancestor.number.saturating_add(1))
5777 };
5778
5779 let reorg_report = self
5780 .recover_dropped_journals(cache, dropped, ReorgReason::Explicit)
5781 .unwrap_or_else(|| ReorgReport {
5782 dropped: Some(*old_tip),
5783 dropped_blocks: Vec::new(),
5784 dropped_inputs: Vec::new(),
5785 rollback_updates: Vec::new(),
5786 rollback_diff: StateDiff::default(),
5787 purge_updates: Vec::new(),
5788 purge_diff: StateDiff::default(),
5789 canceled_resyncs: self
5790 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(old_tip)),
5791 reason: ReorgReason::Explicit,
5792 _network: PhantomData,
5793 });
5794 remove_canceled_resyncs_from_batch(
5795 &mut batch_report.resyncs,
5796 &reorg_report.canceled_resyncs,
5797 );
5798 self.metrics
5799 .reorgs_recovered
5800 .fetch_add(1, Ordering::Relaxed);
5801 reports.push(Arc::new(ReactiveReport::Reorg(reorg_report)));
5802
5803 if self.safe_head.as_ref().is_some_and(|head| {
5804 head.number > common_ancestor.number
5805 || (head.number == common_ancestor.number
5806 && head.hash != common_ancestor.hash)
5807 }) {
5808 self.safe_head = None;
5809 }
5810 if self.finalized_head.as_ref().is_some_and(|head| {
5811 head.number > common_ancestor.number
5812 || (head.number == common_ancestor.number
5813 && head.hash != common_ancestor.hash)
5814 }) {
5815 self.finalized_head = None;
5816 }
5817 let mut enriched_ancestor = *common_ancestor;
5818 if let Some(entry) = self.journal.iter().find(|entry| {
5819 entry.block.number == common_ancestor.number
5820 && entry.block.hash == common_ancestor.hash
5821 }) {
5822 enrich_block_ref(&mut enriched_ancestor, &entry.block);
5823 }
5824 if let Some(current) = self.coverage_head.as_ref()
5825 && current.number == common_ancestor.number
5826 && current.hash == common_ancestor.hash
5827 {
5828 enrich_block_ref(&mut enriched_ancestor, current);
5829 }
5830 self.coverage_head = Some(enriched_ancestor);
5831 cache.advance_compact_block(
5832 enriched_ancestor.number,
5833 enriched_ancestor.hash,
5834 enriched_ancestor.timestamp,
5835 false,
5836 );
5837 let enriched_ancestor = self.journal_entry_mut(&enriched_ancestor).block;
5838 self.coverage_head = Some(enriched_ancestor);
5839 self.trim_journal();
5840 }
5841 }
5842 reports.push(Arc::new(ReactiveReport::ChainControl(ChainControlReport {
5843 control,
5844 })));
5845 }
5846
5847 fn validate_ingest_sequence(
5848 &self,
5849 pre_record_controls: &[ChainControl],
5850 post_record_controls: &[ChainControl],
5851 records: &[(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)],
5852 ) -> Result<ChainControlState, ReactiveError> {
5853 let mut controls =
5854 Vec::with_capacity(pre_record_controls.len() + post_record_controls.len());
5855 controls.extend_from_slice(pre_record_controls);
5856 controls.extend_from_slice(post_record_controls);
5857 let state = CanonicalSequenceState::new(
5858 self.journal.iter().map(|entry| entry.block).collect(),
5859 self.coverage_head,
5860 self.safe_head,
5861 self.finalized_head,
5862 );
5863 let record_metadata = records
5864 .iter()
5865 .map(|(record, _, scope)| (record, *scope))
5866 .collect::<Vec<_>>();
5867 let validation = validate_canonical_sequence_parts(
5868 &state,
5869 &controls,
5870 &record_metadata,
5871 CanonicalSequenceValidationPolicy::ObserveIncompleteRollback,
5872 )
5873 .map_err(CanonicalSequenceError::into_reactive_error)?;
5874 let mut resolved_canonical_blocks = HashMap::new();
5875 for mutation in validation.mutations() {
5876 if let CanonicalSequenceMutation::Canonical(block) = mutation {
5877 resolved_canonical_blocks
5878 .entry((block.number, block.hash))
5879 .and_modify(|known| enrich_block_ref(known, block))
5880 .or_insert(*block);
5881 }
5882 }
5883 Ok(ChainControlState {
5884 journal_invalidated_from: pre_record_controls
5885 .iter()
5886 .filter_map(|control| match control {
5887 ChainControl::Reorg {
5888 common_ancestor, ..
5889 } => Some(common_ancestor.number.saturating_add(1)),
5890 _ => None,
5891 })
5892 .min(),
5893 resolved_canonical_blocks,
5894 })
5895 }
5896
5897 fn recover_for_canonical_input(
5898 &mut self,
5899 cache: &mut EvmCache,
5900 block: &BlockRef,
5901 gap_is_certified: bool,
5902 parentless_replacement_is_proven: bool,
5903 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
5904 ) -> Option<ReorgReport<N>> {
5905 let latest = self
5906 .coverage_head
5907 .or_else(|| self.journal.back().map(|entry| entry.block))?;
5908
5909 if latest.number == block.number && latest.hash == block.hash {
5910 return None;
5911 }
5912
5913 if self
5914 .journal
5915 .iter()
5916 .any(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
5917 {
5918 return None;
5919 }
5920
5921 if latest.number.checked_add(1) == Some(block.number)
5922 && (block.parent_hash == Some(latest.hash)
5923 || (parentless_replacement_is_proven && block.parent_hash.is_none()))
5924 {
5925 return None;
5926 }
5927
5928 if latest
5929 .number
5930 .checked_add(1)
5931 .is_some_and(|next| block.number > next)
5932 {
5933 if !gap_is_certified {
5940 self.metrics.missed_ranges.fetch_add(1, Ordering::Relaxed);
5941 health_reports.extend(self.escalate_trust(block.number));
5942 health_reports.push(Arc::new(ReactiveReport::MissedBlockRange(
5943 MissedRangeReport {
5944 from: latest.number + 1,
5945 to: block.number - 1,
5946 block: block.number,
5947 _network: PhantomData,
5948 },
5949 )));
5950 }
5951 return None;
5952 }
5953
5954 let (dropped, authenticated_anchor) = if let Some(parent_hash) = block.parent_hash {
5955 if let Some(parent_index) = self.journal.iter().rposition(|entry| {
5956 entry.block.number.checked_add(1) == Some(block.number)
5957 && entry.block.hash == parent_hash
5958 }) {
5959 let parent = self.journal[parent_index].block;
5960 cache.invalidate_cached_block_hashes_from(parent.number.saturating_add(1));
5961 (self.drain_journal_after(parent_index), Some(parent))
5962 } else {
5963 let proven_finalized_anchor = self.finalized_head.filter(|finalized| {
5969 finalized.number.checked_add(1) == Some(block.number)
5970 && parent_hash == finalized.hash
5971 });
5972 let invalidated_from = proven_finalized_anchor
5973 .map_or(0, |finalized| finalized.number.saturating_add(1));
5974 cache.invalidate_cached_block_hashes_from(invalidated_from);
5975 if block.number > 0 {
5976 cache.set_cached_block_hash(block.number.saturating_sub(1), parent_hash);
5980 }
5981 health_reports.extend(self.warn_under_recovery(block.number));
5982 let dropped = if let Some(finalized) = proven_finalized_anchor {
5983 self.drain_journal_from_number(finalized.number.saturating_add(1))
5984 } else {
5985 self.drain_journal_from_number(0)
5986 };
5987 (dropped, proven_finalized_anchor)
5988 }
5989 } else {
5990 cache.invalidate_cached_block_hashes_from(0);
5992 health_reports.extend(self.warn_under_recovery(block.number));
5993 (self.drain_journal_from_number(0), None)
5994 };
5995
5996 self.rebase_validation_state_from(
5997 authenticated_anchor.map_or(0, |anchor| anchor.number.saturating_add(1)),
5998 );
5999 let report = self
6000 .recover_dropped_journals(cache, dropped, ReorgReason::ParentMismatch)
6001 .or_else(|| {
6002 Some(ReorgReport {
6003 dropped: Some(latest),
6004 dropped_blocks: Vec::new(),
6005 dropped_inputs: Vec::new(),
6006 rollback_updates: Vec::new(),
6007 rollback_diff: StateDiff::default(),
6008 purge_updates: Vec::new(),
6009 purge_diff: StateDiff::default(),
6010 canceled_resyncs: self
6011 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&latest)),
6012 reason: ReorgReason::ParentMismatch,
6013 _network: PhantomData,
6014 })
6015 });
6016 self.coverage_head = authenticated_anchor;
6017 for head in [&mut self.safe_head, &mut self.finalized_head] {
6018 if head.is_some_and(|head| {
6019 authenticated_anchor.is_none_or(|anchor| {
6020 head.number > anchor.number
6021 || (head.number == anchor.number && head.hash != anchor.hash)
6022 })
6023 }) {
6024 *head = None;
6025 }
6026 }
6027 if let Some(anchor) = authenticated_anchor {
6028 cache.advance_compact_block(anchor.number, anchor.hash, anchor.timestamp, false);
6029 }
6030 report
6031 }
6032
6033 fn recover_for_reorged_input(
6034 &mut self,
6035 cache: &mut EvmCache,
6036 record: &ReactiveInputRecord<N>,
6037 batch_dropped: &mut BatchDroppedCanonical,
6038 health_reports: &mut Vec<Arc<ReactiveReport<N>>>,
6039 ) -> Option<ReorgReport<N>> {
6040 let (incoming_dropped_block, reason) = reorg_signal_block(record)?;
6041 if batch_dropped.contains(&incoming_dropped_block) {
6042 let canceled_resyncs = self
6047 .cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&incoming_dropped_block));
6048 return (!canceled_resyncs.is_empty()).then(|| ReorgReport {
6049 dropped: Some(incoming_dropped_block),
6050 dropped_blocks: vec![incoming_dropped_block],
6051 dropped_inputs: Vec::new(),
6052 rollback_updates: Vec::new(),
6053 rollback_diff: StateDiff::default(),
6054 purge_updates: Vec::new(),
6055 purge_diff: StateDiff::default(),
6056 canceled_resyncs,
6057 reason,
6058 _network: PhantomData,
6059 });
6060 }
6061 let exact_index = self.journal.iter().position(|entry| {
6062 entry.block.number == incoming_dropped_block.number
6063 && entry.block.hash == incoming_dropped_block.hash
6064 });
6065 let mut dropped_block = exact_index
6066 .map(|index| self.journal[index].block)
6067 .or_else(|| {
6068 self.coverage_head.filter(|known| {
6069 known.number == incoming_dropped_block.number
6070 && known.hash == incoming_dropped_block.hash
6071 })
6072 })
6073 .unwrap_or(incoming_dropped_block);
6074 enrich_block_ref(&mut dropped_block, &incoming_dropped_block);
6075 let replacement_is_known = exact_index.is_none()
6076 && (self.journal.iter().any(|entry| {
6077 entry.block.number == dropped_block.number && entry.block.hash != dropped_block.hash
6078 }) || self.coverage_head.is_some_and(|head| {
6079 head.number == dropped_block.number && head.hash != dropped_block.hash
6080 }));
6081
6082 if replacement_is_known {
6083 let canceled_resyncs =
6087 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
6088 return (!canceled_resyncs.is_empty()).then(|| ReorgReport {
6089 dropped: Some(dropped_block),
6090 dropped_blocks: vec![dropped_block],
6091 dropped_inputs: Vec::new(),
6092 rollback_updates: Vec::new(),
6093 rollback_diff: StateDiff::default(),
6094 purge_updates: Vec::new(),
6095 purge_diff: StateDiff::default(),
6096 canceled_resyncs,
6097 reason,
6098 _network: PhantomData,
6099 });
6100 }
6101
6102 let authenticated_anchor = exact_index.and_then(|index| {
6103 let ancestor_number = dropped_block.number.checked_sub(1)?;
6104 let retained = self
6105 .journal
6106 .iter()
6107 .take(index)
6108 .rev()
6109 .find(|entry| entry.block.number == ancestor_number)
6110 .map(|entry| entry.block);
6111 let synthetic_parent = dropped_block.parent_hash.map(|hash| BlockRef {
6112 number: ancestor_number,
6113 hash,
6114 parent_hash: None,
6115 timestamp: None,
6116 });
6117 let finalized_fallback = self
6118 .finalized_head
6119 .filter(|head| head.number == ancestor_number);
6120 let mut anchor = retained.or(synthetic_parent).or(finalized_fallback)?;
6121 for head in [self.safe_head.as_ref(), self.finalized_head.as_ref()]
6122 .into_iter()
6123 .flatten()
6124 {
6125 if head.number == anchor.number && head.hash == anchor.hash {
6126 enrich_block_ref(&mut anchor, head);
6127 }
6128 }
6129 Some(anchor)
6130 });
6131
6132 cache.invalidate_cached_block_hashes_from(dropped_block.number);
6133 let dropped = if let Some(index) = exact_index {
6134 self.drain_journal_from(index)
6135 } else {
6136 health_reports.extend(self.warn_under_recovery(dropped_block.number));
6137 self.drain_journal_from_number(dropped_block.number)
6138 };
6139 let drained_blocks = dropped.iter().map(|entry| entry.block).collect::<Vec<_>>();
6140 batch_dropped.record_drained(&drained_blocks);
6141 batch_dropped.record_identity(&dropped_block);
6142 self.rebase_validation_state_from(dropped_block.number);
6143
6144 let recovered_journal = !dropped.is_empty();
6145 let report = if !recovered_journal {
6146 let canceled_resyncs =
6147 self.cancel_resyncs_for_dropped_blocks(std::slice::from_ref(&dropped_block));
6148 Some(ReorgReport {
6149 dropped: Some(dropped_block),
6150 dropped_blocks: Vec::new(),
6151 dropped_inputs: Vec::new(),
6152 rollback_updates: Vec::new(),
6153 rollback_diff: StateDiff::default(),
6154 purge_updates: Vec::new(),
6155 purge_diff: StateDiff::default(),
6156 canceled_resyncs,
6157 reason,
6158 _network: PhantomData,
6159 })
6160 } else {
6161 self.recover_dropped_journals(cache, dropped, reason)
6162 };
6163
6164 if recovered_journal {
6165 if let Some(anchor) = authenticated_anchor {
6166 self.coverage_head = Some(anchor);
6167 }
6168 let coverage = self.coverage_head;
6169 for head in [&mut self.safe_head, &mut self.finalized_head] {
6170 if head.is_some_and(|head| {
6171 coverage.is_none_or(|coverage| {
6172 head.number > coverage.number
6173 || (head.number == coverage.number && head.hash != coverage.hash)
6174 })
6175 }) {
6176 *head = None;
6177 }
6178 }
6179 }
6180
6181 if recovered_journal
6182 && report.is_some()
6183 && let Some(head) = self.coverage_head
6184 {
6185 cache.advance_compact_block(head.number, head.hash, head.timestamp, false);
6186 }
6187 report
6188 }
6189
6190 fn warn_under_recovery(&mut self, reorg_number: u64) -> Option<Arc<ReactiveReport<N>>> {
6202 let oldest_journaled = self.journal.front().map(|entry| entry.block.number);
6203 tracing::warn!(
6204 reorg_block = reorg_number,
6205 oldest_journaled = ?oldest_journaled,
6206 journal_depth = self.config.journal_depth,
6207 "reactive reorg recovery is incomplete: the reorged block is no longer \
6208 in the journal, so effects from blocks aged out of the journal are \
6209 neither rolled back nor purged (the freshness/validation loop is the \
6210 backstop). Increase ReactiveConfig::journal_depth to recover deeper \
6211 reorgs precisely."
6212 );
6213
6214 self.metrics.deep_reorgs.fetch_add(1, Ordering::Relaxed);
6215
6216 self.escalate_trust(reorg_number)
6217 }
6218
6219 fn record_journal_input(&mut self, block: &BlockRef, input_ref: InputRef) {
6220 advance_or_enrich_coverage(&mut self.coverage_head, block);
6221 let entry = self.journal_entry_mut(block);
6222 let enriched = entry.block;
6223 if !entry.inputs.contains(&input_ref) {
6224 entry.inputs.push(input_ref);
6225 }
6226 advance_or_enrich_coverage(&mut self.coverage_head, &enriched);
6227 self.trim_journal();
6228 }
6229
6230 fn record_journal_applied(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
6231 let entry = self.journal_entry_mut(block);
6232 if !entry.handler_ids.contains(&applied.handler_id) {
6233 entry.handler_ids.push(applied.handler_id.clone());
6234 }
6235 entry.rollback_diffs.push(applied.diff.clone());
6236 entry.applied.push(applied);
6237 self.trim_journal();
6238 }
6239
6240 fn record_journal_applied_if_present(&mut self, block: &BlockRef, applied: AppliedReport<N>) {
6241 let Some(entry) = self
6242 .journal
6243 .iter_mut()
6244 .find(|entry| entry.block.number == block.number && entry.block.hash == block.hash)
6245 else {
6246 return;
6247 };
6248 if !entry.handler_ids.contains(&applied.handler_id) {
6249 entry.handler_ids.push(applied.handler_id.clone());
6250 }
6251 entry.rollback_diffs.push(applied.diff.clone());
6252 entry.applied.push(applied);
6253 }
6254
6255 fn record_journal_resync(&mut self, report: &ResyncReport) {
6256 if report.diff.is_empty() {
6257 return;
6258 }
6259 let Some(block) = single_hash_pinned_resync_block(report) else {
6260 return;
6261 };
6262 let entry = self.journal_entry_mut(&block);
6263 entry.rollback_diffs.push(report.diff.clone());
6264 entry.resynced.push(report.clone());
6265 self.trim_journal();
6266 }
6267
6268 fn journal_entry_mut(&mut self, block: &BlockRef) -> &mut BlockJournal<N> {
6269 if let Some(index) = self
6270 .journal
6271 .iter()
6272 .position(|entry| entry.block.hash == block.hash && entry.block.number == block.number)
6273 {
6274 enrich_block_ref(&mut self.journal[index].block, block);
6275 return &mut self.journal[index];
6276 }
6277
6278 self.journal.push_back(BlockJournal {
6279 block: *block,
6280 inputs: Vec::new(),
6281 applied: Vec::new(),
6282 handler_ids: Vec::new(),
6283 resynced: Vec::new(),
6284 rollback_diffs: Vec::new(),
6285 });
6286 let index = self.journal.len() - 1;
6287 &mut self.journal[index]
6288 }
6289
6290 fn trim_journal(&mut self) {
6291 if self.config.journal_depth == 0 {
6292 self.journal.clear();
6293 return;
6294 }
6295 while self.journal.len() > self.config.journal_depth {
6296 self.journal.pop_front();
6297 }
6298 }
6299
6300 fn drain_journal_after(&mut self, index: usize) -> Vec<BlockJournal<N>> {
6301 self.journal.drain((index + 1)..).collect()
6302 }
6303
6304 fn drain_journal_from(&mut self, index: usize) -> Vec<BlockJournal<N>> {
6305 self.journal.drain(index..).collect()
6306 }
6307
6308 fn drain_journal_from_number(&mut self, number: u64) -> Vec<BlockJournal<N>> {
6309 let Some(index) = self
6310 .journal
6311 .iter()
6312 .position(|entry| entry.block.number >= number)
6313 else {
6314 return Vec::new();
6315 };
6316 self.drain_journal_from(index)
6317 }
6318
6319 fn recover_dropped_journals(
6320 &mut self,
6321 cache: &mut EvmCache,
6322 dropped: Vec<BlockJournal<N>>,
6323 reason: ReorgReason,
6324 ) -> Option<ReorgReport<N>> {
6325 if dropped.is_empty() {
6326 return None;
6327 }
6328
6329 let first_dropped_block = dropped
6330 .iter()
6331 .map(|entry| entry.block.number)
6332 .min()
6333 .expect("non-empty dropped journal set");
6334 self.rebase_validation_state_from(first_dropped_block);
6335 if self
6336 .safe_head
6337 .is_some_and(|head| head.number >= first_dropped_block)
6338 {
6339 self.safe_head = None;
6340 }
6341
6342 let dropped_blocks: Vec<_> = dropped.iter().map(|entry| entry.block).collect();
6343 let dropped_inputs: Vec<_> = dropped
6344 .iter()
6345 .flat_map(|entry| entry.inputs.iter().copied())
6346 .collect();
6347 let canceled_resyncs = self.cancel_resyncs_for_dropped_blocks(&dropped_blocks);
6348 let purge_scopes = purge_scopes_for_dropped_journals(&dropped);
6349 let rollback_updates = rollback_updates_for_dropped_journals(&dropped, &purge_scopes);
6350 let purge_updates: Vec<_> = purge_scopes
6351 .iter()
6352 .map(|(address, scope)| StateUpdate::purge(*address, scope.clone()))
6353 .collect();
6354
6355 let rollback_diff = if rollback_updates.is_empty() {
6356 StateDiff::default()
6357 } else {
6358 cache.apply_updates(&rollback_updates)
6359 };
6360 let purge_diff = if purge_updates.is_empty() {
6361 StateDiff::default()
6362 } else {
6363 cache.apply_updates(&purge_updates)
6364 };
6365 self.coverage_head = self.journal.back().map(|entry| entry.block);
6366
6367 Some(ReorgReport {
6368 dropped: dropped_blocks.first().cloned(),
6369 dropped_blocks,
6370 dropped_inputs,
6371 rollback_updates,
6372 rollback_diff,
6373 purge_updates,
6374 purge_diff,
6375 canceled_resyncs,
6376 reason,
6377 _network: PhantomData,
6378 })
6379 }
6380
6381 fn rebase_validation_state_from(&mut self, first_dropped_block: u64) {
6382 if let Some(freshness) = self.freshness.as_mut() {
6383 freshness.invalidate_valid_through_from(first_dropped_block);
6384 }
6385 self.tracked_roots
6386 .retain(|_, baseline| baseline.last_block < first_dropped_block);
6387 if self
6388 .last_gate_block
6389 .is_some_and(|block| block >= first_dropped_block)
6390 {
6391 self.last_gate_block = self
6392 .tracked_roots
6393 .values()
6394 .map(|baseline| baseline.last_block)
6395 .max();
6396 }
6397 self.touched_since_gate.clear();
6401 }
6402
6403 fn cancel_resyncs_for_dropped_blocks(
6404 &mut self,
6405 dropped_blocks: &[BlockRef],
6406 ) -> Vec<ResyncRequest> {
6407 let mut canceled = Vec::new();
6408 self.pending_resyncs.retain(|request| {
6409 let should_cancel = resync_request_targets_dropped_block(request, dropped_blocks);
6410 if should_cancel {
6411 canceled.push(request.clone());
6412 }
6413 !should_cancel
6414 });
6415 canceled
6416 }
6417
6418 fn remove_pending_resyncs<'a>(&mut self, ids: impl IntoIterator<Item = &'a ResyncId>) {
6419 let ids: HashSet<_> = ids.into_iter().cloned().collect();
6420 self.pending_resyncs
6421 .retain(|request| !ids.contains(&request.id));
6422 }
6423}
6424
6425fn install_preconfirmed_cache_context(cache: &mut EvmCache, flashblock: &FlashblockRef) {
6426 cache.set_block(BlockId::pending());
6427 cache.set_block_context(Some(flashblock.block_number), flashblock.base_fee_per_gas);
6428 cache.set_coinbase(flashblock.beneficiary);
6429 cache.set_prevrandao(flashblock.prevrandao);
6430 cache.set_block_gas_limit(flashblock.gas_limit);
6431 cache.set_timestamp(flashblock.timestamp);
6432}
6433
6434pub fn validate_canonical_sequence<N: Network>(
6466 state: &CanonicalSequenceState,
6467 batch: &ReactiveInputBatch<N>,
6468) -> Result<CanonicalSequenceValidation, ReactiveError> {
6469 validate_canonical_sequence_diagnostic(state, batch)
6470 .map_err(CanonicalSequenceError::into_reactive_error)
6471}
6472
6473pub fn validate_canonical_sequence_diagnostic<N: Network>(
6488 state: &CanonicalSequenceState,
6489 batch: &ReactiveInputBatch<N>,
6490) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6491 validate_canonical_sequence_internal(
6492 state,
6493 batch,
6494 CanonicalSequenceValidationPolicy::RequireCompleteRollback,
6495 )
6496}
6497
6498pub fn normalize_and_validate_canonical_sequence<N: Network>(
6521 state: &CanonicalSequenceState,
6522 batch: &ReactiveInputBatch<N>,
6523) -> Result<CanonicalSequenceValidation, ReactiveError> {
6524 normalize_and_validate_canonical_sequence_diagnostic(state, batch)
6525 .map_err(CanonicalSequenceError::into_reactive_error)
6526}
6527
6528pub fn normalize_and_validate_canonical_sequence_diagnostic<N: Network>(
6543 state: &CanonicalSequenceState,
6544 batch: &ReactiveInputBatch<N>,
6545) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6546 validate_canonical_sequence_internal(
6547 state,
6548 batch,
6549 CanonicalSequenceValidationPolicy::RequireCompleteRollbackNormalizeCoverage,
6550 )
6551}
6552
6553fn validate_canonical_sequence_internal<N: Network>(
6554 state: &CanonicalSequenceState,
6555 batch: &ReactiveInputBatch<N>,
6556 policy: CanonicalSequenceValidationPolicy,
6557) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6558 let records = batch
6559 .records()
6560 .iter()
6561 .enumerate()
6562 .map(|(index, record)| {
6563 (
6564 record.clone(),
6565 DeliveryAudience::All,
6566 batch
6567 .record_delivery_scope(index)
6568 .expect("enumerated record always has a delivery scope"),
6569 )
6570 })
6571 .collect::<Vec<_>>();
6572 let records = sort_scoped_records(dedupe_scoped_records(records)?);
6573 let records = records
6574 .iter()
6575 .map(|(record, _, scope)| (record, *scope))
6576 .collect::<Vec<_>>();
6577 validate_canonical_sequence_parts(state, batch.chain_controls(), &records, policy)
6578}
6579
6580#[derive(Clone, Copy)]
6581enum CanonicalSequenceValidationPolicy {
6582 RequireCompleteRollback,
6583 RequireCompleteRollbackNormalizeCoverage,
6584 ObserveIncompleteRollback,
6585}
6586
6587#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6590#[non_exhaustive]
6591pub enum CanonicalRollbackKind {
6592 Explicit,
6594 Removed,
6596 ImplicitParent,
6598 MissingReplacement,
6600}
6601
6602#[derive(Debug, thiserror::Error)]
6607#[non_exhaustive]
6608pub enum CanonicalSequenceError {
6609 #[error(transparent)]
6611 Invalid(#[from] ReactiveError),
6612 #[error(
6615 "{kind:?} rollback after block {common_ancestor} exceeds retained canonical history starting at {oldest_retained:?}"
6616 )]
6617 IncompleteRollback {
6618 common_ancestor: u64,
6620 oldest_retained: Option<u64>,
6622 kind: CanonicalRollbackKind,
6624 },
6625}
6626
6627#[derive(Clone, Copy, Debug)]
6628struct RequiredReorgAnchor {
6629 number: u64,
6630 block: Option<BlockRef>,
6631 permits_missing_child_parent: bool,
6632 must_be_consumed: bool,
6633}
6634
6635#[derive(Debug)]
6636struct SequenceRewind {
6637 common_ancestor: Option<BlockRef>,
6638 dropped: Vec<BlockRef>,
6639}
6640
6641impl RequiredReorgAnchor {
6642 const fn hash(self) -> Option<B256> {
6643 match self.block {
6644 Some(block) => Some(block.hash),
6645 None => None,
6646 }
6647 }
6648}
6649
6650impl CanonicalSequenceError {
6651 pub const fn requires_history(&self) -> bool {
6654 matches!(self, Self::IncompleteRollback { .. })
6655 }
6656
6657 pub fn into_reactive_error(self) -> ReactiveError {
6659 match self {
6660 Self::Invalid(error) => error,
6661 Self::IncompleteRollback {
6662 common_ancestor,
6663 oldest_retained,
6664 kind,
6665 } => ReactiveError::InvalidChainControl {
6666 message: format!(
6667 "{kind:?} rollback after block {common_ancestor} exceeds retained canonical history starting at {oldest_retained:?}"
6668 ),
6669 },
6670 }
6671 }
6672}
6673
6674impl CanonicalSequenceValidationPolicy {
6675 const fn requires_complete_rollback(self) -> bool {
6676 matches!(
6677 self,
6678 Self::RequireCompleteRollback | Self::RequireCompleteRollbackNormalizeCoverage
6679 )
6680 }
6681
6682 const fn normalizes_coverage(self) -> bool {
6683 matches!(self, Self::RequireCompleteRollbackNormalizeCoverage)
6684 }
6685}
6686
6687fn validate_canonical_sequence_parts<N: Network>(
6688 initial: &CanonicalSequenceState,
6689 controls: &[ChainControl],
6690 records: &[(&ReactiveInputRecord<N>, DeliveryScope)],
6691 policy: CanonicalSequenceValidationPolicy,
6692) -> Result<CanonicalSequenceValidation, CanonicalSequenceError> {
6693 validate_canonical_sequence_snapshot(initial)?;
6694 let control_split = validate_control_phase_order(controls)?;
6695 let (pre_record_controls, post_record_controls) = controls.split_at(control_split);
6696 let mut state = initial.clone();
6697 let mut asserted_blocks = HashMap::<u64, BlockRef>::new();
6698 let mut mutations = Vec::new();
6699 let mut normalized_chain_controls = Vec::with_capacity(controls.len());
6700 let mut batch_dropped = BatchDroppedCanonical::default();
6701 let mut removed_assertions = HashMap::<(u64, B256), BlockRef>::new();
6702 let mut removed_heights_by_hash = HashMap::<B256, u64>::new();
6703 let mut record_proof_control_identities = HashSet::<(u64, B256)>::new();
6704 let rollback_oldest = initial
6705 .retained_canonical_history
6706 .first()
6707 .map(|block| block.number);
6708
6709 for control in pre_record_controls {
6710 normalized_chain_controls.push(control.clone());
6711 validate_sequence_control(&state, control)?;
6712 assert_chain_control_identities(&mut asserted_blocks, control)?;
6713 let ChainControl::Reorg {
6714 common_ancestor,
6715 old_tip,
6716 ..
6717 } = control
6718 else {
6719 unreachable!("phase validation leaves only reorg controls before records")
6720 };
6721 let exact_ancestor = state.retained_canonical_history.iter().any(|block| {
6722 block.number == common_ancestor.number && block.hash == common_ancestor.hash
6723 });
6724 let rollback_horizon_covers_ancestor = state
6725 .retained_canonical_history
6726 .first()
6727 .is_some_and(|oldest| oldest.number <= common_ancestor.number);
6728 if policy.requires_complete_rollback()
6729 && !exact_ancestor
6730 && !rollback_horizon_covers_ancestor
6731 {
6732 return Err(CanonicalSequenceError::IncompleteRollback {
6733 common_ancestor: common_ancestor.number,
6734 oldest_retained: rollback_oldest,
6735 kind: CanonicalRollbackKind::Explicit,
6736 });
6737 }
6738 let dropped = state
6739 .retained_canonical_history
6740 .iter()
6741 .copied()
6742 .filter(|block| block.number > common_ancestor.number)
6743 .collect::<Vec<_>>();
6744 state
6745 .retained_canonical_history
6746 .retain(|block| block.number <= common_ancestor.number);
6747 upsert_sequence_history(&mut state.retained_canonical_history, common_ancestor)?;
6748 let mut enriched_ancestor = *common_ancestor;
6749 if let Some(retained) = state.retained_canonical_history.iter().find(|block| {
6750 block.number == common_ancestor.number && block.hash == common_ancestor.hash
6751 }) {
6752 enrich_block_ref(&mut enriched_ancestor, retained);
6753 }
6754 if let Some(coverage) = state.coverage_head.as_ref()
6755 && coverage.number == common_ancestor.number
6756 && coverage.hash == common_ancestor.hash
6757 {
6758 enrich_block_ref(&mut enriched_ancestor, coverage);
6759 }
6760 upsert_sequence_history(&mut state.retained_canonical_history, &enriched_ancestor)?;
6761 state.coverage_head = Some(enriched_ancestor);
6762 clear_sequence_heads_above(&mut state, &enriched_ancestor);
6763 batch_dropped.record_explicit(common_ancestor, old_tip);
6764 batch_dropped.record_drained(&dropped);
6765 mutations.push(CanonicalSequenceMutation::Rewind {
6766 common_ancestor: Some(enriched_ancestor),
6767 dropped,
6768 });
6769 }
6770 let pre_record_state = state.clone();
6771 let mut required_reorg_anchor = None::<RequiredReorgAnchor>;
6772
6773 for (record, scope) in records {
6774 if !scope.advances_canonical_state() {
6775 continue;
6776 }
6777 if let Some((incoming_dropped_block, _)) = reorg_signal_block(record) {
6778 let incoming_dropped_block =
6779 resolve_record_block_payload_metadata(record, incoming_dropped_block)?;
6780 validate_sequence_matching_metadata(&state, &incoming_dropped_block, "removed record")?;
6781 validate_sequence_adjacent_parent_identity(
6782 &state,
6783 &incoming_dropped_block,
6784 "removed record",
6785 )?;
6786 let mut dropped_block = state
6787 .retained_canonical_history
6788 .iter()
6789 .find(|known| {
6790 known.number == incoming_dropped_block.number
6791 && known.hash == incoming_dropped_block.hash
6792 })
6793 .copied()
6794 .or_else(|| {
6795 state.coverage_head.filter(|known| {
6796 known.number == incoming_dropped_block.number
6797 && known.hash == incoming_dropped_block.hash
6798 })
6799 })
6800 .unwrap_or(incoming_dropped_block);
6801 enrich_block_ref(&mut dropped_block, &incoming_dropped_block);
6802 validate_sequence_implicit_finality(&state, record, None)?;
6803 if dropped_block.number == 0 {
6804 return Err(ReactiveError::InvalidChainControl {
6805 message: "a removed/reorged genesis block has no canonical parent anchor"
6806 .into(),
6807 }
6808 .into());
6809 }
6810 let removed_identity = (dropped_block.number, dropped_block.hash);
6811 if let Some(previous_number) =
6812 removed_heights_by_hash.insert(dropped_block.hash, dropped_block.number)
6813 && previous_number != dropped_block.number
6814 {
6815 return Err(ReactiveError::InvalidChainControl {
6816 message: format!(
6817 "removed hash {:?} is reused at heights {} and {}",
6818 dropped_block.hash, previous_number, dropped_block.number
6819 ),
6820 }
6821 .into());
6822 }
6823 if let Some(previous) = removed_assertions.get_mut(&removed_identity) {
6824 if !optional_block_refs_are_compatible(Some(previous), Some(&dropped_block)) {
6825 return Err(ReactiveError::InvalidChainControl {
6826 message: format!(
6827 "duplicate removed block {}:{:?} carries conflicting metadata",
6828 dropped_block.number, dropped_block.hash
6829 ),
6830 }
6831 .into());
6832 }
6833 enrich_block_ref(previous, &dropped_block);
6834 } else {
6835 removed_assertions.insert(removed_identity, dropped_block);
6836 }
6837 if asserted_blocks
6838 .get(&dropped_block.number)
6839 .is_some_and(|asserted| asserted.hash == dropped_block.hash)
6840 {
6841 return Err(ReactiveError::InvalidChainControl {
6842 message: format!(
6843 "removed block {}:{:?} is asserted canonical by the same envelope",
6844 dropped_block.number, dropped_block.hash
6845 ),
6846 }
6847 .into());
6848 }
6849 if batch_dropped.contains(&dropped_block) {
6850 continue;
6851 }
6852 if let Some(index) = state.retained_canonical_history.iter().position(|block| {
6853 block.number == dropped_block.number && block.hash == dropped_block.hash
6854 }) {
6855 let dropped = state.retained_canonical_history.split_off(index);
6856 batch_dropped.record_drained(&dropped);
6857 let ancestor_number = dropped_block
6858 .number
6859 .checked_sub(1)
6860 .expect("genesis removal was rejected above");
6861 let retained_anchor = state
6862 .retained_canonical_history
6863 .iter()
6864 .rev()
6865 .find(|head| head.number == ancestor_number)
6866 .copied();
6867 let authenticated_anchor = retained_anchor
6868 .or_else(|| {
6869 dropped_block.parent_hash.map(|hash| BlockRef {
6870 number: ancestor_number,
6871 hash,
6872 parent_hash: None,
6873 timestamp: None,
6874 })
6875 })
6876 .or_else(|| {
6877 state
6878 .finalized_head
6879 .filter(|head| head.number == ancestor_number)
6880 });
6881 let authenticated_anchor = authenticated_anchor.map(|mut anchor| {
6882 for head in [state.safe_head.as_ref(), state.finalized_head.as_ref()]
6883 .into_iter()
6884 .flatten()
6885 {
6886 if head.number == anchor.number && head.hash == anchor.hash {
6887 enrich_block_ref(&mut anchor, head);
6888 }
6889 }
6890 anchor
6891 });
6892 required_reorg_anchor = Some(RequiredReorgAnchor {
6893 number: ancestor_number,
6894 block: authenticated_anchor,
6895 permits_missing_child_parent: retained_anchor.is_some(),
6896 must_be_consumed: authenticated_anchor.is_none()
6897 && state.retained_canonical_history.is_empty(),
6898 });
6899 state.coverage_head = authenticated_anchor
6900 .or_else(|| state.retained_canonical_history.last().copied());
6901 if let Some(head) = state.coverage_head {
6902 clear_sequence_heads_above(&mut state, &head);
6903 } else {
6904 state.safe_head = None;
6905 state.finalized_head = None;
6906 }
6907 mutations.push(CanonicalSequenceMutation::Rewind {
6908 common_ancestor: state.coverage_head,
6909 dropped,
6910 });
6911 } else {
6912 let replacement_is_known = state.retained_canonical_history.iter().any(|block| {
6913 block.number == dropped_block.number && block.hash != dropped_block.hash
6914 }) || state.coverage_head.is_some_and(|head| {
6915 head.number == dropped_block.number && head.hash != dropped_block.hash
6916 });
6917 if !replacement_is_known {
6918 if policy.requires_complete_rollback() {
6923 return Err(CanonicalSequenceError::IncompleteRollback {
6924 common_ancestor: dropped_block
6925 .number
6926 .checked_sub(1)
6927 .expect("genesis removal was rejected above"),
6928 oldest_retained: rollback_oldest,
6929 kind: CanonicalRollbackKind::Removed,
6930 });
6931 }
6932 continue;
6933 }
6934 }
6935 continue;
6936 }
6937
6938 let Some(context_block) = canonical_record_block(record) else {
6939 continue;
6940 };
6941 let incoming_block = resolve_record_block_payload_metadata(record, *context_block)?;
6942 if post_record_controls
6943 .iter()
6944 .filter_map(canonical_coverage_control_block)
6945 .any(|asserted| {
6946 asserted.number == incoming_block.number
6947 && asserted.hash == incoming_block.hash
6948 && optional_block_refs_are_compatible(Some(asserted), Some(&incoming_block))
6949 && ((incoming_block.parent_hash.is_none() && asserted.parent_hash.is_some())
6950 || (incoming_block.timestamp.is_none() && asserted.timestamp.is_some()))
6951 })
6952 {
6953 record_proof_control_identities.insert((incoming_block.number, incoming_block.hash));
6954 }
6955 let mut resolved_block = incoming_block;
6956 if let Some(asserted) = asserted_blocks
6957 .get(&incoming_block.number)
6958 .filter(|asserted| asserted.hash == incoming_block.hash)
6959 {
6960 if !optional_block_refs_are_compatible(Some(asserted), Some(&incoming_block)) {
6961 return Err(ReactiveError::InvalidChainControl {
6962 message: format!(
6963 "canonical record {}:{:?} conflicts with the same envelope's asserted metadata",
6964 incoming_block.number, incoming_block.hash
6965 ),
6966 }
6967 .into());
6968 }
6969 enrich_block_ref(&mut resolved_block, asserted);
6970 }
6971 for asserted in post_record_controls
6972 .iter()
6973 .filter_map(chain_control_canonical_assertion)
6974 .filter(|asserted| {
6975 asserted.number == incoming_block.number && asserted.hash == incoming_block.hash
6976 })
6977 {
6978 if !optional_block_refs_are_compatible(Some(&resolved_block), Some(asserted)) {
6979 return Err(ReactiveError::InvalidChainControl {
6980 message: format!(
6981 "canonical record {}:{:?} conflicts with the same envelope's asserted metadata",
6982 incoming_block.number, incoming_block.hash
6983 ),
6984 }
6985 .into());
6986 }
6987 enrich_block_ref(&mut resolved_block, asserted);
6988 }
6989 let replacement_anchor =
6990 required_reorg_anchor.filter(|required| resolved_block.number > required.number);
6991 if resolved_block.parent_hash.is_none()
6992 && replacement_anchor.is_some_and(|anchor| {
6993 anchor.permits_missing_child_parent
6994 && anchor.number.checked_add(1) == Some(resolved_block.number)
6995 })
6996 {
6997 resolved_block.parent_hash = replacement_anchor.and_then(RequiredReorgAnchor::hash);
6998 }
6999 let block = &resolved_block;
7000 if removed_assertions.contains_key(&(block.number, block.hash)) {
7001 return Err(ReactiveError::InvalidChainControl {
7002 message: format!(
7003 "canonical block {}:{:?} is also removed by the same envelope",
7004 block.number, block.hash
7005 ),
7006 }
7007 .into());
7008 }
7009 if let Some(removed_number) = removed_heights_by_hash.get(&block.hash)
7010 && *removed_number != block.number
7011 {
7012 return Err(ReactiveError::InvalidChainControl {
7013 message: format!(
7014 "canonical hash {:?} at height {} is removed at height {} by the same envelope",
7015 block.hash, block.number, removed_number
7016 ),
7017 }
7018 .into());
7019 }
7020 let replacement_proven_by_removal =
7021 validate_replacement_reorg_anchor(replacement_anchor, block, policy, rollback_oldest)?;
7022 if replacement_anchor.is_some() {
7023 required_reorg_anchor = None;
7024 }
7025 validate_sequence_matching_metadata(&state, block, "canonical record")?;
7026 validate_sequence_implicit_finality(&state, record, Some(block))?;
7027 let implicit_replacement_requires_history = if replacement_proven_by_removal {
7028 false
7029 } else {
7030 sequence_implicit_replacement_requires_history(&state, block, policy)?
7031 };
7032 if implicit_replacement_requires_history && policy.requires_complete_rollback() {
7033 return Err(CanonicalSequenceError::IncompleteRollback {
7034 common_ancestor: block.number.saturating_sub(1),
7035 oldest_retained: rollback_oldest,
7036 kind: CanonicalRollbackKind::ImplicitParent,
7037 });
7038 }
7039 assert_canonical_block_identity(&mut asserted_blocks, block, "canonical record")?;
7040 let allow_parentless_extension = replacement_anchor.is_some_and(|anchor| {
7041 anchor.permits_missing_child_parent
7042 && anchor.number.checked_add(1) == Some(block.number)
7043 });
7044 if let Some(rewind) =
7045 apply_sequence_canonical_block(&mut state, block, allow_parentless_extension)?
7046 {
7047 mutations.push(CanonicalSequenceMutation::Rewind {
7048 common_ancestor: rewind.common_ancestor,
7049 dropped: rewind.dropped,
7050 });
7051 }
7052 mutations.push(CanonicalSequenceMutation::Canonical(*block));
7053 }
7054
7055 for control in post_record_controls {
7056 if let Some(block) = chain_control_canonical_assertion(control)
7057 && removed_assertions.contains_key(&(block.number, block.hash))
7058 {
7059 return Err(ReactiveError::InvalidChainControl {
7060 message: format!(
7061 "canonical block {}:{:?} is also removed by the same envelope",
7062 block.number, block.hash
7063 ),
7064 }
7065 .into());
7066 }
7067 if let Some(block) = chain_control_canonical_assertion(control)
7068 && let Some(removed_number) = removed_heights_by_hash.get(&block.hash)
7069 && *removed_number != block.number
7070 {
7071 return Err(ReactiveError::InvalidChainControl {
7072 message: format!(
7073 "canonical hash {:?} at height {} is removed at height {} by the same envelope",
7074 block.hash, block.number, removed_number
7075 ),
7076 }
7077 .into());
7078 }
7079 let replacement_anchor = canonical_coverage_control_block(control).and_then(|block| {
7080 required_reorg_anchor.filter(|required| block.number > required.number)
7081 });
7082 if let Some(block) = canonical_coverage_control_block(control) {
7083 validate_replacement_reorg_anchor(replacement_anchor, block, policy, rollback_oldest)?;
7084 if replacement_anchor.is_some() {
7085 required_reorg_anchor = None;
7086 }
7087 }
7088 assert_chain_control_identities(&mut asserted_blocks, control)?;
7089 let preserves_record_proof =
7090 canonical_coverage_control_block(control).is_some_and(|block| {
7091 record_proof_control_identities.contains(&(block.number, block.hash))
7092 });
7093 if policy.normalizes_coverage()
7094 && !preserves_record_proof
7095 && let Some(block) = canonical_coverage_control_block(control)
7096 && state
7097 .coverage_head
7098 .is_some_and(|head| block.number <= head.number)
7099 {
7100 let is_equal_coverage = state
7101 .coverage_head
7102 .is_some_and(|head| block.number == head.number);
7103 let known = state
7104 .coverage_head
7105 .as_ref()
7106 .filter(|head| head.number == block.number && head.hash == block.hash)
7107 .or_else(|| {
7108 state
7109 .retained_canonical_history
7110 .iter()
7111 .find(|entry| entry.number == block.number && entry.hash == block.hash)
7112 });
7113 if let Some(known) = known
7114 && optional_block_refs_are_compatible(Some(known), Some(block))
7115 && (!is_equal_coverage || !sequence_block_adds_metadata(&state, block))
7116 {
7117 if let ChainControl::Barrier { id, .. } = control {
7118 normalized_chain_controls.push(ChainControl::Barrier {
7119 id: id.clone(),
7120 block: None,
7121 });
7122 }
7123 continue;
7124 }
7125 }
7126 validate_sequence_control(&state, control)?;
7127 normalized_chain_controls.push(control.clone());
7128 match control {
7129 ChainControl::Safe(block) => {
7130 set_or_enrich_block_ref(&mut state.safe_head, block);
7131 mutations.push(CanonicalSequenceMutation::Safe(
7132 state.safe_head.expect("safe head was just installed"),
7133 ));
7134 }
7135 ChainControl::Finalized(block) => {
7136 set_or_enrich_block_ref(&mut state.finalized_head, block);
7137 mutations.push(CanonicalSequenceMutation::Finalized(
7138 state
7139 .finalized_head
7140 .expect("finalized head was just installed"),
7141 ));
7142 }
7143 ChainControl::CanonicalProgress(block)
7144 | ChainControl::Barrier {
7145 block: Some(block), ..
7146 } => {
7147 let allow_parentless_extension = replacement_anchor.is_some_and(|anchor| {
7148 anchor.permits_missing_child_parent
7149 && anchor.number.checked_add(1) == Some(block.number)
7150 }) || (replacement_anchor.is_none()
7151 && block.parent_hash.is_none()
7152 && state
7153 .coverage_head
7154 .is_some_and(|head| head.number.checked_add(1) == Some(block.number)));
7155 if let Some(rewind) =
7156 apply_sequence_canonical_block(&mut state, block, allow_parentless_extension)?
7157 {
7158 mutations.push(CanonicalSequenceMutation::Rewind {
7159 common_ancestor: rewind.common_ancestor,
7160 dropped: rewind.dropped,
7161 });
7162 }
7163 mutations.push(CanonicalSequenceMutation::Canonical(*block));
7164 }
7165 ChainControl::Barrier { block: None, .. } => {}
7166 ChainControl::Reorg { .. } => {
7167 unreachable!("phase validation excludes post-record reorg controls")
7168 }
7169 }
7170 }
7171
7172 if let Some(required) = required_reorg_anchor
7173 && required.must_be_consumed
7174 && policy.requires_complete_rollback()
7175 {
7176 return Err(CanonicalSequenceError::IncompleteRollback {
7177 common_ancestor: required.number,
7178 oldest_retained: rollback_oldest,
7179 kind: CanonicalRollbackKind::MissingReplacement,
7180 });
7181 }
7182
7183 validate_canonical_sequence_snapshot(&state)?;
7184 Ok(CanonicalSequenceValidation {
7185 pre_record_state,
7186 next_state: state,
7187 mutations,
7188 normalized_chain_controls,
7189 })
7190}
7191
7192fn validate_canonical_sequence_snapshot(
7193 state: &CanonicalSequenceState,
7194) -> Result<(), ReactiveError> {
7195 let invalid = |message: String| ReactiveError::InvalidChainControl { message };
7196 let supplied_blocks = state
7197 .retained_canonical_history
7198 .iter()
7199 .chain(state.coverage_head.iter())
7200 .chain(state.safe_head.iter())
7201 .chain(state.finalized_head.iter())
7202 .collect::<Vec<_>>();
7203 validate_known_parent_hash_heights(&supplied_blocks)?;
7204 let mut prior = None::<BlockRef>;
7205 for block in &state.retained_canonical_history {
7206 if let Some(previous) = prior {
7207 if block.number < previous.number {
7208 return Err(invalid(
7209 "retained canonical history is not ordered by block number".into(),
7210 ));
7211 }
7212 if block.number == previous.number {
7213 let qualifier = if optional_block_refs_are_compatible(Some(&previous), Some(block))
7214 {
7215 "duplicate"
7216 } else {
7217 "conflicting"
7218 };
7219 return Err(invalid(format!(
7220 "retained canonical history contains {qualifier} identities at block {}",
7221 block.number
7222 )));
7223 }
7224 if previous.number.checked_add(1) == Some(block.number)
7225 && block.parent_hash.is_some()
7226 && block.parent_hash != Some(previous.hash)
7227 {
7228 return Err(invalid(format!(
7229 "adjacent retained block {}:{:?} does not descend from {}:{:?}",
7230 block.number, block.hash, previous.number, previous.hash
7231 )));
7232 }
7233 }
7234 prior = Some(*block);
7235 }
7236 if state.coverage_head.is_none() && !state.retained_canonical_history.is_empty() {
7237 return Err(invalid(
7238 "retained canonical history requires an authoritative coverage head".into(),
7239 ));
7240 }
7241 if let Some(head) = state.coverage_head.as_ref() {
7242 if let Some(retained) = state
7243 .retained_canonical_history
7244 .iter()
7245 .find(|entry| entry.number == head.number)
7246 && !optional_block_refs_are_compatible(Some(retained), Some(head))
7247 {
7248 return Err(invalid(format!(
7249 "coverage head {}:{:?} conflicts with retained identity {:?}",
7250 head.number, head.hash, retained
7251 )));
7252 }
7253 if state
7254 .retained_canonical_history
7255 .last()
7256 .is_some_and(|retained| retained.number > head.number)
7257 {
7258 return Err(invalid(
7259 "retained canonical history advances beyond the coverage head".into(),
7260 ));
7261 }
7262 if let Some(retained) = state.retained_canonical_history.last()
7263 && retained.number.checked_add(1) == Some(head.number)
7264 && head.parent_hash.is_some()
7265 && head.parent_hash != Some(retained.hash)
7266 {
7267 return Err(invalid(format!(
7268 "coverage head {}:{:?} does not descend from adjacent retained block {}:{:?}",
7269 head.number, head.hash, retained.number, retained.hash
7270 )));
7271 }
7272 }
7273 if let Some(safe) = state.safe_head.as_ref() {
7274 validate_sequence_known_identity(state, safe, "safe")?;
7275 validate_sequence_head_within_coverage(state, safe, "safe")?;
7276 validate_coverage_descends_from_adjacent_head(state.coverage_head.as_ref(), safe, "safe")?;
7277 }
7278 if let Some(finalized) = state.finalized_head.as_ref() {
7279 validate_sequence_known_identity(state, finalized, "finalized")?;
7280 validate_sequence_head_within_coverage(state, finalized, "finalized")?;
7281 validate_coverage_descends_from_adjacent_head(
7282 state.coverage_head.as_ref(),
7283 finalized,
7284 "finalized",
7285 )?;
7286 }
7287 validate_adjacent_finality(state.finalized_head.as_ref(), state.safe_head.as_ref())?;
7288 if let (Some(finalized), Some(safe)) = (state.finalized_head, state.safe_head)
7289 && (finalized.number > safe.number
7290 || (finalized.number == safe.number && finalized.hash != safe.hash))
7291 {
7292 return Err(invalid(
7293 "finalized head cannot advance beyond or conflict with safe head".into(),
7294 ));
7295 }
7296 Ok(())
7297}
7298
7299fn validate_known_parent_hash_heights(blocks: &[&BlockRef]) -> Result<(), ReactiveError> {
7300 let mut heights_by_hash = HashMap::<B256, u64>::with_capacity(blocks.len());
7301 let mut resolved_by_height = HashMap::<u64, BlockRef>::with_capacity(blocks.len());
7302 for block in blocks.iter().copied() {
7303 if let Some(previous_height) = heights_by_hash.insert(block.hash, block.number)
7304 && previous_height != block.number
7305 {
7306 return Err(ReactiveError::InvalidChainControl {
7307 message: format!(
7308 "canonical hash {:?} is reused at heights {} and {}",
7309 block.hash, previous_height, block.number
7310 ),
7311 });
7312 }
7313 if let Some(resolved) = resolved_by_height.get_mut(&block.number) {
7314 if !optional_block_refs_are_compatible(Some(resolved), Some(block)) {
7315 return Err(ReactiveError::InvalidChainControl {
7316 message: format!(
7317 "canonical aliases at height {} carry conflicting identities or metadata",
7318 block.number
7319 ),
7320 });
7321 }
7322 enrich_block_ref(resolved, block);
7323 } else {
7324 resolved_by_height.insert(block.number, *block);
7325 }
7326 }
7327 for child in resolved_by_height.values() {
7328 let Some(parent_hash) = child.parent_hash else {
7329 continue;
7330 };
7331 if let Some(parent_number) = heights_by_hash.get(&parent_hash)
7332 && parent_number.checked_add(1) != Some(child.number)
7333 {
7334 return Err(ReactiveError::InvalidChainControl {
7335 message: format!(
7336 "block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
7337 child.number, child.hash, parent_hash, parent_number
7338 ),
7339 });
7340 }
7341 if let Some(parent_number) = child.number.checked_sub(1)
7342 && let Some(parent) = resolved_by_height.get(&parent_number)
7343 && parent.hash != parent_hash
7344 {
7345 return Err(ReactiveError::InvalidChainControl {
7346 message: format!(
7347 "block {}:{:?} does not descend from supplied adjacent identity {}:{:?}",
7348 child.number, child.hash, parent.number, parent.hash
7349 ),
7350 });
7351 }
7352 }
7353 Ok(())
7354}
7355
7356fn validate_coverage_descends_from_adjacent_head(
7357 coverage: Option<&BlockRef>,
7358 head: &BlockRef,
7359 label: &str,
7360) -> Result<(), ReactiveError> {
7361 let Some(coverage) = coverage else {
7362 return Ok(());
7363 };
7364 if head.number.checked_add(1) == Some(coverage.number)
7365 && coverage
7366 .parent_hash
7367 .is_some_and(|parent| parent != head.hash)
7368 {
7369 return Err(ReactiveError::InvalidChainControl {
7370 message: format!(
7371 "canonical coverage {}:{:?} does not descend from adjacent {label} head {}:{:?}",
7372 coverage.number, coverage.hash, head.number, head.hash
7373 ),
7374 });
7375 }
7376 Ok(())
7377}
7378
7379fn validate_sequence_control(
7380 state: &CanonicalSequenceState,
7381 control: &ChainControl,
7382) -> Result<(), ReactiveError> {
7383 let invalid = |message: String| ReactiveError::InvalidChainControl { message };
7384 match control {
7385 ChainControl::Safe(block) => {
7386 validate_sequence_known_identity(state, block, "safe")?;
7387 validate_sequence_head_within_coverage(state, block, "safe")?;
7388 if let Some(current) = state.safe_head.as_ref()
7389 && (block.number < current.number
7390 || (block.number == current.number
7391 && (block.hash != current.hash
7392 || !optional_block_refs_are_compatible(Some(block), Some(current)))))
7393 {
7394 return Err(invalid(format!(
7395 "safe head {}:{:?} conflicts with current {}:{:?}",
7396 block.number, block.hash, current.number, current.hash
7397 )));
7398 }
7399 if let Some(finalized) = state.finalized_head.as_ref()
7400 && (block.number < finalized.number
7401 || (block.number == finalized.number && block.hash != finalized.hash))
7402 {
7403 return Err(invalid(
7404 "safe head cannot precede or conflict with finalized head".into(),
7405 ));
7406 }
7407 validate_adjacent_finality(state.finalized_head.as_ref(), Some(block))?;
7408 }
7409 ChainControl::Finalized(block) => {
7410 validate_sequence_known_identity(state, block, "finalized")?;
7411 validate_sequence_head_within_coverage(state, block, "finalized")?;
7412 if let Some(current) = state.finalized_head.as_ref()
7413 && (block.number < current.number
7414 || (block.number == current.number
7415 && (block.hash != current.hash
7416 || !optional_block_refs_are_compatible(Some(block), Some(current)))))
7417 {
7418 return Err(invalid(format!(
7419 "finalized head {}:{:?} conflicts with current {}:{:?}",
7420 block.number, block.hash, current.number, current.hash
7421 )));
7422 }
7423 if let Some(safe) = state.safe_head.as_ref()
7424 && (block.number > safe.number
7425 || (block.number == safe.number && block.hash != safe.hash))
7426 {
7427 return Err(invalid(
7428 "finalized head cannot advance beyond or conflict with safe head".into(),
7429 ));
7430 }
7431 validate_adjacent_finality(Some(block), state.safe_head.as_ref())?;
7432 }
7433 ChainControl::CanonicalProgress(block)
7434 | ChainControl::Barrier {
7435 block: Some(block), ..
7436 } => {
7437 validate_sequence_known_identity(state, block, "canonical coverage")?;
7438 if let Some(current) = state.coverage_head.as_ref()
7439 && (block.number < current.number
7440 || (block.number == current.number && block.hash != current.hash))
7441 {
7442 return Err(invalid(format!(
7443 "canonical coverage {}:{:?} conflicts with current {}:{:?}",
7444 block.number, block.hash, current.number, current.hash
7445 )));
7446 }
7447 if let Some(current) = state.coverage_head.as_ref()
7448 && current.number.checked_add(1) == Some(block.number)
7449 && block.parent_hash.is_some()
7450 && block.parent_hash != Some(current.hash)
7451 {
7452 return Err(invalid(format!(
7453 "canonical coverage {}:{:?} does not descend from current {}:{:?}",
7454 block.number, block.hash, current.number, current.hash
7455 )));
7456 }
7457 }
7458 ChainControl::Barrier { block: None, .. } => {}
7459 ChainControl::Reorg {
7460 common_ancestor,
7461 old_tip,
7462 new_tip,
7463 } => {
7464 validate_sequence_known_identity(state, common_ancestor, "reorg common ancestor")?;
7465 validate_reorg_ancestor_against_retained_branch(state, common_ancestor)?;
7466 validate_sequence_known_hash_height(state, old_tip, "reorg old tip")?;
7467 validate_sequence_known_hash_height(state, new_tip, "reorg new tip")?;
7468 validate_sequence_known_parent_height(state, old_tip, "reorg old tip")?;
7469 validate_sequence_known_parent_height(state, new_tip, "reorg new tip")?;
7470 validate_sequence_adjacent_parent_identity(state, old_tip, "reorg old tip")?;
7471 if let Some(current) = state.coverage_head.as_ref()
7472 && (old_tip.number != current.number
7473 || old_tip.hash != current.hash
7474 || !optional_block_refs_are_compatible(Some(old_tip), Some(current)))
7475 {
7476 return Err(invalid(format!(
7477 "reorg old tip {}:{:?} does not exactly match current metadata {}:{:?}",
7478 old_tip.number, old_tip.hash, current.number, current.hash
7479 )));
7480 }
7481 if common_ancestor.number > old_tip.number || common_ancestor.number > new_tip.number {
7482 return Err(invalid(
7483 "reorg common ancestor cannot be above either branch tip".into(),
7484 ));
7485 }
7486 if common_ancestor.number == old_tip.number || common_ancestor.number == new_tip.number
7487 {
7488 return Err(invalid(
7489 "reorg must replace non-empty old and new branches above the common ancestor"
7490 .into(),
7491 ));
7492 }
7493 if old_tip.number == new_tip.number && old_tip.hash == new_tip.hash {
7494 return Err(invalid(
7495 "reorg old and new tips cannot have the same canonical identity".into(),
7496 ));
7497 }
7498 for (label, tip) in [("old", old_tip), ("new", new_tip)] {
7499 if common_ancestor.number.checked_add(1) == Some(tip.number)
7500 && tip.parent_hash != Some(common_ancestor.hash)
7501 {
7502 return Err(invalid(format!(
7503 "reorg {label} tip does not descend from the common ancestor"
7504 )));
7505 }
7506 }
7507 if let Some(finalized) = state.finalized_head.as_ref()
7508 && (common_ancestor.number < finalized.number
7509 || (common_ancestor.number == finalized.number
7510 && common_ancestor.hash != finalized.hash))
7511 {
7512 return Err(invalid(
7513 "reorg would cross or conflict with the finalized head".into(),
7514 ));
7515 }
7516 }
7517 }
7518 Ok(())
7519}
7520
7521fn validate_sequence_known_identity(
7522 state: &CanonicalSequenceState,
7523 block: &BlockRef,
7524 label: &str,
7525) -> Result<(), ReactiveError> {
7526 validate_sequence_known_hash_height(state, block, label)?;
7527 validate_sequence_known_parent_height(state, block, label)?;
7528 let known = state
7529 .coverage_head
7530 .as_ref()
7531 .filter(|head| head.number == block.number)
7532 .or_else(|| {
7533 state
7534 .retained_canonical_history
7535 .iter()
7536 .find(|entry| entry.number == block.number)
7537 });
7538 if let Some(known) = known
7539 && !optional_block_refs_are_compatible(Some(known), Some(block))
7540 {
7541 return Err(ReactiveError::InvalidChainControl {
7542 message: format!(
7543 "{label} block {}:{:?} conflicts with known canonical block {:?}",
7544 block.number, block.hash, known
7545 ),
7546 });
7547 }
7548 Ok(())
7549}
7550
7551fn validate_sequence_known_parent_height(
7552 state: &CanonicalSequenceState,
7553 block: &BlockRef,
7554 label: &str,
7555) -> Result<(), ReactiveError> {
7556 let Some(parent_hash) = block.parent_hash else {
7557 return Ok(());
7558 };
7559 let known_parent = state
7560 .retained_canonical_history
7561 .iter()
7562 .chain(state.coverage_head.iter())
7563 .chain(state.safe_head.iter())
7564 .chain(state.finalized_head.iter())
7565 .find(|known| known.hash == parent_hash);
7566 if let Some(parent) = known_parent
7567 && parent.number.checked_add(1) != Some(block.number)
7568 {
7569 return Err(ReactiveError::InvalidChainControl {
7570 message: format!(
7571 "{label} block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
7572 block.number, block.hash, parent.hash, parent.number
7573 ),
7574 });
7575 }
7576 Ok(())
7577}
7578
7579fn validate_sequence_head_within_coverage(
7580 state: &CanonicalSequenceState,
7581 block: &BlockRef,
7582 label: &str,
7583) -> Result<(), ReactiveError> {
7584 let Some(coverage) = state.coverage_head.as_ref() else {
7585 return Err(ReactiveError::InvalidChainControl {
7586 message: format!("{label} head requires an authoritative coverage head"),
7587 });
7588 };
7589 if block.number > coverage.number
7590 || (block.number == coverage.number
7591 && !optional_block_refs_are_compatible(Some(block), Some(coverage)))
7592 {
7593 return Err(ReactiveError::InvalidChainControl {
7594 message: format!(
7595 "{label} head {}:{:?} advances beyond or conflicts with coverage {}:{:?}",
7596 block.number, block.hash, coverage.number, coverage.hash
7597 ),
7598 });
7599 }
7600 Ok(())
7601}
7602
7603fn validate_sequence_matching_metadata(
7604 state: &CanonicalSequenceState,
7605 block: &BlockRef,
7606 label: &str,
7607) -> Result<(), ReactiveError> {
7608 validate_sequence_known_hash_height(state, block, label)?;
7609 validate_sequence_known_parent_height(state, block, label)?;
7610 let known = state
7611 .coverage_head
7612 .as_ref()
7613 .filter(|head| head.number == block.number && head.hash == block.hash)
7614 .or_else(|| {
7615 state
7616 .retained_canonical_history
7617 .iter()
7618 .find(|entry| entry.number == block.number && entry.hash == block.hash)
7619 });
7620 if let Some(known) = known
7621 && !optional_block_refs_are_compatible(Some(known), Some(block))
7622 {
7623 return Err(ReactiveError::InvalidChainControl {
7624 message: format!(
7625 "{label} block {}:{:?} carries metadata conflicting with known canonical block {:?}",
7626 block.number, block.hash, known
7627 ),
7628 });
7629 }
7630 Ok(())
7631}
7632
7633fn validate_sequence_known_hash_height(
7634 state: &CanonicalSequenceState,
7635 block: &BlockRef,
7636 label: &str,
7637) -> Result<(), ReactiveError> {
7638 let known = state
7639 .retained_canonical_history
7640 .iter()
7641 .chain(state.coverage_head.iter())
7642 .chain(state.safe_head.iter())
7643 .chain(state.finalized_head.iter())
7644 .find(|known| known.hash == block.hash);
7645 if let Some(known) = known
7646 && known.number != block.number
7647 {
7648 return Err(ReactiveError::InvalidChainControl {
7649 message: format!(
7650 "{label} block {}:{:?} reuses a canonical hash already known at height {}",
7651 block.number, block.hash, known.number
7652 ),
7653 });
7654 }
7655 Ok(())
7656}
7657
7658fn validate_reorg_ancestor_against_retained_branch(
7659 state: &CanonicalSequenceState,
7660 ancestor: &BlockRef,
7661) -> Result<(), ReactiveError> {
7662 let adjacent_number = ancestor.number.checked_add(1);
7663 for retained in state
7664 .retained_canonical_history
7665 .iter()
7666 .chain(state.coverage_head.iter())
7667 .chain(state.safe_head.iter())
7668 .chain(state.finalized_head.iter())
7669 {
7670 if Some(retained.number) == adjacent_number
7671 && retained
7672 .parent_hash
7673 .is_some_and(|parent| parent != ancestor.hash)
7674 {
7675 return Err(ReactiveError::InvalidChainControl {
7676 message: format!(
7677 "reorg common ancestor {}:{:?} conflicts with retained child {}:{:?} parent {:?}",
7678 ancestor.number,
7679 ancestor.hash,
7680 retained.number,
7681 retained.hash,
7682 retained.parent_hash
7683 ),
7684 });
7685 }
7686 if retained.parent_hash == Some(ancestor.hash) && Some(retained.number) != adjacent_number {
7687 return Err(ReactiveError::InvalidChainControl {
7688 message: format!(
7689 "reorg common ancestor {}:{:?} is named as the non-adjacent parent of retained block {}:{:?}",
7690 ancestor.number, ancestor.hash, retained.number, retained.hash
7691 ),
7692 });
7693 }
7694 }
7695 Ok(())
7696}
7697
7698fn validate_sequence_adjacent_parent_identity(
7699 state: &CanonicalSequenceState,
7700 block: &BlockRef,
7701 label: &str,
7702) -> Result<(), ReactiveError> {
7703 let Some(parent_hash) = block.parent_hash else {
7704 return Ok(());
7705 };
7706 let Some(parent_number) = block.number.checked_sub(1) else {
7707 return Ok(());
7708 };
7709 let known_parent = state
7710 .retained_canonical_history
7711 .iter()
7712 .chain(state.coverage_head.iter())
7713 .chain(state.safe_head.iter())
7714 .chain(state.finalized_head.iter())
7715 .find(|known| known.number == parent_number);
7716 if let Some(known_parent) = known_parent
7717 && known_parent.hash != parent_hash
7718 {
7719 return Err(ReactiveError::InvalidChainControl {
7720 message: format!(
7721 "{label} block {}:{:?} names parent {:?}, which conflicts with known adjacent block {}:{:?}",
7722 block.number, block.hash, parent_hash, known_parent.number, known_parent.hash
7723 ),
7724 });
7725 }
7726 Ok(())
7727}
7728
7729fn sequence_block_adds_metadata(state: &CanonicalSequenceState, incoming: &BlockRef) -> bool {
7730 state
7731 .coverage_head
7732 .iter()
7733 .chain(state.retained_canonical_history.iter())
7734 .filter(|known| known.number == incoming.number && known.hash == incoming.hash)
7735 .any(|known| {
7736 (known.parent_hash.is_none() && incoming.parent_hash.is_some())
7737 || (known.timestamp.is_none() && incoming.timestamp.is_some())
7738 })
7739}
7740
7741fn validate_sequence_implicit_finality<N: Network>(
7742 state: &CanonicalSequenceState,
7743 record: &ReactiveInputRecord<N>,
7744 resolved_canonical_block: Option<&BlockRef>,
7745) -> Result<(), ReactiveError> {
7746 let Some(finalized) = state.finalized_head.as_ref() else {
7747 return Ok(());
7748 };
7749 if let Some((dropped, _)) = reorg_signal_block(record) {
7750 if dropped.number <= finalized.number {
7751 return Err(ReactiveError::InvalidChainControl {
7752 message: format!(
7753 "implicit reorg at {}:{:?} would cross finalized head {}:{:?}",
7754 dropped.number, dropped.hash, finalized.number, finalized.hash
7755 ),
7756 });
7757 }
7758 return Ok(());
7759 }
7760 let Some(block) = resolved_canonical_block.or_else(|| canonical_record_block(record)) else {
7761 return Ok(());
7762 };
7763 let Some(latest) = state.coverage_head.as_ref() else {
7764 return Ok(());
7765 };
7766 if (block.number == latest.number && block.hash == latest.hash)
7767 || state
7768 .retained_canonical_history
7769 .iter()
7770 .any(|entry| entry.number == block.number && entry.hash == block.hash)
7771 || (latest.number.checked_add(1) == Some(block.number)
7772 && block.parent_hash == Some(latest.hash))
7773 || latest
7774 .number
7775 .checked_add(1)
7776 .is_some_and(|next| block.number > next)
7777 {
7778 return Ok(());
7779 }
7780 let crosses_finalized = if block.number <= finalized.number {
7781 true
7782 } else if let Some(parent_hash) = block.parent_hash {
7783 if finalized.number.checked_add(1) == Some(block.number) && parent_hash == finalized.hash {
7784 false
7785 } else if let Some(parent_index) =
7786 state.retained_canonical_history.iter().rposition(|entry| {
7787 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
7788 })
7789 {
7790 state
7791 .retained_canonical_history
7792 .iter()
7793 .skip(parent_index + 1)
7794 .any(|entry| entry.number <= finalized.number)
7795 } else {
7796 true
7797 }
7798 } else {
7799 true
7800 };
7801 if crosses_finalized {
7802 return Err(ReactiveError::InvalidChainControl {
7803 message: format!(
7804 "canonical input {}:{:?} would replace finalized head {}:{:?}",
7805 block.number, block.hash, finalized.number, finalized.hash
7806 ),
7807 });
7808 }
7809 Ok(())
7810}
7811
7812fn validate_required_reorg_anchor(
7813 required: Option<RequiredReorgAnchor>,
7814 block: &BlockRef,
7815) -> Result<(), ReactiveError> {
7816 let Some(required) = required else {
7817 return Ok(());
7818 };
7819 let ancestor_hash = required.hash();
7820 let restores_ancestor =
7821 block.number == required.number && ancestor_hash.is_some_and(|hash| block.hash == hash);
7822 let replaces_removed_child = required.number.checked_add(1) == Some(block.number)
7823 && ancestor_hash.is_some()
7824 && (block.parent_hash == ancestor_hash
7825 || (block.parent_hash.is_none() && required.permits_missing_child_parent));
7826 if restores_ancestor || replaces_removed_child {
7827 return Ok(());
7828 }
7829 Err(ReactiveError::InvalidChainControl {
7830 message: format!(
7831 "canonical replacement {}:{:?} does not prove the removed tip's parent at block {}",
7832 block.number, block.hash, required.number
7833 ),
7834 })
7835}
7836
7837fn validate_replacement_reorg_anchor(
7838 required: Option<RequiredReorgAnchor>,
7839 block: &BlockRef,
7840 policy: CanonicalSequenceValidationPolicy,
7841 oldest_retained: Option<u64>,
7842) -> Result<bool, CanonicalSequenceError> {
7843 let Some(required) = required else {
7844 return Ok(false);
7845 };
7846 match validate_required_reorg_anchor(Some(required), block) {
7847 Ok(()) => Ok(true),
7848 Err(error) if required.block.is_some() => Err(error.into()),
7849 Err(_) if policy.requires_complete_rollback() => {
7850 Err(CanonicalSequenceError::IncompleteRollback {
7851 common_ancestor: required.number,
7852 oldest_retained,
7853 kind: CanonicalRollbackKind::MissingReplacement,
7854 })
7855 }
7856 Err(_) => Ok(false),
7857 }
7858}
7859
7860fn apply_sequence_canonical_block(
7861 state: &mut CanonicalSequenceState,
7862 block: &BlockRef,
7863 allow_parentless_adjacent_extension: bool,
7864) -> Result<Option<SequenceRewind>, ReactiveError> {
7865 let latest = state.coverage_head;
7866 let already_known = state
7867 .retained_canonical_history
7868 .iter()
7869 .any(|entry| entry.number == block.number && entry.hash == block.hash);
7870 let repeats_tip =
7871 latest.is_some_and(|head| head.number == block.number && head.hash == block.hash);
7872 let extends_tip = latest.is_some_and(|head| {
7873 head.number.checked_add(1) == Some(block.number)
7874 && (block.parent_hash == Some(head.hash)
7875 || (allow_parentless_adjacent_extension && block.parent_hash.is_none()))
7876 });
7877 let forward_gap = latest.is_some_and(|head| {
7878 head.number
7879 .checked_add(1)
7880 .is_some_and(|next| block.number > next)
7881 });
7882 let mut rewind = None;
7883
7884 if latest.is_some() && !already_known && !repeats_tip && !extends_tip && !forward_gap {
7885 let retained_parent = block.parent_hash.and_then(|parent_hash| {
7886 state
7887 .retained_canonical_history
7888 .iter()
7889 .rposition(|entry| {
7890 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
7891 })
7892 .map(|index| (index, state.retained_canonical_history[index]))
7893 });
7894 let finalized_parent = block.parent_hash.and_then(|parent_hash| {
7895 state.finalized_head.filter(|finalized| {
7896 finalized.number.checked_add(1) == Some(block.number)
7897 && finalized.hash == parent_hash
7898 })
7899 });
7900 let (common_ancestor, dropped) = if let Some((parent_index, parent)) = retained_parent {
7901 let dropped = state.retained_canonical_history.split_off(parent_index + 1);
7902 (Some(parent), dropped)
7903 } else if let Some(finalized) = finalized_parent {
7904 let dropped = state
7905 .retained_canonical_history
7906 .iter()
7907 .position(|entry| entry.number > finalized.number)
7908 .map_or_else(Vec::new, |index| {
7909 state.retained_canonical_history.split_off(index)
7910 });
7911 (Some(finalized), dropped)
7912 } else {
7913 (None, std::mem::take(&mut state.retained_canonical_history))
7919 };
7920 state.coverage_head = common_ancestor;
7921 if let Some(common_ancestor) = common_ancestor {
7922 clear_sequence_heads_above(state, &common_ancestor);
7923 } else {
7924 state.safe_head = None;
7925 state.finalized_head = None;
7926 }
7927 rewind = Some(SequenceRewind {
7928 common_ancestor,
7929 dropped,
7930 });
7931 }
7932 upsert_sequence_history(&mut state.retained_canonical_history, block)?;
7933 advance_or_enrich_coverage(&mut state.coverage_head, block);
7934 Ok(rewind)
7935}
7936
7937fn sequence_implicit_replacement_requires_history(
7938 state: &CanonicalSequenceState,
7939 block: &BlockRef,
7940 policy: CanonicalSequenceValidationPolicy,
7941) -> Result<bool, ReactiveError> {
7942 let Some(latest) = state.coverage_head else {
7943 return Ok(false);
7944 };
7945 let already_known = state
7946 .retained_canonical_history
7947 .iter()
7948 .any(|entry| entry.number == block.number && entry.hash == block.hash);
7949 let repeats_tip = block.number == latest.number && block.hash == latest.hash;
7950 let extends_tip = latest.number.checked_add(1) == Some(block.number)
7951 && block.parent_hash == Some(latest.hash);
7952 let forward_gap = latest
7953 .number
7954 .checked_add(1)
7955 .is_some_and(|next| block.number > next);
7956 if already_known || repeats_tip || extends_tip || forward_gap {
7957 return Ok(false);
7958 }
7959 let Some(parent_hash) = block.parent_hash else {
7960 if policy.requires_complete_rollback() {
7961 return Err(ReactiveError::InvalidChainControl {
7962 message: format!(
7963 "implicit canonical replacement {}:{:?} must identify its parent",
7964 block.number, block.hash
7965 ),
7966 });
7967 }
7968 return Ok(true);
7969 };
7970 let known_adjacent_parent = block.number.checked_sub(1).and_then(|parent_number| {
7971 state
7972 .retained_canonical_history
7973 .iter()
7974 .chain(state.coverage_head.iter())
7975 .chain(state.safe_head.iter())
7976 .chain(state.finalized_head.iter())
7977 .find(|known| known.number == parent_number)
7978 });
7979 if let Some(known_parent) = known_adjacent_parent
7980 && known_parent.hash != parent_hash
7981 && policy.requires_complete_rollback()
7982 {
7983 return Err(ReactiveError::InvalidChainControl {
7984 message: format!(
7985 "implicit canonical replacement {}:{:?} names parent {:?}, which conflicts with known adjacent block {}:{:?}",
7986 block.number, block.hash, parent_hash, known_parent.number, known_parent.hash
7987 ),
7988 });
7989 }
7990 let retained_parent = state.retained_canonical_history.iter().any(|entry| {
7991 entry.number.checked_add(1) == Some(block.number) && entry.hash == parent_hash
7992 });
7993 let finalized_parent = state.finalized_head.is_some_and(|finalized| {
7994 finalized.number.checked_add(1) == Some(block.number) && parent_hash == finalized.hash
7995 });
7996 Ok(!retained_parent && !finalized_parent)
7997}
7998
7999fn upsert_sequence_history(
8000 history: &mut Vec<BlockRef>,
8001 block: &BlockRef,
8002) -> Result<(), ReactiveError> {
8003 if let Some(existing) = history
8004 .iter_mut()
8005 .find(|entry| entry.number == block.number)
8006 {
8007 if existing.hash != block.hash {
8008 return Err(ReactiveError::InvalidChainControl {
8009 message: format!(
8010 "canonical block {}:{:?} conflicts with retained identity {:?}",
8011 block.number, block.hash, existing
8012 ),
8013 });
8014 }
8015 if !optional_block_refs_are_compatible(Some(existing), Some(block)) {
8016 return Err(ReactiveError::InvalidChainControl {
8017 message: format!(
8018 "canonical block {}:{:?} carries conflicting retained metadata",
8019 block.number, block.hash
8020 ),
8021 });
8022 }
8023 enrich_block_ref(existing, block);
8024 } else {
8025 history.push(*block);
8026 history.sort_by_key(|entry| entry.number);
8027 }
8028 Ok(())
8029}
8030
8031fn clear_sequence_heads_above(state: &mut CanonicalSequenceState, ancestor: &BlockRef) {
8032 if state.safe_head.as_ref().is_some_and(|head| {
8033 head.number > ancestor.number
8034 || (head.number == ancestor.number && head.hash != ancestor.hash)
8035 }) {
8036 state.safe_head = None;
8037 }
8038 if state.finalized_head.as_ref().is_some_and(|head| {
8039 head.number > ancestor.number
8040 || (head.number == ancestor.number && head.hash != ancestor.hash)
8041 }) {
8042 state.finalized_head = None;
8043 }
8044}
8045
8046fn validate_control_phase_order(controls: &[ChainControl]) -> Result<usize, ReactiveError> {
8047 let split = controls
8048 .iter()
8049 .position(|control| !matches!(control, ChainControl::Reorg { .. }))
8050 .unwrap_or(controls.len());
8051 if controls[split..]
8052 .iter()
8053 .any(|control| matches!(control, ChainControl::Reorg { .. }))
8054 {
8055 return Err(ReactiveError::InvalidChainControl {
8056 message: "reorg controls must precede records and all post-record controls in a batch"
8057 .into(),
8058 });
8059 }
8060 Ok(split)
8061}
8062
8063fn canonical_coverage_control_block(control: &ChainControl) -> Option<&BlockRef> {
8064 match control {
8065 ChainControl::CanonicalProgress(block)
8066 | ChainControl::Barrier {
8067 block: Some(block), ..
8068 } => Some(block),
8069 ChainControl::Reorg { .. }
8070 | ChainControl::Safe(_)
8071 | ChainControl::Finalized(_)
8072 | ChainControl::Barrier { block: None, .. } => None,
8073 }
8074}
8075
8076fn chain_control_canonical_assertion(control: &ChainControl) -> Option<&BlockRef> {
8077 match control {
8078 ChainControl::Safe(block)
8079 | ChainControl::Finalized(block)
8080 | ChainControl::CanonicalProgress(block)
8081 | ChainControl::Barrier {
8082 block: Some(block), ..
8083 } => Some(block),
8084 ChainControl::Reorg { .. } | ChainControl::Barrier { block: None, .. } => None,
8085 }
8086}
8087
8088fn assert_chain_control_identities(
8089 asserted_blocks: &mut HashMap<u64, BlockRef>,
8090 control: &ChainControl,
8091) -> Result<(), ReactiveError> {
8092 match control {
8093 ChainControl::Safe(block)
8094 | ChainControl::Finalized(block)
8095 | ChainControl::CanonicalProgress(block)
8096 | ChainControl::Barrier {
8097 block: Some(block), ..
8098 } => assert_canonical_block_identity(asserted_blocks, block, "chain control"),
8099 ChainControl::Barrier { block: None, .. } => Ok(()),
8100 ChainControl::Reorg {
8101 common_ancestor,
8102 new_tip,
8103 ..
8104 } => {
8105 asserted_blocks.retain(|number, _| *number <= common_ancestor.number);
8106 assert_canonical_block_identity(
8107 asserted_blocks,
8108 common_ancestor,
8109 "reorg common ancestor",
8110 )?;
8111 assert_canonical_block_identity(asserted_blocks, new_tip, "reorg new tip")
8112 }
8113 }
8114}
8115
8116fn assert_canonical_block_identity(
8117 asserted_blocks: &mut HashMap<u64, BlockRef>,
8118 block: &BlockRef,
8119 label: &str,
8120) -> Result<(), ReactiveError> {
8121 for asserted in asserted_blocks.values() {
8122 if asserted.hash == block.hash && asserted.number != block.number {
8123 return Err(ReactiveError::InvalidChainControl {
8124 message: format!(
8125 "{label} hash {:?} is already asserted at height {}, not {}",
8126 block.hash, asserted.number, block.number
8127 ),
8128 });
8129 }
8130 if block
8131 .parent_hash
8132 .is_some_and(|parent| parent == asserted.hash)
8133 && asserted.number.checked_add(1) != Some(block.number)
8134 {
8135 return Err(ReactiveError::InvalidChainControl {
8136 message: format!(
8137 "{label} block {}:{:?} names hash {:?} from known height {} as a non-adjacent parent",
8138 block.number, block.hash, asserted.hash, asserted.number
8139 ),
8140 });
8141 }
8142 if asserted
8143 .parent_hash
8144 .is_some_and(|parent| parent == block.hash)
8145 && block.number.checked_add(1) != Some(asserted.number)
8146 {
8147 return Err(ReactiveError::InvalidChainControl {
8148 message: format!(
8149 "block {}:{:?} asserted earlier names {label} hash {:?} from non-adjacent height {} as its parent",
8150 asserted.number, asserted.hash, block.hash, block.number
8151 ),
8152 });
8153 }
8154 }
8155 if let Some(known) = asserted_blocks.get_mut(&block.number) {
8156 if !optional_block_refs_are_compatible(Some(known), Some(block)) {
8157 return Err(ReactiveError::InvalidChainControl {
8158 message: format!(
8159 "{label} block {}:{:?} conflicts with block identity {:?} asserted earlier in the batch",
8160 block.number, block.hash, known
8161 ),
8162 });
8163 }
8164 enrich_block_ref(known, block);
8165 } else {
8166 asserted_blocks.insert(block.number, *block);
8167 }
8168 Ok(())
8169}
8170
8171fn set_or_enrich_block_ref(current: &mut Option<BlockRef>, incoming: &BlockRef) {
8172 match current {
8173 Some(current) if current.number == incoming.number && current.hash == incoming.hash => {
8174 enrich_block_ref(current, incoming);
8175 }
8176 _ => *current = Some(*incoming),
8177 }
8178}
8179
8180fn advance_or_enrich_coverage(current: &mut Option<BlockRef>, incoming: &BlockRef) {
8181 match current {
8182 Some(current) if current.number == incoming.number && current.hash == incoming.hash => {
8183 enrich_block_ref(current, incoming);
8184 }
8185 Some(current) if current.number >= incoming.number => {}
8186 _ => *current = Some(*incoming),
8187 }
8188}
8189
8190fn validate_adjacent_finality(
8191 finalized: Option<&BlockRef>,
8192 safe: Option<&BlockRef>,
8193) -> Result<(), ReactiveError> {
8194 let Some((finalized, safe)) = finalized.zip(safe) else {
8195 return Ok(());
8196 };
8197 if finalized.number.checked_add(1) == Some(safe.number)
8198 && safe.parent_hash != Some(finalized.hash)
8199 {
8200 return Err(ReactiveError::InvalidChainControl {
8201 message: "adjacent safe head does not descend from finalized head".into(),
8202 });
8203 }
8204 Ok(())
8205}
8206
8207fn collect_diff_addresses(diff: &StateDiff, into: &mut HashSet<Address>) {
8213 into.extend(diff.slots.iter().map(|change| change.address));
8214 into.extend(diff.accounts.iter().map(|change| change.address));
8215 into.extend(diff.purged.iter().map(|purge| purge.address));
8216 into.extend(diff.skipped.iter().map(|skipped| skipped.address));
8217 into.extend(diff.skipped_balances.iter().map(|skipped| skipped.address));
8218 into.extend(diff.skipped_masks.iter().map(|skipped| skipped.address));
8219 into.extend(diff.skipped_accounts.iter().map(|skipped| skipped.address));
8220}
8221
8222fn root_moved_account_resync(
8227 address: Address,
8228 block: u64,
8229 fields: AccountFieldMask,
8230) -> ResyncRequest {
8231 ResyncRequest {
8232 id: ResyncId::new(format!("root-moved:{address:#x}:{block}")),
8233 reason: ResyncReason::RootMoved,
8234 block: ResyncBlock::Number(block),
8235 targets: vec![ResyncTarget::Account { address, fields }],
8236 priority: ResyncPriority::Normal,
8237 }
8238}
8239
8240fn batch_preconfirmation<N: Network>(
8241 batch: &ReactiveInputBatch<N>,
8242) -> Result<Option<FlashblockRef>, ReactiveError> {
8243 let mut flashblock: Option<FlashblockRef> = None;
8244 let mut has_non_preconfirmed = false;
8245 for (index, record) in batch.records().iter().enumerate() {
8246 match &record.context.chain_status {
8247 ChainStatus::Preconfirmed {
8248 flashblock: current,
8249 } => {
8250 if batch.record_delivery_scope(index) != Some(DeliveryScope::Preconfirmed) {
8251 return Err(ReactiveError::InvalidInputRecord {
8252 message: "pre-confirmed input requires pre-confirmed delivery scope".into(),
8253 });
8254 }
8255 if flashblock
8256 .as_ref()
8257 .is_some_and(|known| known != current.as_ref())
8258 {
8259 return Err(ReactiveError::InvalidInputRecord {
8260 message: "one batch cannot mix distinct Flashblock snapshots".into(),
8261 });
8262 }
8263 flashblock.get_or_insert_with(|| current.as_ref().clone());
8264 }
8265 _ => has_non_preconfirmed = true,
8266 }
8267 }
8268 if flashblock.is_some() && (has_non_preconfirmed || !batch.chain_controls().is_empty()) {
8269 return Err(ReactiveError::InvalidInputRecord {
8270 message: "pre-confirmed delivery cannot mix canonical inputs or chain controls".into(),
8271 });
8272 }
8273 Ok(flashblock)
8274}
8275
8276fn canonical_record_block<N: Network>(record: &ReactiveInputRecord<N>) -> Option<&BlockRef> {
8277 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
8278 return None;
8279 }
8280 if is_canonical_status(&record.context.chain_status) {
8281 return context_block_ref(&record.context);
8282 }
8283 None
8284}
8285
8286fn resolve_record_block_payload_metadata<N: Network>(
8287 record: &ReactiveInputRecord<N>,
8288 mut block: BlockRef,
8289) -> Result<BlockRef, ReactiveError> {
8290 let ReactiveInput::Log(log) = &record.input else {
8291 return Ok(block);
8292 };
8293 if log.block_number != Some(block.number) || log.block_hash != Some(block.hash) {
8294 return Err(ReactiveError::InvalidInputRecord {
8295 message: "log payload and canonical context carry different block identities".into(),
8296 });
8297 }
8298 if let Some(timestamp) = log.block_timestamp {
8299 if block.timestamp.is_some_and(|known| known != timestamp) {
8300 return Err(ReactiveError::InvalidInputRecord {
8301 message: "log payload and canonical context carry different block timestamps"
8302 .into(),
8303 });
8304 }
8305 block.timestamp = Some(timestamp);
8306 }
8307 Ok(block)
8308}
8309
8310fn validate_input_record<N: Network>(record: &ReactiveInputRecord<N>) -> Result<(), ReactiveError> {
8311 let invalid = |message: String| ReactiveError::InvalidInputRecord { message };
8312 if let ChainStatus::Preconfirmed { flashblock } = &record.context.chain_status
8313 && record.context.block != Some(flashblock.block_ref())
8314 {
8315 return Err(invalid(
8316 "pre-confirmed status and context carry different partial block identities".into(),
8317 ));
8318 }
8319 let status_block = match &record.context.chain_status {
8320 ChainStatus::Included { block, .. }
8321 | ChainStatus::Safe { block }
8322 | ChainStatus::Finalized { block }
8323 | ChainStatus::Reorged {
8324 dropped_from: block,
8325 } => Some(block),
8326 ChainStatus::Preconfirmed { .. } => record.context.block.as_ref(),
8327 ChainStatus::Pending => None,
8328 };
8329 match (status_block, record.context.block.as_ref()) {
8330 (Some(status), Some(context)) if status == context => {}
8331 (Some(_), Some(_)) => {
8332 return Err(invalid(
8333 "chain status and context carry different block identities".into(),
8334 ));
8335 }
8336 (Some(_), None) => {
8337 return Err(invalid(
8338 "included or reorged input is missing its context block".into(),
8339 ));
8340 }
8341 (None, Some(_)) => {
8342 return Err(invalid(
8343 "pending input cannot carry a canonical context block".into(),
8344 ));
8345 }
8346 (None, None) => {}
8347 }
8348
8349 match &record.input {
8350 ReactiveInput::Log(log) => {
8351 let Some(block) = status_block else {
8352 return Err(invalid(
8353 "log input must carry an included or reorged block identity".into(),
8354 ));
8355 };
8356 if log.removed && !matches!(record.context.chain_status, ChainStatus::Reorged { .. }) {
8357 return Err(invalid(
8358 "removed log must carry reorged chain status".into(),
8359 ));
8360 }
8361 let block_number = log
8362 .block_number
8363 .ok_or_else(|| invalid("log is missing its block number".into()))?;
8364 let block_hash = log
8365 .block_hash
8366 .ok_or_else(|| invalid("log is missing its block hash".into()))?;
8367 log.transaction_hash
8368 .ok_or_else(|| invalid("log is missing its transaction hash".into()))?;
8369 let transaction_index = log
8370 .transaction_index
8371 .ok_or_else(|| invalid("log is missing its transaction index".into()))?;
8372 let log_index = log
8373 .log_index
8374 .ok_or_else(|| invalid("log is missing its log index".into()))?;
8375 if block_number != block.number
8376 || block_hash != block.hash
8377 || !optional_metadata_compatible(
8378 log.block_timestamp.as_ref(),
8379 block.timestamp.as_ref(),
8380 )
8381 {
8382 return Err(invalid(
8383 "log payload and context carry different block identities".into(),
8384 ));
8385 }
8386 if record.context.transaction_index != Some(transaction_index)
8387 || record.context.log_index != Some(log_index)
8388 {
8389 return Err(invalid(
8390 "log payload and context carry different transaction/log positions".into(),
8391 ));
8392 }
8393 }
8394 ReactiveInput::BlockHeader(header) => {
8395 if let Some(block) = status_block {
8396 if header.number() != block.number
8397 || header.hash() != block.hash
8398 || Some(header.parent_hash()) != block.parent_hash
8399 || Some(header.timestamp()) != block.timestamp
8400 {
8401 return Err(invalid(
8402 "block header payload and context carry different block identities".into(),
8403 ));
8404 }
8405 } else if !matches!(record.context.chain_status, ChainStatus::Pending) {
8406 return Err(invalid("block header has an unsupported lifecycle".into()));
8407 }
8408 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8409 return Err(invalid(
8410 "block header context cannot carry transaction/log positions".into(),
8411 ));
8412 }
8413 }
8414 ReactiveInput::FullBlock(block_response) => {
8415 let header = block_response.header();
8416 if let Some(block) = status_block {
8417 if header.number() != block.number
8418 || header.hash() != block.hash
8419 || Some(header.parent_hash()) != block.parent_hash
8420 || Some(header.timestamp()) != block.timestamp
8421 {
8422 return Err(invalid(
8423 "full-block payload and context carry different block identities".into(),
8424 ));
8425 }
8426 } else if !matches!(record.context.chain_status, ChainStatus::Pending) {
8427 return Err(invalid("full block has an unsupported lifecycle".into()));
8428 }
8429 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8430 return Err(invalid(
8431 "full-block context cannot carry transaction/log positions".into(),
8432 ));
8433 }
8434 if let Some(transactions) = block_response.transactions().as_transactions() {
8435 for (index, transaction) in transactions.iter().enumerate() {
8436 if transaction
8437 .block_hash()
8438 .is_some_and(|hash| hash != header.hash())
8439 || transaction
8440 .block_number()
8441 .is_some_and(|number| number != header.number())
8442 || transaction
8443 .transaction_index()
8444 .is_some_and(|position| position != index as u64)
8445 {
8446 return Err(invalid(format!(
8447 "full-block transaction {index} carries contradictory inclusion metadata"
8448 )));
8449 }
8450 if transaction
8451 .chain_id()
8452 .zip(record.context.chain_id)
8453 .is_some_and(|(transaction, context)| transaction != context)
8454 {
8455 return Err(invalid(format!(
8456 "full-block transaction {index} carries a chain id conflicting with its context"
8457 )));
8458 }
8459 }
8460 }
8461 }
8462 ReactiveInput::PendingTxHash(_) => {
8463 if !matches!(record.context.chain_status, ChainStatus::Pending) {
8464 return Err(invalid(
8465 "pending transaction input must carry pending chain status".into(),
8466 ));
8467 }
8468 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8469 return Err(invalid(
8470 "pending transaction context cannot carry canonical positions".into(),
8471 ));
8472 }
8473 }
8474 ReactiveInput::PendingTx(transaction) => {
8475 if !matches!(record.context.chain_status, ChainStatus::Pending) {
8476 return Err(invalid(
8477 "pending transaction input must carry pending chain status".into(),
8478 ));
8479 }
8480 if record.context.transaction_index.is_some() || record.context.log_index.is_some() {
8481 return Err(invalid(
8482 "pending transaction context cannot carry canonical positions".into(),
8483 ));
8484 }
8485 if transaction.block_hash().is_some()
8486 || transaction.block_number().is_some()
8487 || transaction.transaction_index().is_some()
8488 {
8489 return Err(invalid(
8490 "hydrated pending transaction cannot carry inclusion metadata".into(),
8491 ));
8492 }
8493 if transaction
8494 .chain_id()
8495 .zip(record.context.chain_id)
8496 .is_some_and(|(transaction, context)| transaction != context)
8497 {
8498 return Err(invalid(
8499 "pending transaction carries a chain id conflicting with its context".into(),
8500 ));
8501 }
8502 }
8503 }
8504 Ok(())
8505}
8506
8507fn advance_block_for_canonical_record<N: Network>(
8516 cache: &mut EvmCache,
8517 record: &ReactiveInputRecord<N>,
8518) -> Option<Result<(), BlockContextError>> {
8519 if !is_canonical_status(&record.context.chain_status) {
8520 return None;
8521 }
8522 match &record.input {
8523 ReactiveInput::BlockHeader(header) => Some(cache.advance_block(header)),
8524 ReactiveInput::FullBlock(block) => Some(cache.advance_block(block.header())),
8525 _ => None,
8526 }
8527}
8528
8529fn context_block_ref(ctx: &ReactiveContext) -> Option<&BlockRef> {
8530 match &ctx.chain_status {
8531 ChainStatus::Included { block, .. }
8532 | ChainStatus::Safe { block }
8533 | ChainStatus::Finalized { block } => Some(block),
8534 ChainStatus::Reorged { dropped_from } => Some(dropped_from),
8535 ChainStatus::Preconfirmed { .. } => ctx.block.as_ref(),
8536 ChainStatus::Pending => ctx.block.as_ref(),
8537 }
8538}
8539
8540fn reorg_signal_block<N: Network>(
8541 record: &ReactiveInputRecord<N>,
8542) -> Option<(BlockRef, ReorgReason)> {
8543 if matches!(&record.input, ReactiveInput::Log(log) if log.removed) {
8544 return block_ref_from_record(record).map(|block| (block, ReorgReason::RemovedLog));
8545 }
8546
8547 if let ChainStatus::Reorged { dropped_from } = &record.context.chain_status {
8548 return Some((*dropped_from, ReorgReason::ReorgedInput));
8549 }
8550
8551 None
8552}
8553
8554fn block_ref_from_record<N: Network>(record: &ReactiveInputRecord<N>) -> Option<BlockRef> {
8555 context_block_ref(&record.context)
8556 .cloned()
8557 .or_else(|| match &record.input {
8558 ReactiveInput::Log(log) => Some(BlockRef {
8559 number: log.block_number?,
8560 hash: log.block_hash?,
8561 parent_hash: None,
8562 timestamp: log.block_timestamp,
8563 }),
8564 ReactiveInput::BlockHeader(header) => Some(BlockRef {
8565 number: header.number(),
8566 hash: header.hash(),
8567 parent_hash: Some(header.parent_hash()),
8568 timestamp: Some(header.timestamp()),
8569 }),
8570 ReactiveInput::FullBlock(block) => {
8571 let header = block.header();
8572 Some(BlockRef {
8573 number: header.number(),
8574 hash: header.hash(),
8575 parent_hash: Some(header.parent_hash()),
8576 timestamp: Some(header.timestamp()),
8577 })
8578 }
8579 ReactiveInput::PendingTxHash(_) | ReactiveInput::PendingTx(_) => None,
8580 })
8581}
8582
8583fn remove_canceled_resyncs_from_batch(
8584 resyncs: &mut Vec<ResyncRequest>,
8585 canceled: &[ResyncRequest],
8586) {
8587 if canceled.is_empty() {
8588 return;
8589 }
8590 let canceled_ids: HashSet<_> = canceled.iter().map(|request| request.id.clone()).collect();
8591 resyncs.retain(|request| !canceled_ids.contains(&request.id));
8592}
8593
8594fn resync_target_address(target: &ResyncTarget) -> Address {
8595 match target {
8596 ResyncTarget::StorageSlot { address, .. }
8597 | ResyncTarget::StorageSlots { address, .. }
8598 | ResyncTarget::Account { address, .. } => *address,
8599 }
8600}
8601
8602fn resync_request_targets_dropped_block(
8603 request: &ResyncRequest,
8604 dropped_blocks: &[BlockRef],
8605) -> bool {
8606 let ResyncBlock::Hash { number, hash, .. } = &request.block else {
8607 return false;
8608 };
8609 dropped_blocks
8610 .iter()
8611 .any(|block| block.hash == *hash && block.number == *number)
8612}
8613
8614fn single_hash_pinned_resync_block(report: &ResyncReport) -> Option<BlockRef> {
8615 let first = report.requested.first()?.block.clone();
8616 if !report
8617 .requested
8618 .iter()
8619 .all(|request| request.block == first)
8620 {
8621 return None;
8622 }
8623
8624 let ResyncBlock::Hash { number, hash, .. } = first else {
8625 return None;
8626 };
8627
8628 Some(BlockRef {
8629 number,
8630 hash,
8631 parent_hash: None,
8632 timestamp: None,
8633 })
8634}
8635
8636fn purge_scopes_for_dropped_journals<N: Network>(
8637 dropped: &[BlockJournal<N>],
8638) -> Vec<(Address, PurgeScope)> {
8639 let mut scopes: Vec<(Address, PurgeScope)> = Vec::new();
8640 for entry in dropped.iter().rev() {
8641 for diff in entry.rollback_diffs.iter().rev() {
8642 merge_purge_scopes_for_diff(&mut scopes, diff);
8643 }
8644 }
8645 scopes
8646}
8647
8648fn rollback_updates_for_dropped_journals<N: Network>(
8649 dropped: &[BlockJournal<N>],
8650 purge_scopes: &[(Address, PurgeScope)],
8651) -> Vec<StateUpdate> {
8652 let purge_addresses: HashSet<_> = purge_scopes
8653 .iter()
8654 .map(|(address, _scope)| *address)
8655 .collect();
8656 let mut updates = Vec::new();
8657 for entry in dropped.iter().rev() {
8658 for diff in entry.rollback_diffs.iter().rev() {
8659 push_rollback_updates_for_diff(&mut updates, diff, &purge_addresses);
8660 }
8661 }
8662 updates
8663}
8664
8665fn merge_purge_scopes_for_diff(scopes: &mut Vec<(Address, PurgeScope)>, diff: &StateDiff) {
8666 for change in &diff.accounts {
8667 merge_purge_scope(scopes, change.address, PurgeScope::Account);
8668 }
8669 for record in &diff.purged {
8670 merge_purge_scope(scopes, record.address, record.scope.clone());
8671 }
8672}
8673
8674fn push_rollback_updates_for_diff(
8675 updates: &mut Vec<StateUpdate>,
8676 diff: &StateDiff,
8677 purge_addresses: &HashSet<Address>,
8678) {
8679 for change in diff.slots.iter().rev() {
8680 if purge_addresses.contains(&change.address) {
8681 continue;
8682 }
8683 updates.push(StateUpdate::slot(change.address, change.slot, change.old));
8684 }
8685}
8686
8687fn merge_purge_scope(scopes: &mut Vec<(Address, PurgeScope)>, address: Address, scope: PurgeScope) {
8688 if let Some((_existing_address, existing_scope)) = scopes
8689 .iter_mut()
8690 .find(|(existing_address, _scope)| *existing_address == address)
8691 {
8692 *existing_scope = merged_purge_scope(existing_scope.clone(), scope);
8693 } else {
8694 scopes.push((address, scope));
8695 }
8696}
8697
8698fn merged_purge_scope(left: PurgeScope, right: PurgeScope) -> PurgeScope {
8699 match (left, right) {
8700 (PurgeScope::Account, _) | (_, PurgeScope::Account) => PurgeScope::Account,
8701 (PurgeScope::AllStorage, _) | (_, PurgeScope::AllStorage) => PurgeScope::AllStorage,
8702 (PurgeScope::Slots(mut left), PurgeScope::Slots(right)) => {
8703 for slot in right {
8704 if !left.contains(&slot) {
8705 left.push(slot);
8706 }
8707 }
8708 PurgeScope::Slots(left)
8709 }
8710 }
8711}
8712
8713#[derive(Clone, Debug)]
8714struct StorageFetchSlot {
8715 address: Address,
8716 slot: U256,
8717 origins: Vec<StorageFetchOrigin>,
8718}
8719
8720#[derive(Clone, Debug)]
8721struct StorageFetchOrigin {
8722 request_id: ResyncId,
8723 target: ResyncTarget,
8724}
8725
8726#[derive(Clone, Debug)]
8727struct StorageFetchGroup {
8728 block: ResyncBlock,
8729 slots: Vec<StorageFetchSlot>,
8730 seen: HashSet<(Address, U256)>,
8731}
8732
8733#[derive(Clone, Debug)]
8736struct AccountResyncTarget {
8737 request_id: ResyncId,
8738 block: ResyncBlock,
8739 address: Address,
8740 fields: AccountFieldMask,
8741}
8742
8743fn resolve_trace_resyncs(
8744 cache: &EvmCache,
8745 storage_groups: &mut Vec<StorageFetchGroup>,
8746 account_targets: &mut Vec<AccountResyncTarget>,
8747 state_updates: &mut Vec<StateUpdate>,
8748) {
8749 let Some(fetcher) = cache.block_state_diff_fetcher().cloned() else {
8750 return;
8751 };
8752
8753 let mut blocks = Vec::new();
8754 let mut seen = HashSet::new();
8755 for block in storage_groups
8756 .iter()
8757 .map(|group| group.block.clone())
8758 .chain(account_targets.iter().map(|target| target.block.clone()))
8759 {
8760 if seen.insert(block.clone()) {
8761 blocks.push(block);
8762 }
8763 }
8764
8765 let mut traces = HashMap::new();
8766 for block in blocks {
8767 match (fetcher)(resync_block_to_block_id(&block)) {
8768 Ok(diff) => {
8769 traces.insert(block, diff);
8770 }
8771 Err(error) => {
8772 tracing::debug!(
8773 block = ?block,
8774 error = %error,
8775 "block trace resync source failed; falling back to point resync"
8776 );
8777 }
8778 }
8779 }
8780
8781 for group in storage_groups.iter_mut() {
8782 let Some(trace) = traces.get(&group.block) else {
8783 continue;
8784 };
8785 group.slots.retain(|slot| {
8786 if let Some(value) = trace_storage_value(trace, slot.address, slot.slot) {
8787 state_updates.push(StateUpdate::slot(slot.address, slot.slot, value));
8788 return false;
8789 }
8790 cache
8791 .cached_storage_value(slot.address, slot.slot)
8792 .is_none()
8793 });
8794 group.seen = group
8795 .slots
8796 .iter()
8797 .map(|slot| (slot.address, slot.slot))
8798 .collect();
8799 }
8800 storage_groups.retain(|group| !group.slots.is_empty());
8801
8802 let mut unresolved_accounts = Vec::new();
8803 for mut account in account_targets.drain(..) {
8804 let Some(trace) = traces.get(&account.block) else {
8805 unresolved_accounts.push(account);
8806 continue;
8807 };
8808 let Some(trace_account) = trace
8809 .accounts
8810 .iter()
8811 .find(|diff| diff.address == account.address)
8812 else {
8813 unresolved_accounts.push(account);
8814 continue;
8815 };
8816
8817 let mut patch = AccountPatch::default();
8818 let mut unresolved = AccountFieldMask::default();
8819 if account.fields.balance {
8820 if let Some(balance) = trace_account.balance {
8821 patch = patch.balance(balance);
8822 } else {
8823 unresolved.balance = true;
8824 }
8825 }
8826 if account.fields.nonce {
8827 if let Some(nonce) = trace_account.nonce {
8828 patch = patch.nonce(nonce);
8829 } else {
8830 unresolved.nonce = true;
8831 }
8832 }
8833 if account.fields.code {
8834 if let Some(code) = &trace_account.code {
8835 patch = patch.code(code.clone());
8836 } else {
8837 unresolved.code = true;
8838 }
8839 }
8840
8841 if patch.balance.is_some() || patch.nonce.is_some() || patch.code.is_some() {
8842 state_updates.push(StateUpdate::account_upsert(account.address, patch));
8843 }
8844 if !account_field_mask_empty(unresolved) {
8845 account.fields = unresolved;
8846 unresolved_accounts.push(account);
8847 }
8848 }
8849 *account_targets = unresolved_accounts;
8850}
8851
8852fn trace_storage_value(trace: &BlockStateDiff, address: Address, slot: U256) -> Option<U256> {
8853 trace
8854 .accounts
8855 .iter()
8856 .find(|account| account.address == address)
8857 .and_then(|account| {
8858 account
8859 .storage
8860 .iter()
8861 .find(|entry| entry.slot == slot)
8862 .map(|entry| entry.value)
8863 })
8864}
8865
8866fn account_field_mask_empty(mask: AccountFieldMask) -> bool {
8867 !mask.balance && !mask.nonce && !mask.code
8868}
8869
8870fn execute_resync_requests(cache: &mut EvmCache, requests: &[ResyncRequest]) -> ResyncReport {
8871 let mut failed = Vec::new();
8872 let mut storage_groups: Vec<StorageFetchGroup> = Vec::new();
8873 let mut account_targets: Vec<AccountResyncTarget> = Vec::new();
8874
8875 for request in requests {
8876 for target in &request.targets {
8877 match target {
8878 ResyncTarget::StorageSlot { address, slot } => {
8879 push_storage_resync_slot(
8880 &mut storage_groups,
8881 &request.id,
8882 &request.block,
8883 *address,
8884 *slot,
8885 );
8886 }
8887 ResyncTarget::StorageSlots { address, slots } => {
8888 for slot in slots {
8889 push_storage_resync_slot(
8890 &mut storage_groups,
8891 &request.id,
8892 &request.block,
8893 *address,
8894 *slot,
8895 );
8896 }
8897 }
8898 ResyncTarget::Account { address, fields } => {
8899 account_targets.push(AccountResyncTarget {
8900 request_id: request.id.clone(),
8901 block: request.block.clone(),
8902 address: *address,
8903 fields: *fields,
8904 });
8905 }
8906 }
8907 }
8908 }
8909
8910 let mut state_updates = Vec::new();
8911 resolve_trace_resyncs(
8912 cache,
8913 &mut storage_groups,
8914 &mut account_targets,
8915 &mut state_updates,
8916 );
8917
8918 if !storage_groups.is_empty() {
8919 if let Some(fetcher) = cache.storage_batch_fetcher().cloned() {
8920 for group in storage_groups {
8921 let block = group.block.clone();
8922 let fetches: Vec<(Address, U256)> = group
8923 .slots
8924 .iter()
8925 .map(|slot| (slot.address, slot.slot))
8926 .collect();
8927 let results = (fetcher)(fetches, resync_block_to_block_id(&block));
8928 let mut pending: HashMap<(Address, U256), StorageFetchSlot> = group
8929 .slots
8930 .iter()
8931 .cloned()
8932 .map(|slot| ((slot.address, slot.slot), slot))
8933 .collect();
8934
8935 for (address, slot, fetched) in results {
8936 let Some(requested_slot) = pending.remove(&(address, slot)) else {
8937 continue;
8938 };
8939 match fetched {
8940 Ok(value) => state_updates.push(StateUpdate::slot(address, slot, value)),
8941 Err(error) => {
8942 let message = error.to_string();
8943 push_resync_failures(
8944 &mut failed,
8945 &block,
8946 requested_slot.origins,
8947 ResyncFailureKind::StorageFetchFailed,
8948 message,
8949 );
8950 }
8951 }
8952 }
8953
8954 for requested_slot in group.slots {
8955 if pending
8956 .remove(&(requested_slot.address, requested_slot.slot))
8957 .is_some()
8958 {
8959 push_resync_failures(
8960 &mut failed,
8961 &block,
8962 requested_slot.origins,
8963 ResyncFailureKind::StorageFetchOmitted,
8964 "storage batch fetcher did not return a value for slot".to_string(),
8965 );
8966 }
8967 }
8968 }
8969 } else {
8970 for group in storage_groups {
8971 let block = group.block.clone();
8972 for slot in group.slots {
8973 push_resync_failures(
8974 &mut failed,
8975 &block,
8976 slot.origins,
8977 ResyncFailureKind::MissingStorageFetcher,
8978 "storage resync requires a storage batch fetcher".to_string(),
8979 );
8980 }
8981 }
8982 }
8983 }
8984
8985 if !account_targets.is_empty() {
8986 if let Some(fetcher) = cache.account_proof_fetcher().cloned() {
8987 let mut groups: Vec<(BlockId, Vec<_>)> = Vec::new();
8993 for account in account_targets {
8994 let block_id = resync_block_to_block_id(&account.block);
8995 match groups
8996 .iter_mut()
8997 .find(|(group_block, _)| *group_block == block_id)
8998 {
8999 Some((_, group)) => group.push(account),
9000 None => groups.push((block_id, vec![account])),
9001 }
9002 }
9003 for (block_id, group) in groups {
9004 let probes: HashMap<Address, StorageFetchResult<AccountProof>> = (fetcher)(
9005 group
9006 .iter()
9007 .map(|account| (account.address, vec![]))
9008 .collect(),
9009 block_id,
9010 )
9011 .into_iter()
9012 .collect();
9013 for account in group {
9014 match probes.get(&account.address).cloned() {
9018 Some(Ok(proof)) => {
9019 let mut patch = AccountPatch::default();
9024 if account.fields.balance {
9025 patch = patch.balance(proof.balance);
9026 }
9027 if account.fields.nonce {
9028 patch = patch.nonce(proof.nonce);
9029 }
9030 state_updates.push(StateUpdate::account_upsert(account.address, patch));
9036 }
9037 Some(Err(error)) => {
9038 failed.push(ResyncFailure {
9039 request_id: account.request_id,
9040 block: account.block,
9041 target: ResyncTarget::Account {
9042 address: account.address,
9043 fields: account.fields,
9044 },
9045 kind: ResyncFailureKind::AccountFetchFailed,
9046 message: error.to_string(),
9047 });
9048 }
9049 None => {
9050 failed.push(ResyncFailure {
9051 request_id: account.request_id,
9052 block: account.block,
9053 target: ResyncTarget::Account {
9054 address: account.address,
9055 fields: account.fields,
9056 },
9057 kind: ResyncFailureKind::AccountFetchOmitted,
9058 message:
9059 "account proof fetcher did not return a result for address"
9060 .to_string(),
9061 });
9062 }
9063 }
9064 }
9065 }
9066 } else {
9067 for account in account_targets {
9068 failed.push(ResyncFailure {
9069 request_id: account.request_id,
9070 block: account.block,
9071 target: ResyncTarget::Account {
9072 address: account.address,
9073 fields: account.fields,
9074 },
9075 kind: ResyncFailureKind::MissingAccountFetcher,
9076 message: "account resync requires an account proof fetcher".to_string(),
9077 });
9078 }
9079 }
9080 }
9081
9082 let diff = if state_updates.is_empty() {
9083 StateDiff::default()
9084 } else {
9085 cache.apply_updates(&state_updates)
9086 };
9087
9088 ResyncReport {
9089 requested: requests.to_vec(),
9090 state_updates,
9091 diff,
9092 failed,
9093 }
9094}
9095
9096fn push_resync_failures(
9097 failed: &mut Vec<ResyncFailure>,
9098 block: &ResyncBlock,
9099 origins: Vec<StorageFetchOrigin>,
9100 kind: ResyncFailureKind,
9101 message: String,
9102) {
9103 for origin in origins {
9104 failed.push(ResyncFailure {
9105 request_id: origin.request_id,
9106 block: block.clone(),
9107 target: origin.target,
9108 kind,
9109 message: message.clone(),
9110 });
9111 }
9112}
9113
9114fn push_storage_resync_slot(
9115 groups: &mut Vec<StorageFetchGroup>,
9116 request_id: &ResyncId,
9117 block: &ResyncBlock,
9118 address: Address,
9119 slot: U256,
9120) {
9121 let group_index = if let Some(index) = groups.iter().position(|group| group.block == *block) {
9122 index
9123 } else {
9124 groups.push(StorageFetchGroup {
9125 block: block.clone(),
9126 slots: Vec::new(),
9127 seen: HashSet::new(),
9128 });
9129 groups.len() - 1
9130 };
9131
9132 let group = &mut groups[group_index];
9133 let origin = StorageFetchOrigin {
9134 request_id: request_id.clone(),
9135 target: ResyncTarget::StorageSlot { address, slot },
9136 };
9137 if group.seen.insert((address, slot)) {
9138 group.slots.push(StorageFetchSlot {
9139 address,
9140 slot,
9141 origins: vec![origin],
9142 });
9143 } else if let Some(existing) = group
9144 .slots
9145 .iter_mut()
9146 .find(|existing| existing.address == address && existing.slot == slot)
9147 {
9148 existing.origins.push(origin);
9149 }
9150}
9151
9152fn resync_block_to_block_id(block: &ResyncBlock) -> BlockId {
9153 match block {
9154 ResyncBlock::Latest => BlockId::latest(),
9155 ResyncBlock::Pending => BlockId::pending(),
9156 ResyncBlock::Safe => BlockId::safe(),
9157 ResyncBlock::Finalized => BlockId::finalized(),
9158 ResyncBlock::Number(number) => BlockId::number(*number),
9159 ResyncBlock::Hash {
9160 number: _,
9161 hash,
9162 require_canonical,
9163 } => BlockId::from((*hash, Some(*require_canonical))),
9164 }
9165}
9166
9167impl<N: Network> RegisteredHandler<N> {
9168 fn matches(&self, input: &ReactiveInput<N>) -> bool {
9169 self.interests
9170 .iter()
9171 .any(|interest| interest_matches(interest, input))
9172 }
9173
9174 fn route_log(&self, log: &Log) -> Option<ReactiveLogRoute> {
9175 self.interests.iter().find_map(|interest| match interest {
9176 ReactiveInterest::Logs(interest) if interest.matches(log) => Some(ReactiveLogRoute {
9177 handler_id: self.id.clone(),
9178 route_key: interest.route_key(log),
9179 }),
9180 ReactiveInterest::Logs(_)
9181 | ReactiveInterest::Blocks(_)
9182 | ReactiveInterest::PendingTransactions(_) => None,
9183 })
9184 }
9185}
9186
9187fn merge_log_subscription_filter(filters: &mut Vec<Filter>, next: &Filter) {
9188 let mut candidate = next.clone();
9189 let mut insertion_index = filters.len();
9190 let mut index = 0;
9191 while index < filters.len() {
9192 if filters[index].block_option != candidate.block_option {
9193 index += 1;
9194 continue;
9195 }
9196 if let Some(merged) = exact_filter_union(&candidate, &filters[index]) {
9197 candidate = merged;
9198 insertion_index = insertion_index.min(index);
9199 filters.remove(index);
9200 index = 0;
9201 } else {
9202 index += 1;
9203 }
9204 }
9205 filters.insert(insertion_index.min(filters.len()), candidate);
9206}
9207
9208fn exact_filter_union(left: &Filter, right: &Filter) -> Option<Filter> {
9209 if filter_subsumes(left, right) {
9210 return Some(left.clone());
9211 }
9212 if filter_subsumes(right, left) {
9213 return Some(right.clone());
9214 }
9215 let differing_dimensions = usize::from(left.address != right.address)
9216 + left
9217 .topics
9218 .iter()
9219 .zip(right.topics.iter())
9220 .filter(|(left, right)| left != right)
9221 .count();
9222 if differing_dimensions != 1 {
9223 return None;
9224 }
9225
9226 let mut merged = left.clone();
9227 if merged.address != right.address {
9228 merge_filter_set(&mut merged.address, &right.address);
9229 } else {
9230 for (merged_topic, right_topic) in merged.topics.iter_mut().zip(right.topics.iter()) {
9231 if merged_topic != right_topic {
9232 merge_filter_set(merged_topic, right_topic);
9233 break;
9234 }
9235 }
9236 }
9237 Some(merged)
9238}
9239
9240fn filter_subsumes(left: &Filter, right: &Filter) -> bool {
9241 filter_set_subsumes(&left.address, &right.address)
9242 && left
9243 .topics
9244 .iter()
9245 .zip(right.topics.iter())
9246 .all(|(left, right)| filter_set_subsumes(left, right))
9247}
9248
9249fn filter_set_subsumes<T: Eq + Hash>(left: &FilterSet<T>, right: &FilterSet<T>) -> bool {
9250 left.is_empty()
9251 || (!right.is_empty()
9252 && right
9253 .iter()
9254 .all(|value| left.iter().any(|known| known == value)))
9255}
9256
9257fn merge_filter_set<T: Clone + Eq + Hash>(target: &mut FilterSet<T>, source: &FilterSet<T>) {
9258 if target.is_empty() {
9259 return;
9260 }
9261 if source.is_empty() {
9262 *target = FilterSet::default();
9263 return;
9264 }
9265 for value in source.iter() {
9266 target.insert(value.clone());
9267 }
9268}
9269
9270#[derive(Clone, Debug)]
9271struct HandlerExecution {
9272 handler_id: HandlerId,
9273 quality: StateEffectQuality,
9274 tags: Vec<ReportTag>,
9275 state_updates: Vec<StateUpdate>,
9276 invalidations: Vec<InvalidationRequest>,
9277 resyncs: Vec<ResyncRequest>,
9278 speculative: Vec<SpeculativeRequest>,
9279 hook_signals: Vec<HookSignal>,
9280}
9281
9282impl HandlerExecution {
9283 fn from_outcome(
9284 handler_id: HandlerId,
9285 input_ref: InputRef,
9286 outcome: HandlerOutcome,
9287 preconfirmed: bool,
9288 ) -> Self {
9289 let mut state_updates = Vec::new();
9290 let mut invalidations = Vec::new();
9291 let mut resyncs = Vec::new();
9292 let mut speculative = Vec::new();
9293 let mut hook_signals = Vec::new();
9294
9295 for effect in outcome.effects {
9296 match effect {
9297 ReactiveEffect::StateUpdate(update) => state_updates.push(update),
9298 ReactiveEffect::Invalidate(invalidation) => {
9299 state_updates.push(StateUpdate::purge(
9300 invalidation.address,
9301 invalidation.scope.clone(),
9302 ));
9303 invalidations.push(invalidation);
9304 }
9305 ReactiveEffect::Resync(mut request) => {
9306 if preconfirmed {
9307 request.block = ResyncBlock::Pending;
9308 }
9309 resyncs.push(request);
9310 }
9311 ReactiveEffect::Hook(signal) => hook_signals.push(signal),
9312 ReactiveEffect::Speculative(mut request) => {
9313 request.input_ref = input_ref;
9314 speculative.push(request);
9315 }
9316 }
9317 }
9318
9319 Self {
9320 handler_id,
9321 quality: outcome.quality,
9322 tags: outcome.tags,
9323 state_updates,
9324 invalidations,
9325 resyncs,
9326 speculative,
9327 hook_signals,
9328 }
9329 }
9330}
9331
9332fn dedupe_records<N: Network>(
9333 records: Vec<ReactiveInputRecord<N>>,
9334) -> Result<Vec<ReactiveInputRecord<N>>, ReactiveError> {
9335 let mut positions = HashMap::<ReactiveInputIdentity, usize>::new();
9336 let mut deduped = Vec::with_capacity(records.len());
9337 for record in records {
9338 let identity = record.validated_identity()?;
9339 if !record.is_payload_deduplicable() {
9340 deduped.push(record);
9341 continue;
9342 }
9343 if let Some(index) = positions.get(&identity).copied() {
9344 let merged = deduped[index].merge_compatible_duplicate(&record)?;
9345 debug_assert!(merged, "same indexed identity is deduplicable");
9346 } else {
9347 positions.insert(identity, deduped.len());
9348 deduped.push(record);
9349 }
9350 }
9351 Ok(deduped)
9352}
9353
9354fn dedupe_scoped_records<N: Network>(
9355 records: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
9356) -> Result<Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>, ReactiveError> {
9357 let mut positions: HashMap<ReactiveInputIdentity, usize> = HashMap::new();
9358 let mut deduped: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)> =
9359 Vec::with_capacity(records.len());
9360 for (record, audience, delivery_scope) in records {
9361 let identity = record.validated_identity()?;
9362 if !record.is_payload_deduplicable() {
9363 deduped.push((record, audience, delivery_scope));
9364 continue;
9365 }
9366 if let Some(index) = positions.get(&identity).copied() {
9367 let merged = deduped[index].0.merge_compatible_duplicate(&record)?;
9368 debug_assert!(merged, "same indexed identity is deduplicable");
9369 merge_delivery_audience(&mut deduped[index].1, audience);
9370 merge_delivery_scope(&mut deduped[index].2, delivery_scope);
9371 } else {
9372 positions.insert(identity, deduped.len());
9373 deduped.push((record, audience, delivery_scope));
9374 }
9375 }
9376 Ok(deduped)
9377}
9378
9379fn merge_delivery_scope(into: &mut DeliveryScope, incoming: DeliveryScope) {
9380 *into = match (*into, incoming) {
9381 (DeliveryScope::Canonical, _) | (_, DeliveryScope::Canonical) => DeliveryScope::Canonical,
9382 (DeliveryScope::CanonicalProgress, _) | (_, DeliveryScope::CanonicalProgress) => {
9383 DeliveryScope::CanonicalProgress
9384 }
9385 (DeliveryScope::Preconfirmed, DeliveryScope::Preconfirmed)
9386 | (DeliveryScope::Preconfirmed, DeliveryScope::OwnerCatchup)
9387 | (DeliveryScope::OwnerCatchup, DeliveryScope::Preconfirmed) => DeliveryScope::Preconfirmed,
9388 (DeliveryScope::OwnerCatchup, DeliveryScope::OwnerCatchup) => DeliveryScope::OwnerCatchup,
9389 };
9390}
9391
9392fn merge_delivery_audience(into: &mut DeliveryAudience, incoming: DeliveryAudience) {
9393 match (&mut *into, incoming) {
9394 (DeliveryAudience::All, _) => {}
9395 (current, DeliveryAudience::All) => *current = DeliveryAudience::All,
9396 (DeliveryAudience::Owners(current), DeliveryAudience::Owners(incoming)) => {
9397 for owner in incoming {
9398 if !current.contains(&owner) {
9399 current.push(owner);
9400 }
9401 }
9402 }
9403 (DeliveryAudience::AllExcept(current), DeliveryAudience::AllExcept(incoming)) => {
9404 current.retain(|owner| incoming.contains(owner));
9405 }
9406 (DeliveryAudience::AllExcept(excluded), DeliveryAudience::Owners(included)) => {
9407 excluded.retain(|owner| !included.contains(owner));
9408 }
9409 (current @ DeliveryAudience::Owners(_), DeliveryAudience::AllExcept(mut excluded)) => {
9410 let DeliveryAudience::Owners(included) = current else {
9411 unreachable!("match arm restricts the audience variant")
9412 };
9413 excluded.retain(|owner| !included.contains(owner));
9414 *current = DeliveryAudience::AllExcept(excluded);
9415 }
9416 }
9417}
9418
9419fn sort_records<N: Network>(records: Vec<ReactiveInputRecord<N>>) -> Vec<ReactiveInputRecord<N>> {
9420 let mut indexed: Vec<(usize, ReactiveInputRecord<N>)> =
9421 records.into_iter().enumerate().collect();
9422 indexed.sort_by_key(|(index, record)| record_sort_key(*index, record));
9423 indexed.into_iter().map(|(_, record)| record).collect()
9424}
9425
9426fn sort_scoped_records<N: Network>(
9427 records: Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)>,
9428) -> Vec<(ReactiveInputRecord<N>, DeliveryAudience, DeliveryScope)> {
9429 let mut indexed: Vec<_> = records.into_iter().enumerate().collect();
9430 indexed.sort_by_key(|(index, (record, _, _))| record_sort_key(*index, record));
9431 indexed
9432 .into_iter()
9433 .map(|(_, scoped_record)| scoped_record)
9434 .collect()
9435}
9436
9437fn record_sort_key<N: Network>(index: usize, record: &ReactiveInputRecord<N>) -> RecordSortKey {
9438 if let Some((block, _)) = reorg_signal_block(record) {
9439 return RecordSortKey {
9440 class: 0,
9441 block_number: block.number,
9442 record_class: 0,
9443 transaction_index: record.context.transaction_index.unwrap_or(u64::MAX),
9444 log_index: record.context.log_index.unwrap_or(u64::MAX),
9445 original_index: index,
9446 };
9447 }
9448 if is_canonical_status(&record.context.chain_status)
9449 && let Some(block) = record.context.block.as_ref()
9450 {
9451 let (record_class, transaction_index, log_index) = match &record.input {
9452 ReactiveInput::BlockHeader(_) | ReactiveInput::FullBlock(_) => (0, 0, 0),
9453 ReactiveInput::Log(log) if !log.removed => (
9454 1,
9455 log.transaction_index
9456 .or(record.context.transaction_index)
9457 .unwrap_or(u64::MAX),
9458 log.log_index
9459 .or(record.context.log_index)
9460 .unwrap_or(u64::MAX),
9461 ),
9462 ReactiveInput::Log(_)
9463 | ReactiveInput::PendingTxHash(_)
9464 | ReactiveInput::PendingTx(_) => (2, u64::MAX, u64::MAX),
9465 };
9466 return RecordSortKey {
9467 class: 1,
9468 block_number: block.number,
9469 record_class,
9470 transaction_index,
9471 log_index,
9472 original_index: index,
9473 };
9474 }
9475
9476 RecordSortKey {
9477 class: 2,
9478 block_number: 0,
9479 record_class: 0,
9480 transaction_index: 0,
9481 log_index: 0,
9482 original_index: index,
9483 }
9484}
9485
9486#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
9487struct RecordSortKey {
9488 class: u8,
9489 block_number: u64,
9490 record_class: u8,
9491 transaction_index: u64,
9492 log_index: u64,
9493 original_index: usize,
9494}
9495
9496fn interest_matches<N: Network>(interest: &ReactiveInterest<N>, input: &ReactiveInput<N>) -> bool {
9497 match (interest, input) {
9498 (ReactiveInterest::Logs(interest), ReactiveInput::Log(log)) => interest.matches(log),
9499 (
9500 ReactiveInterest::Blocks(BlockInterest {
9501 mode: BlockInterestMode::Header,
9502 }),
9503 ReactiveInput::BlockHeader(_),
9504 ) => true,
9505 (
9506 ReactiveInterest::Blocks(BlockInterest {
9507 mode: BlockInterestMode::FullBlock,
9508 }),
9509 ReactiveInput::FullBlock(_),
9510 ) => true,
9511 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTxHash(_)) => {
9512 interest.matches_hash_only()
9513 }
9514 (ReactiveInterest::PendingTransactions(interest), ReactiveInput::PendingTx(tx)) => {
9515 interest.matches_tx(tx)
9516 }
9517 _ => false,
9518 }
9519}
9520
9521fn validate_effects(
9522 input_ref: InputRef,
9523 ctx: &ReactiveContext,
9524 handler_id: &HandlerId,
9525 effects: &[ReactiveEffect],
9526) -> Result<(), ReactiveError> {
9527 let pending = matches!(ctx.chain_status, ChainStatus::Pending)
9528 || matches!(input_ref, InputRef::PendingTx { .. });
9529 if !pending {
9530 return Ok(());
9531 }
9532
9533 for effect in effects {
9534 let effect_kind = match effect {
9535 ReactiveEffect::StateUpdate(_) => Some("state_update"),
9536 ReactiveEffect::Invalidate(_) => Some("invalidate"),
9537 ReactiveEffect::Resync(_) => Some("resync"),
9538 ReactiveEffect::Hook(_) | ReactiveEffect::Speculative(_) => None,
9539 };
9540 if let Some(effect_kind) = effect_kind {
9541 return Err(ReactiveError::InvalidPendingEffect {
9542 input_ref: Box::new(input_ref),
9543 handler_id: handler_id.clone(),
9544 effect_kind,
9545 });
9546 }
9547 }
9548 Ok(())
9549}
9550
9551fn detect_conflicts(
9552 input_ref: InputRef,
9553 executions: &[HandlerExecution],
9554) -> Result<(), ReactiveError> {
9555 let mut writes: HashMap<EffectTarget, (AbsoluteValue, HandlerId)> = HashMap::new();
9556 for execution in executions {
9557 for update in &execution.state_updates {
9558 for (target, value) in absolute_writes(update) {
9559 if let Some((previous_value, previous_handler)) = writes.get(&target) {
9560 if previous_value != &value {
9561 return Err(ReactiveError::ConflictingEffects {
9562 input_ref: Box::new(input_ref),
9563 target: Box::new(target),
9564 first: previous_handler.clone(),
9565 second: execution.handler_id.clone(),
9566 });
9567 }
9568 } else {
9569 writes.insert(target, (value, execution.handler_id.clone()));
9570 }
9571 }
9572 }
9573 }
9574 Ok(())
9575}
9576
9577fn absolute_writes(update: &StateUpdate) -> Vec<(EffectTarget, AbsoluteValue)> {
9578 match update {
9579 StateUpdate::Slot {
9580 address,
9581 slot,
9582 value,
9583 } => vec![(
9584 EffectTarget::StorageSlot {
9585 address: *address,
9586 slot: *slot,
9587 },
9588 AbsoluteValue::U256(*value),
9589 )],
9590 StateUpdate::SlotMasked {
9591 address,
9592 slot,
9593 mask,
9594 value,
9595 } => vec![(
9596 EffectTarget::MaskedStorageSlot {
9597 address: *address,
9598 slot: *slot,
9599 mask: *mask,
9600 },
9601 AbsoluteValue::U256(*value),
9602 )],
9603 StateUpdate::Account { address, patch } | StateUpdate::AccountUpsert { address, patch } => {
9604 account_patch_writes(*address, patch)
9605 }
9606 StateUpdate::SlotDelta { .. }
9607 | StateUpdate::BalanceDelta { .. }
9608 | StateUpdate::Purge { .. } => Vec::new(),
9609 }
9610}
9611
9612fn account_patch_writes(
9613 address: Address,
9614 patch: &AccountPatch,
9615) -> Vec<(EffectTarget, AbsoluteValue)> {
9616 let mut writes = Vec::new();
9617 if let Some(balance) = patch.balance {
9618 writes.push((
9619 EffectTarget::AccountBalance { address },
9620 AbsoluteValue::U256(balance),
9621 ));
9622 }
9623 if let Some(nonce) = patch.nonce {
9624 writes.push((
9625 EffectTarget::AccountNonce { address },
9626 AbsoluteValue::U64(nonce),
9627 ));
9628 }
9629 if let Some(code) = &patch.code {
9630 writes.push((
9631 EffectTarget::AccountCode { address },
9632 AbsoluteValue::Bytes(code.clone()),
9633 ));
9634 }
9635 writes
9636}
9637
9638fn input_ref<N: Network>(input: &ReactiveInput<N>, ctx: &ReactiveContext) -> InputRef {
9639 match input {
9640 ReactiveInput::Log(log) => InputRef::Log {
9641 chain_id: ctx.chain_id,
9642 block_hash: log
9643 .block_hash
9644 .or(ctx.block.as_ref().map(|block| block.hash))
9645 .unwrap_or_default(),
9646 transaction_hash: log.transaction_hash.unwrap_or_default(),
9647 log_index: log.log_index.or(ctx.log_index).unwrap_or_default(),
9648 },
9649 ReactiveInput::PendingTxHash(hash) => InputRef::PendingTx {
9650 chain_id: ctx.chain_id,
9651 hash: *hash,
9652 },
9653 ReactiveInput::PendingTx(tx) => InputRef::PendingTx {
9654 chain_id: ctx.chain_id,
9655 hash: tx.tx_hash(),
9656 },
9657 ReactiveInput::BlockHeader(header) => InputRef::Block {
9658 chain_id: ctx.chain_id,
9659 hash: header.hash(),
9660 number: header.number(),
9661 },
9662 ReactiveInput::FullBlock(block) => {
9663 let header = block.header();
9664 InputRef::Block {
9665 chain_id: ctx.chain_id,
9666 hash: header.hash(),
9667 number: header.number(),
9668 }
9669 }
9670 }
9671}
9672
9673fn is_canonical_status(status: &ChainStatus) -> bool {
9674 matches!(
9675 status,
9676 ChainStatus::Included { .. } | ChainStatus::Safe { .. } | ChainStatus::Finalized { .. }
9677 )
9678}
9679
9680pub struct EventDecoderHandler {
9682 id: HandlerId,
9683 decoder: Arc<dyn EventDecoder>,
9684 interest: LogInterest,
9685}
9686
9687impl EventDecoderHandler {
9688 pub fn new(id: HandlerId, decoder: Arc<dyn EventDecoder>, interest: LogInterest) -> Self {
9690 Self {
9691 id,
9692 decoder,
9693 interest,
9694 }
9695 }
9696}
9697
9698impl<N: Network> ReactiveHandler<N> for EventDecoderHandler {
9699 fn id(&self) -> HandlerId {
9700 self.id.clone()
9701 }
9702
9703 fn interests(&self) -> Vec<ReactiveInterest<N>> {
9704 vec![ReactiveInterest::Logs(self.interest.clone())]
9705 }
9706
9707 fn handle(
9708 &self,
9709 _ctx: &ReactiveContext,
9710 input: &ReactiveInput<N>,
9711 state: &dyn StateView,
9712 ) -> Result<HandlerOutcome, HandlerError> {
9713 let ReactiveInput::Log(log) = input else {
9714 return Ok(HandlerOutcome::empty(StateEffectQuality::NoStateEffect));
9715 };
9716
9717 Ok(HandlerOutcome {
9718 effects: self
9719 .decoder
9720 .decode(&log.inner, state)
9721 .into_iter()
9722 .map(ReactiveEffect::StateUpdate)
9723 .collect(),
9724 quality: StateEffectQuality::ExactFromInput,
9725 tags: Vec::new(),
9726 })
9727 }
9728}
9729
9730#[derive(
9732 Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
9733)]
9734#[non_exhaustive]
9735pub enum SubscriberCapability {
9736 Logs,
9738 BlockHeaders,
9740 FullBlocks,
9742 PendingTransactionHashes,
9744 PendingTransactions,
9746 HistoricalBackfill,
9748 Live,
9750 DurableReplay,
9760 OwnerScopedDelivery,
9762 DynamicInterests,
9764 ExplicitReorgs,
9766 FinalityUpdates,
9768 Barriers,
9770 Preconfirmations,
9772}
9773
9774#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9780pub struct SubscriberCapabilities {
9781 supported: BTreeSet<SubscriberCapability>,
9782}
9783
9784impl SubscriberCapabilities {
9785 pub fn new(capabilities: impl IntoIterator<Item = SubscriberCapability>) -> Self {
9787 Self {
9788 supported: capabilities.into_iter().collect(),
9789 }
9790 }
9791
9792 pub fn supports(&self, capability: SubscriberCapability) -> bool {
9794 self.supported.contains(&capability)
9795 }
9796
9797 pub fn iter(&self) -> impl Iterator<Item = SubscriberCapability> + '_ {
9799 self.supported.iter().copied()
9800 }
9801
9802 pub fn supports_live(&self) -> bool {
9804 self.supports(SubscriberCapability::Live)
9805 }
9806
9807 pub fn supports_durable_replay(&self) -> bool {
9810 self.supports(SubscriberCapability::DurableReplay)
9811 }
9812
9813 pub fn supports_explicit_reorgs(&self) -> bool {
9815 self.supports(SubscriberCapability::ExplicitReorgs)
9816 }
9817}
9818
9819impl FromIterator<SubscriberCapability> for SubscriberCapabilities {
9820 fn from_iter<T: IntoIterator<Item = SubscriberCapability>>(iter: T) -> Self {
9821 Self::new(iter)
9822 }
9823}
9824
9825pub trait EventSubscriber<N: Network = Ethereum>: Send {
9827 fn chain_id(&self) -> Option<u64> {
9834 None
9835 }
9836
9837 fn capabilities(&self) -> SubscriberCapabilities {
9839 SubscriberCapabilities::default()
9840 }
9841
9842 fn register_interests(
9861 &mut self,
9862 interests: &[ReactiveInterest<N>],
9863 ) -> SubscriberOperation<'_, ()>;
9864
9865 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N>;
9877
9878 fn restore_position(
9901 &mut self,
9902 _position: &SubscriberResumePosition,
9903 ) -> Result<(), SubscriberError> {
9904 Ok(())
9905 }
9906
9907 fn acknowledge_delivery(
9921 &mut self,
9922 _token: SubscriberDeliveryToken,
9923 ) -> SubscriberOperation<'_, ()> {
9924 Box::pin(async { Ok(()) })
9925 }
9926}
9927
9928pub type SubscriberOperation<'a, T> =
9933 Pin<Box<dyn Future<Output = Result<T, SubscriberError>> + Send + 'a>>;
9934
9935pub type SubscriberNextBatch<'a, N> = Pin<
9937 Box<dyn Future<Output = Result<Option<ReactiveInputBatch<N>>, SubscriberError>> + Send + 'a>,
9938>;
9939
9940pub type SubscriberNextScopedBatch<'a, N> = Pin<
9942 Box<dyn Future<Output = Result<Option<SubscriberInputBatch<N>>, SubscriberError>> + Send + 'a>,
9943>;
9944
9945#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
9947pub enum SubscriberMode {
9948 #[default]
9954 Auto,
9955 PubSub,
9957 Polling,
9959}
9960
9961#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9962enum FlashblocksAdapter {
9963 NativeSubscriptions,
9964 PendingStatePolling,
9965}
9966
9967fn flashblocks_adapter(chain_id: u64) -> Option<FlashblocksAdapter> {
9968 match chain_id {
9969 8_453 | 84_532 => Some(FlashblocksAdapter::NativeSubscriptions),
9970 10 | 11_155_420 => Some(FlashblocksAdapter::PendingStatePolling),
9971 _ => None,
9972 }
9973}
9974
9975#[derive(Clone, Debug, PartialEq, Eq)]
9977pub struct SubscriberConfig {
9978 pub preconfirmations: PreconfirmationMode,
9981 pub canonical_head_poll_interval: Duration,
9984 pub canonical_head_request_timeout: Duration,
9987 pub flashblock_poll_interval: Duration,
9995 pub max_consecutive_flashblock_poll_failures: usize,
10001 pub max_pending_transaction_receipts_per_tick: usize,
10009 pub max_flashblock_rpc_requests_per_second: usize,
10019 pub hydrate_pending_transactions: bool,
10021 pub verify_log_block_context: bool,
10032 pub max_batch_size: usize,
10034 pub max_log_addresses_per_subscription: usize,
10038 pub max_pending_records: usize,
10043 pub max_pending_backfills: usize,
10045 pub max_backfill_log_bytes: usize,
10048 pub max_reconcile_requests_in_flight: usize,
10051 pub reconnect: SubscriberReconnectConfig,
10053}
10054
10055impl Default for SubscriberConfig {
10056 fn default() -> Self {
10057 Self {
10058 preconfirmations: PreconfirmationMode::Disabled,
10059 canonical_head_poll_interval: Duration::from_millis(500),
10060 canonical_head_request_timeout: Duration::from_secs(3),
10061 flashblock_poll_interval: Duration::from_millis(250),
10062 max_consecutive_flashblock_poll_failures: 10,
10063 max_pending_transaction_receipts_per_tick: 32,
10064 max_flashblock_rpc_requests_per_second: 40,
10065 hydrate_pending_transactions: false,
10066 verify_log_block_context: false,
10067 max_batch_size: 1024,
10068 max_log_addresses_per_subscription: 1024,
10069 max_pending_records: 16_384,
10070 max_pending_backfills: 4_096,
10071 max_backfill_log_bytes: 64 * 1024 * 1024,
10072 max_reconcile_requests_in_flight: 8,
10073 reconnect: SubscriberReconnectConfig::default(),
10074 }
10075 }
10076}
10077
10078#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10080pub enum FlashblocksDelivery {
10081 NativeSubscriptions,
10083 PendingStatePolling,
10085 #[cfg(feature = "raw-flashblocks-json")]
10087 ExternalUpdates,
10088}
10089
10090#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10092pub struct FlashblocksRpcMetrics {
10093 capability_requests: u64,
10094 provider_pair_chain_requests: u64,
10095 canonical_head_requests: u64,
10096 pending_block_requests: u64,
10097 pending_log_requests: u64,
10098 pending_receipt_requests: u64,
10099 pending_receipts_completed: u64,
10100 pending_receipts_unavailable: u64,
10101 failed_requests: u64,
10102 raced_samples: u64,
10103}
10104
10105impl FlashblocksRpcMetrics {
10106 pub const fn capability_requests(self) -> u64 {
10108 self.capability_requests
10109 }
10110
10111 pub const fn provider_pair_chain_requests(self) -> u64 {
10114 self.provider_pair_chain_requests
10115 }
10116
10117 pub const fn canonical_head_requests(self) -> u64 {
10119 self.canonical_head_requests
10120 }
10121
10122 pub const fn pending_block_requests(self) -> u64 {
10124 self.pending_block_requests
10125 }
10126
10127 pub const fn pending_log_requests(self) -> u64 {
10129 self.pending_log_requests
10130 }
10131
10132 pub const fn pending_receipt_requests(self) -> u64 {
10135 self.pending_receipt_requests
10136 }
10137
10138 pub const fn pending_receipts_completed(self) -> u64 {
10140 self.pending_receipts_completed
10141 }
10142
10143 pub const fn pending_receipts_unavailable(self) -> u64 {
10146 self.pending_receipts_unavailable
10147 }
10148
10149 pub const fn failed_requests(self) -> u64 {
10151 self.failed_requests
10152 }
10153
10154 pub const fn raced_samples(self) -> u64 {
10158 self.raced_samples
10159 }
10160
10161 pub const fn total_requests(self) -> u64 {
10164 self.capability_requests
10165 .saturating_add(self.provider_pair_chain_requests)
10166 .saturating_add(self.canonical_head_requests)
10167 .saturating_add(self.pending_block_requests)
10168 .saturating_add(self.pending_log_requests)
10169 .saturating_add(self.pending_receipt_requests)
10170 }
10171}
10172
10173#[derive(Clone, Debug, PartialEq, Eq)]
10182pub struct FlashblocksPreflight {
10183 chain_id: u64,
10184 provider: ProviderRef,
10185 delivery: FlashblocksDelivery,
10186 pending_log_subscriptions: usize,
10187 pending_log_filters: usize,
10188 advertised_capabilities: Option<serde_json::Value>,
10189}
10190
10191impl FlashblocksPreflight {
10192 pub const fn chain_id(&self) -> u64 {
10194 self.chain_id
10195 }
10196
10197 pub const fn provider(&self) -> &ProviderRef {
10203 &self.provider
10204 }
10205
10206 pub const fn delivery(&self) -> FlashblocksDelivery {
10208 self.delivery
10209 }
10210
10211 pub const fn pending_log_subscriptions(&self) -> usize {
10215 self.pending_log_subscriptions
10216 }
10217
10218 pub const fn pending_log_filters(&self) -> usize {
10221 self.pending_log_filters
10222 }
10223
10224 pub const fn advertised_capabilities(&self) -> Option<&serde_json::Value> {
10227 self.advertised_capabilities.as_ref()
10228 }
10229}
10230
10231#[derive(Clone, Debug, PartialEq, Eq)]
10237pub struct SubscriberReconnectConfig {
10238 pub enabled: bool,
10240 pub initial_delay: Duration,
10242 pub retry_delay: Duration,
10245 pub max_delay: Duration,
10247 pub max_attempts: Option<usize>,
10249 pub dedupe_window: usize,
10252}
10253
10254impl Default for SubscriberReconnectConfig {
10255 fn default() -> Self {
10256 Self {
10257 enabled: true,
10258 initial_delay: Duration::ZERO,
10259 retry_delay: Duration::from_millis(250),
10260 max_delay: Duration::from_secs(30),
10261 max_attempts: Some(3),
10262 dedupe_window: 4096,
10263 }
10264 }
10265}
10266
10267#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10279pub struct SubscriberBackfill {
10280 from_block: u64,
10281 to_block: Option<u64>,
10282 retained_anchor: Option<BlockRef>,
10283}
10284
10285impl SubscriberBackfill {
10286 pub fn range(from_block: u64, to_block: u64) -> Self {
10288 Self {
10289 from_block,
10290 to_block: Some(to_block),
10291 retained_anchor: None,
10292 }
10293 }
10294
10295 pub fn from_block(from_block: u64) -> Self {
10297 Self {
10298 from_block,
10299 to_block: None,
10300 retained_anchor: None,
10301 }
10302 }
10303
10304 pub fn from_canonical_block(block: BlockRef) -> Self {
10311 Self {
10312 from_block: block.number,
10313 to_block: None,
10314 retained_anchor: Some(block),
10315 }
10316 }
10317
10318 pub fn from_canonical_block_through(
10326 block: BlockRef,
10327 to_block: u64,
10328 ) -> Result<Self, SubscriberError> {
10329 if to_block < block.number {
10330 return Err(SubscriberError::InvalidConfig(
10331 "inclusive backfill upper bound precedes its retained anchor",
10332 ));
10333 }
10334 Ok(Self {
10335 from_block: block.number,
10336 to_block: Some(to_block),
10337 retained_anchor: Some(block),
10338 })
10339 }
10340
10341 pub fn after_canonical_block(block: BlockRef) -> Result<Self, SubscriberError> {
10357 Self::after_canonical_block_inner(block, None)
10358 }
10359
10360 pub fn after_canonical_block_through(
10372 block: BlockRef,
10373 to_block: u64,
10374 ) -> Result<Self, SubscriberError> {
10375 if to_block < block.number {
10376 return Err(SubscriberError::InvalidConfig(
10377 "exclusive backfill upper bound precedes its retained baseline",
10378 ));
10379 }
10380 Self::after_canonical_block_inner(block, Some(to_block))
10381 }
10382
10383 fn after_canonical_block_inner(
10384 block: BlockRef,
10385 to_block: Option<u64>,
10386 ) -> Result<Self, SubscriberError> {
10387 let from_block = block
10388 .number
10389 .checked_add(1)
10390 .ok_or(SubscriberError::InvalidConfig(
10391 "cannot construct an exclusive backfill after block u64::MAX",
10392 ))?;
10393 Ok(Self {
10394 from_block,
10395 to_block,
10396 retained_anchor: Some(block),
10397 })
10398 }
10399
10400 pub fn start_block(&self) -> u64 {
10402 self.from_block
10403 }
10404
10405 pub fn end_block(&self) -> Option<u64> {
10407 self.to_block
10408 }
10409
10410 pub fn retained_anchor(&self) -> Option<&BlockRef> {
10412 self.retained_anchor.as_ref()
10413 }
10414}
10415
10416#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
10423pub struct SubscriberOwnerEpoch {
10424 owner: HandlerId,
10425 sequence: u64,
10426}
10427
10428#[derive(Clone, Debug, PartialEq, Eq)]
10435#[non_exhaustive]
10436pub enum SubscriberInputScope {
10437 Canonical {
10439 owners: Vec<SubscriberOwnerEpoch>,
10441 },
10442 CanonicalResidual {
10446 owners: Vec<SubscriberOwnerEpoch>,
10448 excluded: Vec<HandlerId>,
10450 },
10451 OwnerOnly {
10453 owners: Vec<SubscriberOwnerEpoch>,
10455 },
10456 OwnerOnlyHandlers {
10458 owners: Vec<HandlerId>,
10460 },
10461 Preconfirmed,
10464}
10465
10466impl SubscriberInputScope {
10467 pub fn owners(&self) -> &[SubscriberOwnerEpoch] {
10469 match self {
10470 Self::Canonical { owners }
10471 | Self::CanonicalResidual { owners, .. }
10472 | Self::OwnerOnly { owners } => owners,
10473 Self::OwnerOnlyHandlers { .. } | Self::Preconfirmed => &[],
10474 }
10475 }
10476
10477 pub const fn is_canonical(&self) -> bool {
10479 matches!(
10480 self,
10481 Self::Canonical { .. } | Self::CanonicalResidual { .. }
10482 )
10483 }
10484
10485 pub const fn is_preconfirmed(&self) -> bool {
10487 matches!(self, Self::Preconfirmed)
10488 }
10489}
10490
10491#[derive(Clone, Debug)]
10493pub struct SubscriberInputRecord<N: Network = Ethereum> {
10494 record: ReactiveInputRecord<N>,
10495 scope: SubscriberInputScope,
10496}
10497
10498impl<N: Network> SubscriberInputRecord<N> {
10499 pub const fn record(&self) -> &ReactiveInputRecord<N> {
10501 &self.record
10502 }
10503
10504 pub const fn scope(&self) -> &SubscriberInputScope {
10506 &self.scope
10507 }
10508
10509 pub fn into_record(self) -> ReactiveInputRecord<N> {
10511 self.record
10512 }
10513}
10514
10515impl<N: Network> std::ops::Deref for SubscriberInputRecord<N> {
10516 type Target = ReactiveInputRecord<N>;
10517
10518 fn deref(&self) -> &Self::Target {
10519 &self.record
10520 }
10521}
10522
10523#[derive(Clone, Debug)]
10525pub struct SubscriberInputBatch<N: Network = Ethereum> {
10526 records: Vec<SubscriberInputRecord<N>>,
10527 chain_id: Option<u64>,
10528 chain_controls: Vec<ChainControl>,
10529 preconfirmation_invalidated: bool,
10530}
10531
10532#[derive(Debug)]
10535#[non_exhaustive]
10536pub enum SubscriberDriverPoll<C, N: Network = Ethereum> {
10537 Control(C),
10539 Batch(Option<SubscriberInputBatch<N>>),
10541}
10542
10543impl<N: Network> SubscriberInputBatch<N> {
10544 pub fn records(&self) -> &[SubscriberInputRecord<N>] {
10546 &self.records
10547 }
10548
10549 pub fn into_records(self) -> Vec<SubscriberInputRecord<N>> {
10551 self.records
10552 }
10553
10554 pub fn chain_controls(&self) -> &[ChainControl] {
10556 &self.chain_controls
10557 }
10558
10559 pub const fn preconfirmation_invalidated(&self) -> bool {
10562 self.preconfirmation_invalidated
10563 }
10564
10565 pub fn into_reactive_batch(self) -> ReactiveInputBatch<N> {
10571 let chain_id = self.chain_id;
10572 let chain_controls = self.chain_controls;
10573 let mut batch = ReactiveInputBatch::from_scoped_records_with_delivery_scope(
10574 self.records.into_iter().map(|scoped| {
10575 let source = scoped.record.context.source;
10576 let (audience, delivery_scope) = match scoped.scope {
10577 SubscriberInputScope::Canonical { .. } => (
10578 DeliveryAudience::All,
10579 if source == InputSource::Backfill {
10580 DeliveryScope::CanonicalProgress
10581 } else {
10582 DeliveryScope::Canonical
10583 },
10584 ),
10585 SubscriberInputScope::CanonicalResidual { excluded, .. } => (
10586 DeliveryAudience::AllExcept(excluded),
10587 if source == InputSource::Backfill {
10588 DeliveryScope::CanonicalProgress
10589 } else {
10590 DeliveryScope::Canonical
10591 },
10592 ),
10593 SubscriberInputScope::OwnerOnly { owners } => {
10594 let mut handler_ids = Vec::with_capacity(owners.len());
10595 for epoch in owners {
10596 if !handler_ids.contains(epoch.owner()) {
10597 handler_ids.push(epoch.owner().clone());
10598 }
10599 }
10600 (
10601 DeliveryAudience::Owners(handler_ids),
10602 DeliveryScope::OwnerCatchup,
10603 )
10604 }
10605 SubscriberInputScope::OwnerOnlyHandlers { owners } => (
10606 DeliveryAudience::Owners(owners),
10607 DeliveryScope::OwnerCatchup,
10608 ),
10609 SubscriberInputScope::Preconfirmed => {
10610 (DeliveryAudience::All, DeliveryScope::Preconfirmed)
10611 }
10612 };
10613 (scoped.record, audience, delivery_scope)
10614 }),
10615 )
10616 .with_chain_controls(chain_controls);
10617 if let Some(chain_id) = chain_id {
10618 batch = batch.with_chain_id(chain_id);
10619 }
10620 batch
10621 }
10622}
10623
10624impl SubscriberOwnerEpoch {
10625 pub const fn owner(&self) -> &HandlerId {
10627 &self.owner
10628 }
10629
10630 pub const fn sequence(&self) -> u64 {
10632 self.sequence
10633 }
10634}
10635
10636#[derive(Clone, Debug, PartialEq, Eq)]
10638#[non_exhaustive]
10639pub enum SubscriberOwnerStart {
10640 Live,
10642 PostBlock(BlockRef),
10649}
10650
10651#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10653#[non_exhaustive]
10654pub enum SubscriberOwnerState {
10655 Staged,
10658 Active,
10660 Removing,
10662}
10663
10664#[derive(Clone, Debug, PartialEq, Eq)]
10670pub struct SubscriberOwnerProgress {
10671 owner: SubscriberOwnerEpoch,
10672 through: BlockRef,
10673}
10674
10675impl SubscriberOwnerProgress {
10676 pub const fn owner(&self) -> &SubscriberOwnerEpoch {
10678 &self.owner
10679 }
10680
10681 pub const fn through(&self) -> &BlockRef {
10683 &self.through
10684 }
10685}
10686
10687#[derive(Debug, thiserror::Error)]
10689#[non_exhaustive]
10690pub enum SubscriberOwnerError {
10691 #[error(transparent)]
10693 Subscriber(#[from] SubscriberError),
10694 #[error("subscriber interest owner `{0}` is already registered")]
10696 AlreadyRegistered(HandlerId),
10697 #[error("post-block subscriber baseline {0} has no following block")]
10699 PostBlockOverflow(u64),
10700 #[error("subscriber owner epoch sequence exhausted")]
10702 EpochExhausted,
10703 #[error("subscriber owner epoch is not staged")]
10705 NotStaged,
10706 #[error("subscriber owner was staged live-only and has no catch-up baseline")]
10708 MissingBaseline,
10709 #[error("post-block subscriber owners support log interests only")]
10711 UnsupportedPostBlockInterest,
10712 #[error("subscriber reconcile target block {0} was not found")]
10714 BlockUnavailable(u64),
10715 #[error(
10717 "subscriber reconcile target mismatch: expected block {expected_number} {expected_hash}, got block {actual_number} {actual_hash}"
10718 )]
10719 BlockMismatch {
10720 expected_number: u64,
10722 expected_hash: B256,
10724 actual_number: u64,
10726 actual_hash: B256,
10728 },
10729 #[error("subscriber reconcile target block {target} precedes current owner position {current}")]
10731 ProgressRegression {
10732 current: u64,
10734 target: u64,
10736 },
10737 #[error(
10740 "subscriber reconcile conflicts with retained block {number} {current_hash}: target chain references {target_hash}"
10741 )]
10742 ProgressConflict {
10743 number: u64,
10745 current_hash: B256,
10747 target_hash: B256,
10749 },
10750 #[error("subscriber reconcile returned an invalid catch-up log: {0}")]
10752 InvalidBackfillLog(&'static str),
10753}
10754
10755pub trait InterestOwnerSubscriber<N: Network = Ethereum>: EventSubscriber<N> {
10770 fn upsert_interest_owners(
10783 &mut self,
10784 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
10785 ) -> SubscriberOperation<'_, ()> {
10786 Box::pin(async {
10787 Err(SubscriberError::Unsupported(
10788 "subscriber does not implement atomic bulk owner upsert",
10789 ))
10790 })
10791 }
10792
10793 fn replace_interest_owners(
10806 &mut self,
10807 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
10808 ) -> SubscriberOperation<'_, ()> {
10809 Box::pin(async {
10810 Err(SubscriberError::Unsupported(
10811 "subscriber does not implement atomic exact owner replacement",
10812 ))
10813 })
10814 }
10815
10816 fn replace_interest_owners_with_global_backfill(
10840 &mut self,
10841 _owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
10842 _backfill: SubscriberBackfill,
10843 ) -> SubscriberOperation<'_, ()> {
10844 Box::pin(async {
10845 Err(SubscriberError::Unsupported(
10846 "subscriber does not implement atomic owner replacement with global backfill",
10847 ))
10848 })
10849 }
10850
10851 fn add_interest_owner(
10863 &mut self,
10864 owner: HandlerId,
10865 interests: &[ReactiveInterest<N>],
10866 ) -> SubscriberOperation<'_, ()>;
10867
10868 fn add_interest_owner_with_backfill(
10876 &mut self,
10877 owner: HandlerId,
10878 interests: &[ReactiveInterest<N>],
10879 backfill: SubscriberBackfill,
10880 ) -> SubscriberOperation<'_, ()>;
10881
10882 fn add_interest_owner_with_canonical_catchup(
10900 &mut self,
10901 _owner: HandlerId,
10902 _interests: &[ReactiveInterest<N>],
10903 _retained: BlockRef,
10904 ) -> SubscriberOperation<'_, ()> {
10905 Box::pin(async {
10906 Err(SubscriberError::Unsupported(
10907 "subscriber does not implement coordinated canonical owner catch-up",
10908 ))
10909 })
10910 }
10911
10912 fn remove_interest_owner(
10923 &mut self,
10924 owner: &HandlerId,
10925 ) -> SubscriberOperation<'_, Option<Vec<ReactiveInterest<N>>>>;
10926
10927 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]>;
10929}
10930
10931pub struct ReactiveEngine<S, N: Network = Ethereum> {
10972 runtime: ReactiveRuntime<N>,
10973 subscriber: S,
10974 pending_acknowledgement: Option<PendingAcknowledgement<N>>,
10975 pending_checkpoint: Option<PendingCheckpoint<N>>,
10976 last_checkpoint_block: Option<DurableCheckpointBlock>,
10977 last_checkpoint_delivery_token: Option<SubscriberDeliveryToken>,
10978 last_checkpoint_delivery_witness: Option<B256>,
10979 last_subscriber_checkpoint: Option<SubscriberCheckpoint>,
10980 checkpoint_identity: Option<DurableCheckpointIdentity>,
10981}
10982
10983struct PendingAcknowledgement<N: Network> {
10984 token: SubscriberDeliveryToken,
10985 report: ReactiveBatchReport<N>,
10986}
10987
10988struct PendingCheckpoint<N: Network> {
10989 metadata: DurableCheckpointMetadata,
10990 delivery_token: Option<SubscriberDeliveryToken>,
10991 report: ReactiveBatchReport<N>,
10992 saved_to: Option<PathBuf>,
10993 staged_generation: u64,
10994}
10995
10996struct CheckpointStage<N: Network> {
10997 incoming_block: Option<DurableCheckpointBlock>,
10998 delivery_token: Option<SubscriberDeliveryToken>,
10999 delivery_witness: Option<B256>,
11000 subscriber_checkpoint: Option<SubscriberCheckpoint>,
11001 staged_generation: u64,
11002 report: ReactiveBatchReport<N>,
11003}
11004
11005struct DurableResumePlan {
11006 runtime: DurableRuntimeRestorePlan,
11007 position: SubscriberResumePosition,
11008 delivery_witness: Option<B256>,
11009}
11010
11011enum HandlerRegistrationCatchup {
11012 LiveOnly,
11013 OwnerBackfill(SubscriberBackfill),
11014 CoordinatedCanonical(BlockRef),
11015}
11016
11017const DELIVERY_WITNESS_VERSION: u32 = 1;
11018const DELIVERY_WITNESS_DOMAIN: &[u8] = b"evm-fork-cache/reactive-delivery-witness";
11019
11020#[derive(serde::Serialize)]
11021struct DeliveryWitnessEnvelope<'a> {
11022 version: u32,
11023 chain_id: Option<u64>,
11024 records: Vec<DeliveryRecordWitness<'a>>,
11025 chain_controls: &'a [ChainControl],
11026 subscriber_checkpoint: Option<&'a [u8]>,
11027 payload_commitment: Option<B256>,
11028}
11029
11030#[derive(serde::Serialize)]
11031struct DeliveryRecordWitness<'a> {
11032 identity: ReactiveInputIdentity,
11033 context: &'a ReactiveContext,
11034 audience: &'a DeliveryAudience,
11035 scope: DeliveryScope,
11036 payload: DeliveryPayloadWitness<'a>,
11037}
11038
11039#[derive(serde::Serialize)]
11040enum DeliveryPayloadWitness<'a> {
11041 Log {
11044 address: Address,
11045 topics: &'a [B256],
11046 data: &'a Bytes,
11047 block_hash: Option<B256>,
11048 block_number: Option<u64>,
11049 block_timestamp: Option<u64>,
11050 transaction_hash: Option<B256>,
11051 transaction_index: Option<u64>,
11052 log_index: Option<u64>,
11053 removed: bool,
11054 },
11055 IdentityCommitted,
11062}
11063
11064fn durable_delivery_witness<N: Network>(
11065 batch: &ReactiveInputBatch<N>,
11066) -> Result<B256, ReactiveEngineError> {
11067 let requires_payload_commitment = batch.records.iter().any(|record| {
11068 matches!(
11069 &record.input,
11070 ReactiveInput::BlockHeader(_)
11071 | ReactiveInput::FullBlock(_)
11072 | ReactiveInput::PendingTx(_)
11073 )
11074 });
11075 if requires_payload_commitment && batch.payload_commitment.is_none() {
11076 return Err(ReactiveEngineError::MissingPayloadCommitment);
11077 }
11078 let records = batch
11079 .records
11080 .iter()
11081 .enumerate()
11082 .map(|(index, record)| {
11083 let payload = match &record.input {
11084 ReactiveInput::Log(log) => DeliveryPayloadWitness::Log {
11085 address: log.address(),
11086 topics: log.topics(),
11087 data: &log.inner.data.data,
11088 block_hash: log.block_hash,
11089 block_number: log.block_number,
11090 block_timestamp: log.block_timestamp,
11091 transaction_hash: log.transaction_hash,
11092 transaction_index: log.transaction_index,
11093 log_index: log.log_index,
11094 removed: log.removed,
11095 },
11096 ReactiveInput::BlockHeader(_)
11097 | ReactiveInput::FullBlock(_)
11098 | ReactiveInput::PendingTxHash(_)
11099 | ReactiveInput::PendingTx(_) => DeliveryPayloadWitness::IdentityCommitted,
11100 };
11101 Ok(DeliveryRecordWitness {
11102 identity: record.validated_identity()?,
11103 context: &record.context,
11104 audience: batch
11105 .record_audience(index)
11106 .expect("enumerated record always has an audience"),
11107 scope: batch
11108 .record_delivery_scope(index)
11109 .expect("enumerated record always has a delivery scope"),
11110 payload,
11111 })
11112 })
11113 .collect::<Result<Vec<_>, ReactiveError>>()?;
11114 let envelope = DeliveryWitnessEnvelope {
11115 version: DELIVERY_WITNESS_VERSION,
11116 chain_id: batch.chain_id,
11117 records,
11118 chain_controls: &batch.chain_controls,
11119 subscriber_checkpoint: batch
11120 .subscriber_checkpoint
11121 .as_ref()
11122 .map(SubscriberCheckpoint::as_bytes),
11123 payload_commitment: batch
11124 .payload_commitment
11125 .as_ref()
11126 .map(SubscriberPayloadCommitment::digest),
11127 };
11128 let encoded = bincode::DefaultOptions::new()
11129 .with_fixint_encoding()
11130 .serialize(&envelope)
11131 .map_err(|error| ReactiveEngineError::DeliveryWitness(error.to_string()))?;
11132 let mut witness = Keccak256::new();
11133 witness.update(DELIVERY_WITNESS_DOMAIN);
11134 witness.update(encoded);
11135 Ok(witness.finalize())
11136}
11137
11138impl<S, N> ReactiveEngine<S, N>
11139where
11140 N: Network,
11141 S: EventSubscriber<N>,
11142{
11143 pub fn new(runtime: ReactiveRuntime<N>, subscriber: S) -> Self {
11145 Self {
11146 runtime,
11147 subscriber,
11148 pending_acknowledgement: None,
11149 pending_checkpoint: None,
11150 last_checkpoint_block: None,
11151 last_checkpoint_delivery_token: None,
11152 last_checkpoint_delivery_witness: None,
11153 last_subscriber_checkpoint: None,
11154 checkpoint_identity: None,
11155 }
11156 }
11157
11158 pub fn into_parts(self) -> Result<(ReactiveRuntime<N>, S), Box<Self>> {
11172 if self.pending_acknowledgement.is_some() || self.pending_checkpoint.is_some() {
11173 return Err(Box::new(self));
11174 }
11175 Ok((self.runtime, self.subscriber))
11176 }
11177
11178 fn durable_resume_plan(
11179 &self,
11180 metadata: &DurableCheckpointMetadata,
11181 ) -> Result<DurableResumePlan, ReactiveCheckpointRestoreError> {
11182 if !self.subscriber.capabilities().supports_durable_replay() {
11183 return Err(ReactiveCheckpointRestoreError::SubscriberNotDurable);
11184 }
11185 self.ensure_subscriber_restore_chain(metadata.identity.chain_id)?;
11186 if !self.runtime.is_pristine_for_checkpoint_restore()
11187 || self.pending_acknowledgement.is_some()
11188 || self.pending_checkpoint.is_some()
11189 || self.last_checkpoint_block.is_some()
11190 || self.last_checkpoint_delivery_token.is_some()
11191 || self.last_checkpoint_delivery_witness.is_some()
11192 || self.last_subscriber_checkpoint.is_some()
11193 || self.checkpoint_identity.is_some()
11194 {
11195 return Err(ReactiveCheckpointRestoreError::ActiveRuntime);
11196 }
11197
11198 let block = BlockRef {
11199 number: metadata.block.number,
11200 hash: metadata.block.hash,
11201 parent_hash: metadata.block.parent_hash,
11202 timestamp: metadata.block.timestamp,
11203 };
11204 let runtime = match metadata.runtime_checkpoint.as_deref() {
11205 Some(bytes) => self
11206 .runtime
11207 .plan_durable_checkpoint_restore(bytes, &block)?,
11208 None => DurableRuntimeRestorePlan {
11209 checkpoint: None,
11210 fallback_history: (self.runtime.config.journal_depth > 0)
11211 .then_some(block)
11212 .into_iter()
11213 .collect(),
11214 },
11215 };
11216 let delivery_token = metadata
11217 .delivery_token
11218 .clone()
11219 .map(SubscriberDeliveryToken::new);
11220 let subscriber_checkpoint = metadata
11221 .subscriber_checkpoint
11222 .clone()
11223 .map(SubscriberCheckpoint::new);
11224 let position = SubscriberResumePosition::new(
11225 metadata.identity.chain_id,
11226 block,
11227 runtime.canonical_history(),
11228 delivery_token,
11229 subscriber_checkpoint,
11230 );
11231 Ok(DurableResumePlan {
11232 runtime,
11233 position,
11234 delivery_witness: metadata.delivery_witness,
11235 })
11236 }
11237
11238 pub fn preview_durable_resume_position(
11267 &self,
11268 metadata: &DurableCheckpointMetadata,
11269 ) -> Result<SubscriberResumePosition, ReactiveCheckpointRestoreError> {
11270 Ok(self.durable_resume_plan(metadata)?.position)
11271 }
11272
11273 pub fn resume_from_durable_checkpoint(
11295 &mut self,
11296 metadata: &DurableCheckpointMetadata,
11297 ) -> Result<(), ReactiveCheckpointRestoreError> {
11298 let plan = self.durable_resume_plan(metadata)?;
11299 let prior_runtime = self.runtime.checkpoint_state();
11300
11301 let DurableResumePlan {
11302 runtime,
11303 position,
11304 delivery_witness,
11305 } = plan;
11306 self.runtime.apply_durable_checkpoint_restore(runtime);
11307 self.runtime.coverage_head = Some(position.coverage_head);
11308 if let Err(error) = self.subscriber.restore_position(&position) {
11309 self.runtime.restore_state(prior_runtime);
11310 return Err(ReactiveCheckpointRestoreError::Subscriber(error));
11311 }
11312 if let Err(error) = self.ensure_subscriber_restore_chain(metadata.identity.chain_id) {
11313 self.runtime.restore_state(prior_runtime);
11314 return Err(error);
11315 }
11316 self.last_checkpoint_block = Some(metadata.block.clone());
11317 self.last_checkpoint_delivery_token = position.delivery_token;
11318 self.last_checkpoint_delivery_witness = delivery_witness;
11319 self.last_subscriber_checkpoint = position.subscriber_checkpoint;
11320 self.checkpoint_identity = Some(metadata.identity.clone());
11321 Ok(())
11322 }
11323
11324 pub fn restore_durable_checkpoint(
11340 &mut self,
11341 cache: &mut EvmCache,
11342 loaded: LoadedDurableCheckpoint,
11343 expected: &DurableCheckpointIdentity,
11344 ) -> Result<DurableCheckpointMetadata, ReactiveCheckpointRestoreError> {
11345 if !self.subscriber.capabilities().supports_durable_replay() {
11346 return Err(ReactiveCheckpointRestoreError::SubscriberNotDurable);
11347 }
11348 self.ensure_subscriber_restore_chain(expected.chain_id)?;
11349 if !self.runtime.is_pristine_for_checkpoint_restore()
11350 || self.pending_acknowledgement.is_some()
11351 || self.pending_checkpoint.is_some()
11352 || self.last_checkpoint_block.is_some()
11353 || self.last_checkpoint_delivery_token.is_some()
11354 || self.last_checkpoint_delivery_witness.is_some()
11355 || self.last_subscriber_checkpoint.is_some()
11356 || self.checkpoint_identity.is_some()
11357 {
11358 return Err(ReactiveCheckpointRestoreError::ActiveRuntime);
11359 }
11360
11361 let prior_cache = EvmCacheStateSnapshot::capture(cache);
11362 let metadata = loaded.restore_into(cache, expected)?;
11363 if let Err(error) = self.resume_from_durable_checkpoint(&metadata) {
11364 prior_cache.restore(cache);
11365 return Err(error);
11366 }
11367 Ok(metadata)
11368 }
11369
11370 pub fn runtime(&self) -> &ReactiveRuntime<N> {
11372 &self.runtime
11373 }
11374
11375 pub fn runtime_mut(&mut self) -> &mut ReactiveRuntime<N> {
11377 &mut self.runtime
11378 }
11379
11380 pub fn subscriber(&self) -> &S {
11382 &self.subscriber
11383 }
11384
11385 pub fn subscriber_mut(&mut self) -> &mut S {
11387 &mut self.subscriber
11388 }
11389
11390 pub fn adopt_canonical_baseline(
11408 &mut self,
11409 cache: &EvmCache,
11410 baseline: ReactiveCanonicalBaseline,
11411 ) -> Result<(), ReactiveEngineError> {
11412 if self.pending_acknowledgement.is_some()
11413 || self.pending_checkpoint.is_some()
11414 || self.last_checkpoint_block.is_some()
11415 || self.last_checkpoint_delivery_token.is_some()
11416 || self.last_checkpoint_delivery_witness.is_some()
11417 || self.last_subscriber_checkpoint.is_some()
11418 || self.checkpoint_identity.is_some()
11419 {
11420 return Err(ReactiveBaselineError::ActiveRuntime.into());
11421 }
11422 self.runtime
11426 .validate_canonical_baseline_adoption(baseline.block)?;
11427 if baseline.chain_id != cache.chain_id() {
11428 return Err(ReactiveBaselineError::CacheChainMismatch {
11429 baseline_chain_id: baseline.chain_id,
11430 cache_chain_id: cache.chain_id(),
11431 }
11432 .into());
11433 }
11434 self.ensure_subscriber_chain(cache)?;
11435 let exact_selector = BlockId::from((baseline.block.hash, Some(true)));
11436 let context_matches = cache.block_number() == Some(baseline.block.number)
11437 && baseline
11438 .block
11439 .timestamp
11440 .is_none_or(|timestamp| cache.timestamp() == Some(timestamp));
11441 if cache.block() != exact_selector || !context_matches {
11442 return Err(ReactiveBaselineError::CacheBlockMismatch {
11443 number: baseline.block.number,
11444 hash: baseline.block.hash,
11445 }
11446 .into());
11447 }
11448 self.runtime.adopt_canonical_baseline(baseline.block)?;
11449 Ok(())
11450 }
11451
11452 pub fn next_batch(
11465 &mut self,
11466 cache: &EvmCache,
11467 ) -> Result<SubscriberNextBatch<'_, N>, ReactiveEngineError> {
11468 if self.pending_checkpoint.is_some() {
11469 return Err(ReactiveEngineError::PendingCheckpointCommit);
11470 }
11471 if self.pending_acknowledgement.is_some() {
11472 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11473 }
11474 self.ensure_subscriber_chain(cache)?;
11475 Ok(self.subscriber.next_batch())
11476 }
11477
11478 pub fn ingest_batch(
11487 &mut self,
11488 cache: &mut EvmCache,
11489 batch: ReactiveInputBatch<N>,
11490 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
11491 self.ensure_raw_ingest_is_safe(cache, &batch)?;
11492 Ok(self.runtime.ingest_batch(cache, batch)?)
11493 }
11494
11495 pub fn ingest_batch_with_resync(
11505 &mut self,
11506 cache: &mut EvmCache,
11507 batch: ReactiveInputBatch<N>,
11508 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
11509 self.ensure_raw_ingest_is_safe(cache, &batch)?;
11510 Ok(self.runtime.ingest_batch_with_resync(cache, batch)?)
11511 }
11512
11513 fn ensure_raw_ingest_is_safe(
11514 &self,
11515 cache: &EvmCache,
11516 batch: &ReactiveInputBatch<N>,
11517 ) -> Result<(), ReactiveEngineError> {
11518 if self.pending_checkpoint.is_some() {
11519 return Err(ReactiveEngineError::PendingCheckpointCommit);
11520 }
11521 if self.pending_acknowledgement.is_some() {
11522 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11523 }
11524 if batch.delivery_token().is_some() || batch.subscriber_checkpoint().is_some() {
11525 return Err(ReactiveEngineError::UncommittedDeliveryMetadata);
11526 }
11527 self.ensure_subscriber_chain(cache)?;
11528 Ok(())
11529 }
11530
11531 fn ensure_subscriber_chain(&self, cache: &EvmCache) -> Result<(), ReactiveEngineError> {
11532 if let Some(subscriber_chain_id) = self.subscriber.chain_id()
11533 && subscriber_chain_id != cache.chain_id()
11534 {
11535 return Err(ReactiveEngineError::SubscriberChainMismatch {
11536 subscriber_chain_id,
11537 cache_chain_id: cache.chain_id(),
11538 });
11539 }
11540 Ok(())
11541 }
11542
11543 fn ensure_subscriber_restore_chain(
11544 &self,
11545 checkpoint_chain_id: u64,
11546 ) -> Result<(), ReactiveCheckpointRestoreError> {
11547 if let Some(subscriber_chain_id) = self.subscriber.chain_id()
11548 && subscriber_chain_id != checkpoint_chain_id
11549 {
11550 return Err(ReactiveCheckpointRestoreError::SubscriberChainMismatch {
11551 subscriber_chain_id,
11552 checkpoint_chain_id,
11553 });
11554 }
11555 Ok(())
11556 }
11557
11558 pub async fn next_ingest(
11568 &mut self,
11569 cache: &mut EvmCache,
11570 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
11571 self.ensure_subscriber_chain(cache)?;
11572 if self.pending_checkpoint.is_some() {
11573 return Err(ReactiveEngineError::PendingCheckpointCommit);
11574 }
11575 if self.pending_acknowledgement.is_some() {
11576 return self.commit_pending_acknowledgement().await.map(Some);
11577 }
11578 let batch = self.subscriber.next_batch().await?;
11579 self.ensure_subscriber_chain(cache)?;
11580 let Some(mut batch) = batch else {
11581 return Ok(None);
11582 };
11583 let delivery_token = batch.take_delivery_token();
11584 let report = self.runtime.ingest_batch(cache, batch)?;
11585 self.stage_or_return_acknowledgement(delivery_token, report)
11586 .await
11587 }
11588
11589 pub async fn next_ingest_with_resync(
11600 &mut self,
11601 cache: &mut EvmCache,
11602 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
11603 self.ensure_subscriber_chain(cache)?;
11604 if self.pending_checkpoint.is_some() {
11605 return Err(ReactiveEngineError::PendingCheckpointCommit);
11606 }
11607 if self.pending_acknowledgement.is_some() {
11608 return self.commit_pending_acknowledgement().await.map(Some);
11609 }
11610 let batch = self.subscriber.next_batch().await?;
11611 self.ensure_subscriber_chain(cache)?;
11612 let Some(mut batch) = batch else {
11613 return Ok(None);
11614 };
11615 let delivery_token = batch.take_delivery_token();
11616 let report = self.runtime.ingest_batch_with_resync(cache, batch)?;
11617 self.stage_or_return_acknowledgement(delivery_token, report)
11618 .await
11619 }
11620
11621 pub async fn next_ingest_checkpointed(
11652 &mut self,
11653 cache: &mut EvmCache,
11654 store: &DurableCheckpointStore,
11655 identity: &DurableCheckpointIdentity,
11656 ) -> Result<Option<CheckpointedIngest<N>>, ReactiveEngineError> {
11657 if !self.subscriber.capabilities().supports_durable_replay() {
11658 return Err(ReactiveEngineError::SubscriberNotDurable);
11659 }
11660 self.ensure_subscriber_chain(cache)?;
11661 if self.pending_acknowledgement.is_some() {
11662 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11663 }
11664 self.ensure_checkpoint_identity(cache, identity)?;
11665 if self.pending_checkpoint.is_some() {
11666 return self.commit_pending_checkpoint(cache, store).await.map(Some);
11667 }
11668
11669 let batch = self.subscriber.next_batch().await?;
11670 self.ensure_subscriber_chain(cache)?;
11671 let Some(mut batch) = batch else {
11672 return Ok(None);
11673 };
11674 if batch_preconfirmation(&batch)?.is_some() {
11675 return Err(ReactiveEngineError::PreconfirmationNotCheckpointable);
11676 }
11677 self.runtime.discard_preconfirmed_branch(cache);
11678 let delivery_witness = batch
11679 .delivery_token()
11680 .map(|_| durable_delivery_witness(&batch))
11681 .transpose()?;
11682 let delivery_token = batch.take_delivery_token();
11683 let subscriber_checkpoint = batch.take_subscriber_checkpoint();
11684 if let (Some(replay_token), Some(committed_token)) = (
11685 delivery_token.as_ref(),
11686 self.last_checkpoint_delivery_token.as_ref(),
11687 ) && replay_token == committed_token
11688 {
11689 let committed_witness = self
11690 .last_checkpoint_delivery_witness
11691 .ok_or(ReactiveEngineError::MissingReplayWitness)?;
11692 if delivery_witness != Some(committed_witness) {
11693 return Err(ReactiveEngineError::ReplayDeliveryMismatch);
11694 }
11695 self.subscriber
11696 .acknowledge_delivery(replay_token.clone())
11697 .await
11698 .map_err(ReactiveEngineError::Acknowledgement)?;
11699 return Ok(Some(CheckpointedIngest::ReplayAcknowledged));
11700 }
11701
11702 self.ensure_checkpointable_reorgs(&batch)?;
11703
11704 let incoming_block = latest_canonical_batch_block(&batch);
11705 let cache_state = EvmCacheStateSnapshot::capture(cache);
11706 let runtime_state = self.runtime.checkpoint_state();
11707 let report = match self.runtime.ingest_batch_direct(cache, batch) {
11708 Ok(report) => report,
11709 Err(error) => {
11710 cache_state.restore(cache);
11711 self.runtime.restore_transaction_state(runtime_state);
11712 return Err(error.into());
11713 }
11714 };
11715 let reports = report.reports.clone();
11716 let stage = CheckpointStage {
11717 incoming_block,
11718 delivery_token,
11719 delivery_witness,
11720 subscriber_checkpoint,
11721 staged_generation: cache.snapshot_generation(),
11722 report,
11723 };
11724 if let Err(error) = self.stage_checkpoint(identity, stage) {
11725 cache_state.restore(cache);
11726 self.runtime.restore_transaction_state(runtime_state);
11727 return Err(error);
11728 }
11729 self.runtime.dispatch_reports(&reports);
11730 self.commit_pending_checkpoint(cache, store).await.map(Some)
11731 }
11732
11733 pub async fn next_ingest_with_resync_checkpointed(
11744 &mut self,
11745 cache: &mut EvmCache,
11746 store: &DurableCheckpointStore,
11747 identity: &DurableCheckpointIdentity,
11748 ) -> Result<Option<CheckpointedIngest<N>>, ReactiveEngineError> {
11749 if !self.subscriber.capabilities().supports_durable_replay() {
11750 return Err(ReactiveEngineError::SubscriberNotDurable);
11751 }
11752 self.ensure_subscriber_chain(cache)?;
11753 if self.pending_acknowledgement.is_some() {
11754 return Err(ReactiveEngineError::PendingAcknowledgementCommit);
11755 }
11756 self.ensure_checkpoint_identity(cache, identity)?;
11757 if self.pending_checkpoint.is_some() {
11758 return self.commit_pending_checkpoint(cache, store).await.map(Some);
11759 }
11760
11761 let batch = self.subscriber.next_batch().await?;
11762 self.ensure_subscriber_chain(cache)?;
11763 let Some(mut batch) = batch else {
11764 return Ok(None);
11765 };
11766 if batch_preconfirmation(&batch)?.is_some() {
11767 return Err(ReactiveEngineError::PreconfirmationNotCheckpointable);
11768 }
11769 self.runtime.discard_preconfirmed_branch(cache);
11770 let delivery_witness = batch
11771 .delivery_token()
11772 .map(|_| durable_delivery_witness(&batch))
11773 .transpose()?;
11774 let delivery_token = batch.take_delivery_token();
11775 let subscriber_checkpoint = batch.take_subscriber_checkpoint();
11776 if let (Some(replay_token), Some(committed_token)) = (
11777 delivery_token.as_ref(),
11778 self.last_checkpoint_delivery_token.as_ref(),
11779 ) && replay_token == committed_token
11780 {
11781 let committed_witness = self
11782 .last_checkpoint_delivery_witness
11783 .ok_or(ReactiveEngineError::MissingReplayWitness)?;
11784 if delivery_witness != Some(committed_witness) {
11785 return Err(ReactiveEngineError::ReplayDeliveryMismatch);
11786 }
11787 self.subscriber
11788 .acknowledge_delivery(replay_token.clone())
11789 .await
11790 .map_err(ReactiveEngineError::Acknowledgement)?;
11791 return Ok(Some(CheckpointedIngest::ReplayAcknowledged));
11792 }
11793
11794 self.ensure_checkpointable_reorgs(&batch)?;
11795
11796 let incoming_block = latest_canonical_batch_block(&batch);
11797 let cache_state = EvmCacheStateSnapshot::capture(cache);
11798 let runtime_state = self.runtime.checkpoint_state();
11799 let report = match self.runtime.ingest_batch_with_resync_direct(cache, batch) {
11800 Ok(report) => report,
11801 Err(error) => {
11802 cache_state.restore(cache);
11803 self.runtime.restore_transaction_state(runtime_state);
11804 return Err(error.into());
11805 }
11806 };
11807 let reports = report.reports.clone();
11808 let stage = CheckpointStage {
11809 incoming_block,
11810 delivery_token,
11811 delivery_witness,
11812 subscriber_checkpoint,
11813 staged_generation: cache.snapshot_generation(),
11814 report,
11815 };
11816 if let Err(error) = self.stage_checkpoint(identity, stage) {
11817 cache_state.restore(cache);
11818 self.runtime.restore_transaction_state(runtime_state);
11819 return Err(error);
11820 }
11821 self.runtime.dispatch_reports(&reports);
11822 self.commit_pending_checkpoint(cache, store).await.map(Some)
11823 }
11824
11825 fn stage_checkpoint(
11826 &mut self,
11827 identity: &DurableCheckpointIdentity,
11828 stage: CheckpointStage<N>,
11829 ) -> Result<(), ReactiveEngineError> {
11830 let CheckpointStage {
11831 incoming_block,
11832 delivery_token,
11833 delivery_witness,
11834 subscriber_checkpoint,
11835 staged_generation,
11836 report,
11837 } = stage;
11838 if delivery_token.is_some() != delivery_witness.is_some() {
11839 return Err(ReactiveEngineError::DeliveryWitness(
11840 "delivery token and witness must be staged together".into(),
11841 ));
11842 }
11843 let runtime_checkpoint = self.runtime.durable_checkpoint_bytes()?;
11844 let block = self
11845 .runtime
11846 .last_canonical_block()
11847 .map(|block| DurableCheckpointBlock {
11848 number: block.number,
11849 hash: block.hash,
11850 parent_hash: block.parent_hash,
11851 timestamp: block.timestamp,
11852 })
11853 .or(incoming_block)
11854 .or_else(|| self.last_checkpoint_block.clone())
11855 .ok_or(ReactiveEngineError::MissingCheckpointBlock)?;
11856 let metadata = DurableCheckpointMetadata {
11857 identity: identity.clone(),
11858 block,
11859 delivery_token: delivery_token
11860 .as_ref()
11861 .or(self.last_checkpoint_delivery_token.as_ref())
11862 .map(|token| token.as_bytes().to_vec()),
11863 delivery_witness: if delivery_token.is_some() {
11864 delivery_witness
11865 } else {
11866 self.last_checkpoint_delivery_witness
11867 },
11868 subscriber_checkpoint: subscriber_checkpoint
11869 .as_ref()
11870 .or(self.last_subscriber_checkpoint.as_ref())
11871 .map(|checkpoint| checkpoint.as_bytes().to_vec()),
11872 runtime_checkpoint: Some(runtime_checkpoint),
11873 };
11874 self.pending_checkpoint = Some(PendingCheckpoint {
11875 metadata,
11876 delivery_token,
11877 report,
11878 saved_to: None,
11879 staged_generation,
11880 });
11881 Ok(())
11882 }
11883
11884 fn ensure_checkpointable_reorgs(
11885 &self,
11886 batch: &ReactiveInputBatch<N>,
11887 ) -> Result<(), ReactiveEngineError> {
11888 let state = CanonicalSequenceState::new(
11889 self.runtime
11890 .journal
11891 .iter()
11892 .map(|entry| entry.block)
11893 .collect(),
11894 self.runtime.coverage_head,
11895 self.runtime.safe_head,
11896 self.runtime.finalized_head,
11897 );
11898 match validate_canonical_sequence_internal(
11899 &state,
11900 batch,
11901 CanonicalSequenceValidationPolicy::RequireCompleteRollback,
11902 ) {
11903 Ok(_) => Ok(()),
11904 Err(CanonicalSequenceError::Invalid(error)) => Err(error.into()),
11905 Err(CanonicalSequenceError::IncompleteRollback {
11906 common_ancestor,
11907 oldest_retained,
11908 ..
11909 }) => Err(ReactiveEngineError::CheckpointReorgOutsideJournal {
11910 common_ancestor,
11911 oldest_journaled: oldest_retained,
11912 journal_depth: self.runtime.config.journal_depth,
11913 }),
11914 }
11915 }
11916
11917 async fn stage_or_return_acknowledgement(
11918 &mut self,
11919 delivery_token: Option<SubscriberDeliveryToken>,
11920 report: ReactiveBatchReport<N>,
11921 ) -> Result<Option<ReactiveBatchReport<N>>, ReactiveEngineError> {
11922 let Some(token) = delivery_token else {
11923 return Ok(Some(report));
11924 };
11925 self.pending_acknowledgement = Some(PendingAcknowledgement { token, report });
11926 self.commit_pending_acknowledgement().await.map(Some)
11927 }
11928
11929 async fn commit_pending_acknowledgement(
11930 &mut self,
11931 ) -> Result<ReactiveBatchReport<N>, ReactiveEngineError> {
11932 let token = self
11933 .pending_acknowledgement
11934 .as_ref()
11935 .expect("caller checked pending acknowledgement")
11936 .token
11937 .clone();
11938 self.subscriber
11939 .acknowledge_delivery(token)
11940 .await
11941 .map_err(ReactiveEngineError::Acknowledgement)?;
11942 Ok(self
11943 .pending_acknowledgement
11944 .take()
11945 .expect("pending acknowledgement remains until commit")
11946 .report)
11947 }
11948
11949 async fn commit_pending_checkpoint(
11950 &mut self,
11951 cache: &EvmCache,
11952 store: &DurableCheckpointStore,
11953 ) -> Result<CheckpointedIngest<N>, ReactiveEngineError> {
11954 let pending = self
11955 .pending_checkpoint
11956 .as_mut()
11957 .expect("caller checked pending checkpoint");
11958 let cache_generation = cache.snapshot_generation();
11959 if cache_generation != pending.staged_generation {
11960 return Err(ReactiveEngineError::PendingCheckpointCacheChanged {
11961 staged_generation: pending.staged_generation,
11962 current_generation: cache_generation,
11963 });
11964 }
11965 if pending.saved_to.as_deref() != Some(store.path()) {
11966 store
11967 .save_async(cache, pending.metadata.clone())
11968 .await
11969 .map_err(ReactiveEngineError::Checkpoint)?;
11970 pending.saved_to = Some(store.path().to_path_buf());
11971 }
11972 if let Some(token) = pending.delivery_token.clone() {
11973 self.subscriber
11974 .acknowledge_delivery(token)
11975 .await
11976 .map_err(ReactiveEngineError::Acknowledgement)?;
11977 }
11978
11979 let pending = self
11980 .pending_checkpoint
11981 .take()
11982 .expect("pending checkpoint remains until commit");
11983 self.last_checkpoint_block = Some(pending.metadata.block);
11984 self.checkpoint_identity = Some(pending.metadata.identity);
11985 self.last_checkpoint_delivery_token = pending
11986 .metadata
11987 .delivery_token
11988 .map(SubscriberDeliveryToken::new);
11989 self.last_checkpoint_delivery_witness = pending.metadata.delivery_witness;
11990 self.last_subscriber_checkpoint = pending
11991 .metadata
11992 .subscriber_checkpoint
11993 .map(SubscriberCheckpoint::new);
11994 Ok(CheckpointedIngest::Applied(pending.report))
11995 }
11996
11997 fn ensure_checkpoint_identity(
11998 &self,
11999 cache: &EvmCache,
12000 identity: &DurableCheckpointIdentity,
12001 ) -> Result<(), ReactiveEngineError> {
12002 if identity.chain_id != cache.chain_id() {
12003 return Err(ReactiveEngineError::Checkpoint(
12004 DurableCheckpointError::CacheChainMismatch {
12005 cache_chain_id: cache.chain_id(),
12006 checkpoint_chain_id: identity.chain_id,
12007 },
12008 ));
12009 }
12010 if let Some(actual) = self.checkpoint_identity.as_ref()
12011 && actual != identity
12012 {
12013 return Err(ReactiveEngineError::Checkpoint(
12014 DurableCheckpointError::IdentityMismatch {
12015 expected: identity.clone(),
12016 actual: actual.clone(),
12017 },
12018 ));
12019 }
12020 if let Some(pending) = self.pending_checkpoint.as_ref()
12021 && &pending.metadata.identity != identity
12022 {
12023 return Err(ReactiveEngineError::Checkpoint(
12024 DurableCheckpointError::IdentityMismatch {
12025 expected: identity.clone(),
12026 actual: pending.metadata.identity.clone(),
12027 },
12028 ));
12029 }
12030 Ok(())
12031 }
12032}
12033
12034fn latest_canonical_batch_block<N: Network>(
12035 batch: &ReactiveInputBatch<N>,
12036) -> Option<DurableCheckpointBlock> {
12037 let record_block = batch
12038 .records()
12039 .iter()
12040 .enumerate()
12041 .filter(|(index, _)| {
12042 batch
12043 .record_delivery_scope(*index)
12044 .is_some_and(DeliveryScope::advances_canonical_state)
12045 })
12046 .filter_map(|(_, record)| canonical_record_block(record))
12047 .max_by_key(|block| block.number)
12048 .cloned();
12049 let control_block = batch
12050 .chain_controls()
12051 .iter()
12052 .filter_map(|control| match control {
12053 ChainControl::Reorg {
12054 common_ancestor, ..
12055 } => Some(common_ancestor),
12056 ChainControl::Barrier {
12057 block: Some(block), ..
12058 }
12059 | ChainControl::CanonicalProgress(block) => Some(block),
12060 ChainControl::Safe(_)
12061 | ChainControl::Finalized(_)
12062 | ChainControl::Barrier { block: None, .. } => None,
12063 })
12064 .max_by_key(|block| block.number)
12065 .cloned();
12066
12067 record_block
12068 .into_iter()
12069 .chain(control_block)
12070 .max_by_key(|block| block.number)
12071 .map(|block| DurableCheckpointBlock {
12072 number: block.number,
12073 hash: block.hash,
12074 parent_hash: block.parent_hash,
12075 timestamp: block.timestamp,
12076 })
12077}
12078
12079impl<S, N> ReactiveEngine<S, N>
12080where
12081 N: Network,
12082 S: InterestOwnerSubscriber<N>,
12083{
12084 pub async fn register_handler(
12110 &mut self,
12111 handler: Arc<dyn ReactiveHandler<N>>,
12112 ) -> Result<(), ReactiveEngineRegisterError> {
12113 let backfill = self
12114 .runtime
12115 .last_canonical_block()
12116 .filter(|retained| {
12117 self.runtime.journal.iter().any(|entry| {
12118 optional_block_refs_are_compatible(Some(&entry.block), Some(retained))
12119 })
12120 })
12121 .map(HandlerRegistrationCatchup::CoordinatedCanonical)
12122 .unwrap_or(HandlerRegistrationCatchup::LiveOnly);
12123 self.register_handler_inner(handler, backfill).await
12124 }
12125
12126 pub async fn register_handler_with_backfill(
12145 &mut self,
12146 handler: Arc<dyn ReactiveHandler<N>>,
12147 backfill: SubscriberBackfill,
12148 ) -> Result<(), ReactiveEngineRegisterError> {
12149 self.register_handler_inner(handler, HandlerRegistrationCatchup::OwnerBackfill(backfill))
12150 .await
12151 }
12152
12153 pub async fn register_handler_live_only(
12164 &mut self,
12165 handler: Arc<dyn ReactiveHandler<N>>,
12166 ) -> Result<(), ReactiveEngineRegisterError> {
12167 self.register_handler_inner(handler, HandlerRegistrationCatchup::LiveOnly)
12168 .await
12169 }
12170
12171 async fn register_handler_inner(
12172 &mut self,
12173 handler: Arc<dyn ReactiveHandler<N>>,
12174 catchup: HandlerRegistrationCatchup,
12175 ) -> Result<(), ReactiveEngineRegisterError> {
12176 let id = handler.id();
12177 if self.runtime.contains_handler(&id) {
12178 return Err(RegisterError::DuplicateHandler(id).into());
12179 }
12180 let interests = handler.interests();
12181
12182 if let HandlerRegistrationCatchup::OwnerBackfill(backfill) = &catchup {
12183 let retained_anchor = backfill.retained_anchor().copied();
12184 let is_exact_retained_block = retained_anchor.is_some_and(|anchor| {
12185 backfill.start_block() == anchor.number
12186 && backfill.end_block() == Some(anchor.number)
12187 && self.runtime.journal.iter().any(|entry| {
12188 optional_block_refs_are_compatible(Some(&entry.block), Some(&anchor))
12189 })
12190 });
12191 if !is_exact_retained_block {
12192 return Err(ReactiveEngineRegisterError::BackfillOutsideJournal {
12193 start_block: backfill.start_block(),
12194 end_block: backfill.end_block(),
12195 retained_anchor,
12196 });
12197 }
12198 }
12199
12200 let subscribed = match catchup {
12201 HandlerRegistrationCatchup::OwnerBackfill(backfill) => {
12202 self.subscriber
12203 .add_interest_owner_with_backfill(id.clone(), &interests, backfill)
12204 .await
12205 }
12206 HandlerRegistrationCatchup::CoordinatedCanonical(retained) => {
12207 self.subscriber
12208 .add_interest_owner_with_canonical_catchup(id.clone(), &interests, retained)
12209 .await
12210 }
12211 HandlerRegistrationCatchup::LiveOnly => {
12212 self.subscriber
12213 .add_interest_owner(id.clone(), &interests)
12214 .await
12215 }
12216 };
12217 if let Err(error) = subscribed {
12218 return Err(error.into());
12219 }
12220
12221 self.runtime
12226 .registry
12227 .insert_handler_prepared(id, handler, interests);
12228 Ok(())
12229 }
12230
12231 pub async fn sync_handler_interests(&mut self) -> Result<(), SubscriberError> {
12256 let owners = self
12257 .runtime
12258 .handler_ids()
12259 .into_iter()
12260 .map(|id| {
12261 let interests = self
12262 .runtime
12263 .handler_interests(&id)
12264 .map(<[ReactiveInterest<N>]>::to_vec)
12265 .unwrap_or_default();
12266 (id, interests)
12267 })
12268 .collect();
12269 self.subscriber.replace_interest_owners(owners).await
12270 }
12271
12272 pub async fn sync_handler_interests_with_backfill(&mut self) -> Result<(), SubscriberError> {
12300 let baseline =
12301 self.runtime
12302 .last_canonical_block()
12303 .ok_or(SubscriberError::InvalidConfig(
12304 "cannot continuity-sync handlers before a canonical runtime position exists",
12305 ))?;
12306 let backfill = SubscriberBackfill::after_canonical_block(baseline)?;
12307 let owners = self
12308 .runtime
12309 .handler_ids()
12310 .into_iter()
12311 .map(|id| {
12312 let interests = self
12313 .runtime
12314 .handler_interests(&id)
12315 .map(<[ReactiveInterest<N>]>::to_vec)
12316 .unwrap_or_default();
12317 (id, interests)
12318 })
12319 .collect();
12320 self.subscriber
12321 .replace_interest_owners_with_global_backfill(owners, backfill)
12322 .await
12323 }
12324
12325 pub async fn unregister_handler(
12357 &mut self,
12358 id: &HandlerId,
12359 ) -> Result<Option<Arc<dyn ReactiveHandler<N>>>, SubscriberError> {
12360 self.subscriber.remove_interest_owner(id).await?;
12361 Ok(self.runtime.unregister_handler(id))
12362 }
12363}
12364
12365type FlashblockReconnectFuture<N> = Pin<
12366 Box<
12367 dyn Future<
12368 Output = (
12369 SubscriberStreamSource,
12370 Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>,
12371 ),
12372 > + Send,
12373 >,
12374>;
12375
12376pub struct AlloySubscriber<P, N: Network = Ethereum> {
12397 provider: P,
12398 #[cfg(feature = "raw-flashblocks-json")]
12401 external_flashblocks_provider: Option<ProviderRef>,
12402 #[cfg(feature = "raw-flashblocks-json")]
12404 external_flashblock_updates:
12405 Option<tokio::sync::mpsc::Receiver<raw_json_flashblocks::QueuedFlashblockUpdate>>,
12406 #[cfg(feature = "raw-flashblocks-json")]
12408 external_flashblock_update_channel_opened: bool,
12409 #[cfg(feature = "raw-flashblocks-json")]
12411 rejected_external_flashblock_generation: Option<u64>,
12412 #[cfg(feature = "raw-flashblocks-json")]
12415 last_external_flashblock_snapshot: Option<FlashblockSnapshot>,
12416 flashblocks_state_provider: Option<P>,
12420 provider_ref: Option<ProviderRef>,
12423 log_verification_provider: Option<P>,
12427 chain_id: Option<u64>,
12429 mode: SubscriberMode,
12430 config: SubscriberConfig,
12431 base_interests: Vec<ReactiveInterest<N>>,
12432 owned_interests: Vec<OwnedSubscriberInterests<N>>,
12433 next_owner_epoch: u64,
12434 interests: Vec<ReactiveInterest<N>>,
12435 log_source_ids: HashMap<Filter, usize>,
12440 next_log_source_id: usize,
12441 pending_backfills: VecDeque<QueuedSubscriberBackfill>,
12442 pending_source_backfills: VecDeque<SubscriberStreamSource>,
12447 sources_dirty: bool,
12450 stream_revision: u64,
12453 state: AlloySubscriberState<N>,
12454 pending_records: VecDeque<SubscriberInputRecord<N>>,
12455 pending_chain_controls: VecDeque<ChainControl>,
12456 pending_reconcile_owner_records: VecDeque<BufferedSubscriberOwnerRecord<N>>,
12461 resource_error: Option<String>,
12464 last_seen_log_blocks: HashMap<usize, u64>,
12465 verified_log_blocks: HashMap<(u64, B256), BlockRef>,
12466 verified_log_block_order: VecDeque<(u64, B256)>,
12467 recent_input_refs: VecDeque<InputRef>,
12468 recent_input_ref_set: HashSet<InputRef>,
12469 recent_owner_input_refs: HashMap<SubscriberOwnerEpoch, VecDeque<InputRef>>,
12470 recent_owner_input_ref_sets: HashMap<SubscriberOwnerEpoch, HashSet<InputRef>>,
12471 recent_compat_owner_input_refs: HashMap<HandlerId, VecDeque<InputRef>>,
12472 recent_compat_owner_input_ref_sets: HashMap<HandlerId, HashSet<InputRef>>,
12473 base_flashblock_header: Option<(FixedBytes<8>, BaseFlashblockBase)>,
12474 base_flashblock_transactions: Option<(FixedBytes<8>, u64, Vec<B256>, Vec<B256>)>,
12475 unmatched_pending_logs: VecDeque<(usize, Log)>,
12476 latest_preconfirmation: Option<FlashblockRef>,
12477 preconfirmed_seen_logs: HashSet<(B256, u64)>,
12478 preconfirmed_receipted_transactions: HashSet<B256>,
12482 preconfirmed_unavailable_receipts: HashSet<B256>,
12487 last_certified_canonical_head: Option<BlockRef>,
12488 pending_preconfirmation_invalidation: bool,
12489 pending_flashblock_reconnects: FuturesUnordered<FlashblockReconnectFuture<N>>,
12490 pending_flashblock_reconnect_sources: Vec<SubscriberStreamSource>,
12491 flashblocks_rpc_metrics: FlashblocksRpcMetrics,
12492 consecutive_flashblock_poll_failures: usize,
12493 flashblock_rpc_request_times: VecDeque<Instant>,
12494 _network: PhantomData<N>,
12495}
12496
12497struct OwnedSubscriberInterests<N: Network = Ethereum> {
12498 owner: HandlerId,
12499 interests: Vec<ReactiveInterest<N>>,
12500 epoch: Option<SubscriberOwnerEpoch>,
12501 state: SubscriberOwnerState,
12502 baseline: Option<BlockRef>,
12503 progress: Option<SubscriberOwnerProgress>,
12504 progress_stream_revision: Option<u64>,
12505}
12506
12507#[derive(Clone)]
12508struct SubscriberOwnerReconcilePlan<N: Network = Ethereum> {
12509 epoch: SubscriberOwnerEpoch,
12510 interests: Vec<ReactiveInterest<N>>,
12511 retained: BlockRef,
12512 from_block: u64,
12513}
12514
12515struct SubscriberOwnerCatchup {
12516 logs: Vec<Log>,
12517 certified: BlockRef,
12518}
12519
12520#[derive(Clone, Copy)]
12521struct SubscriberOwnerCatchupOptions {
12522 target_preverified: bool,
12523 max_logs: usize,
12524 max_log_bytes: usize,
12525 max_requests_in_flight: usize,
12526}
12527
12528struct SubscriberOwnerReconcileFilter {
12529 filter: Filter,
12530 from_block: u64,
12531}
12532
12533struct BufferedSubscriberOwnerRecord<N: Network = Ethereum> {
12534 record: ReactiveInputRecord<N>,
12535 owners: Vec<SubscriberOwnerEpoch>,
12536}
12537
12538const OWNER_RECONCILE_FILTERS_PER_CHUNK: usize = 256;
12539
12540struct QueuedSubscriberBackfill {
12541 owner: Option<HandlerId>,
12544 epoch: Option<SubscriberOwnerEpoch>,
12545 filters: Vec<Filter>,
12547 backfill: SubscriberBackfill,
12548}
12549
12550#[cfg(feature = "reactive-ws")]
12556fn ensure_ring_crypto_provider() {
12557 use std::sync::Once;
12558 static INSTALL: Once = Once::new();
12559 INSTALL.call_once(|| {
12560 let _ = rustls::crypto::ring::default_provider().install_default();
12561 });
12562}
12563
12564impl<P, N: Network> AlloySubscriber<P, N> {
12565 pub fn new(provider: P, mode: SubscriberMode, config: SubscriberConfig) -> Self {
12567 #[cfg(feature = "reactive-ws")]
12568 ensure_ring_crypto_provider();
12569 Self {
12570 provider,
12571 #[cfg(feature = "raw-flashblocks-json")]
12572 external_flashblocks_provider: None,
12573 #[cfg(feature = "raw-flashblocks-json")]
12574 external_flashblock_updates: None,
12575 #[cfg(feature = "raw-flashblocks-json")]
12576 external_flashblock_update_channel_opened: false,
12577 #[cfg(feature = "raw-flashblocks-json")]
12578 rejected_external_flashblock_generation: None,
12579 #[cfg(feature = "raw-flashblocks-json")]
12580 last_external_flashblock_snapshot: None,
12581 flashblocks_state_provider: None,
12582 provider_ref: None,
12583 log_verification_provider: None,
12584 chain_id: None,
12585 mode,
12586 config,
12587 base_interests: Vec::new(),
12588 owned_interests: Vec::new(),
12589 next_owner_epoch: 0,
12590 interests: Vec::new(),
12591 log_source_ids: HashMap::new(),
12592 next_log_source_id: 0,
12593 pending_backfills: VecDeque::new(),
12594 pending_source_backfills: VecDeque::new(),
12595 sources_dirty: true,
12596 stream_revision: 0,
12597 state: AlloySubscriberState::Uninitialized,
12598 pending_records: VecDeque::new(),
12599 pending_chain_controls: VecDeque::new(),
12600 pending_reconcile_owner_records: VecDeque::new(),
12601 resource_error: None,
12602 last_seen_log_blocks: HashMap::new(),
12603 verified_log_blocks: HashMap::new(),
12604 verified_log_block_order: VecDeque::new(),
12605 recent_input_refs: VecDeque::new(),
12606 recent_input_ref_set: HashSet::new(),
12607 recent_owner_input_refs: HashMap::new(),
12608 recent_owner_input_ref_sets: HashMap::new(),
12609 recent_compat_owner_input_refs: HashMap::new(),
12610 recent_compat_owner_input_ref_sets: HashMap::new(),
12611 base_flashblock_header: None,
12612 base_flashblock_transactions: None,
12613 unmatched_pending_logs: VecDeque::new(),
12614 latest_preconfirmation: None,
12615 preconfirmed_seen_logs: HashSet::new(),
12616 preconfirmed_receipted_transactions: HashSet::new(),
12617 preconfirmed_unavailable_receipts: HashSet::new(),
12618 last_certified_canonical_head: None,
12619 pending_preconfirmation_invalidation: false,
12620 pending_flashblock_reconnects: FuturesUnordered::new(),
12621 pending_flashblock_reconnect_sources: Vec::new(),
12622 flashblocks_rpc_metrics: FlashblocksRpcMetrics::default(),
12623 consecutive_flashblock_poll_failures: 0,
12624 flashblock_rpc_request_times: VecDeque::new(),
12625 _network: PhantomData,
12626 }
12627 }
12628
12629 pub fn provider(&self) -> &P {
12631 &self.provider
12632 }
12633
12634 #[must_use]
12638 pub fn with_provider_ref(mut self, provider: ProviderRef) -> Self {
12639 self.provider_ref = Some(provider);
12640 self
12641 }
12642
12643 #[cfg(feature = "raw-flashblocks-json")]
12669 pub fn configure_external_flashblock_updates(
12670 &mut self,
12671 provider: ProviderRef,
12672 ) -> Result<(), SubscriberError> {
12673 if self.external_flashblocks_provider.is_some() {
12674 return Err(SubscriberError::InvalidConfig(
12675 "external Flashblock updates were already configured",
12676 ));
12677 }
12678 if self.external_flashblock_update_channel_opened
12679 || self.external_flashblock_updates.is_some()
12680 || self.chain_id.is_some()
12681 || !self.base_interests.is_empty()
12682 || !self.owned_interests.is_empty()
12683 || !self.interests.is_empty()
12684 || !self.pending_records.is_empty()
12685 || !self.pending_chain_controls.is_empty()
12686 || !self.pending_backfills.is_empty()
12687 || !matches!(self.state, AlloySubscriberState::Uninitialized)
12688 {
12689 return Err(SubscriberError::InvalidConfig(
12690 "external Flashblock updates must be configured before subscriber registration",
12691 ));
12692 }
12693 self.external_flashblocks_provider = Some(provider);
12694 Ok(())
12695 }
12696
12697 #[cfg(feature = "raw-flashblocks-json")]
12712 pub fn open_external_flashblock_update_channel(
12713 &mut self,
12714 capacity: usize,
12715 ) -> Result<FlashblockUpdateSender, SubscriberError> {
12716 if capacity == 0 {
12717 return Err(SubscriberError::InvalidConfig(
12718 "external Flashblock update channel capacity must be greater than zero",
12719 ));
12720 }
12721 let provider = self.external_flashblocks_provider.clone().ok_or(
12722 SubscriberError::InvalidConfig(
12723 "external Flashblock update channel requires configure_external_flashblock_updates",
12724 ),
12725 )?;
12726 if self.external_flashblock_update_channel_opened {
12727 return Err(SubscriberError::InvalidConfig(
12728 "external Flashblock update channel was already opened",
12729 ));
12730 }
12731 let (sender, receiver) =
12732 raw_json_flashblocks::flashblock_update_channel(provider, capacity);
12733 self.external_flashblock_updates = Some(receiver);
12734 self.external_flashblock_update_channel_opened = true;
12735 self.sources_dirty = true;
12736 Ok(sender)
12737 }
12738
12739 fn uses_external_flashblock_updates(&self) -> bool {
12740 #[cfg(feature = "raw-flashblocks-json")]
12741 {
12742 self.external_flashblocks_provider.is_some()
12743 }
12744 #[cfg(not(feature = "raw-flashblocks-json"))]
12745 {
12746 false
12747 }
12748 }
12749
12750 #[must_use]
12758 pub fn with_flashblocks_state_provider(mut self, provider: P) -> Self {
12759 self.flashblocks_state_provider = Some(provider);
12760 self
12761 }
12762
12763 #[must_use]
12770 pub fn with_log_verification_provider(mut self, provider: P) -> Self {
12771 self.log_verification_provider = Some(provider);
12772 self
12773 }
12774
12775 pub fn mode(&self) -> SubscriberMode {
12777 self.mode
12778 }
12779
12780 pub fn config(&self) -> &SubscriberConfig {
12782 &self.config
12783 }
12784
12785 pub const fn flashblocks_rpc_metrics(&self) -> FlashblocksRpcMetrics {
12788 self.flashblocks_rpc_metrics
12789 }
12790
12791 pub fn registered_interests(&self) -> &[ReactiveInterest<N>] {
12793 &self.interests
12794 }
12795
12796 pub fn stage_interest_owner(
12813 &mut self,
12814 owner: HandlerId,
12815 interests: &[ReactiveInterest<N>],
12816 start: SubscriberOwnerStart,
12817 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
12818 validate_subscriber_config(&self.config)?;
12819 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
12820 && interests
12821 .iter()
12822 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
12823 {
12824 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
12825 }
12826 if self
12827 .owned_interests
12828 .iter()
12829 .any(|entry| entry.owner == owner)
12830 {
12831 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
12832 }
12833
12834 let mut next_owned = self.clone_owned_interests();
12835 next_owned.push(OwnedSubscriberInterests {
12836 owner: owner.clone(),
12837 interests: interests.to_vec(),
12838 epoch: None,
12839 state: SubscriberOwnerState::Staged,
12840 baseline: None,
12841 progress: None,
12842 progress_stream_revision: None,
12843 });
12844 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
12845 validate_supported_interests(self.mode, &self.config, &next_registered)?;
12846
12847 let baseline = match start {
12848 SubscriberOwnerStart::Live => None,
12849 SubscriberOwnerStart::PostBlock(block) => {
12850 block
12851 .number
12852 .checked_add(1)
12853 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
12854 Some(block)
12855 }
12856 };
12857 let sequence = self
12858 .next_owner_epoch
12859 .checked_add(1)
12860 .ok_or(SubscriberOwnerError::EpochExhausted)?;
12861 let epoch = SubscriberOwnerEpoch {
12862 owner: owner.clone(),
12863 sequence,
12864 };
12865
12866 self.next_owner_epoch = sequence;
12867 let entry = next_owned
12868 .last_mut()
12869 .expect("staged owner was appended during preflight");
12870 entry.epoch = Some(epoch.clone());
12871 entry.baseline = baseline;
12872 self.owned_interests = next_owned;
12873 self.interests = next_registered;
12874 self.sources_dirty = true;
12875
12876 Ok(epoch)
12877 }
12878
12879 pub fn stage_interest_owner_replacement(
12893 &mut self,
12894 owner: HandlerId,
12895 interests: &[ReactiveInterest<N>],
12896 start: SubscriberOwnerStart,
12897 ) -> Result<SubscriberOwnerEpoch, SubscriberOwnerError> {
12898 validate_subscriber_config(&self.config)?;
12899 if matches!(&start, SubscriberOwnerStart::PostBlock(_))
12900 && interests
12901 .iter()
12902 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
12903 {
12904 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
12905 }
12906 let active_count = self
12907 .owned_interests
12908 .iter()
12909 .filter(|entry| {
12910 entry.owner == owner
12911 && entry.state == SubscriberOwnerState::Active
12912 && entry.epoch.is_some()
12913 })
12914 .count();
12915 if active_count != 1
12916 || self
12917 .owned_interests
12918 .iter()
12919 .any(|entry| entry.owner == owner && entry.state != SubscriberOwnerState::Active)
12920 {
12921 return Err(SubscriberOwnerError::AlreadyRegistered(owner));
12922 }
12923
12924 let mut next_owned = self.clone_owned_interests();
12925 next_owned.push(OwnedSubscriberInterests {
12926 owner: owner.clone(),
12927 interests: interests.to_vec(),
12928 epoch: None,
12929 state: SubscriberOwnerState::Staged,
12930 baseline: None,
12931 progress: None,
12932 progress_stream_revision: None,
12933 });
12934 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
12935 validate_supported_interests(self.mode, &self.config, &next_registered)?;
12936
12937 let baseline = match start {
12938 SubscriberOwnerStart::Live => None,
12939 SubscriberOwnerStart::PostBlock(block) => {
12940 block
12941 .number
12942 .checked_add(1)
12943 .ok_or(SubscriberOwnerError::PostBlockOverflow(block.number))?;
12944 Some(block)
12945 }
12946 };
12947 let sequence = self
12948 .next_owner_epoch
12949 .checked_add(1)
12950 .ok_or(SubscriberOwnerError::EpochExhausted)?;
12951 let epoch = SubscriberOwnerEpoch {
12952 owner: owner.clone(),
12953 sequence,
12954 };
12955
12956 self.next_owner_epoch = sequence;
12957 let entry = next_owned
12958 .last_mut()
12959 .expect("staged replacement owner was appended during preflight");
12960 entry.epoch = Some(epoch.clone());
12961 entry.baseline = baseline;
12962 self.owned_interests = next_owned;
12963 self.interests = next_registered;
12964 self.sources_dirty = true;
12965 Ok(epoch)
12966 }
12967
12968 pub fn interest_owner_state(
12970 &self,
12971 epoch: &SubscriberOwnerEpoch,
12972 ) -> Option<SubscriberOwnerState> {
12973 self.owned_interests
12974 .iter()
12975 .find(|entry| entry.epoch.as_ref() == Some(epoch))
12976 .map(|entry| entry.state)
12977 }
12978
12979 pub fn interest_owner_progress(
12981 &self,
12982 epoch: &SubscriberOwnerEpoch,
12983 ) -> Option<&SubscriberOwnerProgress> {
12984 self.owned_interests
12985 .iter()
12986 .find(|entry| entry.epoch.as_ref() == Some(epoch))
12987 .and_then(|entry| entry.progress.as_ref())
12988 }
12989
12990 pub fn activate_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
12994 let stream_revision = self.stream_revision;
12995 let sources_dirty = self.sources_dirty;
12996 let Some(entry) = self
12997 .owned_interests
12998 .iter_mut()
12999 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13000 else {
13001 return false;
13002 };
13003 if entry.state != SubscriberOwnerState::Staged
13004 || (entry.baseline.is_some()
13005 && (entry.progress.is_none()
13006 || entry.progress_stream_revision != Some(stream_revision)
13007 || sources_dirty))
13008 {
13009 return false;
13010 }
13011 entry.state = SubscriberOwnerState::Active;
13012 true
13013 }
13014
13015 pub fn commit_interest_owner_replacement(
13017 &mut self,
13018 active: &SubscriberOwnerEpoch,
13019 replacement: &SubscriberOwnerEpoch,
13020 ) -> bool {
13021 let Some(active_index) = self
13022 .owned_interests
13023 .iter()
13024 .position(|entry| entry.epoch.as_ref() == Some(active))
13025 else {
13026 return false;
13027 };
13028 let Some(replacement_index) = self
13029 .owned_interests
13030 .iter()
13031 .position(|entry| entry.epoch.as_ref() == Some(replacement))
13032 else {
13033 return false;
13034 };
13035 if active_index == replacement_index
13036 || active.owner() != replacement.owner()
13037 || self.owned_interests[active_index].state != SubscriberOwnerState::Active
13038 || self.owned_interests[replacement_index].state != SubscriberOwnerState::Staged
13039 || (self.owned_interests[replacement_index].baseline.is_some()
13040 && (self.owned_interests[replacement_index].progress.is_none()
13041 || self.owned_interests[replacement_index].progress_stream_revision
13042 != Some(self.stream_revision)
13043 || self.sources_dirty))
13044 {
13045 return false;
13046 }
13047
13048 self.owned_interests[replacement_index].state = SubscriberOwnerState::Active;
13049 self.owned_interests.remove(active_index);
13050 self.purge_owner_epoch(active);
13051 self.rebuild_registered_interests();
13052 self.retire_unreferenced_filters();
13053 self.sources_dirty = true;
13054 true
13055 }
13056
13057 pub fn prepare_interest_owner_removal(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
13066 let Some(entry) = self
13067 .owned_interests
13068 .iter_mut()
13069 .find(|entry| entry.epoch.as_ref() == Some(epoch))
13070 else {
13071 return false;
13072 };
13073 if entry.state != SubscriberOwnerState::Active {
13074 return false;
13075 }
13076 entry.state = SubscriberOwnerState::Removing;
13077 true
13078 }
13079
13080 pub fn finalize_interest_owner_removal(
13086 &mut self,
13087 epoch: &SubscriberOwnerEpoch,
13088 ) -> Option<Vec<ReactiveInterest<N>>> {
13089 let index = self.owned_interests.iter().position(|entry| {
13090 entry.epoch.as_ref() == Some(epoch) && entry.state == SubscriberOwnerState::Removing
13091 })?;
13092 let removed = self.owned_interests.remove(index).interests;
13093 self.purge_owner_epoch(epoch);
13094 self.rebuild_registered_interests();
13095 self.retire_unreferenced_filters();
13096 self.sources_dirty = true;
13097 Some(removed)
13098 }
13099
13100 pub fn abort_interest_owner(&mut self, epoch: &SubscriberOwnerEpoch) -> bool {
13106 let Some(index) = self
13107 .owned_interests
13108 .iter()
13109 .position(|entry| entry.epoch.as_ref() == Some(epoch))
13110 else {
13111 return false;
13112 };
13113 match self.owned_interests[index].state {
13114 SubscriberOwnerState::Staged => {
13115 self.owned_interests.remove(index);
13116 self.purge_owner_epoch(epoch);
13117 self.rebuild_registered_interests();
13118 self.retire_unreferenced_filters();
13119 self.sources_dirty = true;
13120 true
13121 }
13122 SubscriberOwnerState::Removing => {
13123 self.owned_interests[index].state = SubscriberOwnerState::Active;
13124 true
13125 }
13126 SubscriberOwnerState::Active => false,
13127 }
13128 }
13129
13130 fn purge_owner_epoch(&mut self, epoch: &SubscriberOwnerEpoch) {
13131 self.pending_backfills
13132 .retain(|backfill| backfill.epoch.as_ref() != Some(epoch));
13133 self.pending_records
13134 .retain_mut(|pending| match &mut pending.scope {
13135 SubscriberInputScope::Canonical { owners }
13136 | SubscriberInputScope::CanonicalResidual { owners, .. } => {
13137 owners.retain(|owner| owner != epoch);
13138 true
13139 }
13140 SubscriberInputScope::OwnerOnly { owners } => {
13141 owners.retain(|owner| owner != epoch);
13142 !owners.is_empty()
13143 }
13144 SubscriberInputScope::OwnerOnlyHandlers { .. }
13145 | SubscriberInputScope::Preconfirmed => true,
13146 });
13147 self.pending_reconcile_owner_records.retain_mut(|pending| {
13148 pending.owners.retain(|owner| owner != epoch);
13149 !pending.owners.is_empty()
13150 });
13151 self.recent_owner_input_refs.remove(epoch);
13152 self.recent_owner_input_ref_sets.remove(epoch);
13153 }
13154
13155 pub fn upsert_interest_owners(
13163 &mut self,
13164 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13165 ) -> Result<(), SubscriberError> {
13166 self.upsert_interest_owners_inner(owners, None)
13167 }
13168
13169 pub fn upsert_interest_owners_with_backfill(
13178 &mut self,
13179 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13180 backfill: SubscriberBackfill,
13181 ) -> Result<(), SubscriberError> {
13182 self.upsert_interest_owners_inner(owners, Some(backfill))
13183 }
13184
13185 fn upsert_interest_owners_inner(
13186 &mut self,
13187 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13188 explicit_backfill: Option<SubscriberBackfill>,
13189 ) -> Result<(), SubscriberError> {
13190 validate_subscriber_config(&self.config)?;
13191 let mut seen = HashSet::with_capacity(owners.len());
13192 let mut next_owned = self.clone_owned_interests();
13193 for (owner, interests) in &owners {
13194 if !seen.insert(owner.clone()) {
13195 return Err(SubscriberError::InvalidConfig(
13196 "bulk owner upsert contains a duplicate owner",
13197 ));
13198 }
13199 if self
13200 .owned_interests
13201 .iter()
13202 .any(|entry| &entry.owner == owner && entry.epoch.is_some())
13203 {
13204 return Err(SubscriberError::InvalidConfig(
13205 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
13206 ));
13207 }
13208 if let Some(entry) = next_owned.iter_mut().find(|entry| &entry.owner == owner) {
13209 entry.interests = interests.clone();
13210 entry.state = SubscriberOwnerState::Active;
13211 entry.baseline = None;
13212 entry.progress = None;
13213 entry.progress_stream_revision = None;
13214 } else {
13215 next_owned.push(OwnedSubscriberInterests {
13216 owner: owner.clone(),
13217 interests: interests.clone(),
13218 epoch: None,
13219 state: SubscriberOwnerState::Active,
13220 baseline: None,
13221 progress: None,
13222 progress_stream_revision: None,
13223 });
13224 }
13225 }
13226 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
13227 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13228
13229 let mut replacement_backfills = Vec::new();
13234 for (owner, interests) in &owners {
13235 let previous_filters: Vec<Filter> = self
13236 .owner_interests(owner)
13237 .map(log_filters)
13238 .unwrap_or_default();
13239 let continuity_anchor = previous_filters
13240 .iter()
13241 .filter_map(|filter| self.log_anchor(filter))
13242 .min();
13243 let filters = log_filters(interests);
13244 if let Some(backfill) = explicit_backfill
13245 && !filters.is_empty()
13246 {
13247 replacement_backfills.push(QueuedSubscriberBackfill {
13248 owner: Some(owner.clone()),
13249 epoch: None,
13250 filters: filters.clone(),
13251 backfill,
13252 });
13253 }
13254 let explicit_covers = explicit_backfill.is_some_and(|explicit| {
13255 explicit.end_block().is_none()
13256 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
13257 });
13258 let continuity_filters: Vec<_> = filters
13259 .into_iter()
13260 .filter(|filter| !previous_filters.contains(filter))
13261 .collect();
13262 if let Some(anchor) = continuity_anchor
13263 && !continuity_filters.is_empty()
13264 && !explicit_covers
13265 {
13266 replacement_backfills.push(QueuedSubscriberBackfill {
13267 owner: Some(owner.clone()),
13268 epoch: None,
13269 filters: continuity_filters,
13270 backfill: SubscriberBackfill::from_block(anchor),
13271 });
13272 }
13273 }
13274
13275 let retained_backfills = self
13276 .pending_backfills
13277 .iter()
13278 .filter(|queued| {
13279 queued
13280 .owner
13281 .as_ref()
13282 .is_none_or(|owner| !seen.contains(owner))
13283 })
13284 .map(|queued| queued.filters.len())
13285 .sum::<usize>();
13286 let replacement_units = replacement_backfills
13287 .iter()
13288 .map(|queued| queued.filters.len())
13289 .sum::<usize>();
13290 if retained_backfills.saturating_add(replacement_units) > self.config.max_pending_backfills
13291 {
13292 return Err(SubscriberError::ResourceExhausted(format!(
13293 "bulk owner update would queue more than {} lazy backfills",
13294 self.config.max_pending_backfills
13295 )));
13296 }
13297
13298 self.owned_interests = next_owned;
13302 self.interests = next_registered;
13303 for owner in &seen {
13304 self.recent_compat_owner_input_refs.remove(owner);
13305 self.recent_compat_owner_input_ref_sets.remove(owner);
13306 }
13307 self.retire_unreferenced_filters();
13308 self.sources_dirty = true;
13309 self.pending_backfills.retain(|queued| {
13310 queued
13311 .owner
13312 .as_ref()
13313 .is_none_or(|owner| !seen.contains(owner))
13314 });
13315 self.pending_backfills.extend(replacement_backfills);
13316 Ok(())
13317 }
13318
13319 pub fn replace_interest_owners(
13328 &mut self,
13329 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13330 ) -> Result<(), SubscriberError> {
13331 self.replace_interest_owners_inner(owners, None)
13332 }
13333
13334 pub fn replace_interest_owners_with_global_backfill(
13347 &mut self,
13348 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13349 backfill: SubscriberBackfill,
13350 ) -> Result<(), SubscriberError> {
13351 self.replace_interest_owners_inner(owners, Some(backfill))
13352 }
13353
13354 fn replace_interest_owners_inner(
13355 &mut self,
13356 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
13357 backfill: Option<SubscriberBackfill>,
13358 ) -> Result<(), SubscriberError> {
13359 validate_subscriber_config(&self.config)?;
13360 if self
13361 .owned_interests
13362 .iter()
13363 .any(|entry| entry.epoch.is_some())
13364 {
13365 return Err(SubscriberError::InvalidConfig(
13366 "cannot replace compatibility owners while an epoch-scoped lifecycle exists",
13367 ));
13368 }
13369
13370 let mut seen = HashSet::with_capacity(owners.len());
13371 let mut next_owned = Vec::with_capacity(owners.len());
13372 for (owner, interests) in owners {
13373 if !seen.insert(owner.clone()) {
13374 return Err(SubscriberError::InvalidConfig(
13375 "owner replacement contains a duplicate owner",
13376 ));
13377 }
13378 next_owned.push(OwnedSubscriberInterests {
13379 owner,
13380 interests,
13381 epoch: None,
13382 state: SubscriberOwnerState::Active,
13383 baseline: None,
13384 progress: None,
13385 progress_stream_revision: None,
13386 });
13387 }
13388 let next_registered = aggregate_interests(&[], &next_owned);
13389 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13390 let mut filters = log_filters(&next_registered);
13391 let mut unique_filters = Vec::with_capacity(filters.len());
13392 for filter in filters.drain(..) {
13393 if !unique_filters.contains(&filter) {
13394 unique_filters.push(filter);
13395 }
13396 }
13397 let replacement_backfills: VecDeque<_> = match backfill {
13398 Some(backfill) if !unique_filters.is_empty() => {
13399 VecDeque::from([QueuedSubscriberBackfill {
13400 owner: None,
13401 epoch: None,
13402 filters: unique_filters,
13403 backfill,
13404 }])
13405 }
13406 Some(_) | None => VecDeque::new(),
13407 };
13408 let replacement_units = replacement_backfills
13409 .iter()
13410 .map(|queued| queued.filters.len())
13411 .sum::<usize>();
13412 if replacement_units > self.config.max_pending_backfills {
13413 return Err(SubscriberError::ResourceExhausted(format!(
13414 "owner replacement would queue more than {} lazy backfills",
13415 self.config.max_pending_backfills
13416 )));
13417 }
13418
13419 let revoke_preconfirmation = self.latest_preconfirmation.is_some()
13424 || self.pending_preconfirmation_invalidation
13425 || self.pending_records.iter().any(|record| {
13426 record.scope == SubscriberInputScope::Preconfirmed
13427 || matches!(
13428 &record.record.context.chain_status,
13429 ChainStatus::Preconfirmed { .. }
13430 )
13431 });
13432 self.base_interests.clear();
13433 self.owned_interests = next_owned;
13434 self.interests = next_registered;
13435 self.reset_delivery_state();
13436 self.pending_preconfirmation_invalidation = revoke_preconfirmation;
13437 self.pending_backfills = replacement_backfills;
13438 self.reset_stream_topology();
13439 Ok(())
13440 }
13441
13442 pub fn add_interest_owner(
13465 &mut self,
13466 owner: HandlerId,
13467 interests: &[ReactiveInterest<N>],
13468 ) -> Result<(), SubscriberError> {
13469 self.set_interest_owner(owner, interests, None)
13470 }
13471
13472 pub fn add_interest_owner_with_backfill(
13491 &mut self,
13492 owner: HandlerId,
13493 interests: &[ReactiveInterest<N>],
13494 backfill: SubscriberBackfill,
13495 ) -> Result<(), SubscriberError> {
13496 self.set_interest_owner(owner, interests, Some(backfill))
13497 }
13498
13499 pub fn add_interest_owner_with_canonical_catchup(
13518 &mut self,
13519 owner: HandlerId,
13520 interests: &[ReactiveInterest<N>],
13521 retained: BlockRef,
13522 ) -> Result<(), SubscriberError> {
13523 validate_subscriber_config(&self.config)?;
13524 if self
13525 .owned_interests
13526 .iter()
13527 .any(|entry| entry.owner == owner && entry.epoch.is_some())
13528 {
13529 return Err(SubscriberError::InvalidConfig(
13530 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
13531 ));
13532 }
13533
13534 let mut next_owned = self.clone_owned_interests();
13535 if let Some(entry) = next_owned.iter_mut().find(|entry| entry.owner == owner) {
13536 entry.interests = interests.to_vec();
13537 entry.state = SubscriberOwnerState::Active;
13538 entry.baseline = None;
13539 entry.progress = None;
13540 entry.progress_stream_revision = None;
13541 entry.epoch = None;
13542 } else {
13543 next_owned.push(OwnedSubscriberInterests {
13544 owner: owner.clone(),
13545 interests: interests.to_vec(),
13546 epoch: None,
13547 state: SubscriberOwnerState::Active,
13548 baseline: None,
13549 progress: None,
13550 progress_stream_revision: None,
13551 });
13552 }
13553 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
13554 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13555 if next_registered
13556 .iter()
13557 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
13558 {
13559 return Err(SubscriberError::Unsupported(
13560 "Alloy coordinated registration supports log-only interest topologies",
13561 ));
13562 }
13563
13564 let mut owner_filters = Vec::new();
13565 for filter in log_filters(interests) {
13566 if !owner_filters.contains(&filter) {
13567 owner_filters.push(filter);
13568 }
13569 }
13570 let mut global_filters = Vec::new();
13571 for filter in log_filters(&next_registered) {
13572 if !global_filters.contains(&filter) {
13573 global_filters.push(filter);
13574 }
13575 }
13576 let owner_backfill =
13577 SubscriberBackfill::from_canonical_block_through(retained, retained.number)?;
13578 let global_backfill = SubscriberBackfill::after_canonical_block(retained)?;
13579 let replacement_units = owner_filters.len().saturating_add(global_filters.len());
13580 let retained_units = self
13581 .pending_backfills
13582 .iter()
13583 .filter(|queued| queued.owner.as_ref() != Some(&owner))
13584 .map(|queued| queued.filters.len())
13585 .sum::<usize>();
13586 if retained_units.saturating_add(replacement_units) > self.config.max_pending_backfills {
13587 return Err(SubscriberError::ResourceExhausted(format!(
13588 "coordinated owner registration would queue more than {} lazy backfills",
13589 self.config.max_pending_backfills
13590 )));
13591 }
13592
13593 let mut replacement_backfills = VecDeque::new();
13594 if !owner_filters.is_empty() {
13595 replacement_backfills.push_back(QueuedSubscriberBackfill {
13596 owner: Some(owner.clone()),
13597 epoch: None,
13598 filters: owner_filters,
13599 backfill: owner_backfill,
13600 });
13601 }
13602 replacement_backfills.push_back(QueuedSubscriberBackfill {
13605 owner: None,
13606 epoch: None,
13607 filters: global_filters,
13608 backfill: global_backfill,
13609 });
13610
13611 self.owned_interests = next_owned;
13614 self.interests = next_registered;
13615 self.recent_compat_owner_input_refs.remove(&owner);
13616 self.recent_compat_owner_input_ref_sets.remove(&owner);
13617 self.pending_backfills
13618 .retain(|queued| queued.owner.as_ref() != Some(&owner));
13619 self.pending_backfills.extend(replacement_backfills);
13620 self.retire_unreferenced_filters();
13621 self.sources_dirty = true;
13622 Ok(())
13623 }
13624
13625 pub fn remove_interest_owner(&mut self, owner: &HandlerId) -> Option<Vec<ReactiveInterest<N>>> {
13634 let index = self
13635 .owned_interests
13636 .iter()
13637 .position(|entry| &entry.owner == owner && entry.epoch.is_none())?;
13638 let removed = self.owned_interests.remove(index);
13639 if let Some(epoch) = &removed.epoch {
13640 self.purge_owner_epoch(epoch);
13641 } else {
13642 self.pending_backfills
13643 .retain(|backfill| backfill.owner.as_ref() != Some(owner));
13644 self.recent_compat_owner_input_refs.remove(owner);
13645 self.recent_compat_owner_input_ref_sets.remove(owner);
13646 }
13647 self.rebuild_registered_interests();
13648 self.retire_unreferenced_filters();
13649 self.sources_dirty = true;
13650 Some(removed.interests)
13651 }
13652
13653 pub fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
13655 self.owned_interests
13656 .iter()
13657 .find(|entry| &entry.owner == owner)
13658 .map(|entry| entry.interests.as_slice())
13659 }
13660
13661 fn set_interest_owner(
13662 &mut self,
13663 owner: HandlerId,
13664 interests: &[ReactiveInterest<N>],
13665 backfill: Option<SubscriberBackfill>,
13666 ) -> Result<(), SubscriberError> {
13667 validate_subscriber_config(&self.config)?;
13668 if self
13669 .owned_interests
13670 .iter()
13671 .any(|entry| entry.owner == owner && entry.epoch.is_some())
13672 {
13673 return Err(SubscriberError::InvalidConfig(
13674 "cannot mix compatibility and epoch-scoped owner lifecycle APIs",
13675 ));
13676 }
13677
13678 let mut next_owned = self.clone_owned_interests();
13679 let replaced_epoch = match next_owned.iter_mut().find(|entry| entry.owner == owner) {
13680 Some(entry) => {
13681 entry.interests = interests.to_vec();
13682 entry.state = SubscriberOwnerState::Active;
13683 entry.baseline = None;
13684 entry.progress = None;
13685 entry.progress_stream_revision = None;
13686 entry.epoch.take()
13687 }
13688 None => {
13689 next_owned.push(OwnedSubscriberInterests {
13690 owner: owner.clone(),
13691 interests: interests.to_vec(),
13692 epoch: None,
13693 state: SubscriberOwnerState::Active,
13694 baseline: None,
13695 progress: None,
13696 progress_stream_revision: None,
13697 });
13698 None
13699 }
13700 };
13701 let next_registered = aggregate_interests(&self.base_interests, &next_owned);
13702 validate_supported_interests(self.mode, &self.config, &next_registered)?;
13703
13704 let previous_filters: Vec<Filter> = self
13711 .owner_interests(&owner)
13712 .map(log_filters)
13713 .unwrap_or_default();
13714 let continuity_anchor: Option<u64> = previous_filters
13715 .iter()
13716 .filter_map(|filter| self.log_anchor(filter))
13717 .min();
13718
13719 let mut replacement_backfills = Vec::new();
13723 let filters = log_filters(interests);
13724 if let Some(backfill) = backfill
13725 && !filters.is_empty()
13726 {
13727 replacement_backfills.push(QueuedSubscriberBackfill {
13728 owner: Some(owner.clone()),
13729 epoch: None,
13730 filters: filters.clone(),
13731 backfill,
13732 });
13733 }
13734 let explicit_covers = backfill.is_some_and(|explicit| {
13735 explicit.end_block().is_none()
13736 && continuity_anchor.is_some_and(|anchor| explicit.start_block() <= anchor)
13737 });
13738 let continuity_filters: Vec<_> = filters
13739 .into_iter()
13740 .filter(|filter| !previous_filters.contains(filter))
13741 .collect();
13742 if let Some(anchor) = continuity_anchor
13743 && !continuity_filters.is_empty()
13744 && !explicit_covers
13745 {
13746 replacement_backfills.push(QueuedSubscriberBackfill {
13747 owner: Some(owner.clone()),
13748 epoch: None,
13749 filters: continuity_filters,
13750 backfill: SubscriberBackfill::from_block(anchor),
13751 });
13752 }
13753 let retained_backfills = self
13754 .pending_backfills
13755 .iter()
13756 .filter(|queued| queued.owner.as_ref() != Some(&owner))
13757 .map(|queued| queued.filters.len())
13758 .sum::<usize>();
13759 let replacement_units = replacement_backfills
13760 .iter()
13761 .map(|queued| queued.filters.len())
13762 .sum::<usize>();
13763 if retained_backfills.saturating_add(replacement_units) > self.config.max_pending_backfills
13764 {
13765 return Err(SubscriberError::ResourceExhausted(format!(
13766 "owner update would queue more than {} lazy backfills",
13767 self.config.max_pending_backfills
13768 )));
13769 }
13770
13771 self.owned_interests = next_owned;
13772 self.interests = next_registered;
13773 if let Some(epoch) = replaced_epoch {
13774 self.purge_owner_epoch(&epoch);
13775 } else {
13776 self.recent_compat_owner_input_refs.remove(&owner);
13777 self.recent_compat_owner_input_ref_sets.remove(&owner);
13778 }
13779 self.retire_unreferenced_filters();
13780 self.sources_dirty = true;
13781
13782 self.pending_backfills
13785 .retain(|queued| queued.owner.as_ref() != Some(&owner));
13786 self.pending_backfills.extend(replacement_backfills);
13787 Ok(())
13788 }
13789
13790 fn clone_owned_interests(&self) -> Vec<OwnedSubscriberInterests<N>> {
13791 self.owned_interests
13792 .iter()
13793 .map(|entry| OwnedSubscriberInterests {
13794 owner: entry.owner.clone(),
13795 interests: entry.interests.clone(),
13796 epoch: entry.epoch.clone(),
13797 state: entry.state,
13798 baseline: entry.baseline,
13799 progress: entry.progress.clone(),
13800 progress_stream_revision: entry.progress_stream_revision,
13801 })
13802 .collect()
13803 }
13804
13805 fn rebuild_registered_interests(&mut self) {
13806 self.interests = aggregate_interests(&self.base_interests, &self.owned_interests);
13807 }
13808
13809 fn log_anchor(&self, filter: &Filter) -> Option<u64> {
13812 if let Some(anchor) = self
13813 .log_source_ids
13814 .get(filter)
13815 .and_then(|id| self.last_seen_log_blocks.get(id))
13816 {
13817 return Some(*anchor);
13818 }
13819
13820 self.log_source_ids
13824 .values()
13825 .filter_map(|id| self.last_seen_log_blocks.get(id).copied())
13826 .min()
13827 }
13828
13829 #[allow(clippy::mutable_key_type)]
13836 fn logical_log_filters(&self) -> Vec<Filter> {
13837 let mut filters = log_filters(&self.base_interests);
13838 for entry in &self.owned_interests {
13839 filters.extend(log_filters(&entry.interests));
13840 }
13841 let mut seen = HashSet::new();
13842 filters.retain(|filter| seen.insert(filter.clone()));
13843 filters
13844 }
13845
13846 fn log_stream_filters(&self) -> Vec<Filter> {
13852 let mut merged = Vec::new();
13853 for filter in self.logical_log_filters() {
13854 merge_log_subscription_filter(&mut merged, &filter);
13855 }
13856
13857 let max_addresses = self.config.max_log_addresses_per_subscription.max(1);
13858 let mut planned = Vec::new();
13859 for filter in merged {
13860 let mut addresses: Vec<_> = filter.address.iter().copied().collect();
13861 if addresses.len() <= max_addresses {
13862 planned.push(filter);
13863 continue;
13864 }
13865 addresses.sort_unstable();
13866 for chunk in addresses.chunks(max_addresses) {
13867 let mut split = filter.clone();
13868 split.address = FilterSet::default();
13869 for address in chunk {
13870 split.address.insert(*address);
13871 }
13872 planned.push(split);
13873 }
13874 }
13875 planned
13876 }
13877
13878 #[allow(clippy::mutable_key_type)]
13885 fn retire_unreferenced_filters(&mut self) {
13886 let mut live: HashSet<Filter> = self.log_stream_filters().into_iter().collect();
13887 if let AlloySubscriberState::Active(streams) = &self.state {
13888 for entry in &streams.entries {
13889 match &entry.source {
13890 SubscriberStreamSource::PubSubLog { filter, .. }
13891 | SubscriberStreamSource::BasePendingLog { filter, .. }
13892 | SubscriberStreamSource::PollingLog { filter } => {
13893 live.insert(filter.clone());
13894 }
13895 SubscriberStreamSource::BaseFlashblocks
13896 | SubscriberStreamSource::OpPendingFlashblocks
13897 | SubscriberStreamSource::CanonicalHeadPolling
13898 | SubscriberStreamSource::PubSubPendingHashes
13899 | SubscriberStreamSource::PubSubBlockHeaders
13900 | SubscriberStreamSource::PollingPendingHashes => {}
13901 #[cfg(feature = "raw-flashblocks-json")]
13902 SubscriberStreamSource::ExternalFlashblockUpdates => {}
13903 }
13904 }
13905 }
13906 self.log_source_ids
13907 .retain(|filter, _| live.contains(filter));
13908 let live_ids: HashSet<usize> = self.log_source_ids.values().copied().collect();
13909 self.last_seen_log_blocks
13910 .retain(|id, _| live_ids.contains(id));
13911 }
13912
13913 fn drain_next_scoped_batch(&mut self) -> Option<SubscriberInputBatch<N>> {
13914 if self.pending_records.is_empty()
13915 && self.pending_chain_controls.is_empty()
13916 && !self.pending_preconfirmation_invalidation
13917 {
13918 return None;
13919 }
13920
13921 let first_preconfirmation = self.pending_records.front().and_then(|record| {
13922 if record.scope != SubscriberInputScope::Preconfirmed {
13923 return None;
13924 }
13925 match &record.record.context.chain_status {
13926 ChainStatus::Preconfirmed { flashblock } => Some(flashblock.clone()),
13927 _ => None,
13928 }
13929 });
13930 let len = self
13931 .pending_records
13932 .iter()
13933 .take(self.config.max_batch_size)
13934 .take_while(|record| match &first_preconfirmation {
13935 Some(expected) => {
13936 record.scope == SubscriberInputScope::Preconfirmed
13937 && matches!(
13938 &record.record.context.chain_status,
13939 ChainStatus::Preconfirmed { flashblock } if flashblock == expected
13940 )
13941 }
13942 None => record.scope != SubscriberInputScope::Preconfirmed,
13943 })
13944 .count();
13945 let records = self.pending_records.drain(..len).collect();
13946 let chain_controls = if first_preconfirmation.is_none() && self.pending_records.is_empty() {
13947 self.pending_chain_controls.drain(..).collect()
13948 } else {
13949 Vec::new()
13950 };
13951 Some(SubscriberInputBatch {
13952 records,
13953 chain_id: self.chain_id,
13954 chain_controls,
13955 preconfirmation_invalidated: std::mem::take(
13956 &mut self.pending_preconfirmation_invalidation,
13957 ),
13958 })
13959 }
13960
13961 fn reset_delivery_state(&mut self) {
13962 self.pending_records.clear();
13963 self.pending_chain_controls.clear();
13964 self.pending_reconcile_owner_records.clear();
13965 self.resource_error = None;
13966 self.last_seen_log_blocks.clear();
13967 self.verified_log_blocks.clear();
13968 self.verified_log_block_order.clear();
13969 self.recent_input_refs.clear();
13970 self.recent_input_ref_set.clear();
13971 self.recent_owner_input_refs.clear();
13972 self.recent_owner_input_ref_sets.clear();
13973 self.recent_compat_owner_input_refs.clear();
13974 self.recent_compat_owner_input_ref_sets.clear();
13975 self.pending_backfills.clear();
13976 self.pending_source_backfills.clear();
13977 self.pending_preconfirmation_invalidation = false;
13978 self.pending_flashblock_reconnects.clear();
13979 self.pending_flashblock_reconnect_sources.clear();
13980 self.flashblocks_rpc_metrics = FlashblocksRpcMetrics::default();
13981 self.log_source_ids.clear();
13982 self.next_log_source_id = 0;
13983 self.sources_dirty = true;
13984 self.last_certified_canonical_head = None;
13985 self.reset_flashblock_tracking();
13986 }
13987
13988 fn reset_stream_topology(&mut self) {
13989 #[cfg(feature = "raw-flashblocks-json")]
13990 let external = match &mut self.state {
13991 AlloySubscriberState::Active(streams) => streams
13992 .entries
13993 .iter()
13994 .position(|entry| entry.source.is_external_flashblocks())
13995 .map(|index| streams.entries.remove(index)),
13996 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => None,
13997 };
13998
13999 #[cfg(feature = "raw-flashblocks-json")]
14000 if let Some(external) = external {
14001 let mut streams = SubscriberStreams::new();
14002 streams.entries.push(external);
14003 self.state = AlloySubscriberState::Active(streams);
14004 return;
14005 }
14006
14007 self.state = AlloySubscriberState::Uninitialized;
14008 }
14009
14010 fn reset_flashblock_tracking(&mut self) {
14011 self.base_flashblock_header = None;
14012 self.base_flashblock_transactions = None;
14013 self.unmatched_pending_logs.clear();
14014 self.latest_preconfirmation = None;
14015 self.preconfirmed_seen_logs.clear();
14016 self.preconfirmed_receipted_transactions.clear();
14017 self.preconfirmed_unavailable_receipts.clear();
14018 #[cfg(feature = "raw-flashblocks-json")]
14019 {
14020 self.last_external_flashblock_snapshot = None;
14021 }
14022 self.consecutive_flashblock_poll_failures = 0;
14023 }
14024
14025 fn invalidate_preconfirmation_snapshot(&mut self) {
14032 self.pending_records
14033 .retain(|record| record.scope != SubscriberInputScope::Preconfirmed);
14034 self.pending_preconfirmation_invalidation = true;
14035 self.latest_preconfirmation = None;
14036 self.preconfirmed_seen_logs.clear();
14037 self.preconfirmed_receipted_transactions.clear();
14038 self.preconfirmed_unavailable_receipts.clear();
14039 }
14040
14041 fn bump_stream_revision(&mut self) {
14042 self.stream_revision = self.stream_revision.saturating_add(1);
14043 }
14044}
14045
14046impl<P, N> InterestOwnerSubscriber<N> for AlloySubscriber<P, N>
14047where
14048 P: Provider<N> + Send + Sync,
14049 N: Network + 'static,
14050 N::HeaderResponse: Send + 'static,
14051{
14052 fn upsert_interest_owners(
14053 &mut self,
14054 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14055 ) -> SubscriberOperation<'_, ()> {
14056 Box::pin(async move {
14057 if !owners.is_empty() {
14058 self.ensure_chain_id().await?;
14059 }
14060 AlloySubscriber::upsert_interest_owners(self, owners)
14061 })
14062 }
14063
14064 fn replace_interest_owners(
14065 &mut self,
14066 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14067 ) -> SubscriberOperation<'_, ()> {
14068 Box::pin(async move {
14069 if owners.iter().any(|(_, interests)| !interests.is_empty()) {
14070 self.ensure_chain_id().await?;
14071 }
14072 AlloySubscriber::replace_interest_owners(self, owners)
14073 })
14074 }
14075
14076 fn replace_interest_owners_with_global_backfill(
14077 &mut self,
14078 owners: Vec<(HandlerId, Vec<ReactiveInterest<N>>)>,
14079 backfill: SubscriberBackfill,
14080 ) -> SubscriberOperation<'_, ()> {
14081 Box::pin(async move {
14082 if owners.iter().any(|(_, interests)| !interests.is_empty()) {
14083 self.ensure_chain_id().await?;
14084 }
14085 AlloySubscriber::replace_interest_owners_with_global_backfill(self, owners, backfill)
14086 })
14087 }
14088
14089 fn add_interest_owner(
14090 &mut self,
14091 owner: HandlerId,
14092 interests: &[ReactiveInterest<N>],
14093 ) -> SubscriberOperation<'_, ()> {
14094 let interests = interests.to_vec();
14095 Box::pin(async move {
14096 if !interests.is_empty() {
14097 self.ensure_chain_id().await?;
14098 }
14099 AlloySubscriber::add_interest_owner(self, owner, &interests)
14100 })
14101 }
14102
14103 fn add_interest_owner_with_backfill(
14104 &mut self,
14105 owner: HandlerId,
14106 interests: &[ReactiveInterest<N>],
14107 backfill: SubscriberBackfill,
14108 ) -> SubscriberOperation<'_, ()> {
14109 let interests = interests.to_vec();
14110 Box::pin(async move {
14111 if !interests.is_empty() {
14112 self.ensure_chain_id().await?;
14113 }
14114 AlloySubscriber::add_interest_owner_with_backfill(self, owner, &interests, backfill)
14115 })
14116 }
14117
14118 fn add_interest_owner_with_canonical_catchup(
14119 &mut self,
14120 owner: HandlerId,
14121 interests: &[ReactiveInterest<N>],
14122 retained: BlockRef,
14123 ) -> SubscriberOperation<'_, ()> {
14124 let interests = interests.to_vec();
14125 Box::pin(async move {
14126 self.ensure_chain_id().await?;
14129 AlloySubscriber::add_interest_owner_with_canonical_catchup(
14130 self, owner, &interests, retained,
14131 )
14132 })
14133 }
14134
14135 fn remove_interest_owner(
14136 &mut self,
14137 owner: &HandlerId,
14138 ) -> SubscriberOperation<'_, Option<Vec<ReactiveInterest<N>>>> {
14139 let owner = owner.clone();
14140 Box::pin(async move { Ok(AlloySubscriber::remove_interest_owner(self, &owner)) })
14141 }
14142
14143 fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<N>]> {
14144 AlloySubscriber::owner_interests(self, owner)
14145 }
14146}
14147
14148enum AlloySubscriberState<N: Network> {
14149 Uninitialized,
14150 Active(SubscriberStreams<N>),
14151 Empty,
14152}
14153
14154struct SubscriberStreams<N: Network> {
14155 entries: Vec<SubscriberStreamEntry<N>>,
14156 next_index: usize,
14157}
14158
14159struct SubscriberStreamEntry<N: Network> {
14160 source: SubscriberStreamSource,
14161 stream: BoxStream<'static, SubscriberEvent<N>>,
14162}
14163
14164impl<N: Network> SubscriberStreams<N> {
14165 fn new() -> Self {
14166 Self {
14167 entries: Vec::new(),
14168 next_index: 0,
14169 }
14170 }
14171
14172 fn is_empty(&self) -> bool {
14173 self.entries.is_empty()
14174 }
14175
14176 fn push(
14177 &mut self,
14178 source: SubscriberStreamSource,
14179 stream: BoxStream<'static, SubscriberEvent<N>>,
14180 ) {
14181 self.entries.push(SubscriberStreamEntry { source, stream });
14182 }
14183
14184 #[cfg(test)]
14185 fn len(&self) -> usize {
14186 self.entries.len()
14187 }
14188
14189 fn contains_source(&self, source: &SubscriberStreamSource) -> bool {
14190 self.entries
14191 .iter()
14192 .any(|entry| entry.source.same_key(source))
14193 }
14194
14195 fn retain_sources(&mut self, sources: &[SubscriberStreamSource]) {
14196 self.entries
14197 .retain(|entry| sources.iter().any(|source| entry.source.same_key(source)));
14198 self.normalize_next_index();
14199 }
14200
14201 fn normalize_next_index(&mut self) {
14202 if self.entries.is_empty() {
14203 self.next_index = 0;
14204 } else if self.next_index >= self.entries.len() {
14205 self.next_index %= self.entries.len();
14206 }
14207 }
14208
14209 async fn next(&mut self) -> Option<SubscriberEvent<N>> {
14210 poll_fn(|cx| {
14211 self.normalize_next_index();
14212 if self.entries.is_empty() {
14213 return std::task::Poll::Ready(None);
14214 }
14215
14216 let mut index = self.next_index;
14217 let mut checked = 0usize;
14218 while checked < self.entries.len() {
14219 if index >= self.entries.len() {
14220 index = 0;
14221 }
14222 match self.entries[index].stream.as_mut().poll_next(cx) {
14223 std::task::Poll::Ready(Some(event)) => {
14224 if matches!(event, SubscriberEvent::StreamTerminated(_)) {
14225 self.entries.remove(index);
14226 self.next_index = if self.entries.is_empty() {
14227 0
14228 } else {
14229 index % self.entries.len()
14230 };
14231 } else {
14232 self.next_index = (index + 1) % self.entries.len();
14233 }
14234 return std::task::Poll::Ready(Some(event));
14235 }
14236 std::task::Poll::Ready(None) => {
14237 self.entries.remove(index);
14238 if self.entries.is_empty() {
14239 self.next_index = 0;
14240 return std::task::Poll::Ready(None);
14241 }
14242 }
14243 std::task::Poll::Pending => {
14244 checked += 1;
14245 index += 1;
14246 }
14247 }
14248 }
14249
14250 if self.entries.is_empty() {
14251 std::task::Poll::Ready(None)
14252 } else {
14253 self.next_index = index % self.entries.len();
14254 std::task::Poll::Pending
14255 }
14256 })
14257 .await
14258 }
14259}
14260
14261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14262#[allow(dead_code)]
14263enum SubscriberTransport {
14264 PubSub,
14265 Polling,
14266}
14267
14268#[derive(Clone, Debug)]
14269enum SubscriberStreamSource {
14270 PubSubLog {
14271 id: usize,
14272 filter: Filter,
14273 },
14274 BasePendingLog {
14275 id: usize,
14276 filter: Filter,
14277 },
14278 BaseFlashblocks,
14279 OpPendingFlashblocks,
14280 CanonicalHeadPolling,
14281 PubSubPendingHashes,
14282 PubSubBlockHeaders,
14283 PollingLog {
14284 filter: Filter,
14285 },
14286 PollingPendingHashes,
14287 #[cfg(feature = "raw-flashblocks-json")]
14288 ExternalFlashblockUpdates,
14289}
14290
14291impl SubscriberStreamSource {
14292 fn label(&self) -> &'static str {
14293 match self {
14294 Self::PubSubLog { .. } => "pubsub log",
14295 Self::BasePendingLog { .. } => "OP Stack pendingLogs",
14296 Self::BaseFlashblocks => "OP Stack newFlashblocks",
14297 Self::OpPendingFlashblocks => "Optimism pending Flashblocks",
14298 Self::CanonicalHeadPolling => "certified canonical head",
14299 Self::PubSubPendingHashes => "pubsub pending transaction hash",
14300 Self::PubSubBlockHeaders => "pubsub block header",
14301 Self::PollingLog { .. } => "polling log",
14302 Self::PollingPendingHashes => "polling pending transaction hash",
14303 #[cfg(feature = "raw-flashblocks-json")]
14304 Self::ExternalFlashblockUpdates => "external standardized Flashblock update",
14305 }
14306 }
14307
14308 fn is_pubsub(&self) -> bool {
14309 matches!(
14310 self,
14311 Self::PubSubLog { .. }
14312 | Self::BasePendingLog { .. }
14313 | Self::BaseFlashblocks
14314 | Self::OpPendingFlashblocks
14315 | Self::PubSubPendingHashes
14316 | Self::PubSubBlockHeaders
14317 )
14318 }
14319
14320 fn is_flashblocks(&self) -> bool {
14321 matches!(
14322 self,
14323 Self::BasePendingLog { .. } | Self::BaseFlashblocks | Self::OpPendingFlashblocks
14324 )
14325 }
14326
14327 fn same_key(&self, other: &Self) -> bool {
14328 match (self, other) {
14329 (Self::PubSubLog { filter: left, .. }, Self::PubSubLog { filter: right, .. })
14330 | (
14331 Self::BasePendingLog { filter: left, .. },
14332 Self::BasePendingLog { filter: right, .. },
14333 )
14334 | (Self::PollingLog { filter: left }, Self::PollingLog { filter: right }) => {
14335 left == right
14336 }
14337 (Self::BaseFlashblocks, Self::BaseFlashblocks)
14338 | (Self::OpPendingFlashblocks, Self::OpPendingFlashblocks)
14339 | (Self::CanonicalHeadPolling, Self::CanonicalHeadPolling)
14340 | (Self::PubSubPendingHashes, Self::PubSubPendingHashes)
14341 | (Self::PubSubBlockHeaders, Self::PubSubBlockHeaders)
14342 | (Self::PollingPendingHashes, Self::PollingPendingHashes) => true,
14343 #[cfg(feature = "raw-flashblocks-json")]
14344 (Self::ExternalFlashblockUpdates, Self::ExternalFlashblockUpdates) => true,
14345 _ => false,
14346 }
14347 }
14348
14349 fn is_external_flashblocks(&self) -> bool {
14350 #[cfg(feature = "raw-flashblocks-json")]
14351 {
14352 matches!(self, Self::ExternalFlashblockUpdates)
14353 }
14354 #[cfg(not(feature = "raw-flashblocks-json"))]
14355 {
14356 false
14357 }
14358 }
14359}
14360
14361#[allow(dead_code)]
14362enum SubscriberEvent<N: Network> {
14363 Log {
14364 source_id: usize,
14365 log: Log,
14366 },
14367 BackfilledLogs {
14368 source_id: usize,
14369 logs: Vec<Log>,
14370 },
14371 Logs(Vec<Log>),
14372 BlockHeader(N::HeaderResponse),
14373 PendingHash(B256),
14374 PendingHashes(Vec<B256>),
14375 BasePendingLog {
14376 source_id: usize,
14377 log: Log,
14378 },
14379 BaseFlashblock(BaseFlashblockWirePayload),
14380 OpFlashblockTick,
14381 CanonicalHeadTick,
14382 PreconfirmedLogs {
14383 flashblock: FlashblockRef,
14384 logs: Vec<Log>,
14385 },
14386 FlashblockInvalidated,
14387 FlashblockObserved,
14388 #[cfg(feature = "raw-flashblocks-json")]
14389 ExternalFlashblockUpdate(raw_json_flashblocks::QueuedFlashblockUpdate),
14390 StreamTerminated(SubscriberStreamSource),
14391}
14392
14393enum SubscriberReady<N: Network> {
14394 Event(Option<SubscriberEvent<N>>),
14395 FlashblockReconnect(
14396 SubscriberStreamSource,
14397 Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>,
14398 ),
14399}
14400
14401#[derive(Debug)]
14402enum PendingFlashblockPollError {
14403 Request(SubscriberError),
14404 Integrity(SubscriberError),
14405}
14406
14407impl PendingFlashblockPollError {
14408 fn into_subscriber(self) -> SubscriberError {
14409 match self {
14410 Self::Request(error) | Self::Integrity(error) => error,
14411 }
14412 }
14413}
14414
14415fn pending_flashblock_request_error(error: impl fmt::Display) -> PendingFlashblockPollError {
14416 PendingFlashblockPollError::Request(provider_error(error))
14417}
14418
14419fn normalize_op_pending_block<N: Network>(
14420 mut value: serde_json::Value,
14421) -> Result<N::BlockResponse, SubscriberError> {
14422 let object = value.as_object_mut().ok_or_else(|| {
14423 SubscriberError::Provider("OP pending block response is not an object".into())
14424 })?;
14425 let transactions = object
14426 .get_mut("transactions")
14427 .and_then(serde_json::Value::as_array_mut)
14428 .ok_or_else(|| {
14429 SubscriberError::Provider(
14430 "OP pending block response is missing its transaction array".into(),
14431 )
14432 })?;
14433 for transaction in transactions {
14434 if transaction.is_string() {
14435 continue;
14436 }
14437 let hash = transaction
14438 .as_object()
14439 .and_then(|object| object.get("hash"))
14440 .filter(|hash| hash.is_string())
14441 .cloned()
14442 .ok_or_else(|| {
14443 SubscriberError::Provider("OP pending block transaction is missing its hash".into())
14444 })?;
14445 *transaction = hash;
14446 }
14447 if object.get("hash").is_none_or(serde_json::Value::is_null) {
14448 object.insert(
14449 "hash".into(),
14450 serde_json::Value::String(B256::ZERO.to_string()),
14451 );
14452 }
14453 if object.get("nonce").is_none_or(serde_json::Value::is_null) {
14454 object.insert(
14455 "nonce".into(),
14456 serde_json::Value::String("0x0000000000000000".into()),
14457 );
14458 }
14459 if object.get("miner").is_none_or(serde_json::Value::is_null)
14460 || object
14461 .get("beneficiary")
14462 .is_none_or(serde_json::Value::is_null)
14463 {
14464 object.insert(
14465 "miner".into(),
14466 serde_json::Value::String(Address::ZERO.to_string()),
14467 );
14468 }
14469 serde_json::from_value(value).map_err(|error| {
14470 SubscriberError::Provider(format!(
14471 "failed to decode normalized OP pending block: {error}"
14472 ))
14473 })
14474}
14475
14476fn normalize_pending_transaction_receipt(
14477 expected_transaction_hash: B256,
14478 value: serde_json::Value,
14479) -> Result<Option<Vec<Log>>, SubscriberError> {
14480 if value.is_null() {
14481 return Ok(None);
14482 }
14483 let receipt = value.as_object().ok_or_else(|| {
14484 SubscriberError::Provider("pending transaction receipt response is not an object".into())
14485 })?;
14486 let transaction_hash: B256 =
14487 serde_json::from_value(receipt.get("transactionHash").cloned().ok_or_else(|| {
14488 SubscriberError::Provider(
14489 "pending transaction receipt is missing its transaction hash".into(),
14490 )
14491 })?)
14492 .map_err(|error| {
14493 SubscriberError::Provider(format!(
14494 "failed to decode pending transaction receipt hash: {error}"
14495 ))
14496 })?;
14497 if transaction_hash != expected_transaction_hash {
14498 return Err(SubscriberError::Provider(
14499 "pending transaction receipt hash disagrees with its request".into(),
14500 ));
14501 }
14502 let receipt_logs = receipt
14503 .get("logs")
14504 .and_then(serde_json::Value::as_array)
14505 .ok_or_else(|| {
14506 SubscriberError::Provider("pending transaction receipt is missing its log array".into())
14507 })?;
14508 let mut logs = Vec::new();
14509 for log in receipt_logs {
14510 let log: Log = serde_json::from_value(log.clone()).map_err(|error| {
14511 SubscriberError::Provider(format!(
14512 "failed to decode pending transaction receipt log: {error}"
14513 ))
14514 })?;
14515 if log.transaction_hash != Some(expected_transaction_hash) {
14516 return Err(SubscriberError::Provider(
14517 "pending transaction receipt log hash disagrees with its receipt".into(),
14518 ));
14519 }
14520 logs.push(log);
14521 }
14522 Ok(Some(logs))
14523}
14524
14525impl<P, N> EventSubscriber<N> for AlloySubscriber<P, N>
14526where
14527 P: Provider<N> + Send + Sync,
14528 N: Network + 'static,
14529 N::HeaderResponse: Send + 'static,
14530{
14531 fn chain_id(&self) -> Option<u64> {
14532 self.chain_id
14533 }
14534
14535 fn capabilities(&self) -> SubscriberCapabilities {
14536 let Ok(transport) = resolve_subscriber_transport(self.mode) else {
14537 return SubscriberCapabilities::default();
14538 };
14539 let mut capabilities = vec![
14540 SubscriberCapability::Logs,
14541 SubscriberCapability::PendingTransactionHashes,
14542 SubscriberCapability::HistoricalBackfill,
14543 SubscriberCapability::Live,
14544 SubscriberCapability::OwnerScopedDelivery,
14545 SubscriberCapability::DynamicInterests,
14546 ];
14547 if transport == SubscriberTransport::PubSub {
14548 capabilities.push(SubscriberCapability::BlockHeaders);
14549 }
14550 if self.config.preconfirmations != PreconfirmationMode::Disabled
14551 && (self.uses_external_flashblock_updates()
14552 || (self.provider_ref.is_some()
14553 && self.chain_id.and_then(flashblocks_adapter).is_some()))
14554 {
14555 capabilities.push(SubscriberCapability::Preconfirmations);
14556 }
14557 SubscriberCapabilities::new(capabilities)
14558 }
14559
14560 fn register_interests(
14561 &mut self,
14562 interests: &[ReactiveInterest<N>],
14563 ) -> SubscriberOperation<'_, ()> {
14564 let interests = interests.to_vec();
14565 Box::pin(async move {
14566 validate_subscriber_config(&self.config)?;
14567 validate_supported_interests(self.mode, &self.config, &interests)?;
14568 if !interests.is_empty() {
14569 self.ensure_chain_id().await?;
14570 }
14571 self.validate_flashblocks_setup()?;
14572
14573 self.base_interests = interests;
14574 self.owned_interests.clear();
14575 self.rebuild_registered_interests();
14576 self.reset_delivery_state();
14577 self.reset_stream_topology();
14578 Ok(())
14579 })
14580 }
14581
14582 fn next_batch(&mut self) -> SubscriberNextBatch<'_, N> {
14583 Box::pin(async {
14584 Ok(self
14585 .next_scoped_batch()
14586 .await?
14587 .map(SubscriberInputBatch::into_reactive_batch))
14588 })
14589 }
14590}
14591
14592impl<P, N> AlloySubscriber<P, N>
14593where
14594 P: Provider<N> + Send + Sync,
14595 N: Network + 'static,
14596 N::HeaderResponse: Send + 'static,
14597{
14598 pub async fn establish_flashblocks_preflight(
14620 &mut self,
14621 expected_chain_id: u64,
14622 ) -> Result<FlashblocksPreflight, SubscriberError> {
14623 validate_subscriber_config(&self.config)?;
14624 if self.config.preconfirmations == PreconfirmationMode::Disabled {
14625 return Err(SubscriberError::InvalidConfig(
14626 "Flashblocks preflight requires preconfirmations",
14627 ));
14628 }
14629 if !self
14630 .interests
14631 .iter()
14632 .any(|interest| matches!(interest, ReactiveInterest::Logs(_)))
14633 {
14634 return Err(SubscriberError::InvalidConfig(
14635 "Flashblocks preflight requires at least one active log interest",
14636 ));
14637 }
14638 let chain_id = self.ensure_chain_id().await?;
14639 if chain_id != expected_chain_id {
14640 return Err(SubscriberError::ChainMismatch {
14641 expected: expected_chain_id,
14642 actual: chain_id,
14643 });
14644 }
14645 self.validate_flashblocks_setup()?;
14646 #[cfg(feature = "raw-flashblocks-json")]
14647 if let Some(provider) = self.external_flashblocks_provider.clone() {
14648 self.ensure_streams().await?;
14649 return Ok(FlashblocksPreflight {
14650 chain_id,
14651 provider,
14652 delivery: FlashblocksDelivery::ExternalUpdates,
14653 pending_log_subscriptions: 0,
14654 pending_log_filters: self.log_stream_filters().len(),
14655 advertised_capabilities: None,
14656 });
14657 }
14658 let adapter = flashblocks_adapter(chain_id).ok_or(SubscriberError::Unsupported(
14659 "Flashblocks are currently implemented for Base and OP chains",
14660 ))?;
14661 let provider = self
14662 .provider_ref
14663 .clone()
14664 .ok_or(SubscriberError::InvalidConfig(
14665 "Flashblocks preflight requires a stable provider ref",
14666 ))?;
14667 self.flashblocks_rpc_metrics.capability_requests = self
14668 .flashblocks_rpc_metrics
14669 .capability_requests
14670 .saturating_add(1);
14671 let capability_provider = if adapter == FlashblocksAdapter::PendingStatePolling {
14672 self.flashblocks_state_provider
14673 .as_ref()
14674 .unwrap_or(&self.provider)
14675 } else {
14676 &self.provider
14677 };
14678 let advertised_capabilities = capability_provider
14679 .client()
14680 .request::<_, serde_json::Value>("op_supportedCapabilities", ())
14681 .await
14682 .ok();
14683
14684 self.ensure_streams().await?;
14685 let pending_log_filters = self.log_stream_filters();
14686 if adapter == FlashblocksAdapter::PendingStatePolling
14687 && self.pending_receipt_requests_per_tick_capacity() == 0
14688 {
14689 return Err(SubscriberError::InvalidConfig(
14690 "Flashblocks RPC budget leaves no capacity for OP transaction receipts",
14691 ));
14692 }
14693 let (delivery, pending_log_subscriptions) = match adapter {
14694 FlashblocksAdapter::NativeSubscriptions => {
14695 if resolve_subscriber_transport(self.mode)? != SubscriberTransport::PubSub {
14696 return Err(SubscriberError::Unsupported(
14697 "Base Flashblocks preflight requires pubsub",
14698 ));
14699 }
14700 let pending_sources = self
14701 .pubsub_stream_sources()
14702 .into_iter()
14703 .filter(|source| {
14704 matches!(source, SubscriberStreamSource::BasePendingLog { .. })
14705 })
14706 .collect::<Vec<_>>();
14707 let AlloySubscriberState::Active(streams) = &self.state else {
14708 return Err(SubscriberError::Provider(
14709 "Flashblocks preflight subscriptions did not become active".to_owned(),
14710 ));
14711 };
14712 if !streams.contains_source(&SubscriberStreamSource::BaseFlashblocks)
14713 || pending_sources
14714 .iter()
14715 .any(|source| !streams.contains_source(source))
14716 {
14717 return Err(SubscriberError::Provider(
14718 "Base Flashblocks preflight did not retain both subscription lanes"
14719 .to_owned(),
14720 ));
14721 }
14722 (
14723 FlashblocksDelivery::NativeSubscriptions,
14724 pending_sources.len(),
14725 )
14726 }
14727 FlashblocksAdapter::PendingStatePolling => {
14728 if let Some(state_provider) = self.flashblocks_state_provider.as_ref() {
14729 self.flashblocks_rpc_metrics.provider_pair_chain_requests = self
14730 .flashblocks_rpc_metrics
14731 .provider_pair_chain_requests
14732 .saturating_add(1);
14733 let actual = state_provider
14734 .get_chain_id()
14735 .await
14736 .map_err(provider_error)?;
14737 if actual != expected_chain_id {
14738 return Err(SubscriberError::ChainMismatch {
14739 expected: expected_chain_id,
14740 actual,
14741 });
14742 }
14743 }
14744 let AlloySubscriberState::Active(streams) = &self.state else {
14745 return Err(SubscriberError::Provider(
14746 "Flashblocks preflight streams did not become active".to_owned(),
14747 ));
14748 };
14749 if !streams.contains_source(&SubscriberStreamSource::OpPendingFlashblocks) {
14750 return Err(SubscriberError::Provider(
14751 "Optimism Flashblocks preflight did not retain its pending-state sampler"
14752 .to_owned(),
14753 ));
14754 }
14755 self.probe_pending_state(&pending_log_filters).await?;
14756 (FlashblocksDelivery::PendingStatePolling, 0)
14757 }
14758 };
14759 Ok(FlashblocksPreflight {
14760 chain_id,
14761 provider,
14762 delivery,
14763 pending_log_subscriptions,
14764 pending_log_filters: pending_log_filters.len(),
14765 advertised_capabilities,
14766 })
14767 }
14768
14769 #[cfg(feature = "raw-flashblocks-json")]
14780 pub fn ingest_flashblock_update(
14781 &mut self,
14782 update: FlashblockUpdate,
14783 ) -> Result<(), SubscriberError> {
14784 validate_subscriber_config(&self.config)?;
14785 self.validate_flashblocks_setup()?;
14786 let configured =
14787 self.external_flashblocks_provider
14788 .as_ref()
14789 .ok_or(SubscriberError::InvalidConfig(
14790 "standardized Flashblock updates require configure_external_flashblock_updates",
14791 ))?;
14792
14793 match update {
14794 FlashblockUpdate::Snapshot(batch) => {
14795 if batch.flashblock.provider.endpoint != configured.endpoint {
14796 return Err(SubscriberError::Provider(
14797 "external Flashblock update came from an unexpected provider endpoint"
14798 .into(),
14799 ));
14800 }
14801 if batch.flashblock.provider.generation < configured.generation
14802 || self.latest_preconfirmation.as_ref().is_some_and(|latest| {
14803 latest.provider.endpoint == batch.flashblock.provider.endpoint
14804 && latest.provider.generation > batch.flashblock.provider.generation
14805 })
14806 {
14807 return Ok(());
14808 }
14809 if self
14810 .rejected_external_flashblock_generation
14811 .is_some_and(|rejected| batch.flashblock.provider.generation <= rejected)
14812 {
14813 return Err(SubscriberError::Provider(
14814 "external Flashblock provider generation was previously rejected".into(),
14815 ));
14816 }
14817 validate_standard_flashblock_snapshot(&batch)?;
14818 if self.validate_external_flashblock_sequence(&batch)? {
14819 return Ok(());
14820 }
14821 let required = self.pending_record_count().saturating_add(batch.logs.len());
14822 if required > self.config.max_pending_records {
14823 self.invalidate_preconfirmation_snapshot();
14824 self.last_external_flashblock_snapshot = None;
14825 return Err(SubscriberError::ResourceExhausted(format!(
14826 "external preconfirmation records require {required} pending records, above the configured limit of {}",
14827 self.config.max_pending_records
14828 )));
14829 }
14830 let accepted_snapshot = (*batch).clone();
14831 let FlashblockSnapshot { flashblock, logs } = *batch;
14832 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
14833 self.last_external_flashblock_snapshot = Some(accepted_snapshot);
14834 if let Some(provider) = self.external_flashblocks_provider.as_mut() {
14835 provider.generation = provider.generation.max(flashblock.provider.generation);
14836 }
14837 if !logs.is_empty() {
14838 self.enqueue_event(SubscriberEvent::PreconfirmedLogs { flashblock, logs });
14839 }
14840 }
14841 FlashblockUpdate::Invalidated(invalidation) => {
14842 if invalidation.provider.endpoint != configured.endpoint {
14843 return Err(SubscriberError::Provider(
14844 "external Flashblock invalidation came from an unexpected provider endpoint"
14845 .into(),
14846 ));
14847 }
14848 if self.latest_preconfirmation.as_ref().is_some_and(|latest| {
14849 latest.provider == invalidation.provider
14850 && latest.payload_id == Some(invalidation.payload_id)
14851 }) {
14852 self.invalidate_preconfirmation_snapshot();
14853 self.last_external_flashblock_snapshot = None;
14854 }
14855 }
14856 }
14857 Ok(())
14858 }
14859
14860 #[cfg(feature = "raw-flashblocks-json")]
14861 fn validate_external_flashblock_sequence(
14862 &self,
14863 snapshot: &FlashblockSnapshot,
14864 ) -> Result<bool, SubscriberError> {
14865 let Some(previous) = self.last_external_flashblock_snapshot.as_ref() else {
14866 if snapshot.flashblock.index != Some(0) {
14867 return Err(SubscriberError::Provider(
14868 "external Flashblock payload generation must begin at index zero".into(),
14869 ));
14870 }
14871 return Ok(false);
14872 };
14873
14874 if previous.flashblock.provider == snapshot.flashblock.provider
14875 && previous.flashblock.payload_id == snapshot.flashblock.payload_id
14876 {
14877 let previous_index = previous
14878 .flashblock
14879 .index
14880 .expect("validated indexed snapshot");
14881 let current_index = snapshot
14882 .flashblock
14883 .index
14884 .expect("validated indexed snapshot");
14885 if current_index == previous_index {
14886 if previous == snapshot {
14887 return Ok(true);
14888 }
14889 return Err(SubscriberError::Provider(
14890 "external Flashblock repeated the same index with conflicting content".into(),
14891 ));
14892 }
14893 if current_index < previous_index {
14894 return Err(SubscriberError::Provider(format!(
14895 "external Flashblock index regressed from {previous_index} to {current_index}"
14896 )));
14897 }
14898 if current_index > previous_index.saturating_add(1) {
14899 return Err(SubscriberError::Provider(format!(
14900 "external Flashblock index skipped from {previous_index} to {current_index}"
14901 )));
14902 }
14903 if current_index == previous_index.saturating_add(1)
14904 && !previous.flashblock.same_base_identity(&snapshot.flashblock)
14905 {
14906 return Err(SubscriberError::Provider(
14907 "external Flashblock base identity changed within one payload generation"
14908 .into(),
14909 ));
14910 }
14911 if current_index == previous_index.saturating_add(1)
14912 && !snapshot
14913 .flashblock
14914 .transaction_hashes
14915 .starts_with(&previous.flashblock.transaction_hashes)
14916 {
14917 return Err(SubscriberError::Provider(
14918 "external Flashblock cumulative transaction membership changed its prior prefix"
14919 .into(),
14920 ));
14921 }
14922 let prior_transaction_count =
14923 u64::try_from(previous.flashblock.transaction_hashes.len()).unwrap_or(u64::MAX);
14924 if current_index == previous_index.saturating_add(1)
14925 && snapshot.logs.iter().any(|log| {
14926 log.transaction_index
14927 .is_some_and(|index| index < prior_transaction_count)
14928 })
14929 {
14930 return Err(SubscriberError::Provider(
14931 "external Flashblock delta log does not belong to a newly appended transaction"
14932 .into(),
14933 ));
14934 }
14935 } else if snapshot.flashblock.index != Some(0) {
14936 return Err(SubscriberError::Provider(
14937 "external Flashblock payload generation must begin at index zero".into(),
14938 ));
14939 }
14940 Ok(false)
14941 }
14942
14943 async fn probe_pending_state(&mut self, filters: &[Filter]) -> Result<(), SubscriberError> {
14944 self.flashblocks_rpc_metrics.pending_block_requests = self
14945 .flashblocks_rpc_metrics
14946 .pending_block_requests
14947 .saturating_add(1);
14948 let pending = self
14949 .fetch_op_pending_block()
14950 .await
14951 .map_err(PendingFlashblockPollError::into_subscriber)?
14952 .ok_or_else(|| {
14953 SubscriberError::Provider(
14954 "provider returned no pending block during Flashblocks preflight".into(),
14955 )
14956 })?;
14957 self.certify_op_pending_parent(&pending)
14958 .await
14959 .map_err(PendingFlashblockPollError::into_subscriber)?;
14960 for filter in filters {
14961 self.flashblocks_rpc_metrics.pending_log_requests = self
14962 .flashblocks_rpc_metrics
14963 .pending_log_requests
14964 .saturating_add(1);
14965 self.flashblocks_state_provider
14966 .as_ref()
14967 .unwrap_or(&self.provider)
14968 .get_logs(
14969 &filter
14970 .clone()
14971 .from_block(BlockNumberOrTag::Latest)
14972 .to_block(BlockNumberOrTag::Pending),
14973 )
14974 .await
14975 .map_err(provider_error)?;
14976 }
14977 self.flashblocks_rpc_metrics.pending_receipt_requests = self
14978 .flashblocks_rpc_metrics
14979 .pending_receipt_requests
14980 .saturating_add(1);
14981 let _: serde_json::Value = self
14982 .flashblocks_state_provider
14983 .as_ref()
14984 .unwrap_or(&self.provider)
14985 .raw_request(Cow::Borrowed("eth_getTransactionReceipt"), (B256::ZERO,))
14986 .await
14987 .map_err(provider_error)?;
14988 Ok(())
14989 }
14990
14991 async fn certify_op_pending_parent(
14992 &mut self,
14993 pending: &N::BlockResponse,
14994 ) -> Result<N::HeaderResponse, PendingFlashblockPollError> {
14995 let pending_header = pending.header();
14996 let pending_number = pending_header.number();
14997 let parent_hash = pending_header.parent_hash();
14998 if pending_number == 0 || parent_hash.is_zero() {
14999 return Err(PendingFlashblockPollError::Integrity(
15000 SubscriberError::Provider(
15001 "OP pending block omitted a certifiable canonical parent".into(),
15002 ),
15003 ));
15004 }
15005 self.flashblocks_rpc_metrics.canonical_head_requests = self
15006 .flashblocks_rpc_metrics
15007 .canonical_head_requests
15008 .saturating_add(1);
15009 let parent = self
15010 .flashblocks_state_provider
15011 .as_ref()
15012 .unwrap_or(&self.provider)
15013 .get_block_by_hash(parent_hash)
15014 .await
15015 .map_err(pending_flashblock_request_error)?
15016 .ok_or_else(|| {
15017 PendingFlashblockPollError::Request(SubscriberError::Provider(
15018 "Flashblocks provider returned no exact OP pending parent block".into(),
15019 ))
15020 })?;
15021 let parent_header = parent.header();
15022 if parent_header.hash() != parent_hash
15023 || parent_header.number().checked_add(1) != Some(pending_number)
15024 {
15025 return Err(PendingFlashblockPollError::Integrity(
15026 SubscriberError::Provider(
15027 "OP pending block does not extend its exact certified parent".into(),
15028 ),
15029 ));
15030 }
15031 Ok(parent_header.clone())
15032 }
15033
15034 async fn fetch_op_pending_block(
15035 &mut self,
15036 ) -> Result<Option<N::BlockResponse>, PendingFlashblockPollError> {
15037 let state_provider = self
15038 .flashblocks_state_provider
15039 .as_ref()
15040 .unwrap_or(&self.provider);
15041 let value: Option<serde_json::Value> = state_provider
15042 .raw_request(
15043 Cow::Borrowed("eth_getBlockByNumber"),
15044 (BlockNumberOrTag::Pending, true),
15045 )
15046 .await
15047 .map_err(pending_flashblock_request_error)?;
15048 value
15049 .map(normalize_op_pending_block::<N>)
15050 .transpose()
15051 .map_err(PendingFlashblockPollError::Integrity)
15052 }
15053
15054 async fn ensure_chain_id(&mut self) -> Result<u64, SubscriberError> {
15058 if let Some(chain_id) = self.chain_id {
15059 return Ok(chain_id);
15060 }
15061 let chain_id = self.provider.get_chain_id().await.map_err(provider_error)?;
15062 self.chain_id = Some(chain_id);
15063 Ok(chain_id)
15064 }
15065
15066 fn validate_flashblocks_setup(&self) -> Result<(), SubscriberError> {
15067 if self.config.preconfirmations == PreconfirmationMode::Disabled {
15068 if self.uses_external_flashblock_updates() {
15069 return Err(SubscriberError::InvalidConfig(
15070 "external Flashblock updates require preconfirmations to be preferred or required",
15071 ));
15072 }
15073 return Ok(());
15074 }
15075 if self.uses_external_flashblock_updates() {
15076 return Ok(());
15077 }
15078 if self.provider_ref.is_none() {
15079 return Err(SubscriberError::InvalidConfig(
15080 "Flashblocks require a stable provider ref from a pinned provider lease",
15081 ));
15082 }
15083 let Some(chain_id) = self.chain_id else {
15084 return Ok(());
15085 };
15086 match flashblocks_adapter(chain_id) {
15087 Some(FlashblocksAdapter::NativeSubscriptions)
15088 if resolve_subscriber_transport(self.mode)? != SubscriberTransport::PubSub
15089 && self.config.preconfirmations == PreconfirmationMode::Required =>
15090 {
15091 return Err(SubscriberError::Unsupported(
15092 "Base Flashblocks require pubsub for newFlashblocks and pendingLogs",
15093 ));
15094 }
15095 Some(FlashblocksAdapter::NativeSubscriptions) => {}
15096 Some(_) => {}
15097 None if self.config.preconfirmations == PreconfirmationMode::Required => {
15098 return Err(SubscriberError::Unsupported(
15099 "Flashblocks are currently implemented for Base and OP chains",
15100 ));
15101 }
15102 None => {}
15103 }
15104 Ok(())
15105 }
15106
15107 pub async fn reconcile_interest_owner(
15121 &mut self,
15122 epoch: &SubscriberOwnerEpoch,
15123 through: BlockRef,
15124 ) -> Result<SubscriberOwnerProgress, SubscriberOwnerError>
15125 where
15126 P: Clone,
15127 {
15128 self.reconcile_interest_owners(std::slice::from_ref(epoch), through)
15129 .await?
15130 .pop()
15131 .ok_or(SubscriberOwnerError::NotStaged)
15132 }
15133
15134 pub async fn reconcile_interest_owners(
15158 &mut self,
15159 epochs: &[SubscriberOwnerEpoch],
15160 through: BlockRef,
15161 ) -> Result<Vec<SubscriberOwnerProgress>, SubscriberOwnerError>
15162 where
15163 P: Clone,
15164 {
15165 if epochs.is_empty() {
15166 return Ok(Vec::new());
15167 }
15168
15169 self.ensure_chain_id().await?;
15170
15171 let mut seen = HashSet::new();
15172 let mut plans = Vec::with_capacity(epochs.len());
15173 for epoch in epochs {
15174 if !seen.insert(epoch.clone()) {
15175 continue;
15176 }
15177 let entry = self
15178 .owned_interests
15179 .iter()
15180 .find(|entry| {
15181 entry.epoch.as_ref() == Some(epoch)
15182 && entry.state == SubscriberOwnerState::Staged
15183 })
15184 .ok_or(SubscriberOwnerError::NotStaged)?;
15185 let position = entry
15186 .progress
15187 .as_ref()
15188 .map(|progress| &progress.through)
15189 .or(entry.baseline.as_ref())
15190 .ok_or(SubscriberOwnerError::MissingBaseline)?;
15191 let baseline = position.number;
15192 if through.number < baseline {
15193 return Err(SubscriberOwnerError::ProgressRegression {
15194 current: baseline,
15195 target: through.number,
15196 });
15197 }
15198 let from_block = baseline
15199 .checked_add(1)
15200 .ok_or(SubscriberOwnerError::PostBlockOverflow(baseline))?;
15201 if through.number == baseline && through.hash != position.hash {
15202 return Err(SubscriberOwnerError::ProgressConflict {
15203 number: baseline,
15204 current_hash: position.hash,
15205 target_hash: through.hash,
15206 });
15207 }
15208 if through.number == from_block
15209 && through
15210 .parent_hash
15211 .is_some_and(|parent| parent != position.hash)
15212 {
15213 return Err(SubscriberOwnerError::ProgressConflict {
15214 number: baseline,
15215 current_hash: position.hash,
15216 target_hash: through.parent_hash.expect("checked as present above"),
15217 });
15218 }
15219 if entry
15220 .interests
15221 .iter()
15222 .any(|interest| !matches!(interest, ReactiveInterest::Logs(_)))
15223 {
15224 return Err(SubscriberOwnerError::UnsupportedPostBlockInterest);
15225 }
15226 plans.push(SubscriberOwnerReconcilePlan {
15227 epoch: epoch.clone(),
15228 interests: entry.interests.clone(),
15229 retained: *position,
15230 from_block,
15231 });
15232 }
15233
15234 self.ensure_streams().await?;
15237 let provider = self.provider.clone();
15238 let filters = merged_owner_reconcile_filters(&plans, through.number);
15239 let retained = plans.iter().map(|plan| plan.retained).collect();
15240 let target_epochs: HashSet<_> = plans.iter().map(|plan| plan.epoch.clone()).collect();
15241 let fetch = fetch_owner_catchup::<P, N>(
15242 provider,
15243 filters,
15244 retained,
15245 through,
15246 SubscriberOwnerCatchupOptions {
15247 target_preverified: false,
15248 max_logs: self.config.max_pending_records,
15249 max_log_bytes: self.config.max_backfill_log_bytes,
15250 max_requests_in_flight: self.config.max_reconcile_requests_in_flight,
15251 },
15252 );
15253 let SubscriberOwnerCatchup { logs, certified } =
15254 self.drive_reconcile_fetch(fetch, &target_epochs).await?;
15255
15256 let records = logs
15257 .into_iter()
15258 .map(|log| log_input_record(log, InputSource::Backfill))
15259 .collect();
15260 let mut routed_records = Vec::new();
15261 for record in dedupe_records(sort_records(records)).map_err(|error| {
15262 SubscriberError::InvalidBackfill(format!(
15263 "conflicting duplicate owner catch-up record: {error}"
15264 ))
15265 })? {
15266 let block_number = match &record.input {
15267 ReactiveInput::Log(log) => log
15268 .block_number
15269 .expect("bulk catch-up logs were validated before commit"),
15270 _ => unreachable!("bulk owner catch-up contains log records only"),
15271 };
15272 let owners: Vec<SubscriberOwnerEpoch> = plans
15273 .iter()
15274 .filter(|plan| block_number >= plan.from_block)
15275 .filter(|plan| {
15276 plan.interests
15277 .iter()
15278 .any(|interest| interest_matches(interest, &record.input))
15279 })
15280 .map(|plan| plan.epoch.clone())
15281 .collect();
15282 if !owners.is_empty() {
15283 routed_records.push((record, owners));
15284 }
15285 }
15286 self.ensure_pending_record_capacity(
15287 routed_records.len(),
15288 "owner reconciliation historical records",
15289 )?;
15290
15291 self.pending_backfills.retain(|queued| {
15295 queued
15296 .epoch
15297 .as_ref()
15298 .is_none_or(|epoch| !target_epochs.contains(epoch))
15299 });
15300 for (record, owners) in routed_records {
15301 self.enqueue_owner_record_for_owners_unmerged(record, owners);
15302 }
15303 self.promote_reconcile_owner_records(&target_epochs);
15304 self.seed_reconciled_filter_anchors(&plans, certified.number);
15305
15306 let stream_revision = self.stream_revision;
15307 let mut progress = Vec::with_capacity(plans.len());
15308 for plan in plans {
15309 let item = SubscriberOwnerProgress {
15310 owner: plan.epoch.clone(),
15311 through: certified,
15312 };
15313 let entry = self
15314 .owned_interests
15315 .iter_mut()
15316 .find(|entry| entry.epoch.as_ref() == Some(&plan.epoch))
15317 .expect("bulk reconcile holds exclusive access after epoch preflight");
15318 entry.progress = Some(item.clone());
15319 entry.progress_stream_revision = Some(stream_revision);
15320 progress.push(item);
15321 }
15322 Ok(progress)
15323 }
15324
15325 async fn drive_reconcile_fetch<T, F>(
15326 &mut self,
15327 fetch: F,
15328 target_epochs: &HashSet<SubscriberOwnerEpoch>,
15329 ) -> Result<T, SubscriberOwnerError>
15330 where
15331 F: Future<Output = Result<T, SubscriberOwnerError>>,
15332 {
15333 if !matches!(&self.state, AlloySubscriberState::Active(_)) {
15334 return fetch.await;
15335 }
15336 let mut fetch = Box::pin(fetch);
15337 loop {
15338 let event = {
15339 let live = Box::pin(self.next_event());
15340 match select(fetch, live).await {
15341 Either::Left((result, pending_live)) => {
15342 drop(pending_live);
15343 return result;
15344 }
15345 Either::Right((event, pending_fetch)) => {
15346 fetch = pending_fetch;
15347 event
15348 }
15349 }
15350 };
15351 let event = event?.ok_or_else(|| {
15352 SubscriberError::Provider(
15353 "Alloy subscriber streams ended during owner reconcile".to_owned(),
15354 )
15355 })?;
15356 self.buffer_reconcile_event_for_owners(&event, target_epochs);
15357 self.enqueue_event_excluding_owners(event, target_epochs);
15358 self.check_resource_error()?;
15359 }
15360 }
15361
15362 pub async fn next_scoped_batch_or<C, F>(
15381 &mut self,
15382 control: Pin<&mut F>,
15383 ) -> Result<SubscriberDriverPoll<C, N>, SubscriberError>
15384 where
15385 C: Send,
15386 F: Future<Output = C> + Send,
15387 {
15388 let batch = self.next_scoped_batch();
15389 match select(control, batch).await {
15390 Either::Left((control, pending_batch)) => {
15391 drop(pending_batch);
15392 Ok(SubscriberDriverPoll::Control(control))
15393 }
15394 Either::Right((batch, _pending_control)) => batch.map(SubscriberDriverPoll::Batch),
15395 }
15396 }
15397
15398 pub fn next_scoped_batch(&mut self) -> SubscriberNextScopedBatch<'_, N> {
15409 Box::pin(async {
15410 self.check_resource_error()?;
15411 if self.chain_id.is_none()
15412 && (!self.pending_records.is_empty()
15413 || !self.pending_chain_controls.is_empty()
15414 || !self.pending_backfills.is_empty()
15415 || !self.interests.is_empty())
15416 {
15417 self.ensure_chain_id().await?;
15418 }
15419 if let Some(batch) = self.drain_next_scoped_batch() {
15420 return Ok(Some(batch));
15421 }
15422
15423 self.ensure_streams().await?;
15428 self.check_resource_error()?;
15429 if let Some(batch) = self.drain_next_scoped_batch() {
15430 return Ok(Some(batch));
15431 }
15432
15433 self.drain_pending_backfills().await?;
15434 self.check_resource_error()?;
15435 if let Some(batch) = self.drain_next_scoped_batch() {
15436 return Ok(Some(batch));
15437 }
15438
15439 if self.interests.is_empty() {
15440 return Ok(None);
15441 }
15442
15443 loop {
15444 let Some(event) = self.next_event().await? else {
15445 return Ok(None);
15446 };
15447
15448 self.enqueue_event(event);
15449 self.check_resource_error()?;
15450 if let Some(batch) = self.drain_next_scoped_batch() {
15451 return Ok(Some(batch));
15452 }
15453 }
15454 })
15455 }
15456
15457 async fn ensure_streams(&mut self) -> Result<(), SubscriberError> {
15472 if !self.sources_dirty {
15473 return Ok(());
15474 }
15475 if matches!(self.state, AlloySubscriberState::Uninitialized) && self.interests.is_empty() {
15480 self.bump_stream_revision();
15481 self.sources_dirty = false;
15482 return Ok(());
15483 }
15484
15485 let desired = self.stream_sources()?;
15486 let missing: Vec<SubscriberStreamSource> = match &self.state {
15487 AlloySubscriberState::Active(streams) => desired
15488 .iter()
15489 .filter(|source| !streams.contains_source(source))
15490 .cloned()
15491 .collect(),
15492 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => desired.clone(),
15493 };
15494
15495 for source in missing {
15496 let stream = match self.connect_source_stream(source.clone()).await {
15497 Ok(stream) => stream,
15498 Err(error)
15499 if source.is_flashblocks()
15500 && self.config.preconfirmations == PreconfirmationMode::Preferred =>
15501 {
15502 tracing::warn!(
15503 stream = source.label(),
15504 error = %error,
15505 "Flashblocks source unavailable; canonical delivery remains active"
15506 );
15507 if self.config.reconnect.enabled {
15508 self.schedule_flashblock_reconnect(
15509 source,
15510 self.config.reconnect.retry_delay,
15511 );
15512 }
15513 continue;
15514 }
15515 Err(error) => return Err(error),
15516 };
15517 self.install_source_stream(source.clone(), stream);
15521 if self.source_requires_backfill(&source) {
15522 self.queue_source_backfill(source);
15523 }
15524 }
15525
15526 while let Some(source) = self.pending_source_backfills.front().cloned() {
15527 let desired_and_live = desired.iter().any(|item| item.same_key(&source))
15528 && matches!(
15529 &self.state,
15530 AlloySubscriberState::Active(streams) if streams.contains_source(&source)
15531 );
15532 if !desired_and_live {
15533 self.pending_source_backfills.pop_front();
15534 continue;
15535 }
15536
15537 let event = self.backfill_reconnected_source(&source).await?;
15543 self.pending_source_backfills.pop_front();
15544 if let Some(event) = event {
15545 self.enqueue_event(event);
15546 }
15547 }
15548
15549 if let AlloySubscriberState::Active(streams) = &mut self.state {
15550 streams.retain_sources(&desired);
15551 if streams.is_empty() {
15552 self.state = AlloySubscriberState::Empty;
15553 }
15554 }
15555
15556 self.bump_stream_revision();
15557 self.sources_dirty = false;
15558 self.retire_unreferenced_filters();
15559 Ok(())
15560 }
15561
15562 fn install_source_stream(
15563 &mut self,
15564 source: SubscriberStreamSource,
15565 stream: BoxStream<'static, SubscriberEvent<N>>,
15566 ) {
15567 match &mut self.state {
15568 AlloySubscriberState::Active(streams) => {
15569 if streams.contains_source(&source) {
15570 return;
15571 }
15572 streams.push(source, stream);
15573 }
15574 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
15575 let mut streams = SubscriberStreams::new();
15576 streams.push(source, stream);
15577 self.state = AlloySubscriberState::Active(streams);
15578 }
15579 }
15580 self.bump_stream_revision();
15583 }
15584
15585 fn schedule_flashblock_reconnect(
15586 &mut self,
15587 source: SubscriberStreamSource,
15588 first_delay: Duration,
15589 ) {
15590 if self
15591 .pending_flashblock_reconnect_sources
15592 .iter()
15593 .any(|pending| pending.same_key(&source))
15594 {
15595 return;
15596 }
15597 self.pending_flashblock_reconnect_sources
15598 .push(source.clone());
15599 self.pending_flashblock_reconnects
15600 .push(flashblock_reconnect_future(
15601 self.provider.root().clone(),
15602 source,
15603 self.config.max_batch_size,
15604 self.config.reconnect.clone(),
15605 first_delay,
15606 self.config.flashblock_poll_interval,
15607 ));
15608 }
15609
15610 fn reschedule_preferred_flashblock(&mut self, source: SubscriberStreamSource) {
15611 if !self.config.reconnect.enabled {
15612 return;
15613 }
15614 self.schedule_flashblock_reconnect(source, self.config.reconnect.max_delay);
15615 }
15616
15617 fn source_requires_backfill(&self, source: &SubscriberStreamSource) -> bool {
15618 matches!(source, SubscriberStreamSource::PubSubLog { id, .. }
15619 if self.last_seen_log_blocks.contains_key(id))
15620 }
15621
15622 fn queue_source_backfill(&mut self, source: SubscriberStreamSource) {
15623 if !self
15624 .pending_source_backfills
15625 .iter()
15626 .any(|pending| pending.same_key(&source))
15627 {
15628 self.pending_source_backfills.push_back(source);
15629 }
15630 }
15631
15632 async fn drain_pending_backfills(&mut self) -> Result<(), SubscriberError> {
15644 while let Some(queued) = self.pending_backfills.front() {
15645 let epoch = queued.epoch.clone();
15647 let owner = queued.owner.clone();
15648 let owner_exists = match (&epoch, &owner) {
15649 (Some(epoch), _) => self.interest_owner_state(epoch).is_some(),
15650 (None, Some(owner)) => self.owner_interests(owner).is_some(),
15651 (None, None) => true,
15652 };
15653 if !owner_exists {
15654 self.pending_backfills.pop_front();
15655 continue;
15656 }
15657 let filters = queued.filters.clone();
15658 let backfill = queued.backfill;
15659
15660 let to_block = match backfill.end_block() {
15661 Some(to_block) => to_block,
15662 None => self
15663 .provider
15664 .get_block_number()
15665 .await
15666 .map_err(provider_error)?,
15667 };
15668 if to_block < backfill.start_block() {
15669 let certified = if let Some(retained) = backfill.retained_anchor() {
15674 let actual =
15675 fetch_provider_block_ref::<P, N>(&self.provider, retained.number).await?;
15676 if !block_ref_satisfies_expected(&actual, retained) {
15677 return Err(SubscriberError::InvalidBackfill(format!(
15678 "retained anchor {}:{:?} conflicts with provider block {}:{:?}",
15679 retained.number, retained.hash, actual.number, actual.hash
15680 )));
15681 }
15682 if to_block < retained.number {
15683 return Err(SubscriberError::InvalidBackfill(format!(
15684 "backfill upper bound {to_block} precedes retained anchor {}",
15685 retained.number
15686 )));
15687 }
15688 Some(actual)
15689 } else {
15690 None
15691 };
15692 self.pending_backfills.pop_front();
15693 for filter in &filters {
15694 let source_id = self.log_source_id(filter);
15695 if let Some(certified) = certified {
15696 self.last_seen_log_blocks
15697 .entry(source_id)
15698 .and_modify(|anchor| *anchor = (*anchor).max(certified.number))
15699 .or_insert(certified.number);
15700 }
15701 }
15702 if owner.is_none()
15703 && let Some(certified) = certified
15704 {
15705 self.pending_chain_controls
15706 .push_back(global_backfill_barrier(backfill, certified));
15707 }
15708 if !self.pending_chain_controls.is_empty() {
15709 break;
15710 }
15711 continue;
15712 }
15713
15714 let through = fetch_provider_block_ref::<P, N>(&self.provider, to_block).await?;
15715 let request_filters =
15716 merged_lazy_backfill_filters(&filters, backfill.start_block(), through.number);
15717 let retained = backfill.retained_anchor().copied().into_iter().collect();
15718 let SubscriberOwnerCatchup {
15719 mut logs,
15720 certified,
15721 } = fetch_owner_catchup::<&P, N>(
15722 &self.provider,
15723 request_filters,
15724 retained,
15725 through,
15726 SubscriberOwnerCatchupOptions {
15727 target_preverified: true,
15728 max_logs: self.config.max_pending_records,
15729 max_log_bytes: self.config.max_backfill_log_bytes,
15730 max_requests_in_flight: self.config.max_reconcile_requests_in_flight,
15731 },
15732 )
15733 .await
15734 .map_err(lazy_backfill_error)?;
15735 logs.sort_by_key(|log| {
15736 (
15737 log.block_number.unwrap_or_default(),
15738 log.transaction_index.unwrap_or_default(),
15739 log.log_index.unwrap_or_default(),
15740 )
15741 });
15742 logs.dedup();
15743 self.ensure_pending_record_capacity(logs.len(), "lazy subscriber backfill records")?;
15744
15745 self.pending_backfills.pop_front();
15748 if let Some(epoch) = epoch.as_ref() {
15749 self.enqueue_backfilled_logs(logs, None, Some(epoch), Some(backfill));
15750 } else if let Some(owner) = owner.as_ref() {
15751 self.enqueue_compat_owner_backfilled_logs(logs, owner, backfill);
15752 } else {
15753 self.enqueue_backfilled_logs(logs, None, None, Some(backfill));
15754 self.pending_chain_controls
15755 .push_back(global_backfill_barrier(backfill, certified));
15756 }
15757 for filter in &filters {
15758 let source_id = self.log_source_id(filter);
15759 let anchor = self
15760 .last_seen_log_blocks
15761 .entry(source_id)
15762 .or_insert(certified.number);
15763 *anchor = (*anchor).max(certified.number);
15764 }
15765
15766 if !self.pending_records.is_empty() || !self.pending_chain_controls.is_empty() {
15767 break;
15768 }
15769 }
15770 Ok(())
15771 }
15772
15773 fn stream_sources(&mut self) -> Result<Vec<SubscriberStreamSource>, SubscriberError> {
15774 let sources = match resolve_subscriber_transport(self.mode)? {
15775 SubscriberTransport::PubSub => self.pubsub_stream_sources(),
15776 SubscriberTransport::Polling => self.polling_stream_sources(),
15777 };
15778 #[cfg(feature = "raw-flashblocks-json")]
15779 let sources = {
15780 let mut sources = sources;
15781 if self.external_flashblock_update_channel_opened {
15782 sources.push(SubscriberStreamSource::ExternalFlashblockUpdates);
15783 }
15784 sources
15785 };
15786 Ok(sources)
15787 }
15788
15789 fn pubsub_stream_sources(&mut self) -> Vec<SubscriberStreamSource> {
15790 let mut sources = Vec::new();
15791 let inherited_anchor = self.last_seen_log_blocks.values().copied().min();
15792
15793 for filter in self.log_stream_filters() {
15794 let id = self.log_source_id(&filter);
15795 if let Some(anchor) = inherited_anchor {
15796 self.last_seen_log_blocks.entry(id).or_insert(anchor);
15797 }
15798 sources.push(SubscriberStreamSource::PubSubLog { id, filter });
15799 }
15800
15801 if needs_pending_hash_stream(&self.interests) {
15802 sources.push(SubscriberStreamSource::PubSubPendingHashes);
15803 }
15804
15805 if needs_header_block_stream(&self.interests) {
15806 if !self.uses_external_flashblock_updates()
15807 && self.config.preconfirmations != PreconfirmationMode::Disabled
15808 && self.chain_id.and_then(flashblocks_adapter).is_some()
15809 {
15810 sources.push(SubscriberStreamSource::CanonicalHeadPolling);
15811 } else {
15812 sources.push(SubscriberStreamSource::PubSubBlockHeaders);
15813 }
15814 }
15815
15816 if self.config.preconfirmations != PreconfirmationMode::Disabled
15817 && !self.uses_external_flashblock_updates()
15818 {
15819 match self.chain_id.and_then(flashblocks_adapter) {
15820 Some(FlashblocksAdapter::NativeSubscriptions) => {
15821 sources.push(SubscriberStreamSource::BaseFlashblocks);
15822 for filter in self.log_stream_filters() {
15823 let id = self.log_source_id(&filter);
15824 sources.push(SubscriberStreamSource::BasePendingLog { id, filter });
15825 }
15826 }
15827 Some(FlashblocksAdapter::PendingStatePolling) => {
15828 sources.push(SubscriberStreamSource::OpPendingFlashblocks);
15829 }
15830 None => {}
15831 }
15832 }
15833
15834 sources
15835 }
15836
15837 fn polling_stream_sources(&self) -> Vec<SubscriberStreamSource> {
15838 let mut sources = Vec::new();
15839
15840 for filter in self.log_stream_filters() {
15841 sources.push(SubscriberStreamSource::PollingLog { filter });
15842 }
15843
15844 if needs_pending_hash_stream(&self.interests) {
15845 sources.push(SubscriberStreamSource::PollingPendingHashes);
15846 }
15847
15848 if self.config.preconfirmations != PreconfirmationMode::Disabled
15849 && !self.uses_external_flashblock_updates()
15850 && self.chain_id.and_then(flashblocks_adapter)
15851 == Some(FlashblocksAdapter::PendingStatePolling)
15852 {
15853 sources.push(SubscriberStreamSource::OpPendingFlashblocks);
15854 }
15855
15856 sources
15857 }
15858
15859 fn log_source_id(&mut self, filter: &Filter) -> usize {
15860 if let Some(id) = self.log_source_ids.get(filter) {
15861 return *id;
15862 }
15863
15864 let id = self.next_log_source_id;
15865 self.next_log_source_id = self.next_log_source_id.saturating_add(1);
15866 self.log_source_ids.insert(filter.clone(), id);
15867 id
15868 }
15869
15870 async fn connect_source_stream(
15871 &mut self,
15872 source: SubscriberStreamSource,
15873 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15874 match source {
15875 SubscriberStreamSource::PubSubLog { id, filter } => {
15876 self.connect_pubsub_log_stream(id, filter).await
15877 }
15878 SubscriberStreamSource::BasePendingLog { id, filter } => {
15879 self.connect_base_pending_log_stream(id, filter).await
15880 }
15881 SubscriberStreamSource::BaseFlashblocks => self.connect_base_flashblock_stream().await,
15882 SubscriberStreamSource::OpPendingFlashblocks => {
15883 self.connect_op_flashblock_tick_stream()
15884 }
15885 SubscriberStreamSource::CanonicalHeadPolling => {
15886 self.connect_canonical_head_tick_stream()
15887 }
15888 SubscriberStreamSource::PubSubPendingHashes => {
15889 self.connect_pubsub_pending_hash_stream().await
15890 }
15891 SubscriberStreamSource::PubSubBlockHeaders => {
15892 self.connect_pubsub_block_header_stream().await
15893 }
15894 SubscriberStreamSource::PollingLog { filter } => {
15895 self.connect_polling_log_stream(filter).await
15896 }
15897 SubscriberStreamSource::PollingPendingHashes => {
15898 self.connect_polling_pending_hash_stream().await
15899 }
15900 #[cfg(feature = "raw-flashblocks-json")]
15901 SubscriberStreamSource::ExternalFlashblockUpdates => {
15902 let receiver = self.external_flashblock_updates.take().ok_or_else(|| {
15903 SubscriberError::Provider(
15904 "external Flashblock update channel receiver is unavailable".into(),
15905 )
15906 })?;
15907 let updates = stream::unfold(receiver, |mut receiver| async move {
15908 receiver
15909 .recv()
15910 .await
15911 .map(|update| (SubscriberEvent::ExternalFlashblockUpdate(update), receiver))
15912 });
15913 Ok(stream_with_termination(
15914 updates,
15915 SubscriberStreamSource::ExternalFlashblockUpdates,
15916 ))
15917 }
15918 }
15919 }
15920
15921 async fn connect_pubsub_log_stream(
15922 &mut self,
15923 id: usize,
15924 filter: Filter,
15925 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15926 #[cfg(feature = "reactive-ws")]
15927 {
15928 let source = SubscriberStreamSource::PubSubLog {
15929 id,
15930 filter: filter.clone(),
15931 };
15932 let stream = self
15933 .provider
15934 .subscribe_logs(&filter)
15935 .channel_size(self.config.max_batch_size.max(1))
15936 .await
15937 .map_err(provider_error)?
15938 .into_stream()
15939 .map(move |log| SubscriberEvent::Log { source_id: id, log });
15940 Ok(stream_with_termination(stream, source))
15941 }
15942
15943 #[cfg(not(feature = "reactive-ws"))]
15944 {
15945 let _ = (id, filter);
15946 Err(SubscriberError::Unsupported(
15947 "AlloySubscriber pubsub mode requires the reactive-ws feature",
15948 ))
15949 }
15950 }
15951
15952 async fn connect_base_pending_log_stream(
15953 &mut self,
15954 id: usize,
15955 filter: Filter,
15956 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15957 #[cfg(feature = "reactive-ws")]
15958 {
15959 let source = SubscriberStreamSource::BasePendingLog {
15960 id,
15961 filter: filter.clone(),
15962 };
15963 let params = base_pending_log_filter(&filter)?;
15964 let stream = self
15965 .provider
15966 .subscribe::<_, Log>(("pendingLogs", params))
15967 .channel_size(self.config.max_batch_size.max(1))
15968 .await
15969 .map_err(provider_error)?
15970 .into_stream()
15971 .map(move |log| SubscriberEvent::BasePendingLog { source_id: id, log });
15972 Ok(stream_with_termination(stream, source))
15973 }
15974
15975 #[cfg(not(feature = "reactive-ws"))]
15976 {
15977 let _ = (id, filter);
15978 Err(SubscriberError::Unsupported(
15979 "Base Flashblocks require the reactive-ws feature",
15980 ))
15981 }
15982 }
15983
15984 async fn connect_base_flashblock_stream(
15985 &mut self,
15986 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
15987 #[cfg(feature = "reactive-ws")]
15988 {
15989 let stream = self
15990 .provider
15991 .subscribe::<_, BaseFlashblockWirePayload>(("newFlashblocks",))
15992 .channel_size(self.config.max_batch_size.max(1))
15993 .await
15994 .map_err(provider_error)?
15995 .into_stream()
15996 .map(SubscriberEvent::BaseFlashblock);
15997 Ok(stream_with_termination(
15998 stream,
15999 SubscriberStreamSource::BaseFlashblocks,
16000 ))
16001 }
16002
16003 #[cfg(not(feature = "reactive-ws"))]
16004 {
16005 Err(SubscriberError::Unsupported(
16006 "Base Flashblocks require the reactive-ws feature",
16007 ))
16008 }
16009 }
16010
16011 fn connect_canonical_head_tick_stream(
16012 &self,
16013 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16014 let mut interval = tokio::time::interval(self.config.canonical_head_poll_interval);
16015 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
16016 let stream = stream::unfold(interval, |mut interval| async move {
16017 interval.tick().await;
16018 Some((SubscriberEvent::CanonicalHeadTick, interval))
16019 });
16020 Ok(stream_with_termination(
16021 stream,
16022 SubscriberStreamSource::CanonicalHeadPolling,
16023 ))
16024 }
16025
16026 fn connect_op_flashblock_tick_stream(
16027 &self,
16028 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16029 let first_tick = tokio::time::Instant::now() + self.config.flashblock_poll_interval;
16030 let mut interval =
16031 tokio::time::interval_at(first_tick, self.config.flashblock_poll_interval);
16032 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
16033 let stream = stream::unfold(interval, |mut interval| async move {
16034 interval.tick().await;
16035 Some((SubscriberEvent::OpFlashblockTick, interval))
16036 });
16037 Ok(stream_with_termination(
16038 stream,
16039 SubscriberStreamSource::OpPendingFlashblocks,
16040 ))
16041 }
16042
16043 async fn connect_pubsub_pending_hash_stream(
16044 &mut self,
16045 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16046 #[cfg(feature = "reactive-ws")]
16047 {
16048 let stream = self
16049 .provider
16050 .subscribe_pending_transactions()
16051 .channel_size(self.config.max_batch_size.max(1))
16052 .await
16053 .map_err(provider_error)?
16054 .into_stream()
16055 .map(SubscriberEvent::PendingHash);
16056 Ok(stream_with_termination(
16057 stream,
16058 SubscriberStreamSource::PubSubPendingHashes,
16059 ))
16060 }
16061
16062 #[cfg(not(feature = "reactive-ws"))]
16063 {
16064 Err(SubscriberError::Unsupported(
16065 "AlloySubscriber pubsub mode requires the reactive-ws feature",
16066 ))
16067 }
16068 }
16069
16070 async fn connect_pubsub_block_header_stream(
16071 &mut self,
16072 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16073 #[cfg(feature = "reactive-ws")]
16074 {
16075 let stream = self
16076 .provider
16077 .subscribe_blocks()
16078 .channel_size(self.config.max_batch_size.max(1))
16079 .await
16080 .map_err(provider_error)?
16081 .into_stream()
16082 .map(SubscriberEvent::BlockHeader);
16083 Ok(stream_with_termination(
16084 stream,
16085 SubscriberStreamSource::PubSubBlockHeaders,
16086 ))
16087 }
16088
16089 #[cfg(not(feature = "reactive-ws"))]
16090 {
16091 Err(SubscriberError::Unsupported(
16092 "AlloySubscriber pubsub mode requires the reactive-ws feature",
16093 ))
16094 }
16095 }
16096
16097 async fn connect_polling_log_stream(
16098 &mut self,
16099 filter: Filter,
16100 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16101 #[cfg(feature = "reactive-polling")]
16102 {
16103 let source = SubscriberStreamSource::PollingLog {
16104 filter: filter.clone(),
16105 };
16106 let stream = self
16107 .provider
16108 .watch_logs(&filter)
16109 .await
16110 .map_err(provider_error)?
16111 .with_channel_size(self.config.max_batch_size.max(1))
16112 .into_stream()
16113 .map(SubscriberEvent::Logs);
16114 Ok(stream_with_termination(stream, source))
16115 }
16116
16117 #[cfg(not(feature = "reactive-polling"))]
16118 {
16119 let _ = filter;
16120 Err(SubscriberError::Unsupported(
16121 "AlloySubscriber polling mode requires the reactive-polling feature",
16122 ))
16123 }
16124 }
16125
16126 async fn connect_polling_pending_hash_stream(
16127 &mut self,
16128 ) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError> {
16129 #[cfg(feature = "reactive-polling")]
16130 {
16131 let stream = self
16132 .provider
16133 .watch_pending_transactions()
16134 .await
16135 .map_err(provider_error)?
16136 .with_channel_size(self.config.max_batch_size.max(1))
16137 .into_stream()
16138 .map(SubscriberEvent::PendingHashes);
16139 Ok(stream_with_termination(
16140 stream,
16141 SubscriberStreamSource::PollingPendingHashes,
16142 ))
16143 }
16144
16145 #[cfg(not(feature = "reactive-polling"))]
16146 {
16147 Err(SubscriberError::Unsupported(
16148 "AlloySubscriber polling mode requires the reactive-polling feature",
16149 ))
16150 }
16151 }
16152
16153 async fn next_event(&mut self) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
16154 loop {
16155 let ready = match &mut self.state {
16156 AlloySubscriberState::Active(streams)
16157 if !self.pending_flashblock_reconnects.is_empty() =>
16158 {
16159 let stream_event = Box::pin(streams.next());
16160 let reconnect = Box::pin(self.pending_flashblock_reconnects.next());
16161 match select(reconnect, stream_event).await {
16162 Either::Left((reconnect, pending_event)) => {
16163 drop(pending_event);
16164 let Some((source, result)) = reconnect else {
16165 continue;
16166 };
16167 SubscriberReady::FlashblockReconnect(source, result)
16168 }
16169 Either::Right((event, pending_reconnect)) => {
16170 drop(pending_reconnect);
16171 SubscriberReady::Event(event)
16172 }
16173 }
16174 }
16175 AlloySubscriberState::Active(streams) => {
16176 SubscriberReady::Event(streams.next().await)
16177 }
16178 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty
16179 if !self.pending_flashblock_reconnects.is_empty() =>
16180 {
16181 let Some((source, result)) = self.pending_flashblock_reconnects.next().await
16182 else {
16183 continue;
16184 };
16185 SubscriberReady::FlashblockReconnect(source, result)
16186 }
16187 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => {
16188 return Ok(None);
16189 }
16190 };
16191
16192 let event = match ready {
16193 SubscriberReady::Event(event) => event,
16194 SubscriberReady::FlashblockReconnect(source, result) => {
16195 self.pending_flashblock_reconnect_sources
16196 .retain(|pending| !pending.same_key(&source));
16197 match result {
16198 Ok(stream) => {
16199 self.install_source_stream(source, stream);
16200 }
16201 Err(error)
16202 if self.config.preconfirmations == PreconfirmationMode::Preferred =>
16203 {
16204 tracing::warn!(
16205 stream = source.label(),
16206 error = %error,
16207 "Flashblocks reconnect window exhausted; canonical delivery remains active"
16208 );
16209 self.reschedule_preferred_flashblock(source);
16210 }
16211 Err(error) => return Err(error),
16212 }
16213 continue;
16214 }
16215 };
16216
16217 let Some(event) = event else {
16218 return Err(SubscriberError::Provider(
16219 "Alloy subscriber streams terminated before the subscriber was stopped"
16220 .to_owned(),
16221 ));
16222 };
16223
16224 match event {
16225 SubscriberEvent::StreamTerminated(source) => {
16226 if source.is_external_flashblocks() {
16227 #[cfg(feature = "raw-flashblocks-json")]
16228 {
16229 self.external_flashblock_update_channel_opened = false;
16230 }
16231 if let AlloySubscriberState::Active(streams) = &mut self.state {
16232 streams
16233 .entries
16234 .retain(|entry| !entry.source.is_external_flashblocks());
16235 streams.normalize_next_index();
16236 }
16237 self.invalidate_preconfirmation_snapshot();
16238 if self.config.preconfirmations == PreconfirmationMode::Required {
16239 return Err(SubscriberError::Provider(
16240 "required external Flashblock update channel closed".into(),
16241 ));
16242 }
16243 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
16244 }
16245 if source.is_flashblocks() {
16249 self.invalidate_flashblock_generation();
16250 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
16251 }
16252 self.sources_dirty = true;
16253 self.bump_stream_revision();
16254 if let Some(backfill_event) = self.reconnect_source_stream(source).await? {
16255 self.sources_dirty = false;
16256 if let Some(backfill_event) =
16257 self.normalize_flashblock_event(backfill_event).await?
16258 {
16259 self.verify_event_log_blocks(&backfill_event).await?;
16260 return Ok(Some(backfill_event));
16261 }
16262 }
16263 self.sources_dirty = false;
16264 }
16265 event => {
16266 let Some(event) = self.normalize_flashblock_event(event).await? else {
16267 continue;
16268 };
16269 self.verify_event_log_blocks(&event).await?;
16270 return Ok(Some(event));
16271 }
16272 }
16273 }
16274 }
16275
16276 fn invalidate_flashblock_generation(&mut self) {
16277 self.pending_records
16278 .retain(|record| record.scope != SubscriberInputScope::Preconfirmed);
16279 self.pending_preconfirmation_invalidation = true;
16280 self.reset_flashblock_tracking();
16281 if let Some(provider) = self.provider_ref.as_mut() {
16282 provider.generation = provider.generation.saturating_add(1);
16283 }
16284 if let AlloySubscriberState::Active(streams) = &mut self.state {
16285 streams
16286 .entries
16287 .retain(|entry| !entry.source.is_flashblocks());
16288 streams.normalize_next_index();
16289 }
16290 let reconnect_sources = self
16291 .stream_sources()
16292 .unwrap_or_default()
16293 .into_iter()
16294 .filter(SubscriberStreamSource::is_flashblocks)
16295 .collect::<Vec<_>>();
16296 self.pending_flashblock_reconnects.clear();
16297 self.pending_flashblock_reconnect_sources.clear();
16298 if self.config.preconfirmations == PreconfirmationMode::Required
16299 || self.config.reconnect.enabled
16300 {
16301 for source in reconnect_sources {
16302 self.schedule_flashblock_reconnect(source, self.config.reconnect.initial_delay);
16303 }
16304 }
16305 self.sources_dirty = false;
16306 self.bump_stream_revision();
16307 }
16308
16309 async fn normalize_flashblock_event(
16310 &mut self,
16311 event: SubscriberEvent<N>,
16312 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
16313 match event {
16314 #[cfg(feature = "raw-flashblocks-json")]
16315 SubscriberEvent::ExternalFlashblockUpdate(queued) => {
16316 let provider = queued.update.provider().clone();
16317 match self.ingest_flashblock_update(queued.update) {
16318 Ok(()) => {
16319 let _ = queued.acknowledgement.send(Ok(()));
16320 Ok(Some(SubscriberEvent::FlashblockObserved))
16321 }
16322 Err(error)
16323 if self.config.preconfirmations == PreconfirmationMode::Preferred =>
16324 {
16325 let recoverable_capacity =
16326 matches!(error, SubscriberError::ResourceExhausted(_));
16327 if !recoverable_capacity
16328 && let Some(configured) = self.external_flashblocks_provider.as_mut()
16329 && configured.endpoint == provider.endpoint
16330 {
16331 self.rejected_external_flashblock_generation = Some(
16332 self.rejected_external_flashblock_generation
16333 .map_or(provider.generation, |rejected| {
16334 rejected.max(provider.generation)
16335 }),
16336 );
16337 configured.generation = configured
16338 .generation
16339 .max(provider.generation.saturating_add(1));
16340 }
16341 self.invalidate_preconfirmation_snapshot();
16342 self.last_external_flashblock_snapshot = None;
16343 let _ = queued
16344 .acknowledgement
16345 .send(Err(FlashblockUpdateChannelError::Rejected));
16346 tracing::warn!(
16347 provider = %provider.endpoint,
16348 generation = provider.generation,
16349 error = %error,
16350 "external Flashblock update rejected; canonical delivery remains active"
16351 );
16352 Ok(Some(SubscriberEvent::FlashblockInvalidated))
16353 }
16354 Err(error) => {
16355 let _ = queued
16356 .acknowledgement
16357 .send(Err(FlashblockUpdateChannelError::Rejected));
16358 Err(error)
16359 }
16360 }
16361 }
16362 SubscriberEvent::BasePendingLog { source_id, log } => {
16363 let block_number = log.block_number.ok_or_else(|| {
16364 SubscriberError::Provider(
16365 "pendingLogs item is missing its pending block number".into(),
16366 )
16367 })?;
16368 let transaction_hash = log.transaction_hash.ok_or_else(|| {
16369 SubscriberError::Provider(
16370 "pendingLogs item is missing its transaction hash".into(),
16371 )
16372 })?;
16373 let matching = self.latest_preconfirmation.as_ref().filter(|flashblock| {
16374 flashblock.block_number == block_number
16375 && flashblock.contains_transaction(&transaction_hash)
16376 });
16377 let Some(flashblock) = matching.cloned() else {
16378 if self
16379 .latest_preconfirmation
16380 .as_ref()
16381 .is_some_and(|latest| block_number < latest.block_number)
16382 {
16383 return Ok(None);
16384 }
16385 if self.unmatched_pending_logs.len() >= self.config.max_pending_records {
16386 return Err(SubscriberError::ResourceExhausted(
16387 "unmatched pendingLogs exceeded max_pending_records".into(),
16388 ));
16389 }
16390 self.unmatched_pending_logs.push_back((source_id, log));
16391 return Ok(None);
16392 };
16393 let logs = self.filter_preconfirmed_logs(&flashblock, vec![log])?;
16394 Ok(Some(if logs.is_empty() {
16395 SubscriberEvent::FlashblockObserved
16396 } else {
16397 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
16398 }))
16399 }
16400 SubscriberEvent::BaseFlashblock(payload) => {
16401 let (flashblock, recover_pending_snapshot) =
16402 self.accept_base_flashblock(payload)?;
16403 let mut logs = Vec::new();
16404 let mut retained = VecDeque::new();
16405 while let Some((source_id, log)) = self.unmatched_pending_logs.pop_front() {
16406 let transaction_hash = log.transaction_hash;
16407 if log.block_number == Some(flashblock.block_number)
16408 && transaction_hash
16409 .as_ref()
16410 .is_some_and(|hash| flashblock.contains_transaction(hash))
16411 {
16412 let _ = source_id;
16413 logs.push(log);
16414 } else if log
16415 .block_number
16416 .is_some_and(|number| number >= flashblock.block_number)
16417 {
16418 retained.push_back((source_id, log));
16419 } else {
16420 }
16423 }
16424 self.unmatched_pending_logs = retained;
16425
16426 let indexed_recovery = recover_pending_snapshot.then(|| {
16427 let payload_id = flashblock
16428 .payload_id
16429 .expect("indexed recovery carries a payload id");
16430 let index = flashblock.index.expect("indexed recovery carries an index");
16431 let last_diff = self
16432 .base_flashblock_transactions
16433 .as_ref()
16434 .filter(|(known_payload, known_index, _, _)| {
16435 *known_payload == payload_id && *known_index == index
16436 })
16437 .map(|(_, _, _, last_diff)| last_diff.clone())
16438 .unwrap_or_default();
16439 (payload_id, index, last_diff)
16440 });
16441 if recover_pending_snapshot {
16442 if let Some(event) = self
16443 .fetch_pending_flashblock(indexed_recovery)
16444 .await
16445 .map_err(PendingFlashblockPollError::into_subscriber)?
16446 {
16447 return Ok(Some(event));
16448 }
16449 self.invalidate_flashblock_generation();
16450 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
16451 }
16452 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
16453 Ok(Some(if logs.is_empty() {
16454 SubscriberEvent::FlashblockObserved
16455 } else {
16456 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
16457 }))
16458 }
16459 SubscriberEvent::OpFlashblockTick => self.poll_op_pending_flashblock().await,
16460 SubscriberEvent::CanonicalHeadTick => self.fetch_certified_canonical_head().await,
16461 SubscriberEvent::PreconfirmedLogs { flashblock, logs } => {
16462 let logs = self.filter_preconfirmed_logs(&flashblock, logs)?;
16463 Ok(Some(if logs.is_empty() {
16464 SubscriberEvent::FlashblockObserved
16465 } else {
16466 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
16467 }))
16468 }
16469 SubscriberEvent::FlashblockObserved => Ok(None),
16470 event => Ok(Some(event)),
16471 }
16472 }
16473
16474 async fn fetch_certified_canonical_head(
16475 &mut self,
16476 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
16477 tokio::time::timeout(
16478 self.config.canonical_head_request_timeout,
16479 self.fetch_certified_canonical_head_inner(),
16480 )
16481 .await
16482 .map_err(|_| {
16483 SubscriberError::Provider(format!(
16484 "canonical head certification timed out after {:?}",
16485 self.config.canonical_head_request_timeout
16486 ))
16487 })?
16488 }
16489
16490 async fn fetch_certified_canonical_head_inner(
16491 &mut self,
16492 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
16493 if self.chain_id.and_then(flashblocks_adapter)
16494 == Some(FlashblocksAdapter::PendingStatePolling)
16495 {
16496 if !self.reserve_flashblock_rpc_methods(2) {
16497 return Ok(None);
16498 }
16499 self.flashblocks_rpc_metrics.pending_block_requests = self
16500 .flashblocks_rpc_metrics
16501 .pending_block_requests
16502 .saturating_add(1);
16503 let pending = self
16504 .fetch_op_pending_block()
16505 .await
16506 .map_err(PendingFlashblockPollError::into_subscriber)?
16507 .ok_or_else(|| {
16508 SubscriberError::Provider(
16509 "provider returned no OP pending block while certifying its parent".into(),
16510 )
16511 })?;
16512 let header = self
16513 .certify_op_pending_parent(&pending)
16514 .await
16515 .map_err(PendingFlashblockPollError::into_subscriber)?;
16516 let certified = BlockRef {
16517 number: header.number(),
16518 hash: header.hash(),
16519 parent_hash: Some(header.parent_hash()),
16520 timestamp: Some(header.timestamp()),
16521 };
16522 if self.last_certified_canonical_head.as_ref() == Some(&certified) {
16523 return Ok(None);
16524 }
16525 self.last_certified_canonical_head = Some(certified);
16526 return Ok(Some(SubscriberEvent::BlockHeader(header)));
16527 }
16528 self.flashblocks_rpc_metrics.canonical_head_requests = self
16529 .flashblocks_rpc_metrics
16530 .canonical_head_requests
16531 .saturating_add(1);
16532 let block = self
16533 .provider
16534 .get_block_by_number(BlockNumberOrTag::Latest)
16535 .await
16536 .map_err(provider_error)?
16537 .ok_or_else(|| {
16538 SubscriberError::Provider(
16539 "provider returned no latest block while certifying canonical head".into(),
16540 )
16541 })?;
16542 let header = block.header();
16543 if header.hash().is_zero() {
16544 return Err(SubscriberError::Provider(
16545 "provider returned a placeholder hash for the latest canonical head".into(),
16546 ));
16547 }
16548 let certified = BlockRef {
16549 number: header.number(),
16550 hash: header.hash(),
16551 parent_hash: Some(header.parent_hash()),
16552 timestamp: Some(header.timestamp()),
16553 };
16554 if self.last_certified_canonical_head.as_ref() == Some(&certified) {
16555 return Ok(None);
16556 }
16557 self.last_certified_canonical_head = Some(certified);
16558 Ok(Some(SubscriberEvent::BlockHeader(header.clone())))
16559 }
16560
16561 fn accept_base_flashblock(
16562 &mut self,
16563 payload: BaseFlashblockWirePayload,
16564 ) -> Result<(FlashblockRef, bool), SubscriberError> {
16565 let provider = self.provider_ref.clone().ok_or({
16566 SubscriberError::InvalidConfig(
16567 "Flashblocks require a stable provider ref from a pinned provider lease",
16568 )
16569 })?;
16570
16571 let (flashblock, recover_pending_snapshot) = match payload {
16572 BaseFlashblockWirePayload::Indexed(payload) => {
16573 if payload.index == 0 {
16574 let base = payload.base.clone().ok_or_else(|| {
16575 SubscriberError::Provider(
16576 "indexed newFlashblocks item zero omitted its base header".into(),
16577 )
16578 })?;
16579 self.base_flashblock_header = Some((payload.payload_id, base));
16580 }
16581
16582 let base = self
16583 .base_flashblock_header
16584 .as_ref()
16585 .filter(|(payload_id, _)| *payload_id == payload.payload_id)
16586 .map(|(_, base)| base);
16587 let block_number = base.map(|base| base.block_number).or_else(|| {
16588 payload
16589 .metadata
16590 .as_ref()
16591 .map(|metadata| metadata.block_number)
16592 });
16593 let block_number = block_number.ok_or_else(|| {
16594 SubscriberError::Provider(
16595 "indexed newFlashblocks payload omitted both base and metadata block number"
16596 .into(),
16597 )
16598 })?;
16599 let diff_transactions = flashblock_transaction_hashes(&payload.diff.transactions)?;
16600 let transaction_hashes = match self.base_flashblock_transactions.as_mut() {
16601 Some((known_payload, known_index, transactions, last_diff))
16602 if *known_payload == payload.payload_id =>
16603 {
16604 if payload.index < *known_index {
16605 return self
16606 .latest_preconfirmation
16607 .clone()
16608 .map(|flashblock| (flashblock, false))
16609 .ok_or_else(|| {
16610 SubscriberError::Provider(
16611 "regressive indexed Flashblock arrived without an active snapshot"
16612 .into(),
16613 )
16614 });
16615 }
16616 if payload.index == *known_index {
16617 if *last_diff != diff_transactions {
16618 return Err(SubscriberError::Provider(
16619 "conflicting duplicate indexed Flashblock payload".into(),
16620 ));
16621 }
16622 } else {
16623 if diff_transactions
16624 .iter()
16625 .any(|hash| transactions.contains(hash))
16626 {
16627 return Err(SubscriberError::Provider(
16628 "indexed Flashblock repeated a transaction from an earlier diff"
16629 .into(),
16630 ));
16631 }
16632 transactions.extend(diff_transactions.iter().copied());
16633 *known_index = payload.index;
16634 *last_diff = diff_transactions;
16635 }
16636 transactions.clone()
16637 }
16638 _ => {
16639 self.base_flashblock_transactions = Some((
16640 payload.payload_id,
16641 payload.index,
16642 diff_transactions.clone(),
16643 diff_transactions.clone(),
16644 ));
16645 diff_transactions
16646 }
16647 };
16648 let partial_block_hash = non_placeholder_hash(payload.diff.block_hash);
16649 let transactions_root = payload
16650 .diff
16651 .transactions_root
16652 .and_then(non_placeholder_hash);
16653 let parent_hash = base.and_then(|base| non_placeholder_hash(base.parent_hash));
16654 let state_root = non_placeholder_hash(payload.diff.state_root);
16655 let timestamp = base.map(|base| base.timestamp);
16656 let base_fee_per_gas = base.and_then(|base| base.base_fee_per_gas);
16657 let beneficiary = base.and_then(|base| base.beneficiary);
16658 let prevrandao = base
16659 .and_then(|base| base.prevrandao)
16660 .and_then(non_placeholder_hash);
16661 let gas_limit = base.and_then(|base| base.gas_limit);
16662 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
16663 provider: &provider,
16664 payload_id: Some(payload.payload_id),
16665 index: Some(payload.index),
16666 block_number,
16667 partial_block_hash,
16668 parent_hash,
16669 state_root,
16670 transactions_root,
16671 transaction_hashes: &transaction_hashes,
16672 timestamp,
16673 base_fee_per_gas,
16674 beneficiary,
16675 prevrandao,
16676 gas_limit,
16677 });
16678 let flashblock = FlashblockRef {
16679 provider,
16680 payload_id: Some(payload.payload_id),
16681 index: Some(payload.index),
16682 block_number,
16683 content_hash,
16684 partial_block_hash,
16685 parent_hash,
16686 state_root,
16687 transactions_root,
16688 transaction_hashes,
16689 timestamp,
16690 base_fee_per_gas,
16691 beneficiary,
16692 prevrandao,
16693 gas_limit,
16694 };
16695 if let Some(previous) = self.latest_preconfirmation.as_ref()
16696 && previous.same_payload(&flashblock)
16697 && previous.index == flashblock.index
16698 && previous.content_hash != flashblock.content_hash
16699 {
16700 return Err(SubscriberError::Provider(
16701 "conflicting duplicate indexed Flashblock content".into(),
16702 ));
16703 }
16704 let recover = match self.latest_preconfirmation.as_ref() {
16705 Some(previous) if previous.same_payload(&flashblock) => {
16706 if let (Some(previous), Some(current)) = (previous.index, flashblock.index)
16707 {
16708 if current < previous {
16709 return Ok((flashblock, false));
16710 }
16711 current > previous.saturating_add(1)
16712 } else {
16713 false
16714 }
16715 }
16716 Some(_) => payload.index != 0,
16717 None => payload.index != 0,
16718 };
16719 (flashblock, recover)
16720 }
16721 BaseFlashblockWirePayload::Block(payload) => {
16722 let transaction_hashes = flashblock_transaction_hashes(&payload.transactions)?;
16723 let parent_hash = non_placeholder_hash(payload.parent_hash);
16724 let state_root = non_placeholder_hash(payload.state_root);
16725 let transactions_root = payload.transactions_root.and_then(non_placeholder_hash);
16726 let partial_block_hash = non_placeholder_hash(payload.hash);
16727 let prevrandao = payload.mix_hash.and_then(non_placeholder_hash);
16728 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
16729 provider: &provider,
16730 payload_id: None,
16731 index: None,
16732 block_number: payload.number,
16733 partial_block_hash,
16734 parent_hash,
16735 state_root,
16736 transactions_root,
16737 transaction_hashes: &transaction_hashes,
16738 timestamp: Some(payload.timestamp),
16739 base_fee_per_gas: payload.base_fee_per_gas,
16740 beneficiary: payload.miner,
16741 prevrandao,
16742 gas_limit: payload.gas_limit,
16743 });
16744 let flashblock = FlashblockRef {
16745 provider,
16746 payload_id: None,
16747 index: None,
16748 block_number: payload.number,
16749 content_hash,
16750 partial_block_hash,
16751 parent_hash,
16752 state_root,
16753 transactions_root,
16754 transaction_hashes,
16755 timestamp: Some(payload.timestamp),
16756 base_fee_per_gas: payload.base_fee_per_gas,
16757 beneficiary: payload.miner,
16758 prevrandao,
16759 gas_limit: payload.gas_limit,
16760 };
16761 if let Some(previous) = self.latest_preconfirmation.as_ref()
16762 && flashblock.same_payload(previous)
16763 && flashblock.content_hash != previous.content_hash
16764 && !flashblock.is_cumulative_successor_of(previous)
16765 {
16766 return Err(SubscriberError::Provider(
16767 "cumulative Flashblock transaction membership is non-monotonic".into(),
16768 ));
16769 }
16770 (flashblock, false)
16771 }
16772 };
16773 Ok((flashblock, recover_pending_snapshot))
16774 }
16775
16776 async fn poll_op_pending_flashblock(
16777 &mut self,
16778 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
16779 match self.fetch_pending_flashblock(None).await {
16780 Ok(event) => {
16781 self.consecutive_flashblock_poll_failures = 0;
16782 Ok(event)
16783 }
16784 Err(PendingFlashblockPollError::Request(error)) => {
16785 self.flashblocks_rpc_metrics.failed_requests = self
16786 .flashblocks_rpc_metrics
16787 .failed_requests
16788 .saturating_add(1);
16789 self.consecutive_flashblock_poll_failures =
16790 self.consecutive_flashblock_poll_failures.saturating_add(1);
16791 if self.consecutive_flashblock_poll_failures
16792 >= self.config.max_consecutive_flashblock_poll_failures
16793 {
16794 return Err(error);
16795 }
16796 tracing::warn!(
16797 consecutive_failures = self.consecutive_flashblock_poll_failures,
16798 failure_limit = self.config.max_consecutive_flashblock_poll_failures,
16799 error = %error,
16800 "Optimism pending-state Flashblocks request failed; retrying on the next tick"
16801 );
16802 Ok(None)
16803 }
16804 Err(PendingFlashblockPollError::Integrity(error)) => Err(error),
16805 }
16806 }
16807
16808 async fn fetch_pending_flashblock(
16809 &mut self,
16810 indexed_recovery: Option<(FixedBytes<8>, u64, Vec<B256>)>,
16811 ) -> Result<Option<SubscriberEvent<N>>, PendingFlashblockPollError> {
16812 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
16813 == Some(FlashblocksAdapter::PendingStatePolling);
16814 if samples_pending_range {
16815 let fixed_methods = 2_usize.saturating_add(self.log_stream_filters().len());
16816 if !self.reserve_flashblock_rpc_methods(fixed_methods) {
16817 return Ok(None);
16818 }
16819 }
16820 let state_provider = if samples_pending_range {
16821 self.flashblocks_state_provider
16822 .as_ref()
16823 .unwrap_or(&self.provider)
16824 } else {
16825 &self.provider
16826 };
16827 let latest = if samples_pending_range {
16828 None
16829 } else {
16830 self.flashblocks_rpc_metrics.canonical_head_requests = self
16831 .flashblocks_rpc_metrics
16832 .canonical_head_requests
16833 .saturating_add(1);
16834 Some(
16835 state_provider
16836 .get_block_number()
16837 .await
16838 .map_err(pending_flashblock_request_error)?,
16839 )
16840 };
16841 self.flashblocks_rpc_metrics.pending_block_requests = self
16842 .flashblocks_rpc_metrics
16843 .pending_block_requests
16844 .saturating_add(1);
16845 let pending_block = if samples_pending_range {
16846 self.fetch_op_pending_block().await?
16847 } else {
16848 self.provider
16849 .get_block_by_number(BlockNumberOrTag::Pending)
16850 .await
16851 .map_err(pending_flashblock_request_error)?
16852 };
16853 let Some(block) = pending_block else {
16854 if self.config.preconfirmations == PreconfirmationMode::Required {
16855 return Err(PendingFlashblockPollError::Request(
16856 SubscriberError::Provider(
16857 "Flashblocks provider returned no pending block".into(),
16858 ),
16859 ));
16860 }
16861 return Ok(None);
16862 };
16863 let latest = if samples_pending_range {
16864 self.certify_op_pending_parent(&block).await?.number()
16865 } else {
16866 latest.expect("non-OP pending recovery fetched a canonical height")
16867 };
16868 let header = block.header();
16869 if header.number() <= latest {
16870 return Ok(None);
16871 }
16872
16873 let provider = self.provider_ref.clone().ok_or({
16874 PendingFlashblockPollError::Integrity(SubscriberError::InvalidConfig(
16875 "Flashblocks require a stable provider ref from a pinned provider lease",
16876 ))
16877 })?;
16878 let parent_hash = Some(header.parent_hash());
16879 let transaction_hashes = if let Some(hashes) = block.transactions().as_hashes() {
16880 hashes.to_vec()
16881 } else if let Some(transactions) = block.transactions().as_transactions() {
16882 transactions
16883 .iter()
16884 .map(|transaction| transaction.tx_hash())
16885 .collect()
16886 } else {
16887 Vec::new()
16888 };
16889 let state_root = non_placeholder_hash(header.state_root());
16890 let transactions_root = non_placeholder_hash(header.transactions_root());
16891 let partial_block_hash = non_placeholder_hash(header.hash());
16892 let prevrandao = header.mix_hash().and_then(non_placeholder_hash);
16893 let content_hash = flashblock_content_hash(FlashblockContentCommitment {
16894 provider: &provider,
16895 payload_id: None,
16896 index: None,
16897 block_number: header.number(),
16898 partial_block_hash,
16899 parent_hash,
16900 state_root,
16901 transactions_root,
16902 transaction_hashes: &transaction_hashes,
16903 timestamp: Some(header.timestamp()),
16904 base_fee_per_gas: header.base_fee_per_gas(),
16905 beneficiary: Some(header.beneficiary()),
16906 prevrandao,
16907 gas_limit: Some(header.gas_limit()),
16908 });
16909 let flashblock = FlashblockRef {
16910 provider,
16911 payload_id: None,
16912 index: None,
16913 block_number: header.number(),
16914 content_hash,
16915 partial_block_hash,
16916 parent_hash,
16917 state_root,
16918 transactions_root,
16919 transaction_hashes,
16920 timestamp: Some(header.timestamp()),
16921 base_fee_per_gas: header.base_fee_per_gas(),
16922 beneficiary: Some(header.beneficiary()),
16923 prevrandao,
16924 gas_limit: Some(header.gas_limit()),
16925 };
16926 if samples_pending_range
16927 && self
16928 .latest_preconfirmation
16929 .as_ref()
16930 .is_some_and(|previous| !previous.same_payload(&flashblock))
16931 {
16932 self.invalidate_preconfirmation_snapshot();
16935 }
16936 if let Some((payload_id, index, last_diff)) = indexed_recovery {
16937 self.base_flashblock_transactions = Some((
16938 payload_id,
16939 index,
16940 flashblock.transaction_hashes.clone(),
16941 last_diff,
16942 ));
16943 }
16944 let repeats_pending_snapshot = self
16945 .latest_preconfirmation
16946 .as_ref()
16947 .is_some_and(|previous| previous == &flashblock);
16948 if repeats_pending_snapshot && !samples_pending_range {
16949 return Ok(None);
16950 }
16951
16952 if let Some(previous) = self.latest_preconfirmation.as_ref()
16953 && flashblock.same_payload(previous)
16954 && !flashblock.is_cumulative_successor_of(previous)
16955 {
16956 if samples_pending_range {
16957 self.invalidate_preconfirmation_snapshot();
16962 return Ok(Some(SubscriberEvent::FlashblockInvalidated));
16963 }
16964 return Err(PendingFlashblockPollError::Integrity(
16965 SubscriberError::Provider(
16966 "sampled cumulative Flashblock transaction membership is non-monotonic".into(),
16967 ),
16968 ));
16969 }
16970
16971 let mut logs = self.fetch_pending_logs(flashblock.block_number).await?;
16972 if samples_pending_range {
16973 let (mut receipt_logs, completed_receipts, unavailable_receipts) =
16974 self.fetch_pending_transaction_receipts(&flashblock).await?;
16975 logs.append(&mut receipt_logs);
16976 logs.retain(|log| log.block_number == Some(flashblock.block_number));
16977 for log in &logs {
16978 let transaction_hash = log.transaction_hash.ok_or_else(|| {
16979 PendingFlashblockPollError::Integrity(SubscriberError::Provider(
16980 "pre-confirmed log is missing its transaction hash".into(),
16981 ))
16982 })?;
16983 if !flashblock.contains_transaction(&transaction_hash) {
16984 self.flashblocks_rpc_metrics.raced_samples =
16985 self.flashblocks_rpc_metrics.raced_samples.saturating_add(1);
16986 return Ok(None);
16987 }
16988 }
16989 let logs = self
16990 .filter_preconfirmed_logs(&flashblock, logs)
16991 .map_err(PendingFlashblockPollError::Integrity)?;
16992 self.preconfirmed_unavailable_receipts
16993 .extend(unavailable_receipts);
16994 for transaction_hash in &completed_receipts {
16995 self.preconfirmed_unavailable_receipts
16996 .remove(transaction_hash);
16997 }
16998 self.preconfirmed_receipted_transactions
16999 .extend(completed_receipts);
17000 if repeats_pending_snapshot && logs.is_empty() {
17001 return Ok(None);
17002 }
17003 return Ok(Some(if logs.is_empty() {
17004 SubscriberEvent::FlashblockObserved
17005 } else {
17006 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
17007 }));
17008 }
17009 let logs = self
17010 .filter_preconfirmed_logs(&flashblock, logs)
17011 .map_err(PendingFlashblockPollError::Integrity)?;
17012 Ok(Some(if logs.is_empty() {
17013 SubscriberEvent::FlashblockObserved
17014 } else {
17015 SubscriberEvent::PreconfirmedLogs { flashblock, logs }
17016 }))
17017 }
17018
17019 async fn fetch_pending_logs(
17020 &mut self,
17021 pending_block_number: u64,
17022 ) -> Result<Vec<Log>, PendingFlashblockPollError> {
17023 let mut logs = Vec::new();
17024 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
17025 == Some(FlashblocksAdapter::PendingStatePolling);
17026 let state_provider = if samples_pending_range {
17027 self.flashblocks_state_provider
17028 .as_ref()
17029 .unwrap_or(&self.provider)
17030 } else {
17031 &self.provider
17032 };
17033 for filter in self.log_stream_filters() {
17034 self.flashblocks_rpc_metrics.pending_log_requests = self
17035 .flashblocks_rpc_metrics
17036 .pending_log_requests
17037 .saturating_add(1);
17038 let filter = if samples_pending_range {
17039 filter
17040 .from_block(pending_block_number)
17041 .to_block(BlockNumberOrTag::Pending)
17042 } else {
17043 filter
17044 .from_block(BlockNumberOrTag::Pending)
17045 .to_block(BlockNumberOrTag::Pending)
17046 };
17047 logs.extend(
17048 state_provider
17049 .get_logs(&filter)
17050 .await
17051 .map_err(pending_flashblock_request_error)?,
17052 );
17053 }
17054 if samples_pending_range {
17055 logs.retain(|log| log.block_number == Some(pending_block_number));
17056 }
17057 Ok(logs)
17058 }
17059
17060 async fn fetch_pending_transaction_receipts(
17061 &mut self,
17062 flashblock: &FlashblockRef,
17063 ) -> Result<(Vec<Log>, Vec<B256>, Vec<B256>), PendingFlashblockPollError> {
17064 let receipt_allowance = self.pending_receipt_request_allowance();
17065 let receipt_limit = self
17066 .config
17067 .max_pending_transaction_receipts_per_tick
17068 .min(receipt_allowance);
17069 if receipt_limit == 0 {
17070 return Ok((Vec::new(), Vec::new(), Vec::new()));
17071 }
17072 let mut transaction_hashes = Vec::with_capacity(receipt_limit);
17073 for transaction_hash in &flashblock.transaction_hashes {
17074 if !self
17075 .preconfirmed_receipted_transactions
17076 .contains(transaction_hash)
17077 && !self
17078 .preconfirmed_unavailable_receipts
17079 .contains(transaction_hash)
17080 {
17081 transaction_hashes.push(*transaction_hash);
17082 if transaction_hashes.len() == receipt_limit {
17083 break;
17084 }
17085 }
17086 }
17087 if transaction_hashes.len() < receipt_limit {
17088 for transaction_hash in &flashblock.transaction_hashes {
17089 if self
17090 .preconfirmed_unavailable_receipts
17091 .contains(transaction_hash)
17092 {
17093 transaction_hashes.push(*transaction_hash);
17094 if transaction_hashes.len() == receipt_limit {
17095 break;
17096 }
17097 }
17098 }
17099 }
17100 if transaction_hashes.is_empty() {
17101 return Ok((Vec::new(), Vec::new(), Vec::new()));
17102 }
17103 let reserved = self.reserve_flashblock_rpc_methods(transaction_hashes.len());
17104 debug_assert!(reserved, "receipt allowance must remain reserved until use");
17105 if !reserved {
17106 return Ok((Vec::new(), Vec::new(), Vec::new()));
17107 }
17108 self.flashblocks_rpc_metrics.pending_receipt_requests = self
17109 .flashblocks_rpc_metrics
17110 .pending_receipt_requests
17111 .saturating_add(transaction_hashes.len() as u64);
17112 let state_provider = self
17113 .flashblocks_state_provider
17114 .as_ref()
17115 .unwrap_or(&self.provider);
17116 let client = state_provider.client();
17117 let mut batch = BatchRequest::new(client);
17118 let mut waiters = Vec::with_capacity(transaction_hashes.len());
17119 for transaction_hash in transaction_hashes {
17120 let waiter = batch
17121 .add_call::<_, serde_json::Value>("eth_getTransactionReceipt", &(transaction_hash,))
17122 .map_err(pending_flashblock_request_error)?;
17123 waiters.push((transaction_hash, waiter));
17124 }
17125 batch
17126 .send()
17127 .await
17128 .map_err(pending_flashblock_request_error)?;
17129 let mut logs = Vec::new();
17130 let mut completed = Vec::new();
17131 let mut unavailable = Vec::new();
17132 for (transaction_hash, waiter) in waiters {
17133 let value = waiter.await.map_err(pending_flashblock_request_error)?;
17134 if let Some(mut receipt_logs) =
17135 normalize_pending_transaction_receipt(transaction_hash, value)
17136 .map_err(PendingFlashblockPollError::Integrity)?
17137 {
17138 self.flashblocks_rpc_metrics.pending_receipts_completed = self
17139 .flashblocks_rpc_metrics
17140 .pending_receipts_completed
17141 .saturating_add(1);
17142 logs.append(&mut receipt_logs);
17143 completed.push(transaction_hash);
17144 } else {
17145 self.flashblocks_rpc_metrics.pending_receipts_unavailable = self
17146 .flashblocks_rpc_metrics
17147 .pending_receipts_unavailable
17148 .saturating_add(1);
17149 unavailable.push(transaction_hash);
17150 }
17151 }
17152 Ok((logs, completed, unavailable))
17153 }
17154
17155 fn pending_receipt_request_allowance(&mut self) -> usize {
17156 self.prune_flashblock_rpc_request_times();
17157 let rolling_capacity = self
17158 .config
17159 .max_flashblock_rpc_requests_per_second
17160 .saturating_sub(self.flashblock_rpc_request_times.len());
17161 rolling_capacity.min(self.pending_receipt_requests_per_tick_capacity())
17162 }
17163
17164 fn pending_receipt_requests_per_tick_capacity(&self) -> usize {
17165 let interval_nanos = self.config.flashblock_poll_interval.as_nanos().max(1);
17166 let ticks_per_second = Duration::from_secs(1).as_nanos().div_ceil(interval_nanos);
17167 let ticks_per_second = usize::try_from(ticks_per_second).unwrap_or(usize::MAX);
17168 self.pending_receipt_requests_per_second_capacity()
17169 .checked_div(ticks_per_second)
17170 .unwrap_or(0)
17171 }
17172
17173 fn pending_receipt_requests_per_second_capacity(&self) -> usize {
17174 let interval_nanos = self.config.flashblock_poll_interval.as_nanos().max(1);
17175 let ticks_per_second = Duration::from_secs(1).as_nanos().div_ceil(interval_nanos);
17176 let ticks_per_second = usize::try_from(ticks_per_second).unwrap_or(usize::MAX);
17177 let fixed_methods_per_tick = 2_usize.saturating_add(self.log_stream_filters().len());
17178 let mut reserved_methods = ticks_per_second.saturating_mul(fixed_methods_per_tick);
17179 if needs_header_block_stream(&self.interests) {
17180 let canonical_interval_nanos =
17181 self.config.canonical_head_poll_interval.as_nanos().max(1);
17182 let canonical_ticks = Duration::from_secs(1)
17183 .as_nanos()
17184 .div_ceil(canonical_interval_nanos);
17185 let canonical_ticks = usize::try_from(canonical_ticks).unwrap_or(usize::MAX);
17186 reserved_methods = reserved_methods.saturating_add(canonical_ticks.saturating_mul(2));
17187 }
17188 self.config
17189 .max_flashblock_rpc_requests_per_second
17190 .saturating_sub(reserved_methods)
17191 }
17192
17193 fn reserve_flashblock_rpc_methods(&mut self, methods: usize) -> bool {
17194 self.prune_flashblock_rpc_request_times();
17195 if self
17196 .flashblock_rpc_request_times
17197 .len()
17198 .saturating_add(methods)
17199 > self.config.max_flashblock_rpc_requests_per_second
17200 {
17201 return false;
17202 }
17203 let now = Instant::now();
17204 for _ in 0..methods {
17205 self.flashblock_rpc_request_times.push_back(now);
17206 }
17207 true
17208 }
17209
17210 fn prune_flashblock_rpc_request_times(&mut self) {
17211 let now = Instant::now();
17212 while self
17213 .flashblock_rpc_request_times
17214 .front()
17215 .is_some_and(|requested| now.duration_since(*requested) >= Duration::from_secs(1))
17216 {
17217 self.flashblock_rpc_request_times.pop_front();
17218 }
17219 }
17220
17221 fn filter_preconfirmed_logs(
17222 &mut self,
17223 flashblock: &FlashblockRef,
17224 mut logs: Vec<Log>,
17225 ) -> Result<Vec<Log>, SubscriberError> {
17226 let samples_pending_range = self.chain_id.and_then(flashblocks_adapter)
17227 == Some(FlashblocksAdapter::PendingStatePolling);
17228 if self
17229 .latest_preconfirmation
17230 .as_ref()
17231 .is_some_and(|previous| !previous.same_payload(flashblock))
17232 {
17233 self.invalidate_preconfirmation_snapshot();
17238 }
17239 if self
17240 .latest_preconfirmation
17241 .as_ref()
17242 .is_none_or(|previous| !previous.same_payload(flashblock))
17243 {
17244 self.preconfirmed_seen_logs.clear();
17245 }
17246 if let Some(previous) = self.latest_preconfirmation.as_ref()
17247 && previous.same_payload(flashblock)
17248 && let (Some(previous_index), Some(current_index)) = (previous.index, flashblock.index)
17249 && current_index < previous_index
17250 {
17251 return Ok(Vec::new());
17252 }
17253 self.latest_preconfirmation = Some(flashblock.clone());
17254
17255 logs.sort_by_key(|log| (log.transaction_index.unwrap_or(u64::MAX), log.log_index));
17256 let mut filtered = Vec::new();
17257 for mut log in logs {
17258 if log.removed || log.block_number != Some(flashblock.block_number) {
17259 return Err(SubscriberError::Provider(
17260 "pre-confirmed log disagrees with its Flashblock snapshot".into(),
17261 ));
17262 }
17263 let transaction_hash = log.transaction_hash.ok_or_else(|| {
17264 SubscriberError::Provider(
17265 "pre-confirmed log is missing its transaction hash".into(),
17266 )
17267 })?;
17268 let log_index = log.log_index.ok_or_else(|| {
17269 SubscriberError::Provider("pre-confirmed log is missing its log index".into())
17270 })?;
17271 let transaction_index =
17272 flashblock
17273 .transaction_index(&transaction_hash)
17274 .ok_or_else(|| {
17275 SubscriberError::Provider(
17276 "pre-confirmed log transaction is absent from the cumulative Flashblock"
17277 .into(),
17278 )
17279 })?;
17280 if log
17281 .transaction_index
17282 .is_some_and(|reported| reported != transaction_index)
17283 {
17284 return Err(SubscriberError::Provider(
17285 "pre-confirmed log transaction index disagrees with cumulative membership"
17286 .into(),
17287 ));
17288 }
17289 let reported_hash = log.block_hash.and_then(non_placeholder_hash);
17290 if !samples_pending_range
17291 && let Some(reported) = reported_hash
17292 && reported != flashblock.content_hash
17293 && flashblock
17294 .partial_block_hash
17295 .is_some_and(|expected| reported != expected)
17296 {
17297 return Err(SubscriberError::Provider(
17298 "pre-confirmed log partial block hash disagrees with its Flashblock snapshot"
17299 .into(),
17300 ));
17301 }
17302 log.block_hash = Some(flashblock.content_hash);
17303 log.block_timestamp = flashblock.timestamp.or(log.block_timestamp);
17304 log.transaction_index = Some(transaction_index);
17305 if self
17306 .preconfirmed_seen_logs
17307 .insert((transaction_hash, log_index))
17308 && log_matches_any_interest(&log, &self.interests)
17309 {
17310 filtered.push(log);
17311 }
17312 }
17313 Ok(filtered)
17314 }
17315
17316 async fn verify_event_log_blocks(
17317 &mut self,
17318 event: &SubscriberEvent<N>,
17319 ) -> Result<(), SubscriberError> {
17320 if !self.config.verify_log_block_context {
17321 return Ok(());
17322 }
17323 match event {
17324 SubscriberEvent::Log { log, .. } => self.verify_log_block_context(log).await,
17325 SubscriberEvent::BackfilledLogs { logs, .. } | SubscriberEvent::Logs(logs) => {
17326 for log in logs {
17327 self.verify_log_block_context(log).await?;
17328 }
17329 Ok(())
17330 }
17331 #[cfg(feature = "raw-flashblocks-json")]
17332 SubscriberEvent::ExternalFlashblockUpdate(_) => Ok(()),
17333 SubscriberEvent::BlockHeader(_)
17334 | SubscriberEvent::PendingHash(_)
17335 | SubscriberEvent::PendingHashes(_)
17336 | SubscriberEvent::BasePendingLog { .. }
17337 | SubscriberEvent::BaseFlashblock(_)
17338 | SubscriberEvent::OpFlashblockTick
17339 | SubscriberEvent::CanonicalHeadTick
17340 | SubscriberEvent::PreconfirmedLogs { .. }
17341 | SubscriberEvent::FlashblockInvalidated
17342 | SubscriberEvent::FlashblockObserved
17343 | SubscriberEvent::StreamTerminated(_) => Ok(()),
17344 }
17345 }
17346
17347 async fn verify_log_block_context(&mut self, log: &Log) -> Result<(), SubscriberError> {
17348 if log.removed {
17349 return Ok(());
17350 }
17351 let number = log.block_number.ok_or_else(|| {
17352 SubscriberError::Provider(
17353 "canonical log is missing its block number during context verification".into(),
17354 )
17355 })?;
17356 let hash = log.block_hash.ok_or_else(|| {
17357 SubscriberError::Provider(
17358 "canonical log is missing its block hash during context verification".into(),
17359 )
17360 })?;
17361 let key = (number, hash);
17362 if self.verified_log_blocks.contains_key(&key) {
17363 return Ok(());
17364 }
17365 let provider = self
17366 .log_verification_provider
17367 .as_ref()
17368 .unwrap_or(&self.provider);
17369 let block = provider
17370 .get_block_by_number(BlockNumberOrTag::Number(number))
17371 .await
17372 .map_err(provider_error)?
17373 .ok_or_else(|| {
17374 SubscriberError::Provider(format!(
17375 "canonical log block {number} is unavailable during context verification"
17376 ))
17377 })?;
17378 let header = block.header();
17379 let verified = BlockRef {
17380 number: header.number(),
17381 hash: header.hash(),
17382 parent_hash: Some(header.parent_hash()),
17383 timestamp: Some(header.timestamp()),
17384 };
17385 if verified.number != number
17386 || verified.hash != hash
17387 || log
17388 .block_timestamp
17389 .is_some_and(|timestamp| verified.timestamp != Some(timestamp))
17390 {
17391 return Err(SubscriberError::Provider(format!(
17392 "canonical log block {number}:{hash:?} disagrees with the provider's current canonical identity"
17393 )));
17394 }
17395 self.verified_log_blocks.insert(key, verified);
17396 self.verified_log_block_order.push_back(key);
17397 let capacity = self.config.reconnect.dedupe_window.max(1);
17398 while self.verified_log_block_order.len() > capacity {
17399 if let Some(evicted) = self.verified_log_block_order.pop_front() {
17400 self.verified_log_blocks.remove(&evicted);
17401 }
17402 }
17403 Ok(())
17404 }
17405
17406 fn enqueue_event(&mut self, event: SubscriberEvent<N>) {
17407 self.enqueue_event_with_excluded_owners(event, None);
17408 }
17409
17410 fn buffer_reconcile_event_for_owners(
17411 &mut self,
17412 event: &SubscriberEvent<N>,
17413 target_epochs: &HashSet<SubscriberOwnerEpoch>,
17414 ) {
17415 match event {
17416 SubscriberEvent::Log { log, .. } => {
17417 self.buffer_reconcile_log_for_owners(log, InputSource::Subscription, target_epochs)
17418 }
17419 SubscriberEvent::BackfilledLogs { logs, .. } => {
17420 for log in logs {
17421 self.buffer_reconcile_log_for_owners(log, InputSource::Backfill, target_epochs);
17422 }
17423 }
17424 SubscriberEvent::Logs(logs) => {
17425 for log in logs {
17426 self.buffer_reconcile_log_for_owners(log, InputSource::Poll, target_epochs);
17427 }
17428 }
17429 #[cfg(feature = "raw-flashblocks-json")]
17430 SubscriberEvent::ExternalFlashblockUpdate(_) => {}
17431 SubscriberEvent::BlockHeader(_)
17432 | SubscriberEvent::PendingHash(_)
17433 | SubscriberEvent::PendingHashes(_)
17434 | SubscriberEvent::BasePendingLog { .. }
17435 | SubscriberEvent::BaseFlashblock(_)
17436 | SubscriberEvent::OpFlashblockTick
17437 | SubscriberEvent::CanonicalHeadTick
17438 | SubscriberEvent::PreconfirmedLogs { .. }
17439 | SubscriberEvent::FlashblockInvalidated
17440 | SubscriberEvent::FlashblockObserved
17441 | SubscriberEvent::StreamTerminated(_) => {}
17442 }
17443 }
17444
17445 fn buffer_reconcile_log_for_owners(
17446 &mut self,
17447 log: &Log,
17448 source: InputSource,
17449 target_epochs: &HashSet<SubscriberOwnerEpoch>,
17450 ) {
17451 let record = self.with_chain_id(log_input_record(log.clone(), source));
17452 let owners = self
17453 .staged_owners_for_record(&record)
17454 .into_iter()
17455 .filter(|owner| target_epochs.contains(owner))
17456 .collect::<Vec<_>>();
17457 if !owners.is_empty() {
17458 self.push_pending_reconcile_record(BufferedSubscriberOwnerRecord { record, owners });
17459 }
17460 }
17461
17462 fn promote_reconcile_owner_records(&mut self, target_epochs: &HashSet<SubscriberOwnerEpoch>) {
17463 let mut retained = VecDeque::new();
17464 while let Some(mut buffered) = self.pending_reconcile_owner_records.pop_front() {
17465 let mut promoted = Vec::new();
17466 buffered.owners.retain(|owner| {
17467 if target_epochs.contains(owner) {
17468 promoted.push(owner.clone());
17469 false
17470 } else {
17471 true
17472 }
17473 });
17474 if promoted.is_empty() {
17475 retained.push_back(buffered);
17476 continue;
17477 }
17478 let promoted_record = if buffered.owners.is_empty() {
17479 buffered.record
17480 } else {
17481 let record = buffered.record.clone();
17482 retained.push_back(buffered);
17483 record
17484 };
17485 self.enqueue_owner_record_for_owners_unmerged(promoted_record, promoted);
17486 }
17487 self.pending_reconcile_owner_records = retained;
17488 }
17489
17490 fn seed_reconciled_filter_anchors(
17491 &mut self,
17492 plans: &[SubscriberOwnerReconcilePlan<N>],
17493 through: u64,
17494 ) {
17495 for filter in plans.iter().flat_map(|plan| log_filters(&plan.interests)) {
17496 let Some(source_id) = self.log_source_ids.get(&filter).copied() else {
17497 continue;
17498 };
17499 let anchor = self
17500 .last_seen_log_blocks
17501 .entry(source_id)
17502 .or_insert(through);
17503 *anchor = (*anchor).max(through);
17504 }
17505 }
17506
17507 fn enqueue_event_excluding_owners(
17508 &mut self,
17509 event: SubscriberEvent<N>,
17510 excluded: &HashSet<SubscriberOwnerEpoch>,
17511 ) {
17512 self.enqueue_event_with_excluded_owners(event, Some(excluded));
17513 }
17514
17515 fn enqueue_event_with_excluded_owners(
17516 &mut self,
17517 event: SubscriberEvent<N>,
17518 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
17519 ) {
17520 match event {
17521 SubscriberEvent::Log { source_id, log } => {
17522 if log_matches_any_interest(&log, &self.interests) {
17523 let record = log_input_record(log, InputSource::Subscription);
17524 self.note_log_block(source_id, &record);
17525 self.enqueue_record_with_excluded_owners(record, excluded);
17526 }
17527 }
17528 SubscriberEvent::BackfilledLogs { source_id, logs } => {
17529 self.enqueue_backfilled_logs_with_excluded_owners(
17530 logs,
17531 Some(source_id),
17532 None,
17533 None,
17534 excluded,
17535 );
17536 }
17537 SubscriberEvent::Logs(logs) => {
17538 for log in logs {
17539 if log_matches_any_interest(&log, &self.interests) {
17540 self.enqueue_record_with_excluded_owners(
17541 log_input_record(log, InputSource::Poll),
17542 excluded,
17543 );
17544 }
17545 }
17546 }
17547 SubscriberEvent::BlockHeader(header) => {
17548 if needs_header_block_stream(&self.interests) {
17549 let record = block_header_input_record::<N>(header);
17550 self.enqueue_record_with_excluded_owners(record, excluded);
17551 }
17552 }
17553 SubscriberEvent::PendingHash(hash) => {
17554 let record = pending_hash_input_record::<N>(hash, InputSource::Subscription);
17555 self.enqueue_record_with_excluded_owners(record, excluded);
17556 }
17557 SubscriberEvent::PendingHashes(hashes) => {
17558 for hash in hashes {
17559 self.enqueue_record_with_excluded_owners(
17560 pending_hash_input_record::<N>(hash, InputSource::Poll),
17561 excluded,
17562 );
17563 }
17564 }
17565 SubscriberEvent::PreconfirmedLogs { flashblock, logs } => {
17566 for log in logs {
17567 let record = self
17568 .with_chain_id(preconfirmed_log_input_record::<N>(log, flashblock.clone()));
17569 self.push_pending_record(SubscriberInputRecord {
17570 record,
17571 scope: SubscriberInputScope::Preconfirmed,
17572 });
17573 }
17574 }
17575 SubscriberEvent::FlashblockInvalidated => {
17576 self.pending_preconfirmation_invalidation = true;
17577 }
17578 SubscriberEvent::BasePendingLog { .. }
17579 | SubscriberEvent::BaseFlashblock(_)
17580 | SubscriberEvent::OpFlashblockTick
17581 | SubscriberEvent::CanonicalHeadTick
17582 | SubscriberEvent::FlashblockObserved => {}
17583 #[cfg(feature = "raw-flashblocks-json")]
17584 SubscriberEvent::ExternalFlashblockUpdate(_) => {}
17585 SubscriberEvent::StreamTerminated(_) => {}
17586 }
17587 }
17588
17589 fn enqueue_backfilled_logs(
17590 &mut self,
17591 logs: Vec<Log>,
17592 source_id: Option<usize>,
17593 owner: Option<&SubscriberOwnerEpoch>,
17594 range: Option<SubscriberBackfill>,
17595 ) {
17596 self.enqueue_backfilled_logs_with_excluded_owners(logs, source_id, owner, range, None);
17597 }
17598
17599 fn enqueue_backfilled_logs_with_excluded_owners(
17600 &mut self,
17601 logs: Vec<Log>,
17602 source_id: Option<usize>,
17603 owner: Option<&SubscriberOwnerEpoch>,
17604 range: Option<SubscriberBackfill>,
17605 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
17606 ) {
17607 for log in logs {
17608 if range.as_ref().is_some_and(|range| {
17609 log.block_number.is_some_and(|block| {
17610 block < range.start_block() || range.end_block().is_some_and(|end| block > end)
17611 })
17612 }) {
17613 continue;
17614 }
17615 let matches = match owner {
17616 Some(epoch) => self
17617 .owned_interests
17618 .iter()
17619 .find(|entry| entry.epoch.as_ref() == Some(epoch))
17620 .is_some_and(|entry| log_matches_any_interest(&log, &entry.interests)),
17621 None => log_matches_any_interest(&log, &self.interests),
17622 };
17623 if matches {
17624 let record = log_input_record(log, InputSource::Backfill);
17625 if let Some(epoch) = owner {
17626 self.enqueue_owner_record(record, epoch.clone());
17627 } else {
17628 if let Some(source_id) = source_id {
17629 self.note_log_block(source_id, &record);
17630 }
17631 self.enqueue_record_with_excluded_owners(record, excluded);
17632 }
17633 }
17634 }
17635 }
17636
17637 fn enqueue_compat_owner_backfilled_logs(
17638 &mut self,
17639 logs: Vec<Log>,
17640 owner: &HandlerId,
17641 range: SubscriberBackfill,
17642 ) {
17643 let interests = self
17644 .owned_interests
17645 .iter()
17646 .find(|entry| {
17647 &entry.owner == owner
17648 && entry.epoch.is_none()
17649 && entry.state == SubscriberOwnerState::Active
17650 })
17651 .map(|entry| entry.interests.clone());
17652 let Some(interests) = interests else {
17653 return;
17654 };
17655 for log in logs {
17656 if log.block_number.is_some_and(|block| {
17657 block < range.start_block() || range.end_block().is_some_and(|end| block > end)
17658 }) || !log_matches_any_interest(&log, &interests)
17659 {
17660 continue;
17661 }
17662 let record = log_input_record(log, InputSource::Backfill);
17663 self.enqueue_compat_owner_record(record, owner.clone());
17664 }
17665 }
17666
17667 async fn reconnect_source_stream(
17668 &mut self,
17669 source: SubscriberStreamSource,
17670 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17671 if !source.is_pubsub() {
17672 return Err(stream_terminated_error(&source));
17673 }
17674
17675 if !self.config.reconnect.enabled {
17676 return Err(SubscriberError::Provider(format!(
17677 "Alloy subscriber {} stream terminated and reconnect is disabled",
17678 source.label()
17679 )));
17680 }
17681
17682 let mut attempts = 0usize;
17683 let mut delay = self.config.reconnect.initial_delay;
17684 let mut retry_delay = self.config.reconnect.retry_delay;
17685
17686 loop {
17687 attempts = attempts.saturating_add(1);
17688 if !delay.is_zero() {
17689 tokio::time::sleep(delay).await;
17690 }
17691
17692 match self.reconnect_source_once(source.clone()).await {
17693 Ok(backfill_event) => return Ok(backfill_event),
17694 Err(error) if reconnect_attempts_exhausted(attempts, &self.config.reconnect) => {
17695 return Err(SubscriberError::Provider(format!(
17696 "Alloy subscriber {} stream terminated and reconnect failed after {attempts} attempt(s): {error}",
17697 source.label()
17698 )));
17699 }
17700 Err(error) => {
17701 tracing::warn!(
17702 stream = source.label(),
17703 attempts,
17704 error = %error,
17705 "Alloy subscriber reconnect attempt failed"
17706 );
17707 delay = retry_delay;
17708 retry_delay =
17709 next_reconnect_delay(retry_delay, self.config.reconnect.max_delay);
17710 }
17711 }
17712 }
17713 }
17714
17715 async fn reconnect_source_once(
17716 &mut self,
17717 source: SubscriberStreamSource,
17718 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17719 if matches!(
17720 &self.state,
17721 AlloySubscriberState::Active(streams) if streams.contains_source(&source)
17722 ) {
17723 let backfill_event = self.backfill_reconnected_source(&source).await?;
17727 self.pending_source_backfills
17728 .retain(|pending| !pending.same_key(&source));
17729 return Ok(backfill_event);
17730 }
17731 let stream = self.connect_source_stream(source.clone()).await?;
17732 if !matches!(self.state, AlloySubscriberState::Active(_)) {
17733 return Err(SubscriberError::Provider(
17734 "Alloy subscriber state changed before reconnect completed".to_owned(),
17735 ));
17736 }
17737 self.install_source_stream(source.clone(), stream);
17738 if self.source_requires_backfill(&source) {
17739 self.queue_source_backfill(source.clone());
17740 }
17741 let backfill_event = self.backfill_reconnected_source(&source).await?;
17742 self.pending_source_backfills
17743 .retain(|pending| !pending.same_key(&source));
17744
17745 Ok(backfill_event)
17746 }
17747
17748 async fn backfill_reconnected_source(
17749 &mut self,
17750 source: &SubscriberStreamSource,
17751 ) -> Result<Option<SubscriberEvent<N>>, SubscriberError> {
17752 if source.is_flashblocks() {
17753 return Ok(None);
17754 }
17755 let SubscriberStreamSource::PubSubLog { id, filter } = source else {
17756 return Ok(None);
17757 };
17758 let Some(from_block) = self.last_seen_log_blocks.get(id).copied() else {
17759 return Ok(None);
17760 };
17761
17762 let latest = self
17763 .provider
17764 .get_block_number()
17765 .await
17766 .map_err(provider_error)?;
17767 if latest < from_block {
17768 return Ok(None);
17769 }
17770
17771 let logs = self
17772 .provider
17773 .get_logs(&filter.clone().from_block(from_block).to_block(latest))
17774 .await
17775 .map_err(provider_error)?;
17776 Ok(Some(SubscriberEvent::BackfilledLogs {
17777 source_id: *id,
17778 logs,
17779 }))
17780 }
17781
17782 fn note_log_block(&mut self, source_id: usize, record: &ReactiveInputRecord<N>) {
17783 if let Some(block) = record.context.block.as_ref() {
17784 self.last_seen_log_blocks.insert(source_id, block.number);
17785 }
17786 }
17787
17788 fn enqueue_record_with_excluded_owners(
17789 &mut self,
17790 record: ReactiveInputRecord<N>,
17791 excluded: Option<&HashSet<SubscriberOwnerEpoch>>,
17792 ) {
17793 let record = self.with_chain_id(record);
17794 let mut owners = self.staged_owners_for_record(&record);
17795 if let Some(excluded) = excluded {
17796 owners.retain(|owner| !excluded.contains(owner));
17797 }
17798 let canonical_duplicate = self.should_skip_recent_duplicate(&record);
17799 let owners = self.filter_recent_owner_duplicates(&record, owners);
17800 let compatibility_owners = self.compatibility_owners_for_record(&record);
17801 let (already_served, newly_served): (Vec<_>, Vec<_>) = compatibility_owners
17802 .into_iter()
17803 .partition(|owner| self.compatibility_owner_has_seen(&record, owner));
17804 if canonical_duplicate {
17805 if !owners.is_empty() {
17806 self.push_pending_record(SubscriberInputRecord {
17807 record: record.clone(),
17808 scope: SubscriberInputScope::OwnerOnly { owners },
17809 });
17810 }
17811 if !newly_served.is_empty() {
17812 for owner in &newly_served {
17813 self.remember_compatibility_owner_record(&record, owner);
17814 }
17815 self.push_pending_record(SubscriberInputRecord {
17816 record,
17817 scope: SubscriberInputScope::OwnerOnlyHandlers {
17818 owners: newly_served,
17819 },
17820 });
17821 }
17822 return;
17823 }
17824 self.remember_record(&record);
17825 for owner in already_served.iter().chain(&newly_served) {
17826 self.remember_compatibility_owner_record(&record, owner);
17827 }
17828 self.push_pending_record(SubscriberInputRecord {
17829 record,
17830 scope: if already_served.is_empty() {
17831 SubscriberInputScope::Canonical { owners }
17832 } else {
17833 SubscriberInputScope::CanonicalResidual {
17834 owners,
17835 excluded: already_served,
17836 }
17837 },
17838 });
17839 }
17840
17841 fn enqueue_compat_owner_record(&mut self, record: ReactiveInputRecord<N>, owner: HandlerId) {
17842 let record = self.with_chain_id(record);
17843 if self.compatibility_owner_has_seen(&record, &owner) {
17844 return;
17845 }
17846 self.remember_compatibility_owner_record(&record, &owner);
17847 self.push_pending_record(SubscriberInputRecord {
17848 record,
17849 scope: SubscriberInputScope::OwnerOnlyHandlers {
17850 owners: vec![owner],
17851 },
17852 });
17853 }
17854
17855 fn compatibility_owners_for_record(&self, record: &ReactiveInputRecord<N>) -> Vec<HandlerId> {
17856 self.owned_interests
17857 .iter()
17858 .filter(|entry| entry.epoch.is_none() && entry.state == SubscriberOwnerState::Active)
17859 .filter(|entry| {
17860 entry
17861 .interests
17862 .iter()
17863 .any(|interest| interest_matches(interest, &record.input))
17864 })
17865 .map(|entry| entry.owner.clone())
17866 .collect()
17867 }
17868
17869 fn compatibility_owner_has_seen(
17870 &self,
17871 record: &ReactiveInputRecord<N>,
17872 owner: &HandlerId,
17873 ) -> bool {
17874 should_dedupe_record(record)
17875 && self
17876 .recent_compat_owner_input_ref_sets
17877 .get(owner)
17878 .is_some_and(|seen| seen.contains(&record.input_ref()))
17879 }
17880
17881 fn remember_compatibility_owner_record(
17882 &mut self,
17883 record: &ReactiveInputRecord<N>,
17884 owner: &HandlerId,
17885 ) {
17886 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
17887 return;
17888 }
17889 let input_ref = record.input_ref();
17890 let seen = self
17891 .recent_compat_owner_input_ref_sets
17892 .entry(owner.clone())
17893 .or_default();
17894 if !seen.insert(input_ref) {
17895 return;
17896 }
17897 let recent = self
17898 .recent_compat_owner_input_refs
17899 .entry(owner.clone())
17900 .or_default();
17901 recent.push_back(input_ref);
17902 while recent.len() > self.config.reconnect.dedupe_window {
17903 if let Some(evicted) = recent.pop_front() {
17904 seen.remove(&evicted);
17905 }
17906 }
17907 }
17908
17909 fn enqueue_owner_record(
17910 &mut self,
17911 record: ReactiveInputRecord<N>,
17912 owner: SubscriberOwnerEpoch,
17913 ) {
17914 self.enqueue_owner_record_for_owners(record, vec![owner]);
17915 }
17916
17917 fn enqueue_owner_record_for_owners(
17918 &mut self,
17919 record: ReactiveInputRecord<N>,
17920 owners: Vec<SubscriberOwnerEpoch>,
17921 ) {
17922 self.enqueue_owner_record_for_owners_inner(record, owners, true);
17923 }
17924
17925 fn enqueue_owner_record_for_owners_unmerged(
17926 &mut self,
17927 record: ReactiveInputRecord<N>,
17928 owners: Vec<SubscriberOwnerEpoch>,
17929 ) {
17930 self.enqueue_owner_record_for_owners_inner(record, owners, false);
17931 }
17932
17933 fn enqueue_owner_record_for_owners_inner(
17934 &mut self,
17935 record: ReactiveInputRecord<N>,
17936 owners: Vec<SubscriberOwnerEpoch>,
17937 merge_pending: bool,
17938 ) {
17939 let record = self.with_chain_id(record);
17940 let owners = self.filter_recent_owner_duplicates(&record, owners);
17941 if owners.is_empty() {
17942 return;
17943 }
17944 if merge_pending
17945 && should_dedupe_record(&record)
17946 && self.config.reconnect.dedupe_window != 0
17947 {
17948 let input_ref = record.input_ref();
17949 if let Some(pending) = self
17950 .pending_records
17951 .iter_mut()
17952 .rev()
17953 .find(|pending| pending.record.input_ref() == input_ref)
17954 {
17955 let pending_owners = match &mut pending.scope {
17956 SubscriberInputScope::Canonical { owners }
17957 | SubscriberInputScope::CanonicalResidual { owners, .. }
17958 | SubscriberInputScope::OwnerOnly { owners } => Some(owners),
17959 SubscriberInputScope::OwnerOnlyHandlers { .. }
17960 | SubscriberInputScope::Preconfirmed => None,
17961 };
17962 if let Some(pending_owners) = pending_owners {
17963 for owner in owners {
17964 if !pending_owners.contains(&owner) {
17965 pending_owners.push(owner);
17966 }
17967 }
17968 return;
17969 }
17970 }
17971 }
17972 self.push_pending_record(SubscriberInputRecord {
17973 record,
17974 scope: SubscriberInputScope::OwnerOnly { owners },
17975 });
17976 }
17977
17978 fn push_pending_record(&mut self, record: SubscriberInputRecord<N>) {
17979 if self.pending_record_count() >= self.config.max_pending_records {
17980 self.note_resource_error(format!(
17981 "pending record queues reached the configured limit of {}",
17982 self.config.max_pending_records
17983 ));
17984 return;
17985 }
17986 self.pending_records.push_back(record);
17987 }
17988
17989 fn ensure_pending_record_capacity(
17990 &mut self,
17991 additional: usize,
17992 operation: &str,
17993 ) -> Result<(), SubscriberError> {
17994 let required = self.pending_record_count().saturating_add(additional);
17995 if required > self.config.max_pending_records {
17996 self.note_resource_error(format!(
17997 "{operation} require {required} pending records, above the configured limit of {}",
17998 self.config.max_pending_records
17999 ));
18000 return self.check_resource_error();
18001 }
18002 Ok(())
18003 }
18004
18005 fn push_pending_reconcile_record(&mut self, record: BufferedSubscriberOwnerRecord<N>) {
18006 if self.pending_record_count() >= self.config.max_pending_records {
18007 self.note_resource_error(format!(
18008 "pending record queues reached the configured limit of {}",
18009 self.config.max_pending_records
18010 ));
18011 return;
18012 }
18013 self.pending_reconcile_owner_records.push_back(record);
18014 }
18015
18016 fn pending_record_count(&self) -> usize {
18017 self.pending_records
18018 .len()
18019 .saturating_add(self.pending_reconcile_owner_records.len())
18020 }
18021
18022 fn note_resource_error(&mut self, message: String) {
18023 if self.resource_error.is_none() {
18024 self.resource_error = Some(message);
18025 }
18026 }
18027
18028 fn check_resource_error(&self) -> Result<(), SubscriberError> {
18029 match &self.resource_error {
18030 Some(message) => Err(SubscriberError::ResourceExhausted(message.clone())),
18031 None => Ok(()),
18032 }
18033 }
18034
18035 fn with_chain_id(&self, mut record: ReactiveInputRecord<N>) -> ReactiveInputRecord<N> {
18036 record.context.chain_id = self.chain_id;
18037 if self.config.verify_log_block_context
18038 && let ReactiveInput::Log(log) = &record.input
18039 && !log.removed
18040 && let (Some(number), Some(hash)) = (log.block_number, log.block_hash)
18041 && let Some(verified) = self.verified_log_blocks.get(&(number, hash)).copied()
18042 {
18043 record.context.block = Some(verified);
18044 record.context.chain_status = ChainStatus::Included {
18045 block: verified,
18046 confirmations: 0,
18047 };
18048 }
18049 record
18050 }
18051
18052 fn staged_owners_for_record(
18053 &self,
18054 record: &ReactiveInputRecord<N>,
18055 ) -> Vec<SubscriberOwnerEpoch> {
18056 self.owned_interests
18057 .iter()
18058 .filter(|entry| entry.state == SubscriberOwnerState::Staged)
18059 .filter(|entry| {
18060 entry
18061 .interests
18062 .iter()
18063 .any(|interest| interest_matches(interest, &record.input))
18064 })
18065 .filter_map(|entry| entry.epoch.clone())
18066 .collect()
18067 }
18068
18069 fn filter_recent_owner_duplicates(
18070 &mut self,
18071 record: &ReactiveInputRecord<N>,
18072 owners: Vec<SubscriberOwnerEpoch>,
18073 ) -> Vec<SubscriberOwnerEpoch> {
18074 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
18075 return owners;
18076 }
18077 let input_ref = record.input_ref();
18078 let window = self.config.reconnect.dedupe_window;
18079 owners
18080 .into_iter()
18081 .filter(|owner| {
18082 let seen = self
18083 .recent_owner_input_ref_sets
18084 .entry(owner.clone())
18085 .or_default();
18086 if !seen.insert(input_ref) {
18087 return false;
18088 }
18089 let recent = self
18090 .recent_owner_input_refs
18091 .entry(owner.clone())
18092 .or_default();
18093 recent.push_back(input_ref);
18094 while recent.len() > window {
18095 if let Some(evicted) = recent.pop_front() {
18096 seen.remove(&evicted);
18097 }
18098 }
18099 true
18100 })
18101 .collect()
18102 }
18103
18104 fn should_skip_recent_duplicate(&self, record: &ReactiveInputRecord<N>) -> bool {
18105 if !should_dedupe_record(record) {
18106 return false;
18107 }
18108 self.recent_input_ref_set.contains(&record.input_ref())
18109 }
18110
18111 fn remember_record(&mut self, record: &ReactiveInputRecord<N>) {
18112 if !should_dedupe_record(record) || self.config.reconnect.dedupe_window == 0 {
18113 return;
18114 }
18115
18116 let input_ref = record.input_ref();
18117 if !self.recent_input_ref_set.insert(input_ref) {
18118 return;
18119 }
18120 self.recent_input_refs.push_back(input_ref);
18121
18122 while self.recent_input_refs.len() > self.config.reconnect.dedupe_window {
18123 if let Some(evicted) = self.recent_input_refs.pop_front() {
18124 self.recent_input_ref_set.remove(&evicted);
18125 }
18126 }
18127 }
18128}
18129
18130fn stream_with_termination<N, S>(
18131 stream: S,
18132 source: SubscriberStreamSource,
18133) -> BoxStream<'static, SubscriberEvent<N>>
18134where
18135 N: Network + 'static,
18136 S: futures::Stream<Item = SubscriberEvent<N>> + Send + 'static,
18137{
18138 stream
18139 .chain(stream::once(async move {
18140 SubscriberEvent::StreamTerminated(source)
18141 }))
18142 .boxed()
18143}
18144
18145fn flashblock_reconnect_future<N>(
18146 provider: RootProvider<N>,
18147 source: SubscriberStreamSource,
18148 channel_size: usize,
18149 reconnect: SubscriberReconnectConfig,
18150 first_delay: Duration,
18151 flashblock_poll_interval: Duration,
18152) -> FlashblockReconnectFuture<N>
18153where
18154 N: Network + 'static,
18155{
18156 Box::pin(async move {
18157 if !reconnect.enabled {
18158 let error = SubscriberError::Provider(format!(
18159 "Alloy subscriber {} stream terminated and reconnect is disabled",
18160 source.label()
18161 ));
18162 return (source, Err(error));
18163 }
18164
18165 let mut attempts = 0_usize;
18166 let mut delay = first_delay;
18167 let mut retry_delay = reconnect.retry_delay;
18168 loop {
18169 attempts = attempts.saturating_add(1);
18170 if !delay.is_zero() {
18171 tokio::time::sleep(delay).await;
18172 }
18173 match connect_flashblock_source_once(
18174 &provider,
18175 source.clone(),
18176 channel_size,
18177 flashblock_poll_interval,
18178 )
18179 .await
18180 {
18181 Ok(stream) => return (source, Ok(stream)),
18182 Err(error) if reconnect_attempts_exhausted(attempts, &reconnect) => {
18183 return (
18184 source.clone(),
18185 Err(SubscriberError::Provider(format!(
18186 "Alloy subscriber {} stream reconnect failed after {attempts} attempt(s): {error}",
18187 source.label()
18188 ))),
18189 );
18190 }
18191 Err(error) => {
18192 tracing::warn!(
18193 stream = source.label(),
18194 attempts,
18195 error = %error,
18196 "Flashblocks reconnect attempt failed"
18197 );
18198 delay = retry_delay;
18199 retry_delay = next_reconnect_delay(retry_delay, reconnect.max_delay);
18200 }
18201 }
18202 }
18203 })
18204}
18205
18206async fn connect_flashblock_source_once<N>(
18207 provider: &RootProvider<N>,
18208 source: SubscriberStreamSource,
18209 channel_size: usize,
18210 flashblock_poll_interval: Duration,
18211) -> Result<BoxStream<'static, SubscriberEvent<N>>, SubscriberError>
18212where
18213 N: Network + 'static,
18214{
18215 #[cfg(not(feature = "reactive-ws"))]
18216 let _ = provider;
18217
18218 match source {
18219 SubscriberStreamSource::BasePendingLog { id, filter } => {
18220 #[cfg(feature = "reactive-ws")]
18221 {
18222 let source = SubscriberStreamSource::BasePendingLog {
18223 id,
18224 filter: filter.clone(),
18225 };
18226 let params = base_pending_log_filter(&filter)?;
18227 let stream = provider
18228 .subscribe::<_, Log>(("pendingLogs", params))
18229 .channel_size(channel_size.max(1))
18230 .await
18231 .map_err(provider_error)?
18232 .into_stream()
18233 .map(move |log| SubscriberEvent::BasePendingLog { source_id: id, log });
18234 Ok(stream_with_termination(stream, source))
18235 }
18236 #[cfg(not(feature = "reactive-ws"))]
18237 {
18238 let _ = (id, filter, channel_size);
18239 Err(SubscriberError::Unsupported(
18240 "Base Flashblocks require the reactive-ws feature",
18241 ))
18242 }
18243 }
18244 SubscriberStreamSource::BaseFlashblocks => {
18245 #[cfg(feature = "reactive-ws")]
18246 {
18247 let stream = provider
18248 .subscribe::<_, BaseFlashblockWirePayload>(("newFlashblocks",))
18249 .channel_size(channel_size.max(1))
18250 .await
18251 .map_err(provider_error)?
18252 .into_stream()
18253 .map(SubscriberEvent::BaseFlashblock);
18254 Ok(stream_with_termination(
18255 stream,
18256 SubscriberStreamSource::BaseFlashblocks,
18257 ))
18258 }
18259 #[cfg(not(feature = "reactive-ws"))]
18260 {
18261 let _ = channel_size;
18262 Err(SubscriberError::Unsupported(
18263 "Base Flashblocks require the reactive-ws feature",
18264 ))
18265 }
18266 }
18267 SubscriberStreamSource::OpPendingFlashblocks => {
18268 let first_tick = tokio::time::Instant::now();
18269 let mut interval = tokio::time::interval_at(first_tick, flashblock_poll_interval);
18270 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
18271 let stream = stream::unfold(interval, |mut interval| async move {
18272 interval.tick().await;
18273 Some((SubscriberEvent::OpFlashblockTick, interval))
18274 });
18275 Ok(stream_with_termination(
18276 stream,
18277 SubscriberStreamSource::OpPendingFlashblocks,
18278 ))
18279 }
18280 source => Err(SubscriberError::InvalidConfig(match source {
18281 SubscriberStreamSource::PubSubLog { .. }
18282 | SubscriberStreamSource::CanonicalHeadPolling
18283 | SubscriberStreamSource::PubSubPendingHashes
18284 | SubscriberStreamSource::PubSubBlockHeaders
18285 | SubscriberStreamSource::PollingLog { .. }
18286 | SubscriberStreamSource::PollingPendingHashes => {
18287 "Flashblocks reconnect received a canonical source"
18288 }
18289 SubscriberStreamSource::BasePendingLog { .. }
18290 | SubscriberStreamSource::BaseFlashblocks
18291 | SubscriberStreamSource::OpPendingFlashblocks => unreachable!(),
18292 #[cfg(feature = "raw-flashblocks-json")]
18293 SubscriberStreamSource::ExternalFlashblockUpdates => {
18294 "Flashblocks reconnect cannot own an application-managed source"
18295 }
18296 })),
18297 }
18298}
18299
18300fn aggregate_interests<N: Network>(
18301 base: &[ReactiveInterest<N>],
18302 owned: &[OwnedSubscriberInterests<N>],
18303) -> Vec<ReactiveInterest<N>> {
18304 base.iter()
18305 .cloned()
18306 .chain(
18307 owned
18308 .iter()
18309 .flat_map(|entry| entry.interests.iter().cloned()),
18310 )
18311 .collect()
18312}
18313
18314fn stream_terminated_error(source: &SubscriberStreamSource) -> SubscriberError {
18315 SubscriberError::Provider(format!(
18316 "Alloy subscriber {} stream terminated before the subscriber was stopped",
18317 source.label()
18318 ))
18319}
18320
18321fn reconnect_attempts_exhausted(attempts: usize, config: &SubscriberReconnectConfig) -> bool {
18322 config
18323 .max_attempts
18324 .is_some_and(|max_attempts| attempts >= max_attempts)
18325}
18326
18327fn next_reconnect_delay(current: Duration, max: Duration) -> Duration {
18328 if current.is_zero() {
18329 return current;
18330 }
18331 current.checked_mul(2).unwrap_or(max).min(max)
18332}
18333
18334fn should_dedupe_record<N: Network>(record: &ReactiveInputRecord<N>) -> bool {
18335 match &record.input {
18336 ReactiveInput::Log(log) => {
18337 is_canonical_status(&record.context.chain_status) && !log.removed
18338 }
18339 ReactiveInput::BlockHeader(_) | ReactiveInput::PendingTxHash(_) => true,
18340 ReactiveInput::FullBlock(_) | ReactiveInput::PendingTx(_) => false,
18341 }
18342}
18343
18344#[cfg(test)]
18345mod subscriber_helper_tests {
18346 use super::*;
18347 use alloy_json_rpc::{RequestPacket, ResponsePacket};
18348 use alloy_provider::ProviderBuilder;
18349 use alloy_rpc_client::RpcClient;
18350 use alloy_transport::{TransportError, TransportFut, mock::Asserter};
18351 use std::task::{Context, Poll};
18352 use tower::Service;
18353
18354 #[derive(Clone, Debug)]
18355 struct NeverRespondingTransport;
18356
18357 impl Service<RequestPacket> for NeverRespondingTransport {
18358 type Response = ResponsePacket;
18359 type Error = TransportError;
18360 type Future = TransportFut<'static>;
18361
18362 fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
18363 Poll::Ready(Ok(()))
18364 }
18365
18366 fn call(&mut self, _request: RequestPacket) -> Self::Future {
18367 Box::pin(futures::future::pending())
18368 }
18369 }
18370
18371 fn indexed_flashblock(transaction_hash: B256, state_root: B256) -> BaseFlashblockWirePayload {
18372 BaseFlashblockWirePayload::Indexed(BaseFlashblockPayload {
18373 payload_id: FixedBytes::repeat_byte(0x11),
18374 index: 0,
18375 base: Some(BaseFlashblockBase {
18376 parent_hash: B256::repeat_byte(100),
18377 block_number: 101,
18378 timestamp: 1_700_000_101,
18379 gas_limit: Some(30_000_000),
18380 base_fee_per_gas: Some(7),
18381 beneficiary: Some(Address::repeat_byte(0xcb)),
18382 prevrandao: Some(B256::repeat_byte(0x77)),
18383 }),
18384 diff: BaseFlashblockDiff {
18385 state_root,
18386 block_hash: B256::ZERO,
18387 transactions: vec![serde_json::Value::String(format!("{transaction_hash:#x}"))],
18388 transactions_root: None,
18389 },
18390 metadata: None,
18391 })
18392 }
18393
18394 #[test]
18395 fn duplicate_flashblock_transaction_membership_is_rejected() {
18396 let transaction = format!("{:#x}", B256::repeat_byte(0x41));
18397 let transactions = vec![
18398 serde_json::Value::String(transaction.clone()),
18399 serde_json::Value::String(transaction),
18400 ];
18401 assert!(matches!(
18402 flashblock_transaction_hashes(&transactions),
18403 Err(SubscriberError::Provider(ref message)) if message.contains("duplicate")
18404 ));
18405 }
18406
18407 #[test]
18408 fn conflicting_duplicate_indexed_flashblock_is_rejected() {
18409 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18410 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18411 provider,
18412 SubscriberMode::PubSub,
18413 SubscriberConfig::default(),
18414 )
18415 .with_provider_ref(ProviderRef::new("base-paid", 7));
18416 subscriber.chain_id = Some(8_453);
18417
18418 subscriber
18419 .accept_base_flashblock(indexed_flashblock(
18420 B256::repeat_byte(0x41),
18421 B256::repeat_byte(0xa1),
18422 ))
18423 .expect("first indexed preview");
18424 assert!(matches!(
18425 subscriber.accept_base_flashblock(indexed_flashblock(
18426 B256::repeat_byte(0x42),
18427 B256::repeat_byte(0xa2),
18428 )),
18429 Err(SubscriberError::Provider(ref message))
18430 if message.contains("conflicting duplicate")
18431 ));
18432 }
18433
18434 #[tokio::test]
18435 async fn duplicate_index_with_changed_commitment_is_rejected() {
18436 let transaction = B256::repeat_byte(0x41);
18437 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18438 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18439 provider,
18440 SubscriberMode::PubSub,
18441 SubscriberConfig::default(),
18442 )
18443 .with_provider_ref(ProviderRef::new("base-paid", 7));
18444 subscriber.chain_id = Some(8_453);
18445
18446 subscriber
18447 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(indexed_flashblock(
18448 transaction,
18449 B256::repeat_byte(0xa1),
18450 )))
18451 .await
18452 .expect("first indexed preview");
18453
18454 let BaseFlashblockWirePayload::Indexed(mut conflicting) =
18455 indexed_flashblock(transaction, B256::repeat_byte(0xa1))
18456 else {
18457 unreachable!()
18458 };
18459 conflicting.diff.state_root = B256::repeat_byte(0xbb);
18460 assert!(matches!(
18461 subscriber
18462 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(
18463 BaseFlashblockWirePayload::Indexed(conflicting),
18464 ))
18465 .await,
18466 Err(SubscriberError::Provider(ref message))
18467 if message.contains("conflicting duplicate indexed Flashblock content")
18468 ));
18469 }
18470
18471 #[tokio::test]
18472 async fn indexed_gap_recovery_seeds_later_cumulative_membership() {
18473 let transaction_a = B256::repeat_byte(0x41);
18474 let transaction_b = B256::repeat_byte(0x42);
18475 let transaction_c = B256::repeat_byte(0x43);
18476 let transaction_d = B256::repeat_byte(0x44);
18477 let asserter = Asserter::new();
18478 asserter.push_success(&100_u64);
18479 let pending = rpc_block(101, B256::ZERO).with_transactions(
18480 alloy_network::primitives::BlockTransactions::Hashes(vec![
18481 transaction_a,
18482 transaction_b,
18483 transaction_c,
18484 ]),
18485 );
18486 asserter.push_success(&Some(pending));
18487 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
18488 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18489 provider,
18490 SubscriberMode::PubSub,
18491 SubscriberConfig::default(),
18492 )
18493 .with_provider_ref(ProviderRef::new("base-paid", 7));
18494 subscriber.chain_id = Some(8_453);
18495
18496 subscriber
18497 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(indexed_flashblock(
18498 transaction_a,
18499 B256::repeat_byte(0xa1),
18500 )))
18501 .await
18502 .expect("index zero preview");
18503 let BaseFlashblockWirePayload::Indexed(mut gap) =
18504 indexed_flashblock(transaction_c, B256::repeat_byte(0xa3))
18505 else {
18506 unreachable!()
18507 };
18508 gap.index = 2;
18509 gap.base = None;
18510 gap.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
18511 subscriber
18512 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(
18513 BaseFlashblockWirePayload::Indexed(gap),
18514 ))
18515 .await
18516 .expect("the missing index is recovered from pending state");
18517
18518 let BaseFlashblockWirePayload::Indexed(mut next) =
18519 indexed_flashblock(transaction_d, B256::repeat_byte(0xa4))
18520 else {
18521 unreachable!()
18522 };
18523 next.index = 3;
18524 next.base = None;
18525 next.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
18526 let (next, recover) = subscriber
18527 .accept_base_flashblock(BaseFlashblockWirePayload::Indexed(next))
18528 .expect("the next diff extends the recovered cumulative set");
18529 assert!(!recover);
18530 assert_eq!(
18531 next.transaction_hashes,
18532 vec![transaction_a, transaction_b, transaction_c, transaction_d]
18533 );
18534 }
18535
18536 #[tokio::test]
18537 async fn unrecoverable_indexed_gap_revokes_the_generation() {
18538 let asserter = Asserter::new();
18539 asserter.push_success(&100_u64);
18540 asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(0x64))));
18541 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
18542 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18543 provider,
18544 SubscriberMode::PubSub,
18545 SubscriberConfig {
18546 preconfirmations: PreconfirmationMode::Preferred,
18547 ..SubscriberConfig::default()
18548 },
18549 )
18550 .with_provider_ref(ProviderRef::new("base-paid", 7));
18551 subscriber.chain_id = Some(8_453);
18552
18553 subscriber
18554 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(indexed_flashblock(
18555 B256::repeat_byte(0x41),
18556 B256::repeat_byte(0xa1),
18557 )))
18558 .await
18559 .expect("index zero preview");
18560 let BaseFlashblockWirePayload::Indexed(mut gap) =
18561 indexed_flashblock(B256::repeat_byte(0x43), B256::repeat_byte(0xa3))
18562 else {
18563 unreachable!()
18564 };
18565 gap.index = 2;
18566 gap.base = None;
18567 gap.metadata = Some(BaseFlashblockMetadata { block_number: 101 });
18568 let event = subscriber
18569 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(
18570 BaseFlashblockWirePayload::Indexed(gap),
18571 ))
18572 .await
18573 .expect("preferred mode fails closed without pending recovery")
18574 .expect("generation invalidation is observable");
18575 assert!(matches!(event, SubscriberEvent::FlashblockInvalidated));
18576 assert!(subscriber.latest_preconfirmation.is_none());
18577 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 8);
18578 }
18579
18580 #[test]
18581 fn base_flashblock_wire_decodes_cumulative_block_shape() {
18582 let payload: BaseFlashblockWirePayload = serde_json::from_str(
18583 r#"{
18584 "hash":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
18585 "number":"0x2ef403b",
18586 "parentHash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
18587 "stateRoot":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
18588 "timestamp":"0x6a68dd59",
18589 "transactions":[]
18590 }"#,
18591 )
18592 .expect("decode current Base newFlashblocks shape");
18593 let BaseFlashblockWirePayload::Block(payload) = payload else {
18594 panic!("expected cumulative block-shaped payload")
18595 };
18596 assert_eq!(payload.number, 49_233_979);
18597 assert_eq!(payload.timestamp, 1_785_257_305);
18598 assert_eq!(payload.hash, B256::repeat_byte(0xaa));
18599 assert_eq!(payload.parent_hash, B256::repeat_byte(0xbb));
18600 assert_eq!(payload.state_root, B256::repeat_byte(0xcc));
18601 }
18602
18603 #[tokio::test]
18604 async fn zero_hash_pending_log_waits_for_the_preview_containing_its_transaction() {
18605 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18606 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18607 provider,
18608 SubscriberMode::PubSub,
18609 SubscriberConfig::default(),
18610 )
18611 .with_provider_ref(ProviderRef::new("base-paid", 7));
18612 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
18613 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
18614 local_matcher: None,
18615 route_key: None,
18616 })];
18617 subscriber.interests = subscriber.base_interests.clone();
18618
18619 let first: BaseFlashblockWirePayload = serde_json::from_str(
18620 r#"{
18621 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
18622 "number":"0x65",
18623 "parentHash":"0x6464646464646464646464646464646464646464646464646464646464646464",
18624 "stateRoot":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
18625 "transactionsRoot":"0x1111111111111111111111111111111111111111111111111111111111111111",
18626 "timestamp":"0x6553f165",
18627 "transactions":["0x4141414141414141414141414141414141414141414141414141414141414141"]
18628 }"#,
18629 )
18630 .expect("decode first cumulative preview");
18631 subscriber
18632 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(first))
18633 .await
18634 .expect("first preview is accepted");
18635
18636 let mut second_log = rpc_log(false);
18637 second_log.block_hash = Some(B256::ZERO);
18638 second_log.block_number = Some(102);
18639 second_log.block_timestamp = Some(1_700_000_102);
18640 second_log.transaction_hash = Some(B256::repeat_byte(0x42));
18641 second_log.transaction_index = Some(0);
18642 second_log.log_index = Some(0);
18643
18644 let before_preview = subscriber
18645 .normalize_flashblock_event(SubscriberEvent::BasePendingLog {
18646 source_id: 0,
18647 log: second_log,
18648 })
18649 .await
18650 .expect("a zero-hash log for the next block must be buffered");
18651 assert!(before_preview.is_none());
18652
18653 let second: BaseFlashblockWirePayload = serde_json::from_str(
18654 r#"{
18655 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
18656 "number":"0x66",
18657 "parentHash":"0x6565656565656565656565656565656565656565656565656565656565656565",
18658 "stateRoot":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
18659 "transactionsRoot":"0x2222222222222222222222222222222222222222222222222222222222222222",
18660 "timestamp":"0x6553f166",
18661 "transactions":["0x4242424242424242424242424242424242424242424242424242424242424242"]
18662 }"#,
18663 )
18664 .expect("decode second cumulative preview");
18665 let event = subscriber
18666 .normalize_flashblock_event(SubscriberEvent::BaseFlashblock(second))
18667 .await
18668 .expect("second preview is accepted")
18669 .expect("the matching buffered log is released");
18670 let SubscriberEvent::PreconfirmedLogs { flashblock, logs } = event else {
18671 panic!("expected a preconfirmed log batch")
18672 };
18673 assert_eq!(flashblock.block_number, 102);
18674 assert_ne!(flashblock.content_hash, B256::ZERO);
18675 assert_eq!(flashblock.partial_block_hash, None);
18676 assert_eq!(logs.len(), 1);
18677 assert_eq!(logs[0].transaction_hash, Some(B256::repeat_byte(0x42)));
18678 assert_eq!(logs[0].block_hash, Some(flashblock.content_hash));
18679 }
18680
18681 #[test]
18682 fn flashblock_endpoints_certify_canonical_heads_instead_of_trusting_newheads() {
18683 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18684 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18685 provider,
18686 SubscriberMode::PubSub,
18687 SubscriberConfig {
18688 preconfirmations: PreconfirmationMode::Required,
18689 ..SubscriberConfig::default()
18690 },
18691 )
18692 .with_provider_ref(ProviderRef::new("base-paid", 7));
18693 subscriber.chain_id = Some(8_453);
18694 subscriber.interests = vec![ReactiveInterest::Blocks(BlockInterest::default())];
18695
18696 let sources = subscriber.pubsub_stream_sources();
18697 assert!(
18698 sources
18699 .iter()
18700 .any(|source| matches!(source, SubscriberStreamSource::CanonicalHeadPolling))
18701 );
18702 assert!(
18703 !sources
18704 .iter()
18705 .any(|source| matches!(source, SubscriberStreamSource::PubSubBlockHeaders))
18706 );
18707 }
18708
18709 #[test]
18710 #[cfg(feature = "raw-flashblocks-json")]
18711 fn external_flashblocks_keep_normal_canonical_pubsub_sources_on_any_chain() {
18712 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18713 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18714 provider,
18715 SubscriberMode::PubSub,
18716 SubscriberConfig {
18717 preconfirmations: PreconfirmationMode::Required,
18718 ..SubscriberConfig::default()
18719 },
18720 );
18721 subscriber
18722 .configure_external_flashblock_updates(ProviderRef::new("raw-json", 4))
18723 .expect("configure external source");
18724 subscriber.chain_id = Some(1);
18725 subscriber.base_interests = vec![
18726 ReactiveInterest::Blocks(BlockInterest::default()),
18727 log_interest_matching_rpc_log(),
18728 ];
18729 subscriber.interests = subscriber.base_interests.clone();
18730
18731 let pubsub = subscriber.pubsub_stream_sources();
18732 assert!(
18733 pubsub
18734 .iter()
18735 .any(|source| matches!(source, SubscriberStreamSource::PubSubBlockHeaders))
18736 );
18737 assert!(
18738 pubsub
18739 .iter()
18740 .any(|source| matches!(source, SubscriberStreamSource::PubSubLog { .. }))
18741 );
18742 assert!(pubsub.iter().all(|source| !matches!(
18743 source,
18744 SubscriberStreamSource::BaseFlashblocks
18745 | SubscriberStreamSource::BasePendingLog { .. }
18746 | SubscriberStreamSource::OpPendingFlashblocks
18747 | SubscriberStreamSource::CanonicalHeadPolling
18748 )));
18749 assert!(
18750 subscriber
18751 .polling_stream_sources()
18752 .iter()
18753 .all(|source| { !matches!(source, SubscriberStreamSource::OpPendingFlashblocks) })
18754 );
18755 assert!(
18756 subscriber
18757 .capabilities()
18758 .supports(SubscriberCapability::Preconfirmations)
18759 );
18760 }
18761
18762 #[test]
18763 #[cfg(feature = "raw-flashblocks-json")]
18764 fn external_flashblocks_are_rejected_when_preconfirmations_are_disabled() {
18765 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18766 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18767 provider,
18768 SubscriberMode::PubSub,
18769 SubscriberConfig::default(),
18770 );
18771 subscriber
18772 .configure_external_flashblock_updates(ProviderRef::new("raw-json", 4))
18773 .expect("configure external source");
18774 subscriber.chain_id = Some(1);
18775
18776 assert!(matches!(
18777 subscriber.validate_flashblocks_setup(),
18778 Err(SubscriberError::InvalidConfig(message))
18779 if message.contains("require preconfirmations")
18780 ));
18781 }
18782
18783 #[tokio::test]
18784 #[cfg(feature = "raw-flashblocks-json")]
18785 async fn external_flashblocks_configuration_is_rejected_after_registration_starts() {
18786 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18787 let mut fresh = AlloySubscriber::<_, Ethereum>::new(
18788 provider,
18789 SubscriberMode::PubSub,
18790 SubscriberConfig {
18791 preconfirmations: PreconfirmationMode::Preferred,
18792 ..SubscriberConfig::default()
18793 },
18794 );
18795 fresh
18796 .configure_external_flashblock_updates(ProviderRef::new("raw-json", 4))
18797 .expect("construction-time external source");
18798
18799 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18800 let mut started = AlloySubscriber::<_, Ethereum>::new(
18801 provider,
18802 SubscriberMode::PubSub,
18803 SubscriberConfig {
18804 preconfirmations: PreconfirmationMode::Preferred,
18805 ..SubscriberConfig::default()
18806 },
18807 )
18808 .with_provider_ref(ProviderRef::new("canonical", 3));
18809 started.chain_id = Some(8_453);
18810 started
18811 .register_interests(&[log_interest_matching_rpc_log()])
18812 .await
18813 .expect("register canonical topology");
18814
18815 assert!(matches!(
18816 started.configure_external_flashblock_updates(ProviderRef::new("raw-json", 4)),
18817 Err(SubscriberError::InvalidConfig(message))
18818 if message.contains("before subscriber registration")
18819 ));
18820 }
18821
18822 #[tokio::test]
18823 #[cfg(feature = "raw-flashblocks-json")]
18824 async fn external_flashblocks_preflight_performs_no_flashblocks_rpc() {
18825 let asserter = Asserter::new();
18826 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
18827 let source = ProviderRef::new("raw-json", 4);
18828 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18829 provider,
18830 SubscriberMode::PubSub,
18831 SubscriberConfig {
18832 preconfirmations: PreconfirmationMode::Required,
18833 ..SubscriberConfig::default()
18834 },
18835 );
18836 subscriber
18837 .configure_external_flashblock_updates(source.clone())
18838 .expect("configure external source");
18839 subscriber.chain_id = Some(1);
18840 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18841 subscriber.interests = subscriber.base_interests.clone();
18842 let desired = subscriber.pubsub_stream_sources();
18843 let mut streams = SubscriberStreams::new();
18844 for source in desired {
18845 streams.push(source, stream::pending().boxed());
18846 }
18847 subscriber.state = AlloySubscriberState::Active(streams);
18848 subscriber.sources_dirty = false;
18849
18850 let preflight = subscriber
18851 .establish_flashblocks_preflight(1)
18852 .await
18853 .expect("external source preflight");
18854 assert_eq!(preflight.provider(), &source);
18855 assert_eq!(preflight.delivery(), FlashblocksDelivery::ExternalUpdates);
18856 assert_eq!(preflight.pending_log_subscriptions(), 0);
18857 assert_eq!(subscriber.flashblocks_rpc_metrics().total_requests(), 0);
18858 assert!(asserter.read_q().is_empty());
18859 }
18860
18861 #[tokio::test]
18862 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
18863 async fn bounded_external_channel_survives_subscriber_move_and_closure_keeps_canonical_stream()
18864 {
18865 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
18866 let source = ProviderRef::new("raw-json", 4);
18867 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
18868 provider,
18869 SubscriberMode::PubSub,
18870 SubscriberConfig {
18871 preconfirmations: PreconfirmationMode::Preferred,
18872 ..SubscriberConfig::default()
18873 },
18874 );
18875 subscriber
18876 .configure_external_flashblock_updates(source.clone())
18877 .expect("configure external source");
18878 subscriber.chain_id = Some(1);
18879 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
18880 subscriber.interests = subscriber.base_interests.clone();
18881 let filter = subscriber.log_stream_filters().remove(0);
18882 let source_id = subscriber.log_source_id(&filter);
18883 let mut streams = SubscriberStreams::new();
18884 streams.push(
18885 SubscriberStreamSource::PubSubLog {
18886 id: source_id,
18887 filter,
18888 },
18889 stream::pending().boxed(),
18890 );
18891 subscriber.state = AlloySubscriberState::Active(streams);
18892 subscriber.sources_dirty = false;
18893
18894 let sender = subscriber
18895 .open_external_flashblock_update_channel(2)
18896 .expect("bounded external queue");
18897 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
18898 let update_stream = subscriber
18899 .connect_source_stream(external.clone())
18900 .await
18901 .expect("attach receiver as subscriber source");
18902 subscriber.install_source_stream(external, update_stream);
18903 subscriber.sources_dirty = false;
18904
18905 let mut adapter = RawJsonFlashblocksAdapter::new(source);
18906 let frame = br#"{
18907 "payload_id":"0x1111111111111111",
18908 "index":0,
18909 "base":{
18910 "parent_hash":"0x0606060606060606060606060606060606060606060606060606060606060606",
18911 "block_number":"0x7",
18912 "timestamp":"0x6553f107"
18913 },
18914 "diff":{
18915 "state_root":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
18916 "block_hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
18917 "transactions":["0x01"]
18918 },
18919 "metadata":{
18920 "block_number":7,
18921 "receipts":{
18922 "0x5fe7f977e71dba2ea1a68e21057beebb9be2ac30c6410aa38d4f3fbe41dcffd2":{
18923 "logs":[{
18924 "address":"0x4242424242424242424242424242424242424242",
18925 "topics":["0x0101010101010101010101010101010101010101010101010101010101010101"],
18926 "data":"0x"
18927 }]
18928 }
18929 }
18930 }
18931 }"#;
18932 let update = adapter
18933 .ingest_json(frame)
18934 .expect("valid raw update")
18935 .expect("snapshot update");
18936 let valid_update = update.clone();
18937 let sending = {
18938 let sender = sender.clone();
18939 tokio::spawn(async move { sender.send(update).await })
18940 };
18941
18942 let preview = subscriber
18943 .next_scoped_batch()
18944 .await
18945 .expect("poll preview")
18946 .expect("preview batch");
18947 assert_eq!(preview.records().len(), 1);
18948 assert!(preview.records()[0].scope().is_preconfirmed());
18949 assert_eq!(
18950 preview.records()[0].context.source,
18951 InputSource::Flashblocks
18952 );
18953 assert!(subscriber.latest_preconfirmation.is_some());
18954 assert_eq!(sending.await.expect("sender task"), Ok(()));
18955
18956 let mut invalid_update = valid_update.clone();
18957 let FlashblockUpdate::Snapshot(snapshot) = &mut invalid_update else {
18958 unreachable!("fixture is a snapshot")
18959 };
18960 snapshot.logs[0].block_hash = Some(B256::repeat_byte(0xee));
18961 let rejecting = {
18962 let sender = sender.clone();
18963 tokio::spawn(async move { sender.send(invalid_update).await })
18964 };
18965 let rejected = subscriber
18966 .next_scoped_batch()
18967 .await
18968 .expect("preferred mode keeps polling")
18969 .expect("rejected update invalidation");
18970 assert!(rejected.preconfirmation_invalidated());
18971 assert!(rejected.records().is_empty());
18972 assert!(subscriber.latest_preconfirmation.is_none());
18973 assert_eq!(
18974 rejecting.await.expect("sender task"),
18975 Err(FlashblockUpdateChannelError::Rejected)
18976 );
18977 subscriber
18978 .ingest_flashblock_update(valid_update)
18979 .expect("rejected generation is ignored thereafter");
18980 assert!(subscriber.latest_preconfirmation.is_none());
18981
18982 let _reset = adapter
18983 .reset(ProviderRef::new("raw-json", 5))
18984 .expect("advance rejected source generation");
18985 let recovered_update = adapter
18986 .ingest_json(frame)
18987 .expect("valid replacement generation")
18988 .expect("replacement snapshot update");
18989 let recovering = {
18990 let sender = sender.clone();
18991 tokio::spawn(async move { sender.send(recovered_update).await })
18992 };
18993 let recovered = subscriber
18994 .next_scoped_batch()
18995 .await
18996 .expect("poll replacement generation")
18997 .expect("replacement preview batch");
18998 assert_eq!(recovered.records().len(), 1);
18999 assert!(matches!(
19000 &recovered.records()[0].context.chain_status,
19001 ChainStatus::Preconfirmed { flashblock }
19002 if flashblock.provider == ProviderRef::new("raw-json", 5)
19003 ));
19004 assert_eq!(recovering.await.expect("sender task"), Ok(()));
19005
19006 drop(sender);
19007 let invalidation = subscriber
19008 .next_scoped_batch()
19009 .await
19010 .expect("poll channel closure")
19011 .expect("closure invalidation");
19012 assert!(invalidation.preconfirmation_invalidated());
19013 assert!(invalidation.records().is_empty());
19014 assert!(subscriber.latest_preconfirmation.is_none());
19015 assert!(matches!(
19016 &subscriber.state,
19017 AlloySubscriberState::Active(streams)
19018 if streams.entries.iter().any(|entry| matches!(
19019 entry.source,
19020 SubscriberStreamSource::PubSubLog { id, .. } if id == source_id
19021 ))
19022 ));
19023 }
19024
19025 #[tokio::test]
19026 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
19027 async fn required_external_channel_closure_fails_the_subscriber_closed() {
19028 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19029 let source = ProviderRef::new("raw-json", 4);
19030 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19031 provider,
19032 SubscriberMode::PubSub,
19033 SubscriberConfig {
19034 preconfirmations: PreconfirmationMode::Required,
19035 ..SubscriberConfig::default()
19036 },
19037 );
19038 subscriber
19039 .configure_external_flashblock_updates(source)
19040 .expect("configure external source");
19041 subscriber.chain_id = Some(1);
19042 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19043 subscriber.interests = subscriber.base_interests.clone();
19044 subscriber.state = AlloySubscriberState::Active(SubscriberStreams::new());
19045 subscriber.sources_dirty = false;
19046
19047 let sender = subscriber
19048 .open_external_flashblock_update_channel(1)
19049 .expect("bounded external queue");
19050 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
19051 let update_stream = subscriber
19052 .connect_source_stream(external.clone())
19053 .await
19054 .expect("attach receiver as subscriber source");
19055 subscriber.install_source_stream(external, update_stream);
19056 subscriber.sources_dirty = false;
19057 drop(sender);
19058
19059 assert!(matches!(
19060 subscriber.next_scoped_batch().await,
19061 Err(SubscriberError::Provider(ref message))
19062 if message.contains("required external Flashblock update channel closed")
19063 ));
19064 }
19065
19066 #[tokio::test]
19067 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
19068 async fn required_external_channel_rejects_a_queued_malformed_update() {
19069 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19070 let source = ProviderRef::new("raw-json", 4);
19071 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19072 provider,
19073 SubscriberMode::PubSub,
19074 SubscriberConfig {
19075 preconfirmations: PreconfirmationMode::Required,
19076 ..SubscriberConfig::default()
19077 },
19078 );
19079 subscriber
19080 .configure_external_flashblock_updates(source.clone())
19081 .expect("configure external source");
19082 subscriber.chain_id = Some(1);
19083 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19084 subscriber.interests = subscriber.base_interests.clone();
19085 subscriber.state = AlloySubscriberState::Active(SubscriberStreams::new());
19086 subscriber.sources_dirty = false;
19087
19088 let sender = subscriber
19089 .open_external_flashblock_update_channel(1)
19090 .expect("bounded external queue");
19091 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
19092 let update_stream = subscriber
19093 .connect_source_stream(external.clone())
19094 .await
19095 .expect("attach receiver as subscriber source");
19096 subscriber.install_source_stream(external, update_stream);
19097 subscriber.sources_dirty = false;
19098
19099 let mut adapter = RawJsonFlashblocksAdapter::new(source);
19100 let mut update = adapter
19101 .ingest_json(
19102 br#"{
19103 "payload_id":"0x1111111111111111",
19104 "index":0,
19105 "base":{
19106 "parent_hash":"0x0606060606060606060606060606060606060606060606060606060606060606",
19107 "block_number":"0x7",
19108 "timestamp":"0x6553f107"
19109 },
19110 "diff":{
19111 "state_root":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
19112 "block_hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
19113 "transactions":[]
19114 },
19115 "metadata":{"block_number":7,"receipts":{}}
19116 }"#,
19117 )
19118 .expect("valid raw frame")
19119 .expect("snapshot update");
19120 let FlashblockUpdate::Snapshot(snapshot) = &mut update else {
19121 unreachable!("fixture is a snapshot")
19122 };
19123 snapshot.flashblock.content_hash = B256::ZERO;
19124 let sending = tokio::spawn(async move { sender.send(update).await });
19125
19126 assert!(matches!(
19127 subscriber.next_scoped_batch().await,
19128 Err(SubscriberError::Provider(ref message))
19129 if message.contains("content commitment is invalid")
19130 ));
19131 assert_eq!(
19132 sending.await.expect("sender task"),
19133 Err(FlashblockUpdateChannelError::Rejected)
19134 );
19135 }
19136
19137 #[tokio::test]
19138 #[cfg(all(feature = "raw-flashblocks-json", feature = "reactive-ws"))]
19139 async fn bounded_external_channel_reports_capacity_rejection_and_accepts_a_new_generation() {
19140 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19141 let source = ProviderRef::new("raw-json", 4);
19142 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19143 provider,
19144 SubscriberMode::PubSub,
19145 SubscriberConfig {
19146 preconfirmations: PreconfirmationMode::Preferred,
19147 max_pending_records: 1,
19148 ..SubscriberConfig::default()
19149 },
19150 );
19151 subscriber
19152 .configure_external_flashblock_updates(source.clone())
19153 .expect("configure external source");
19154 subscriber.chain_id = Some(1);
19155 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19156 subscriber.interests = subscriber.base_interests.clone();
19157 subscriber.state = AlloySubscriberState::Active(SubscriberStreams::new());
19158 subscriber.sources_dirty = false;
19159
19160 let sender = subscriber
19161 .open_external_flashblock_update_channel(1)
19162 .expect("bounded external queue");
19163 let external = SubscriberStreamSource::ExternalFlashblockUpdates;
19164 let update_stream = subscriber
19165 .connect_source_stream(external.clone())
19166 .await
19167 .expect("attach receiver as subscriber source");
19168 subscriber.install_source_stream(external, update_stream);
19169 subscriber.sources_dirty = false;
19170
19171 let first_frame = br#"{
19172 "payload_id":"0x1111111111111111",
19173 "index":0,
19174 "base":{
19175 "parent_hash":"0x0606060606060606060606060606060606060606060606060606060606060606",
19176 "block_number":"0x7",
19177 "timestamp":"0x6553f107"
19178 },
19179 "diff":{
19180 "state_root":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
19181 "block_hash":"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
19182 "transactions":["0x01"]
19183 },
19184 "metadata":{
19185 "block_number":7,
19186 "receipts":{
19187 "0x5fe7f977e71dba2ea1a68e21057beebb9be2ac30c6410aa38d4f3fbe41dcffd2":{
19188 "logs":[{
19189 "address":"0x4242424242424242424242424242424242424242",
19190 "topics":["0x0101010101010101010101010101010101010101010101010101010101010101"],
19191 "data":"0x"
19192 }]
19193 }
19194 }
19195 }
19196 }"#;
19197 let second_frame = br#"{
19198 "payload_id":"0x1111111111111111",
19199 "index":1,
19200 "diff":{
19201 "state_root":"0xabababababababababababababababababababababababababababababababab",
19202 "block_hash":"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
19203 "transactions":["0x02"]
19204 },
19205 "metadata":{
19206 "block_number":7,
19207 "receipts":{
19208 "0xf2ee15ea639b73fa3db9b34a245bdfa015c260c598b211bf05a1ecc4b3e3b4f2":{
19209 "logs":[
19210 {"address":"0x4444444444444444444444444444444444444444","topics":[],"data":"0x"},
19211 {"address":"0x4545454545454545454545454545454545454545","topics":[],"data":"0x"}
19212 ]
19213 }
19214 }
19215 }
19216 }"#;
19217 let mut adapter = RawJsonFlashblocksAdapter::new(source);
19218 let first = adapter
19219 .ingest_json(first_frame)
19220 .expect("valid first frame")
19221 .expect("first snapshot");
19222 let first_send = {
19223 let sender = sender.clone();
19224 tokio::spawn(async move { sender.send(first).await })
19225 };
19226 let first_batch = subscriber
19227 .next_scoped_batch()
19228 .await
19229 .expect("poll first preview")
19230 .expect("first preview batch");
19231 assert_eq!(first_batch.records().len(), 1);
19232 assert_eq!(first_send.await.expect("sender task"), Ok(()));
19233
19234 let oversized = adapter
19235 .ingest_json(second_frame)
19236 .expect("valid oversized delta")
19237 .expect("oversized standardized snapshot");
19238 let rejected_send = {
19239 let sender = sender.clone();
19240 tokio::spawn(async move { sender.send(oversized).await })
19241 };
19242 let invalidation = subscriber
19243 .next_scoped_batch()
19244 .await
19245 .expect("poll capacity rejection")
19246 .expect("capacity invalidation batch");
19247 assert!(invalidation.preconfirmation_invalidated());
19248 assert_eq!(
19249 rejected_send.await.expect("sender task"),
19250 Err(FlashblockUpdateChannelError::Rejected)
19251 );
19252 assert_eq!(subscriber.rejected_external_flashblock_generation, None);
19253
19254 let _ = adapter
19255 .reset(ProviderRef::new("raw-json", 5))
19256 .expect("advance after local capacity rejection");
19257 let recovered = adapter
19258 .ingest_json(first_frame)
19259 .expect("valid recovered frame")
19260 .expect("recovered snapshot");
19261 let recovered_send = {
19262 let sender = sender.clone();
19263 tokio::spawn(async move { sender.send(recovered).await })
19264 };
19265 let recovered_batch = subscriber
19266 .next_scoped_batch()
19267 .await
19268 .expect("poll recovered generation")
19269 .expect("recovered preview batch");
19270 assert_eq!(recovered_batch.records().len(), 1);
19271 assert!(matches!(
19272 &recovered_batch.records()[0].context.chain_status,
19273 ChainStatus::Preconfirmed { flashblock }
19274 if flashblock.provider == ProviderRef::new("raw-json", 5)
19275 ));
19276 assert_eq!(recovered_send.await.expect("sender task"), Ok(()));
19277 }
19278
19279 #[tokio::test]
19280 async fn certified_canonical_heads_are_deduplicated_and_reject_placeholder_hashes() {
19281 let asserter = Asserter::new();
19282 let certified = rpc_block(101, B256::repeat_byte(0x65));
19283 asserter.push_success(&Some(certified.clone()));
19284 asserter.push_success(&Some(certified));
19285 asserter.push_success(&Some(rpc_block(102, B256::ZERO)));
19286 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
19287 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19288 provider,
19289 SubscriberMode::PubSub,
19290 SubscriberConfig::default(),
19291 );
19292
19293 assert!(matches!(
19294 subscriber
19295 .fetch_certified_canonical_head()
19296 .await
19297 .expect("first certified head"),
19298 Some(SubscriberEvent::BlockHeader(_))
19299 ));
19300 assert!(
19301 subscriber
19302 .fetch_certified_canonical_head()
19303 .await
19304 .expect("duplicate certified head")
19305 .is_none()
19306 );
19307 assert!(matches!(
19308 subscriber.fetch_certified_canonical_head().await,
19309 Err(SubscriberError::Provider(ref message))
19310 if message.contains("placeholder hash")
19311 ));
19312 }
19313
19314 #[tokio::test]
19315 async fn canonical_head_certification_times_out_a_silent_provider() {
19316 let provider =
19317 ProviderBuilder::new().connect_client(RpcClient::new(NeverRespondingTransport, true));
19318 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19319 provider,
19320 SubscriberMode::PubSub,
19321 SubscriberConfig {
19322 preconfirmations: PreconfirmationMode::Required,
19323 canonical_head_request_timeout: Duration::from_millis(10),
19324 ..SubscriberConfig::default()
19325 },
19326 );
19327 subscriber.chain_id = Some(8_453);
19328
19329 let result = tokio::time::timeout(
19330 Duration::from_millis(100),
19331 subscriber.fetch_certified_canonical_head(),
19332 )
19333 .await
19334 .expect("subscriber must bound a silent provider request");
19335 assert!(matches!(
19336 result,
19337 Err(SubscriberError::Provider(ref message))
19338 if message.contains("canonical head certification timed out")
19339 ));
19340 }
19341
19342 #[tokio::test]
19343 async fn optimism_canonical_head_is_the_exact_parent_of_pending() {
19344 let asserter = Asserter::new();
19345 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
19346 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19347 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19348 provider,
19349 SubscriberMode::PubSub,
19350 SubscriberConfig {
19351 preconfirmations: PreconfirmationMode::Required,
19352 ..SubscriberConfig::default()
19353 },
19354 );
19355 subscriber.chain_id = Some(10);
19356 subscriber.interests = vec![ReactiveInterest::Blocks(BlockInterest::default())];
19357
19358 let event = subscriber
19359 .fetch_certified_canonical_head()
19360 .await
19361 .expect("OP pending parent can be certified")
19362 .expect("the first certified parent is emitted");
19363 let SubscriberEvent::BlockHeader(header) = event else {
19364 panic!("expected a certified canonical block header")
19365 };
19366 assert_eq!(header.number(), 100);
19367 assert_eq!(header.hash, B256::repeat_byte(0x64));
19368 assert_eq!(
19369 subscriber
19370 .flashblocks_rpc_metrics()
19371 .pending_block_requests(),
19372 1
19373 );
19374 assert_eq!(
19375 subscriber
19376 .flashblocks_rpc_metrics()
19377 .canonical_head_requests(),
19378 1
19379 );
19380 assert!(asserter.read_q().is_empty());
19381 }
19382
19383 #[test]
19384 fn optimism_uses_one_bounded_pending_state_stream() {
19385 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19386 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19387 provider,
19388 SubscriberMode::PubSub,
19389 SubscriberConfig {
19390 preconfirmations: PreconfirmationMode::Required,
19391 ..SubscriberConfig::default()
19392 },
19393 )
19394 .with_provider_ref(ProviderRef::new("op-paid", 11));
19395 subscriber.chain_id = Some(10);
19396 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
19397 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
19398 local_matcher: None,
19399 route_key: None,
19400 })];
19401 subscriber.interests = subscriber.base_interests.clone();
19402
19403 let sources = subscriber.pubsub_stream_sources();
19404 assert_eq!(
19405 sources
19406 .iter()
19407 .filter(|source| matches!(source, SubscriberStreamSource::OpPendingFlashblocks))
19408 .count(),
19409 1
19410 );
19411 assert!(sources.iter().all(|source| !matches!(
19412 source,
19413 SubscriberStreamSource::BaseFlashblocks | SubscriberStreamSource::BasePendingLog { .. }
19414 )));
19415 }
19416
19417 #[test]
19418 fn optimism_default_receipt_budget_reserves_every_fixed_sampler_method() {
19419 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
19420 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19421 provider,
19422 SubscriberMode::PubSub,
19423 SubscriberConfig {
19424 preconfirmations: PreconfirmationMode::Required,
19425 ..SubscriberConfig::default()
19426 },
19427 );
19428 subscriber.chain_id = Some(10);
19429 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19430 subscriber.interests = subscriber.base_interests.clone();
19431
19432 assert_eq!(
19436 subscriber.pending_receipt_requests_per_second_capacity(),
19437 28
19438 );
19439 assert_eq!(subscriber.pending_receipt_requests_per_tick_capacity(), 7);
19440
19441 subscriber
19442 .interests
19443 .push(ReactiveInterest::Blocks(BlockInterest::default()));
19444 assert_eq!(
19445 subscriber.pending_receipt_requests_per_second_capacity(),
19446 24
19447 );
19448 assert_eq!(subscriber.pending_receipt_requests_per_tick_capacity(), 6);
19449 subscriber.interests.pop();
19450
19451 for _ in 0..4 {
19452 assert!(subscriber.reserve_flashblock_rpc_methods(3));
19453 assert_eq!(subscriber.pending_receipt_request_allowance(), 7);
19454 assert!(subscriber.reserve_flashblock_rpc_methods(7));
19455 }
19456 assert!(!subscriber.reserve_flashblock_rpc_methods(1));
19457 subscriber.reset_flashblock_tracking();
19458 assert!(
19459 !subscriber.reserve_flashblock_rpc_methods(1),
19460 "a reconnect must not reset an endpoint's rolling quota window"
19461 );
19462 }
19463
19464 #[test]
19465 fn flashblocks_config_rejects_a_zero_rpc_budget() {
19466 let config = SubscriberConfig {
19467 preconfirmations: PreconfirmationMode::Required,
19468 max_flashblock_rpc_requests_per_second: 0,
19469 ..SubscriberConfig::default()
19470 };
19471
19472 assert!(matches!(
19473 validate_subscriber_config(&config),
19474 Err(SubscriberError::InvalidConfig(
19475 "SubscriberConfig::max_flashblock_rpc_requests_per_second must be greater than zero"
19476 ))
19477 ));
19478 }
19479
19480 #[test]
19481 fn flashblocks_config_rejects_a_zero_canonical_head_request_timeout() {
19482 let config = SubscriberConfig {
19483 preconfirmations: PreconfirmationMode::Required,
19484 canonical_head_request_timeout: Duration::ZERO,
19485 ..SubscriberConfig::default()
19486 };
19487
19488 assert!(matches!(
19489 validate_subscriber_config(&config),
19490 Err(SubscriberError::InvalidConfig(
19491 "SubscriberConfig::canonical_head_request_timeout must be greater than zero"
19492 ))
19493 ));
19494 }
19495
19496 #[tokio::test]
19497 async fn optimism_preflight_rejects_a_budget_without_receipt_capacity() {
19498 let asserter = Asserter::new();
19499 asserter.push_success(&serde_json::json!(["flashblocksv1"]));
19500 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19501 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19502 provider,
19503 SubscriberMode::PubSub,
19504 SubscriberConfig {
19505 preconfirmations: PreconfirmationMode::Required,
19506 max_flashblock_rpc_requests_per_second: 15,
19509 ..SubscriberConfig::default()
19510 },
19511 )
19512 .with_provider_ref(ProviderRef::new("op-paid", 12));
19513 subscriber.chain_id = Some(10);
19514 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19515 subscriber.interests = subscriber.base_interests.clone();
19516 let desired = subscriber.pubsub_stream_sources();
19517 let mut streams = SubscriberStreams::new();
19518 for source in desired {
19519 streams.push(source, stream::pending().boxed());
19520 }
19521 subscriber.state = AlloySubscriberState::Active(streams);
19522 subscriber.sources_dirty = false;
19523 assert!(matches!(
19524 subscriber.establish_flashblocks_preflight(10).await,
19525 Err(SubscriberError::InvalidConfig(message))
19526 if message.contains("leaves no capacity for OP transaction receipts")
19527 ));
19528 assert!(asserter.read_q().is_empty());
19529 }
19530
19531 #[cfg(feature = "reactive-ws")]
19532 #[tokio::test]
19533 async fn flashblocks_preflight_proves_chain_and_both_subscription_lanes() {
19534 let asserter = Asserter::new();
19535 asserter.push_success(&serde_json::json!({"flashblocks": true}));
19536 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
19537 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19538 provider,
19539 SubscriberMode::PubSub,
19540 SubscriberConfig {
19541 preconfirmations: PreconfirmationMode::Required,
19542 ..SubscriberConfig::default()
19543 },
19544 )
19545 .with_provider_ref(ProviderRef::new("base-paid", 7));
19546 subscriber.chain_id = Some(8_453);
19547 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19548 subscriber.interests = subscriber.base_interests.clone();
19549 let desired = subscriber.pubsub_stream_sources();
19550 let mut streams = SubscriberStreams::new();
19551 for source in desired {
19552 streams.push(source, stream::pending().boxed());
19553 }
19554 subscriber.state = AlloySubscriberState::Active(streams);
19555 subscriber.sources_dirty = false;
19556
19557 let preflight = subscriber
19558 .establish_flashblocks_preflight(8_453)
19559 .await
19560 .expect("preflight succeeds");
19561
19562 assert_eq!(preflight.chain_id(), 8_453);
19563 assert_eq!(preflight.provider(), &ProviderRef::new("base-paid", 7));
19564 assert_eq!(
19565 preflight.delivery(),
19566 FlashblocksDelivery::NativeSubscriptions
19567 );
19568 assert_eq!(preflight.pending_log_subscriptions(), 1);
19569 assert_eq!(preflight.pending_log_filters(), 1);
19570 assert_eq!(
19571 preflight.advertised_capabilities(),
19572 Some(&serde_json::json!({"flashblocks": true}))
19573 );
19574 }
19575
19576 #[tokio::test]
19577 async fn optimism_preflight_probes_pending_state_without_native_subscriptions() {
19578 let asserter = Asserter::new();
19579 asserter.push_success(&serde_json::json!(["flashblocksv1"]));
19580 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
19581 asserter.push_success(&Vec::<Log>::new());
19582 asserter.push_success(&serde_json::json!([]));
19583 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19584 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19585 provider,
19586 SubscriberMode::PubSub,
19587 SubscriberConfig {
19588 preconfirmations: PreconfirmationMode::Required,
19589 ..SubscriberConfig::default()
19590 },
19591 )
19592 .with_provider_ref(ProviderRef::new("op-paid", 12));
19593 subscriber.chain_id = Some(10);
19594 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19595 subscriber.interests = subscriber.base_interests.clone();
19596 let desired = subscriber.pubsub_stream_sources();
19597 let mut streams = SubscriberStreams::new();
19598 for source in desired {
19599 streams.push(source, stream::pending().boxed());
19600 }
19601 subscriber.state = AlloySubscriberState::Active(streams);
19602 subscriber.sources_dirty = false;
19603
19604 let preflight = subscriber
19605 .establish_flashblocks_preflight(10)
19606 .await
19607 .expect("Optimism pending-state preflight succeeds");
19608
19609 assert_eq!(preflight.chain_id(), 10);
19610 assert_eq!(preflight.provider(), &ProviderRef::new("op-paid", 12));
19611 assert_eq!(
19612 preflight.delivery(),
19613 FlashblocksDelivery::PendingStatePolling
19614 );
19615 assert_eq!(preflight.pending_log_subscriptions(), 0);
19616 assert_eq!(preflight.pending_log_filters(), 1);
19617 assert_eq!(
19618 preflight.advertised_capabilities(),
19619 Some(&serde_json::json!(["flashblocksv1"]))
19620 );
19621 assert!(asserter.read_q().is_empty());
19622 }
19623
19624 #[test]
19625 fn optimism_full_pending_block_normalizes_op_transaction_types_to_hashes() {
19626 let transaction_hash = B256::repeat_byte(0x7e);
19627 let mut value = serde_json::to_value(rpc_block(101, B256::ZERO))
19628 .expect("serialize pending block fixture");
19629 value["transactions"] = serde_json::json!([{
19630 "type": "0x7e",
19631 "hash": transaction_hash,
19632 "sourceHash": B256::repeat_byte(0x11),
19633 "from": Address::repeat_byte(0x22),
19634 "to": Address::repeat_byte(0x33)
19635 }]);
19636
19637 let block = normalize_op_pending_block::<Ethereum>(value)
19638 .expect("OP-specific transaction bodies are reduced to hashes");
19639
19640 assert_eq!(
19641 block.transactions().as_hashes(),
19642 Some(&[transaction_hash][..])
19643 );
19644 }
19645
19646 #[tokio::test]
19647 async fn optimism_sampler_does_not_retry_malformed_pending_content() {
19648 let asserter = Asserter::new();
19649 let mut pending = serde_json::to_value(rpc_block(101, B256::ZERO))
19650 .expect("serialize pending block fixture");
19651 pending["transactions"] = serde_json::json!([{"type": "0x7e"}]);
19652 asserter.push_success(&Some(pending));
19653 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19654 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19655 provider,
19656 SubscriberMode::PubSub,
19657 SubscriberConfig {
19658 preconfirmations: PreconfirmationMode::Required,
19659 ..SubscriberConfig::default()
19660 },
19661 )
19662 .with_provider_ref(ProviderRef::new("op-paid", 12));
19663 subscriber.chain_id = Some(10);
19664
19665 let error = match subscriber
19666 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19667 .await
19668 {
19669 Err(error) => error,
19670 Ok(_) => panic!("malformed provider content must fail immediately"),
19671 };
19672 assert!(
19673 error
19674 .to_string()
19675 .contains("transaction is missing its hash")
19676 );
19677 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
19678 assert!(asserter.read_q().is_empty());
19679 }
19680
19681 #[tokio::test]
19682 async fn optimism_sampler_certifies_the_pending_block_by_exact_parent_hash() {
19683 let asserter = Asserter::new();
19684 queue_op_pending(&asserter, rpc_block(101, B256::ZERO));
19685 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
19686 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19687 provider,
19688 SubscriberMode::PubSub,
19689 SubscriberConfig {
19690 preconfirmations: PreconfirmationMode::Required,
19691 ..SubscriberConfig::default()
19692 },
19693 )
19694 .with_provider_ref(ProviderRef::new("op-paid", 12));
19695 subscriber.chain_id = Some(10);
19696
19697 assert!(
19698 subscriber
19699 .fetch_pending_flashblock(None)
19700 .await
19701 .expect("the exact parent certifies the pending payload")
19702 .is_some()
19703 );
19704 }
19705
19706 #[tokio::test]
19707 async fn optimism_sampler_rejects_a_nonconsecutive_pending_parent() {
19708 let asserter = Asserter::new();
19709 asserter.push_success(&Some(rpc_block(101, B256::ZERO)));
19710 asserter.push_success(&Some(rpc_block(99, B256::repeat_byte(0x64))));
19711 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19712 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19713 provider,
19714 SubscriberMode::PubSub,
19715 SubscriberConfig {
19716 preconfirmations: PreconfirmationMode::Required,
19717 ..SubscriberConfig::default()
19718 },
19719 )
19720 .with_provider_ref(ProviderRef::new("op-paid", 12));
19721 subscriber.chain_id = Some(10);
19722
19723 assert!(matches!(
19724 subscriber.fetch_pending_flashblock(None).await,
19725 Err(PendingFlashblockPollError::Integrity(SubscriberError::Provider(
19726 ref message
19727 ))) if message.contains("does not extend its exact certified parent")
19728 ));
19729 assert!(asserter.read_q().is_empty());
19730 }
19731
19732 #[tokio::test]
19733 async fn optimism_sampler_rechecks_unchanged_content_without_republishing_logs() {
19734 let asserter = Asserter::new();
19735 let pending = rpc_block(101, B256::ZERO);
19736 queue_op_pending(&asserter, pending.clone());
19737 asserter.push_success(&Vec::<Log>::new());
19738 queue_op_pending(&asserter, pending);
19739 asserter.push_success(&Vec::<Log>::new());
19740 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19741 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19742 provider,
19743 SubscriberMode::PubSub,
19744 SubscriberConfig {
19745 preconfirmations: PreconfirmationMode::Required,
19746 ..SubscriberConfig::default()
19747 },
19748 )
19749 .with_provider_ref(ProviderRef::new("op-paid", 12));
19750 subscriber.chain_id = Some(10);
19751 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19752 subscriber.interests = subscriber.base_interests.clone();
19753
19754 assert!(
19755 subscriber
19756 .fetch_pending_flashblock(None)
19757 .await
19758 .expect("first cumulative pending view")
19759 .is_some()
19760 );
19761 assert!(
19762 subscriber
19763 .fetch_pending_flashblock(None)
19764 .await
19765 .expect("duplicate cumulative pending view")
19766 .is_none()
19767 );
19768
19769 assert_eq!(
19770 subscriber.flashblocks_rpc_metrics(),
19771 FlashblocksRpcMetrics {
19772 capability_requests: 0,
19773 provider_pair_chain_requests: 0,
19774 canonical_head_requests: 2,
19775 pending_block_requests: 2,
19776 pending_log_requests: 2,
19777 pending_receipt_requests: 0,
19778 pending_receipts_completed: 0,
19779 pending_receipts_unavailable: 0,
19780 failed_requests: 0,
19781 raced_samples: 0,
19782 }
19783 );
19784 assert!(asserter.read_q().is_empty());
19785 }
19786
19787 #[tokio::test]
19788 async fn optimism_sampler_rechecks_logs_for_an_unchanged_pending_view() {
19789 let asserter = Asserter::new();
19790 let transaction = B256::repeat_byte(0x42);
19791 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
19792 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
19793 );
19794 let mut log = rpc_log(false);
19795 log.block_number = Some(101);
19796 log.block_hash = Some(B256::repeat_byte(0xa2));
19797 log.transaction_hash = Some(transaction);
19798 log.transaction_index = Some(0);
19799 log.log_index = Some(0);
19800 queue_op_pending(&asserter, pending.clone());
19801 asserter.push_success(&Vec::<Log>::new());
19802 asserter.push_success(&serde_json::Value::Null);
19803 queue_op_pending(&asserter, pending);
19804 asserter.push_success(&vec![log]);
19805 asserter.push_success(&serde_json::Value::Null);
19806 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19807 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19808 provider,
19809 SubscriberMode::PubSub,
19810 SubscriberConfig {
19811 preconfirmations: PreconfirmationMode::Required,
19812 ..SubscriberConfig::default()
19813 },
19814 )
19815 .with_provider_ref(ProviderRef::new("op-paid", 12));
19816 subscriber.chain_id = Some(10);
19817 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19818 subscriber.interests = subscriber.base_interests.clone();
19819
19820 assert!(matches!(
19821 subscriber
19822 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19823 .await
19824 .expect("first pending view is coherent"),
19825 Some(SubscriberEvent::FlashblockObserved)
19826 ));
19827 assert!(matches!(
19828 subscriber
19829 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19830 .await
19831 .expect("the unchanged view is checked again for lagging logs"),
19832 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
19833 ));
19834 assert!(asserter.read_q().is_empty());
19835 }
19836
19837 #[tokio::test]
19838 async fn optimism_sampler_hydrates_exact_receipts_when_filtered_logs_are_empty() {
19839 let asserter = Asserter::new();
19840 let transaction = B256::repeat_byte(0x42);
19841 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
19842 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
19843 );
19844 let mut log = rpc_log(false);
19845 log.block_number = Some(101);
19846 log.block_hash = Some(B256::repeat_byte(0xa2));
19847 log.transaction_hash = Some(transaction);
19848 log.transaction_index = Some(0);
19849 log.log_index = Some(0);
19850 queue_op_pending(&asserter, pending);
19851 asserter.push_success(&Vec::<Log>::new());
19852 asserter.push_success(&serde_json::json!({
19853 "transactionHash": transaction,
19854 "logs": [log]
19855 }));
19856 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19857 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19858 provider,
19859 SubscriberMode::PubSub,
19860 SubscriberConfig {
19861 preconfirmations: PreconfirmationMode::Required,
19862 ..SubscriberConfig::default()
19863 },
19864 )
19865 .with_provider_ref(ProviderRef::new("op-paid", 12));
19866 subscriber.chain_id = Some(10);
19867 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19868 subscriber.interests = subscriber.base_interests.clone();
19869
19870 let event = subscriber
19871 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19872 .await
19873 .expect("pending receipt fallback succeeds");
19874 assert!(matches!(
19875 event,
19876 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
19877 ));
19878 assert_eq!(
19879 subscriber
19880 .flashblocks_rpc_metrics()
19881 .pending_receipt_requests(),
19882 1
19883 );
19884 assert!(asserter.read_q().is_empty());
19885 }
19886
19887 #[tokio::test]
19888 async fn optimism_receipt_hydration_is_bounded_and_resumes_on_the_next_tick() {
19889 let asserter = Asserter::new();
19890 let transaction_a = B256::repeat_byte(0x41);
19891 let transaction_b = B256::repeat_byte(0x42);
19892 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
19893 alloy_network::primitives::BlockTransactions::Hashes(vec![
19894 transaction_a,
19895 transaction_b,
19896 ]),
19897 );
19898 let mut log = rpc_log(false);
19899 log.block_number = Some(101);
19900 log.transaction_hash = Some(transaction_b);
19901 log.transaction_index = Some(1);
19902 queue_op_pending(&asserter, pending.clone());
19903 asserter.push_success(&Vec::<Log>::new());
19904 asserter.push_success(&serde_json::json!({
19905 "transactionHash": transaction_a,
19906 "logs": []
19907 }));
19908 queue_op_pending(&asserter, pending);
19909 asserter.push_success(&Vec::<Log>::new());
19910 asserter.push_success(&serde_json::json!({
19911 "transactionHash": transaction_b,
19912 "logs": [log]
19913 }));
19914 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19915 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19916 provider,
19917 SubscriberMode::PubSub,
19918 SubscriberConfig {
19919 preconfirmations: PreconfirmationMode::Required,
19920 max_pending_transaction_receipts_per_tick: 1,
19921 ..SubscriberConfig::default()
19922 },
19923 )
19924 .with_provider_ref(ProviderRef::new("op-paid", 12));
19925 subscriber.chain_id = Some(10);
19926 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19927 subscriber.interests = subscriber.base_interests.clone();
19928
19929 assert!(matches!(
19930 subscriber
19931 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19932 .await
19933 .expect("the first bounded receipt is hydrated"),
19934 Some(SubscriberEvent::FlashblockObserved)
19935 ));
19936 assert!(matches!(
19937 subscriber
19938 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19939 .await
19940 .expect("the remaining receipt is hydrated on the next tick"),
19941 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
19942 ));
19943 assert_eq!(
19944 subscriber
19945 .flashblocks_rpc_metrics()
19946 .pending_receipt_requests(),
19947 2
19948 );
19949 assert_eq!(subscriber.preconfirmed_receipted_transactions.len(), 2);
19950 assert!(asserter.read_q().is_empty());
19951 }
19952
19953 #[tokio::test]
19954 async fn optimism_receipt_hydration_prioritizes_unattempted_hashes_over_null_retries() {
19955 let asserter = Asserter::new();
19956 let transaction_a = B256::repeat_byte(0x41);
19957 let transaction_b = B256::repeat_byte(0x42);
19958 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
19959 alloy_network::primitives::BlockTransactions::Hashes(vec![
19960 transaction_a,
19961 transaction_b,
19962 ]),
19963 );
19964 let mut log = rpc_log(false);
19965 log.block_number = Some(101);
19966 log.transaction_hash = Some(transaction_b);
19967 log.transaction_index = Some(1);
19968 queue_op_pending(&asserter, pending.clone());
19969 asserter.push_success(&Vec::<Log>::new());
19970 asserter.push_success(&serde_json::Value::Null);
19971 queue_op_pending(&asserter, pending);
19972 asserter.push_success(&Vec::<Log>::new());
19973 asserter.push_success(&serde_json::json!({
19974 "transactionHash": transaction_b,
19975 "logs": [log]
19976 }));
19977 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
19978 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
19979 provider,
19980 SubscriberMode::PubSub,
19981 SubscriberConfig {
19982 preconfirmations: PreconfirmationMode::Required,
19983 max_pending_transaction_receipts_per_tick: 1,
19984 ..SubscriberConfig::default()
19985 },
19986 )
19987 .with_provider_ref(ProviderRef::new("op-paid", 12));
19988 subscriber.chain_id = Some(10);
19989 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
19990 subscriber.interests = subscriber.base_interests.clone();
19991
19992 assert!(matches!(
19993 subscriber
19994 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
19995 .await
19996 .expect("the first null receipt remains retryable"),
19997 Some(SubscriberEvent::FlashblockObserved)
19998 ));
19999 assert!(matches!(
20000 subscriber
20001 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20002 .await
20003 .expect("the next unattempted receipt is not starved"),
20004 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
20005 ));
20006 assert!(
20007 subscriber
20008 .preconfirmed_unavailable_receipts
20009 .contains(&transaction_a)
20010 );
20011 assert!(
20012 subscriber
20013 .preconfirmed_receipted_transactions
20014 .contains(&transaction_b)
20015 );
20016 assert!(asserter.read_q().is_empty());
20017 }
20018
20019 #[tokio::test]
20020 async fn optimism_receipt_batch_commits_dedupe_only_after_every_response_succeeds() {
20021 let asserter = Asserter::new();
20022 let transaction_a = B256::repeat_byte(0x41);
20023 let transaction_b = B256::repeat_byte(0x42);
20024 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
20025 alloy_network::primitives::BlockTransactions::Hashes(vec![
20026 transaction_a,
20027 transaction_b,
20028 ]),
20029 );
20030 let mut log = rpc_log(false);
20031 log.block_number = Some(101);
20032 log.transaction_hash = Some(transaction_b);
20033 log.transaction_index = Some(1);
20034 queue_op_pending(&asserter, pending.clone());
20035 asserter.push_success(&Vec::<Log>::new());
20036 asserter.push_success(&serde_json::json!({
20037 "transactionHash": transaction_a,
20038 "logs": []
20039 }));
20040 asserter.push_failure_msg("receipt temporarily unavailable");
20041 queue_op_pending(&asserter, pending);
20042 asserter.push_success(&Vec::<Log>::new());
20043 asserter.push_success(&serde_json::json!({
20044 "transactionHash": transaction_a,
20045 "logs": []
20046 }));
20047 asserter.push_success(&serde_json::json!({
20048 "transactionHash": transaction_b,
20049 "logs": [log]
20050 }));
20051 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20052 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20053 provider,
20054 SubscriberMode::PubSub,
20055 SubscriberConfig {
20056 preconfirmations: PreconfirmationMode::Required,
20057 max_pending_transaction_receipts_per_tick: 2,
20058 max_consecutive_flashblock_poll_failures: 2,
20059 ..SubscriberConfig::default()
20060 },
20061 )
20062 .with_provider_ref(ProviderRef::new("op-paid", 12));
20063 subscriber.chain_id = Some(10);
20064 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20065 subscriber.interests = subscriber.base_interests.clone();
20066
20067 assert!(
20068 subscriber
20069 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20070 .await
20071 .expect("one failed receipt response remains retryable")
20072 .is_none()
20073 );
20074 assert!(subscriber.preconfirmed_receipted_transactions.is_empty());
20075 assert!(matches!(
20076 subscriber
20077 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20078 .await
20079 .expect("the complete batch is retried transactionally"),
20080 Some(SubscriberEvent::PreconfirmedLogs { ref logs, .. }) if logs.len() == 1
20081 ));
20082 assert_eq!(subscriber.preconfirmed_receipted_transactions.len(), 2);
20083 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 1);
20084 assert_eq!(
20085 subscriber
20086 .flashblocks_rpc_metrics()
20087 .pending_receipt_requests(),
20088 4
20089 );
20090 assert!(asserter.read_q().is_empty());
20091 }
20092
20093 #[tokio::test]
20094 async fn optimism_sampler_rejects_a_receipt_for_a_different_transaction() {
20095 let asserter = Asserter::new();
20096 let sampled_transaction = B256::repeat_byte(0x41);
20097 let advanced_transaction = B256::repeat_byte(0x42);
20098 let pending = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
20099 alloy_network::primitives::BlockTransactions::Hashes(vec![sampled_transaction]),
20100 );
20101 let mut log = rpc_log(false);
20102 log.block_number = Some(101);
20103 log.transaction_hash = Some(advanced_transaction);
20104 queue_op_pending(&asserter, pending);
20105 asserter.push_success(&Vec::<Log>::new());
20106 asserter.push_success(&serde_json::json!({
20107 "transactionHash": advanced_transaction,
20108 "logs": [log]
20109 }));
20110 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20111 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20112 provider,
20113 SubscriberMode::PubSub,
20114 SubscriberConfig {
20115 preconfirmations: PreconfirmationMode::Required,
20116 ..SubscriberConfig::default()
20117 },
20118 )
20119 .with_provider_ref(ProviderRef::new("op-paid", 12));
20120 subscriber.chain_id = Some(10);
20121 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20122 subscriber.interests = subscriber.base_interests.clone();
20123
20124 let error = match subscriber
20125 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20126 .await
20127 {
20128 Err(error) => error,
20129 Ok(_) => panic!("a receipt for another transaction must fail closed"),
20130 };
20131 assert!(
20132 error
20133 .to_string()
20134 .contains("hash disagrees with its request")
20135 );
20136 assert!(asserter.read_q().is_empty());
20137 }
20138
20139 #[tokio::test]
20140 async fn optimism_sampler_revokes_then_recovers_from_a_regressive_pending_view() {
20141 let asserter = Asserter::new();
20142 let transaction_a = B256::repeat_byte(0x41);
20143 let transaction_b = B256::repeat_byte(0x42);
20144 let first = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
20145 alloy_network::primitives::BlockTransactions::Hashes(vec![
20146 transaction_a,
20147 transaction_b,
20148 ]),
20149 );
20150 let regressive = rpc_block(101, B256::repeat_byte(0xa2)).with_transactions(
20151 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction_a]),
20152 );
20153 queue_op_pending(&asserter, first);
20154 asserter.push_success(&Vec::<Log>::new());
20155 asserter.push_success(&serde_json::Value::Null);
20156 asserter.push_success(&serde_json::Value::Null);
20157 queue_op_pending(&asserter, regressive.clone());
20158 queue_op_pending(&asserter, regressive);
20159 asserter.push_success(&Vec::<Log>::new());
20160 asserter.push_success(&serde_json::Value::Null);
20161 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20162 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20163 provider,
20164 SubscriberMode::PubSub,
20165 SubscriberConfig {
20166 preconfirmations: PreconfirmationMode::Required,
20167 ..SubscriberConfig::default()
20168 },
20169 )
20170 .with_provider_ref(ProviderRef::new("op-paid", 12));
20171 subscriber.chain_id = Some(10);
20172 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20173 subscriber.interests = subscriber.base_interests.clone();
20174
20175 assert!(
20176 subscriber
20177 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20178 .await
20179 .expect("first pending view is coherent")
20180 .is_some()
20181 );
20182 assert!(matches!(
20183 subscriber
20184 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20185 .await
20186 .expect("regression revokes instead of terminating the stream"),
20187 Some(SubscriberEvent::FlashblockInvalidated)
20188 ));
20189 assert!(subscriber.latest_preconfirmation.is_none());
20190 assert!(subscriber.pending_preconfirmation_invalidation);
20191 assert!(
20192 subscriber
20193 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20194 .await
20195 .expect("a later coherent view establishes a fresh snapshot")
20196 .is_some()
20197 );
20198 assert!(subscriber.latest_preconfirmation.is_some());
20199 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 12);
20200 assert!(asserter.read_q().is_empty());
20201 }
20202
20203 #[tokio::test]
20204 async fn optimism_new_quiet_payload_revokes_the_previous_snapshot() {
20205 let asserter = Asserter::new();
20206 let first = rpc_block(101, B256::repeat_byte(0xa1));
20207 let second = rpc_block(102, B256::repeat_byte(0xa2));
20208 queue_op_pending(&asserter, first);
20209 asserter.push_success(&Vec::<Log>::new());
20210 queue_op_pending(&asserter, second);
20211 asserter.push_success(&Vec::<Log>::new());
20212 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20213 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20214 provider,
20215 SubscriberMode::PubSub,
20216 SubscriberConfig {
20217 preconfirmations: PreconfirmationMode::Required,
20218 ..SubscriberConfig::default()
20219 },
20220 )
20221 .with_provider_ref(ProviderRef::new("op-paid", 12));
20222 subscriber.chain_id = Some(10);
20223 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20224 subscriber.interests = subscriber.base_interests.clone();
20225
20226 subscriber
20227 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20228 .await
20229 .expect("first quiet payload is observed");
20230 assert!(!subscriber.pending_preconfirmation_invalidation);
20231 subscriber
20232 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20233 .await
20234 .expect("replacement quiet payload is observed");
20235 assert!(subscriber.pending_preconfirmation_invalidation);
20236 assert_eq!(
20237 subscriber
20238 .latest_preconfirmation
20239 .as_ref()
20240 .map(|flashblock| flashblock.block_number),
20241 Some(102)
20242 );
20243 assert!(asserter.read_q().is_empty());
20244 }
20245
20246 #[tokio::test]
20247 async fn optimism_sampler_rejects_malformed_pending_receipts() {
20248 let asserter = Asserter::new();
20249 let transaction = B256::repeat_byte(0x42);
20250 let pending = rpc_block(101, B256::ZERO).with_transactions(
20251 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction]),
20252 );
20253 queue_op_pending(&asserter, pending);
20254 asserter.push_success(&Vec::<Log>::new());
20255 asserter.push_success(&serde_json::json!({"transactionHash": transaction}));
20256 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20257 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20258 provider,
20259 SubscriberMode::PubSub,
20260 SubscriberConfig {
20261 preconfirmations: PreconfirmationMode::Required,
20262 ..SubscriberConfig::default()
20263 },
20264 )
20265 .with_provider_ref(ProviderRef::new("op-paid", 12));
20266 subscriber.chain_id = Some(10);
20267 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20268 subscriber.interests = subscriber.base_interests.clone();
20269
20270 let error = match subscriber
20271 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20272 .await
20273 {
20274 Err(error) => error,
20275 Ok(_) => panic!("malformed receipt content must fail closed"),
20276 };
20277 assert!(error.to_string().contains("missing its log array"));
20278 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
20279 assert!(asserter.read_q().is_empty());
20280 }
20281
20282 #[tokio::test]
20283 async fn optimism_sampler_retries_when_logs_advance_past_the_sampled_block() {
20284 let asserter = Asserter::new();
20285 let transaction_a = B256::repeat_byte(0x41);
20286 let transaction_b = B256::repeat_byte(0x42);
20287 let first = rpc_block(101, B256::repeat_byte(0xa1)).with_transactions(
20288 alloy_network::primitives::BlockTransactions::Hashes(vec![transaction_a]),
20289 );
20290 let second = rpc_block(101, B256::repeat_byte(0xa2)).with_transactions(
20291 alloy_network::primitives::BlockTransactions::Hashes(vec![
20292 transaction_a,
20293 transaction_b,
20294 ]),
20295 );
20296 let mut log = rpc_log(false);
20297 log.block_number = Some(101);
20298 log.block_hash = Some(B256::repeat_byte(0xa2));
20299 log.transaction_hash = Some(transaction_b);
20300 log.transaction_index = Some(1);
20301 log.log_index = Some(0);
20302 queue_op_pending(&asserter, first);
20303 asserter.push_success(&vec![log.clone()]);
20304 asserter.push_success(&serde_json::Value::Null);
20305 queue_op_pending(&asserter, second);
20306 asserter.push_success(&vec![log.clone()]);
20307 asserter.push_success(&serde_json::Value::Null);
20308 asserter.push_success(&serde_json::json!({
20309 "transactionHash": transaction_b,
20310 "logs": [log]
20311 }));
20312 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20313 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20314 provider,
20315 SubscriberMode::PubSub,
20316 SubscriberConfig {
20317 preconfirmations: PreconfirmationMode::Required,
20318 ..SubscriberConfig::default()
20319 },
20320 )
20321 .with_provider_ref(ProviderRef::new("op-paid", 12));
20322 subscriber.chain_id = Some(10);
20323 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20324 subscriber.interests = subscriber.base_interests.clone();
20325
20326 assert!(
20327 subscriber
20328 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20329 .await
20330 .expect("a cross-request race remains retryable")
20331 .is_none()
20332 );
20333 assert!(
20334 subscriber
20335 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20336 .await
20337 .expect("the next coherent cumulative view is delivered")
20338 .is_some()
20339 );
20340 assert_eq!(subscriber.flashblocks_rpc_metrics().raced_samples(), 1);
20341 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 0);
20342 assert!(asserter.read_q().is_empty());
20343 }
20344
20345 #[tokio::test]
20346 async fn optimism_sampler_uses_the_paired_pending_state_provider() {
20347 let stream_asserter = Asserter::new();
20348 let stream_provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
20349 let state_asserter = Asserter::new();
20350 queue_op_pending(&state_asserter, rpc_block(101, B256::ZERO));
20351 state_asserter.push_success(&Vec::<Log>::new());
20352 let state_provider = ProviderBuilder::new().connect_mocked_client(state_asserter.clone());
20353 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20354 stream_provider,
20355 SubscriberMode::PubSub,
20356 SubscriberConfig {
20357 preconfirmations: PreconfirmationMode::Required,
20358 ..SubscriberConfig::default()
20359 },
20360 )
20361 .with_provider_ref(ProviderRef::new("op-paid", 12))
20362 .with_flashblocks_state_provider(state_provider);
20363 subscriber.chain_id = Some(10);
20364 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20365 subscriber.interests = subscriber.base_interests.clone();
20366
20367 assert!(
20368 subscriber
20369 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20370 .await
20371 .expect("paired pending-state reads succeed")
20372 .is_some()
20373 );
20374 assert!(state_asserter.read_q().is_empty());
20375 assert!(stream_asserter.read_q().is_empty());
20376 }
20377
20378 #[tokio::test]
20379 async fn optimism_sampler_retries_an_isolated_provider_request_failure() {
20380 let asserter = Asserter::new();
20381 let pending = rpc_block(101, B256::ZERO);
20382 queue_op_pending(&asserter, pending.clone());
20383 asserter.push_failure_msg("temporarily unavailable");
20384 queue_op_pending(&asserter, pending);
20385 asserter.push_success(&Vec::<Log>::new());
20386 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20387 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20388 provider,
20389 SubscriberMode::PubSub,
20390 SubscriberConfig {
20391 preconfirmations: PreconfirmationMode::Required,
20392 max_consecutive_flashblock_poll_failures: 2,
20393 ..SubscriberConfig::default()
20394 },
20395 )
20396 .with_provider_ref(ProviderRef::new("op-paid", 12));
20397 subscriber.chain_id = Some(10);
20398 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20399 subscriber.interests = subscriber.base_interests.clone();
20400
20401 assert!(
20402 subscriber
20403 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20404 .await
20405 .expect("one request failure stays retryable")
20406 .is_none()
20407 );
20408 assert!(
20409 subscriber
20410 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20411 .await
20412 .expect("the next cumulative view retries the missing logs")
20413 .is_some()
20414 );
20415 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 1);
20416 assert!(asserter.read_q().is_empty());
20417 }
20418
20419 #[tokio::test]
20420 async fn optimism_sampler_surfaces_sustained_provider_request_failures() {
20421 let asserter = Asserter::new();
20422 asserter.push_failure_msg("temporarily unavailable");
20423 asserter.push_failure_msg("still unavailable");
20424 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
20425 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20426 provider,
20427 SubscriberMode::PubSub,
20428 SubscriberConfig {
20429 preconfirmations: PreconfirmationMode::Required,
20430 max_consecutive_flashblock_poll_failures: 2,
20431 ..SubscriberConfig::default()
20432 },
20433 )
20434 .with_provider_ref(ProviderRef::new("op-paid", 12));
20435 subscriber.chain_id = Some(10);
20436
20437 assert!(
20438 subscriber
20439 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20440 .await
20441 .expect("the first request failure stays retryable")
20442 .is_none()
20443 );
20444 let error = match subscriber
20445 .normalize_flashblock_event(SubscriberEvent::OpFlashblockTick)
20446 .await
20447 {
20448 Err(error) => error,
20449 Ok(_) => panic!("the configured consecutive-failure limit must fail closed"),
20450 };
20451 assert!(error.to_string().contains("still unavailable"));
20452 assert_eq!(subscriber.flashblocks_rpc_metrics().failed_requests(), 2);
20453 assert!(asserter.read_q().is_empty());
20454 }
20455
20456 #[tokio::test]
20457 async fn flashblocks_preflight_rejects_a_mismatched_chain_before_subscribing() {
20458 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20459 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20460 provider,
20461 SubscriberMode::PubSub,
20462 SubscriberConfig {
20463 preconfirmations: PreconfirmationMode::Required,
20464 ..SubscriberConfig::default()
20465 },
20466 )
20467 .with_provider_ref(ProviderRef::new("wrong-chain", 1));
20468 subscriber.chain_id = Some(10);
20469 subscriber.interests = vec![log_interest_matching_rpc_log()];
20470
20471 assert!(matches!(
20472 subscriber.establish_flashblocks_preflight(8_453).await,
20473 Err(SubscriberError::ChainMismatch {
20474 expected: 8_453,
20475 actual: 10
20476 })
20477 ));
20478 }
20479
20480 #[tokio::test]
20481 async fn optimism_preflight_rejects_a_mismatched_paired_provider() {
20482 let stream_asserter = Asserter::new();
20483 let stream_provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
20484 let state_asserter = Asserter::new();
20485 state_asserter.push_success(&serde_json::json!(["flashblocksv1"]));
20486 state_asserter.push_success(&8_453_u64);
20487 let state_provider = ProviderBuilder::new().connect_mocked_client(state_asserter.clone());
20488 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20489 stream_provider,
20490 SubscriberMode::PubSub,
20491 SubscriberConfig {
20492 preconfirmations: PreconfirmationMode::Required,
20493 ..SubscriberConfig::default()
20494 },
20495 )
20496 .with_provider_ref(ProviderRef::new("op-paid", 12))
20497 .with_flashblocks_state_provider(state_provider);
20498 subscriber.chain_id = Some(10);
20499 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
20500 subscriber.interests = subscriber.base_interests.clone();
20501 let desired = subscriber.pubsub_stream_sources();
20502 let mut streams = SubscriberStreams::new();
20503 for source in desired {
20504 streams.push(source, stream::pending().boxed());
20505 }
20506 subscriber.state = AlloySubscriberState::Active(streams);
20507 subscriber.sources_dirty = false;
20508 assert!(matches!(
20509 subscriber.establish_flashblocks_preflight(10).await,
20510 Err(SubscriberError::ChainMismatch {
20511 expected: 10,
20512 actual: 8_453
20513 })
20514 ));
20515 assert!(state_asserter.read_q().is_empty());
20516 assert!(stream_asserter.read_q().is_empty());
20517 }
20518
20519 #[test]
20520 fn unproven_parent_replacement_rewind_discards_every_unauthenticated_identity() {
20521 let parent = BlockRef {
20522 number: 79,
20523 hash: B256::repeat_byte(0x79),
20524 parent_hash: Some(B256::repeat_byte(0x78)),
20525 timestamp: Some(1_700_000_079),
20526 };
20527 let old_tip = BlockRef {
20528 number: 80,
20529 hash: B256::repeat_byte(0x80),
20530 parent_hash: Some(parent.hash),
20531 timestamp: Some(1_700_000_080),
20532 };
20533 let replacement = BlockRef {
20534 hash: B256::repeat_byte(0xe0),
20535 parent_hash: Some(B256::repeat_byte(0xdf)),
20536 ..old_tip
20537 };
20538 let mut state =
20539 CanonicalSequenceState::new(vec![parent, old_tip], Some(old_tip), Some(parent), None);
20540
20541 let rewind = apply_sequence_canonical_block(&mut state, &replacement, false)
20542 .expect("replacement metadata is structurally valid")
20543 .expect("unknown parent is an observable rewind");
20544
20545 assert_eq!(rewind.common_ancestor, None);
20546 assert_eq!(rewind.dropped, vec![parent, old_tip]);
20547 assert_eq!(state.retained_canonical_history(), &[replacement]);
20548 assert_eq!(state.coverage_head(), Some(&replacement));
20549 assert_eq!(state.safe_head(), None);
20550 assert_eq!(state.finalized_head(), None);
20551 }
20552
20553 #[test]
20554 fn handler_ids_are_non_empty_across_construction_and_deserialization() {
20555 assert_eq!(HandlerId::try_new("").unwrap_err(), HandlerIdError);
20556 let valid = HandlerId::try_new("owner-1").expect("non-empty id");
20557 let encoded = serde_json::to_string(&valid).expect("serialize id");
20558 assert_eq!(
20559 serde_json::from_str::<HandlerId>(&encoded).expect("deserialize valid id"),
20560 valid
20561 );
20562 assert!(serde_json::from_str::<HandlerId>(r#"""#).is_err());
20563 }
20564
20565 fn rpc_log(removed: bool) -> Log {
20566 Log {
20567 inner: alloy_primitives::Log::new_unchecked(
20568 Address::repeat_byte(0x42),
20569 vec![B256::repeat_byte(0x01)],
20570 Bytes::new(),
20571 ),
20572 block_hash: Some(B256::repeat_byte(0x02)),
20573 block_number: Some(7),
20574 block_timestamp: Some(1_700_000_000),
20575 transaction_hash: Some(B256::repeat_byte(0x03)),
20576 transaction_index: Some(4),
20577 log_index: Some(5),
20578 removed,
20579 }
20580 }
20581
20582 fn rpc_transaction(chain_id: Option<u64>) -> alloy_rpc_types_eth::Transaction {
20583 use alloy_consensus::SignableTransaction as _;
20584
20585 let envelope: alloy_consensus::TxEnvelope = alloy_consensus::TxLegacy {
20586 chain_id,
20587 ..Default::default()
20588 }
20589 .into_signed(alloy_primitives::Signature::test_signature())
20590 .into();
20591 alloy_rpc_types_eth::Transaction {
20592 inner: alloy_consensus::transaction::Recovered::new_unchecked(envelope, Address::ZERO),
20593 block_hash: None,
20594 block_number: None,
20595 transaction_index: None,
20596 effective_gas_price: None,
20597 }
20598 }
20599
20600 #[cfg(feature = "reactive-ws")]
20601 fn rpc_log_at(block_number: u64, transaction_index: u64, log_index: u64) -> Log {
20602 Log {
20603 inner: alloy_primitives::Log::new_unchecked(
20604 Address::repeat_byte(0x42),
20605 vec![B256::repeat_byte(0x01)],
20606 Bytes::new(),
20607 ),
20608 block_hash: Some(B256::repeat_byte(block_number as u8)),
20609 block_number: Some(block_number),
20610 block_timestamp: Some(1_700_000_000 + block_number),
20611 transaction_hash: Some(B256::repeat_byte(0x20 + transaction_index as u8)),
20612 transaction_index: Some(transaction_index),
20613 log_index: Some(log_index),
20614 removed: false,
20615 }
20616 }
20617
20618 #[cfg(any(feature = "reactive-polling", feature = "reactive-ws"))]
20619 fn rpc_block(number: u64, hash: B256) -> alloy_rpc_types_eth::Block {
20620 alloy_rpc_types_eth::Block::empty(alloy_rpc_types_eth::Header {
20621 hash,
20622 inner: alloy_consensus::Header {
20623 number,
20624 parent_hash: B256::repeat_byte(number.saturating_sub(1) as u8),
20625 timestamp: 1_700_000_000 + number,
20626 ..Default::default()
20627 },
20628 total_difficulty: None,
20629 size: None,
20630 })
20631 }
20632
20633 fn queue_op_pending(asserter: &Asserter, pending: alloy_rpc_types_eth::Block) {
20634 let parent = rpc_block(
20635 pending.header().number().saturating_sub(1),
20636 pending.header().parent_hash(),
20637 );
20638 asserter.push_success(&Some(pending));
20639 asserter.push_success(&Some(parent));
20640 }
20641
20642 #[tokio::test(flavor = "multi_thread")]
20643 #[cfg(feature = "reactive-ws")]
20644 async fn verified_log_context_fetches_and_caches_exact_parent_identity() {
20645 let stream_asserter = Asserter::new();
20646 let provider = ProviderBuilder::new().connect_mocked_client(stream_asserter.clone());
20647 let verification_asserter = Asserter::new();
20648 verification_asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(7))));
20649 let verification_provider =
20650 ProviderBuilder::new().connect_mocked_client(verification_asserter.clone());
20651 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20652 provider,
20653 SubscriberMode::PubSub,
20654 SubscriberConfig {
20655 verify_log_block_context: true,
20656 ..SubscriberConfig::default()
20657 },
20658 )
20659 .with_log_verification_provider(verification_provider);
20660 let log = rpc_log_at(7, 0, 0);
20661
20662 subscriber
20663 .verify_log_block_context(&log)
20664 .await
20665 .expect("verify live log block");
20666 subscriber
20667 .verify_log_block_context(&log)
20668 .await
20669 .expect("reuse verified block cache");
20670 let record = subscriber.with_chain_id(log_input_record(log, InputSource::Subscription));
20671
20672 assert_eq!(
20673 record.context.block.expect("verified block").parent_hash,
20674 Some(B256::repeat_byte(6))
20675 );
20676 assert!(
20677 verification_asserter.read_q().is_empty(),
20678 "one provider lookup should verify every log in the same block"
20679 );
20680 assert!(
20681 stream_asserter.read_q().is_empty(),
20682 "verification must not use the high-volume stream provider"
20683 );
20684 }
20685
20686 #[tokio::test(flavor = "multi_thread")]
20687 async fn stream_with_termination_yields_terminal_source_marker() {
20688 let mut stream = stream_with_termination::<Ethereum, _>(
20689 stream::iter([SubscriberEvent::<Ethereum>::PendingHash(B256::repeat_byte(
20690 0xaa,
20691 ))]),
20692 SubscriberStreamSource::PubSubPendingHashes,
20693 );
20694
20695 assert!(matches!(
20696 stream.next().await,
20697 Some(SubscriberEvent::PendingHash(hash)) if hash == B256::repeat_byte(0xaa)
20698 ));
20699 assert!(matches!(
20700 stream.next().await,
20701 Some(SubscriberEvent::StreamTerminated(source)) if source.is_pubsub()
20702 ));
20703 assert!(stream.next().await.is_none());
20704 }
20705
20706 #[test]
20707 fn reconnect_delay_doubles_until_capped() {
20708 assert_eq!(
20709 next_reconnect_delay(Duration::from_millis(250), Duration::from_secs(1)),
20710 Duration::from_millis(500)
20711 );
20712 assert_eq!(
20713 next_reconnect_delay(Duration::from_millis(750), Duration::from_secs(1)),
20714 Duration::from_secs(1)
20715 );
20716 assert_eq!(
20717 next_reconnect_delay(Duration::ZERO, Duration::from_secs(1)),
20718 Duration::ZERO
20719 );
20720 }
20721
20722 #[test]
20723 fn canonical_logs_are_deduped_but_removed_logs_are_not() {
20724 let included = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
20725 let removed = log_input_record::<Ethereum>(rpc_log(true), InputSource::Subscription);
20726
20727 assert!(should_dedupe_record(&included));
20728 assert!(!should_dedupe_record(&removed));
20729 }
20730
20731 #[test]
20732 fn owner_reconcile_dedupe_rejects_conflicts_and_preserves_compatible_enrichment() {
20733 let set_context_timestamp = |record: &mut ReactiveInputRecord<Ethereum>,
20734 timestamp: Option<u64>| {
20735 record.context.block.as_mut().expect("block").timestamp = timestamp;
20736 match &mut record.context.chain_status {
20737 ChainStatus::Included { block, .. }
20738 | ChainStatus::Safe { block }
20739 | ChainStatus::Finalized { block }
20740 | ChainStatus::Reorged {
20741 dropped_from: block,
20742 } => block.timestamp = timestamp,
20743 ChainStatus::Pending | ChainStatus::Preconfirmed { .. } => {
20744 panic!("log record is canonical")
20745 }
20746 }
20747 };
20748
20749 let mut payload_only = log_input_record::<Ethereum>(rpc_log(false), InputSource::Backfill);
20750 let payload_timestamp = match &payload_only.input {
20751 ReactiveInput::Log(log) => log.block_timestamp.expect("timestamp"),
20752 _ => unreachable!(),
20753 };
20754 set_context_timestamp(&mut payload_only, None);
20755 let mut context_only = payload_only.clone();
20756 if let ReactiveInput::Log(log) = &mut context_only.input {
20757 log.block_timestamp = None;
20758 }
20759 set_context_timestamp(&mut context_only, Some(payload_timestamp + 1));
20760 assert!(matches!(
20761 dedupe_records(vec![payload_only, context_only]),
20762 Err(ReactiveError::InvalidInputRecord { .. })
20763 ));
20764
20765 let mut partial = log_input_record::<Ethereum>(rpc_log(false), InputSource::Backfill);
20766 if let ReactiveInput::Log(log) = &mut partial.input {
20767 log.block_timestamp = None;
20768 }
20769 set_context_timestamp(&mut partial, None);
20770 let complete = log_input_record::<Ethereum>(rpc_log(false), InputSource::Subscription);
20771 let deduped =
20772 dedupe_records(vec![partial, complete]).expect("compatible metadata enriches");
20773 assert_eq!(deduped.len(), 1);
20774 deduped[0]
20775 .validated_identity()
20776 .expect("merged record remains coherent");
20777 let resolved = resolve_record_block_payload_metadata(
20778 &deduped[0],
20779 *canonical_record_block(&deduped[0]).expect("canonical"),
20780 )
20781 .expect("effective block");
20782 assert_eq!(resolved.timestamp, Some(payload_timestamp));
20783 }
20784
20785 #[test]
20786 fn full_block_bodies_are_never_suppressed_from_header_hash_alone() {
20787 use alloy_rpc_types_eth::{Block, Header};
20788
20789 let block_ref = BlockRef {
20790 number: 7,
20791 hash: B256::repeat_byte(0x77),
20792 parent_hash: Some(B256::repeat_byte(0x66)),
20793 timestamp: Some(1_700_000_007),
20794 };
20795 let block = Block::empty(Header {
20796 hash: block_ref.hash,
20797 inner: alloy_consensus::Header {
20798 number: block_ref.number,
20799 parent_hash: block_ref.parent_hash.expect("parent"),
20800 timestamp: block_ref.timestamp.expect("timestamp"),
20801 ..Default::default()
20802 },
20803 total_difficulty: None,
20804 size: None,
20805 });
20806 let record = ReactiveInputRecord::<Ethereum>::new(
20807 ReactiveInput::FullBlock(block),
20808 ReactiveContext {
20809 chain_id: Some(1),
20810 source: InputSource::Subscription,
20811 chain_status: ChainStatus::Included {
20812 block: block_ref,
20813 confirmations: 0,
20814 },
20815 block: Some(block_ref),
20816 transaction_index: None,
20817 log_index: None,
20818 },
20819 );
20820
20821 assert!(!record.is_payload_deduplicable());
20822 assert!(!record.same_deduplicable_payload(&record));
20823 let retained = dedupe_scoped_records(vec![
20824 (
20825 record.clone(),
20826 DeliveryAudience::All,
20827 DeliveryScope::Canonical,
20828 ),
20829 (record, DeliveryAudience::All, DeliveryScope::Canonical),
20830 ])
20831 .expect("non-deduplicable bodies are preserved, not treated as conflicts");
20832 assert_eq!(retained.len(), 2);
20833 }
20834
20835 #[test]
20836 fn hydrated_transaction_wrappers_reject_inclusion_and_chain_identity_conflicts() {
20837 let pending_context = ReactiveContext {
20838 chain_id: Some(1),
20839 source: InputSource::Batch,
20840 chain_status: ChainStatus::Pending,
20841 block: None,
20842 transaction_index: None,
20843 log_index: None,
20844 };
20845 let mut included_pending = rpc_transaction(Some(1));
20846 included_pending.block_hash = Some(B256::repeat_byte(0xaa));
20847 assert!(matches!(
20848 ReactiveInputRecord::<Ethereum>::new(
20849 ReactiveInput::PendingTx(included_pending),
20850 pending_context.clone(),
20851 )
20852 .validated_identity(),
20853 Err(ReactiveError::InvalidInputRecord { .. })
20854 ));
20855 assert!(matches!(
20856 ReactiveInputRecord::<Ethereum>::new(
20857 ReactiveInput::PendingTx(rpc_transaction(Some(2))),
20858 pending_context,
20859 )
20860 .validated_identity(),
20861 Err(ReactiveError::InvalidInputRecord { .. })
20862 ));
20863
20864 let block_ref = BlockRef {
20865 number: 8,
20866 hash: B256::repeat_byte(0x88),
20867 parent_hash: Some(B256::repeat_byte(0x77)),
20868 timestamp: Some(1_700_000_008),
20869 };
20870 let header = alloy_rpc_types_eth::Header {
20871 hash: block_ref.hash,
20872 inner: alloy_consensus::Header {
20873 number: block_ref.number,
20874 parent_hash: block_ref.parent_hash.expect("parent"),
20875 timestamp: block_ref.timestamp.expect("timestamp"),
20876 ..Default::default()
20877 },
20878 total_difficulty: None,
20879 size: None,
20880 };
20881 let context = ReactiveContext {
20882 chain_id: Some(1),
20883 source: InputSource::Batch,
20884 chain_status: ChainStatus::Included {
20885 block: block_ref,
20886 confirmations: 0,
20887 },
20888 block: Some(block_ref),
20889 transaction_index: None,
20890 log_index: None,
20891 };
20892 for transaction in [
20893 alloy_rpc_types_eth::Transaction {
20894 block_hash: Some(B256::repeat_byte(0xff)),
20895 ..rpc_transaction(Some(1))
20896 },
20897 alloy_rpc_types_eth::Transaction {
20898 block_hash: Some(block_ref.hash),
20899 block_number: Some(block_ref.number),
20900 transaction_index: Some(1),
20901 ..rpc_transaction(Some(1))
20902 },
20903 rpc_transaction(Some(2)),
20904 ] {
20905 let block = alloy_rpc_types_eth::Block::new(
20906 header.clone(),
20907 alloy_network::primitives::BlockTransactions::Full(vec![transaction]),
20908 );
20909 assert!(matches!(
20910 ReactiveInputRecord::<Ethereum>::new(
20911 ReactiveInput::FullBlock(block),
20912 context.clone(),
20913 )
20914 .validated_identity(),
20915 Err(ReactiveError::InvalidInputRecord { .. })
20916 ));
20917 }
20918 }
20919
20920 #[test]
20921 fn compatibility_owner_backfill_and_live_overlap_split_exact_audiences() {
20922 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20923 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20924 provider,
20925 SubscriberMode::Auto,
20926 SubscriberConfig::default(),
20927 );
20928 let owner = HandlerId::new("compat-owner");
20929 subscriber
20930 .add_interest_owner(
20931 owner.clone(),
20932 &[ReactiveInterest::Logs(LogInterest {
20933 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
20934 local_matcher: None,
20935 route_key: None,
20936 })],
20937 )
20938 .unwrap();
20939 let log = rpc_log(false);
20940
20941 subscriber.enqueue_compat_owner_record(
20942 log_input_record(log.clone(), InputSource::Backfill),
20943 owner.clone(),
20944 );
20945 subscriber.enqueue_event(SubscriberEvent::Log { source_id: 0, log });
20946
20947 let batch = subscriber
20948 .drain_next_scoped_batch()
20949 .expect("owner catch-up and residual live copies");
20950 assert_eq!(batch.records.len(), 2);
20951 assert_eq!(
20952 batch.records[0].scope,
20953 SubscriberInputScope::OwnerOnlyHandlers {
20954 owners: vec![owner.clone()]
20955 }
20956 );
20957 assert_eq!(
20958 batch.records[1].scope,
20959 SubscriberInputScope::CanonicalResidual {
20960 owners: Vec::new(),
20961 excluded: vec![owner.clone()]
20962 }
20963 );
20964
20965 let reactive = batch.into_reactive_batch();
20966 assert_eq!(
20967 reactive.record_audience(0),
20968 Some(&DeliveryAudience::Owners(vec![owner.clone()]))
20969 );
20970 assert_eq!(
20971 reactive.record_delivery_scope(0),
20972 Some(DeliveryScope::OwnerCatchup)
20973 );
20974 assert_eq!(
20975 reactive.record_audience(1),
20976 Some(&DeliveryAudience::AllExcept(vec![owner]))
20977 );
20978 assert_eq!(
20979 reactive.record_delivery_scope(1),
20980 Some(DeliveryScope::Canonical)
20981 );
20982 }
20983
20984 #[test]
20985 fn active_owner_replacement_commits_atomically_to_one_new_epoch() {
20986 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
20987 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
20988 provider,
20989 SubscriberMode::Auto,
20990 SubscriberConfig::default(),
20991 );
20992 let owner = HandlerId::new("replace-owner");
20993 let original = ReactiveInterest::Logs(LogInterest {
20994 provider_filter: Filter::new().address(Address::repeat_byte(0x41)),
20995 local_matcher: None,
20996 route_key: None,
20997 });
20998 let replacement_interest = ReactiveInterest::Logs(LogInterest {
20999 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21000 local_matcher: None,
21001 route_key: None,
21002 });
21003 let active = subscriber
21004 .stage_interest_owner(owner.clone(), &[original], SubscriberOwnerStart::Live)
21005 .unwrap();
21006 assert!(subscriber.activate_interest_owner(&active));
21007 let replacement = subscriber
21008 .stage_interest_owner_replacement(
21009 owner,
21010 &[replacement_interest],
21011 SubscriberOwnerStart::Live,
21012 )
21013 .unwrap();
21014
21015 assert!(subscriber.commit_interest_owner_replacement(&active, &replacement));
21016 assert_eq!(subscriber.interest_owner_state(&active), None);
21017 assert_eq!(
21018 subscriber.interest_owner_state(&replacement),
21019 Some(SubscriberOwnerState::Active)
21020 );
21021 assert_eq!(subscriber.registered_interests().len(), 1);
21022 }
21023
21024 #[test]
21025 fn compatibility_and_epoch_owner_lifecycles_cannot_mix() {
21026 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21027 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21028 provider,
21029 SubscriberMode::Auto,
21030 SubscriberConfig::default(),
21031 );
21032 let owner = HandlerId::new("one-lifecycle");
21033 let interest = ReactiveInterest::Logs(LogInterest {
21034 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21035 local_matcher: None,
21036 route_key: None,
21037 });
21038 let epoch = subscriber
21039 .stage_interest_owner(
21040 owner.clone(),
21041 std::slice::from_ref(&interest),
21042 SubscriberOwnerStart::Live,
21043 )
21044 .expect("stage epoch owner");
21045
21046 assert!(matches!(
21047 subscriber.add_interest_owner(owner.clone(), std::slice::from_ref(&interest)),
21048 Err(SubscriberError::InvalidConfig(_))
21049 ));
21050 assert_eq!(
21051 subscriber.interest_owner_state(&epoch),
21052 Some(SubscriberOwnerState::Staged)
21053 );
21054 assert!(subscriber.abort_interest_owner(&epoch));
21055 subscriber
21056 .add_interest_owner(owner.clone(), std::slice::from_ref(&interest))
21057 .expect("compatibility owner after epoch abort");
21058 assert!(matches!(
21059 subscriber.stage_interest_owner_replacement(
21060 owner,
21061 std::slice::from_ref(&interest),
21062 SubscriberOwnerStart::Live,
21063 ),
21064 Err(SubscriberOwnerError::AlreadyRegistered(_))
21065 ));
21066 }
21067
21068 #[test]
21069 fn pending_record_overflow_is_sticky_and_fail_closed() {
21070 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21071 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21072 provider,
21073 SubscriberMode::Polling,
21074 SubscriberConfig {
21075 max_pending_records: 1,
21076 ..SubscriberConfig::default()
21077 },
21078 );
21079 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
21080 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21081 local_matcher: None,
21082 route_key: None,
21083 })];
21084 subscriber.enqueue_event(SubscriberEvent::Log {
21085 source_id: 0,
21086 log: rpc_log(false),
21087 });
21088 let mut second = rpc_log(false);
21089 second.log_index = Some(6);
21090 second.transaction_hash = Some(B256::repeat_byte(0x04));
21091 subscriber.enqueue_event(SubscriberEvent::Log {
21092 source_id: 0,
21093 log: second,
21094 });
21095
21096 assert_eq!(subscriber.pending_records.len(), 1);
21097 assert!(matches!(
21098 subscriber.check_resource_error(),
21099 Err(SubscriberError::ResourceExhausted(_))
21100 ));
21101 subscriber.reset_delivery_state();
21102 assert!(subscriber.check_resource_error().is_ok());
21103 }
21104
21105 #[test]
21106 fn historical_log_payload_bytes_are_bounded_independently_of_log_count() {
21107 let baseline = rpc_log(false);
21108 let fixed_bytes =
21109 validate_backfill_resource_limits(std::slice::from_ref(&baseline), 1, usize::MAX)
21110 .expect("measure fixed log accounting");
21111 let mut large = baseline;
21112 large.inner = alloy_primitives::Log::new_unchecked(
21113 Address::repeat_byte(0x42),
21114 vec![B256::repeat_byte(0x01)],
21115 Bytes::from(vec![0u8; 256]),
21116 );
21117
21118 assert!(matches!(
21119 validate_backfill_resource_limits(&[large], 1, fixed_bytes + 255),
21120 Err(SubscriberError::ResourceExhausted(_))
21121 ));
21122 }
21123
21124 #[tokio::test(flavor = "multi_thread")]
21125 #[cfg(feature = "reactive-polling")]
21126 async fn reconcile_capacity_failure_does_not_publish_progress_or_partial_history() {
21127 use alloy_rpc_types_eth::{Block, Header};
21128
21129 let asserter = Asserter::new();
21130 let baseline = BlockRef {
21131 number: 6,
21132 hash: B256::repeat_byte(6),
21133 parent_hash: Some(B256::repeat_byte(5)),
21134 timestamp: Some(1_700_000_006),
21135 };
21136 let through = BlockRef {
21137 number: 7,
21138 hash: B256::repeat_byte(7),
21139 parent_hash: Some(baseline.hash),
21140 timestamp: Some(1_700_000_007),
21141 };
21142 let rpc_block = || -> Block {
21143 Block::empty(Header {
21144 hash: through.hash,
21145 inner: alloy_consensus::Header {
21146 number: through.number,
21147 parent_hash: through.parent_hash.expect("parent"),
21148 timestamp: through.timestamp.expect("timestamp"),
21149 ..Default::default()
21150 },
21151 total_difficulty: None,
21152 size: None,
21153 })
21154 };
21155 let mut historical = rpc_log(false);
21156 historical.block_hash = Some(through.hash);
21157 historical.block_timestamp = through.timestamp;
21158 asserter.push_success(&Some(rpc_block()));
21159 asserter.push_success(&vec![historical]);
21160 asserter.push_success(&Some(rpc_block()));
21161 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
21162 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21163 provider,
21164 SubscriberMode::Polling,
21165 SubscriberConfig {
21166 max_pending_records: 1,
21167 ..SubscriberConfig::default()
21168 },
21169 );
21170 subscriber.chain_id = Some(1);
21171 let interest = ReactiveInterest::Logs(LogInterest {
21172 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21173 local_matcher: None,
21174 route_key: None,
21175 });
21176 let epoch = subscriber
21177 .stage_interest_owner(
21178 HandlerId::new("capacity-owner"),
21179 std::slice::from_ref(&interest),
21180 SubscriberOwnerStart::PostBlock(baseline),
21181 )
21182 .expect("stage owner");
21183 subscriber.sources_dirty = false;
21186 subscriber.state = AlloySubscriberState::Empty;
21187 subscriber.push_pending_record(SubscriberInputRecord {
21188 record: log_input_record(rpc_log(false), InputSource::Poll),
21189 scope: SubscriberInputScope::Canonical { owners: Vec::new() },
21190 });
21191
21192 let error = subscriber
21193 .reconcile_interest_owner(&epoch, through)
21194 .await
21195 .expect_err("historical delivery cannot displace the queued live record");
21196 assert!(matches!(
21197 error,
21198 SubscriberOwnerError::Subscriber(SubscriberError::ResourceExhausted(_))
21199 ));
21200 assert!(subscriber.interest_owner_progress(&epoch).is_none());
21201 assert_eq!(subscriber.pending_records.len(), 1);
21202 assert!(matches!(
21203 subscriber.pending_records[0].scope,
21204 SubscriberInputScope::Canonical { .. }
21205 ));
21206 }
21207
21208 #[test]
21209 fn lazy_backfill_queue_capacity_failure_is_atomic() {
21210 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21211 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21212 provider,
21213 SubscriberMode::Auto,
21214 SubscriberConfig {
21215 max_pending_backfills: 1,
21216 ..SubscriberConfig::default()
21217 },
21218 );
21219 let interest = |address| {
21220 ReactiveInterest::Logs(LogInterest {
21221 provider_filter: Filter::new().address(address),
21222 local_matcher: None,
21223 route_key: None,
21224 })
21225 };
21226 subscriber
21227 .add_interest_owner_with_backfill(
21228 HandlerId::new("owner-a"),
21229 &[interest(Address::repeat_byte(0x41))],
21230 SubscriberBackfill::from_block(10),
21231 )
21232 .expect("first queued backfill");
21233
21234 let error = subscriber
21235 .add_interest_owner_with_backfill(
21236 HandlerId::new("owner-b"),
21237 &[interest(Address::repeat_byte(0x42))],
21238 SubscriberBackfill::from_block(10),
21239 )
21240 .expect_err("second backfill must exceed capacity");
21241
21242 assert!(matches!(error, SubscriberError::ResourceExhausted(_)));
21243 assert!(
21244 subscriber
21245 .owner_interests(&HandlerId::new("owner-b"))
21246 .is_none()
21247 );
21248 assert_eq!(subscriber.pending_backfills.len(), 1);
21249 }
21250
21251 #[test]
21252 fn exact_owner_replacement_is_atomic_and_removes_crash_stale_owners() {
21253 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21254 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21255 provider,
21256 SubscriberMode::Auto,
21257 SubscriberConfig {
21258 max_pending_backfills: 1,
21259 ..SubscriberConfig::default()
21260 },
21261 );
21262 let interest = |address| {
21263 ReactiveInterest::Logs(LogInterest {
21264 provider_filter: Filter::new().address(address),
21265 local_matcher: None,
21266 route_key: None,
21267 })
21268 };
21269 subscriber
21270 .add_interest_owner(
21271 HandlerId::new("crash-stale"),
21272 &[interest(Address::repeat_byte(0xee))],
21273 )
21274 .expect("seed stale owner");
21275 subscriber.base_interests = vec![interest(Address::repeat_byte(0xdd))];
21276 subscriber.rebuild_registered_interests();
21277 subscriber.push_pending_record(SubscriberInputRecord {
21278 record: log_input_record(rpc_log(false), InputSource::Poll),
21279 scope: SubscriberInputScope::Canonical { owners: Vec::new() },
21280 });
21281 let baseline = BlockRef {
21282 number: 100,
21283 hash: B256::repeat_byte(100),
21284 parent_hash: Some(B256::repeat_byte(99)),
21285 timestamp: Some(1_700_000_100),
21286 };
21287 let backfill = SubscriberBackfill::after_canonical_block(baseline).expect("C + 1");
21288
21289 let error = subscriber
21290 .replace_interest_owners_with_global_backfill(
21291 vec![
21292 (
21293 HandlerId::new("pool-a"),
21294 vec![interest(Address::repeat_byte(0xa1))],
21295 ),
21296 (
21297 HandlerId::new("pool-b"),
21298 vec![ReactiveInterest::Logs(LogInterest {
21299 provider_filter: Filter::new()
21302 .address(Address::repeat_byte(0xb2))
21303 .from_block(7),
21304 local_matcher: None,
21305 route_key: None,
21306 })],
21307 ),
21308 ],
21309 backfill,
21310 )
21311 .expect_err("two backfills exceed atomic capacity");
21312 assert!(matches!(error, SubscriberError::ResourceExhausted(_)));
21313 assert!(
21314 subscriber
21315 .owner_interests(&HandlerId::new("crash-stale"))
21316 .is_some(),
21317 "failed replacement must preserve the prior topology"
21318 );
21319 assert!(
21320 subscriber
21321 .owner_interests(&HandlerId::new("pool-a"))
21322 .is_none()
21323 );
21324 assert_eq!(subscriber.base_interests.len(), 1);
21325 assert_eq!(subscriber.pending_records.len(), 1);
21326
21327 subscriber
21328 .replace_interest_owners_with_global_backfill(
21329 vec![(
21330 HandlerId::new("pool-a"),
21331 vec![interest(Address::repeat_byte(0xa1))],
21332 )],
21333 backfill,
21334 )
21335 .expect("replacement within capacity");
21336 assert!(
21337 subscriber
21338 .owner_interests(&HandlerId::new("crash-stale"))
21339 .is_none(),
21340 "successful exact replacement removes stale owners"
21341 );
21342 assert!(
21343 subscriber.base_interests.is_empty(),
21344 "successful exact replacement removes stale unowned interests"
21345 );
21346 assert!(
21347 subscriber.drain_next_scoped_batch().is_none(),
21348 "stale canonical delivery must not escape before C + 1 recovery"
21349 );
21350 assert!(
21351 subscriber
21352 .owner_interests(&HandlerId::new("pool-a"))
21353 .is_some()
21354 );
21355 assert_eq!(subscriber.pending_backfills.len(), 1);
21356 assert_eq!(subscriber.pending_backfills[0].backfill, backfill);
21357 assert!(
21358 subscriber.pending_backfills[0].owner.is_none(),
21359 "startup history must be global canonical catch-up, not owner-only"
21360 );
21361 }
21362
21363 #[test]
21364 fn exclusive_canonical_backfill_rejects_block_number_overflow() {
21365 let baseline = BlockRef {
21366 number: u64::MAX,
21367 hash: B256::repeat_byte(0xff),
21368 parent_hash: None,
21369 timestamp: None,
21370 };
21371 assert!(matches!(
21372 SubscriberBackfill::after_canonical_block(baseline),
21373 Err(SubscriberError::InvalidConfig(_))
21374 ));
21375 }
21376
21377 #[tokio::test(flavor = "multi_thread")]
21378 async fn exclusive_canonical_backfill_validates_the_retained_baseline_hash() {
21379 let asserter = Asserter::new();
21380 asserter.push_success(&101u64);
21381 asserter.push_success(&Some(rpc_block(101, B256::repeat_byte(101))));
21382 asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(0xee))));
21383 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
21384 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21385 provider,
21386 SubscriberMode::Auto,
21387 SubscriberConfig::default(),
21388 );
21389 let baseline = BlockRef {
21390 number: 100,
21391 hash: B256::repeat_byte(0xaa),
21392 parent_hash: None,
21393 timestamp: None,
21394 };
21395 let backfill = SubscriberBackfill::after_canonical_block(baseline).expect("C + 1");
21396 subscriber
21397 .add_interest_owner_with_backfill(
21398 HandlerId::new("pool"),
21399 &[ReactiveInterest::Logs(LogInterest {
21400 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
21401 local_matcher: None,
21402 route_key: None,
21403 })],
21404 backfill,
21405 )
21406 .expect("queue post-baseline backfill");
21407
21408 let error = subscriber
21409 .drain_pending_backfills()
21410 .await
21411 .expect_err("provider branch differs at retained baseline");
21412 assert!(matches!(error, SubscriberError::InvalidBackfill(_)));
21413 assert_eq!(subscriber.pending_backfills.len(), 1);
21414 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 101);
21415 assert!(subscriber.pending_records.is_empty());
21416 }
21417
21418 #[tokio::test(flavor = "multi_thread")]
21419 #[cfg(feature = "reactive-ws")]
21420 async fn coordinated_multifilter_windows_are_globally_sorted_for_owner_and_canonical_delivery()
21421 {
21422 let asserter = Asserter::new();
21423 let retained = BlockRef {
21424 number: 10,
21425 hash: B256::repeat_byte(10),
21426 parent_hash: Some(B256::repeat_byte(9)),
21427 timestamp: Some(1_700_000_010),
21428 };
21429 let activation = BlockRef {
21430 number: 12,
21431 hash: B256::repeat_byte(12),
21432 parent_hash: Some(B256::repeat_byte(11)),
21433 timestamp: Some(1_700_000_012),
21434 };
21435
21436 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
21440 asserter.push_success(&vec![rpc_log_at(10, 2, 2)]);
21441 asserter.push_success(&vec![rpc_log_at(10, 1, 1)]);
21442 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
21443 asserter.push_success(&activation.number);
21444 asserter.push_success(&Some(rpc_block(activation.number, activation.hash)));
21445 asserter.push_success(&Some(rpc_block(retained.number, retained.hash)));
21446 asserter.push_success(&vec![rpc_log_at(12, 2, 2)]);
21447 asserter.push_success(&vec![rpc_log_at(11, 1, 1)]);
21448 asserter.push_success(&Some(rpc_block(activation.number, activation.hash)));
21449 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
21450 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21451 provider,
21452 SubscriberMode::Auto,
21453 SubscriberConfig::default(),
21454 );
21455 let interests = (0..257)
21456 .map(|start| {
21457 ReactiveInterest::Logs(LogInterest {
21458 provider_filter: Filter::new()
21459 .address(Address::repeat_byte(0x42))
21460 .event_signature(B256::repeat_byte(0x01))
21461 .from_block(start),
21462 local_matcher: None,
21463 route_key: None,
21464 })
21465 })
21466 .collect::<Vec<_>>();
21467 subscriber
21468 .add_interest_owner_with_canonical_catchup(
21469 HandlerId::new("many-filters"),
21470 &interests,
21471 retained,
21472 )
21473 .expect("queue coordinated windows");
21474 assert_eq!(subscriber.pending_backfills.len(), 2);
21475 assert_eq!(subscriber.pending_backfills[0].filters.len(), 257);
21476 assert_eq!(subscriber.pending_backfills[1].filters.len(), 257);
21477
21478 subscriber
21479 .drain_pending_backfills()
21480 .await
21481 .expect("owner filter group");
21482 let owner = subscriber
21483 .drain_next_scoped_batch()
21484 .expect("owner ordered batch");
21485 assert_eq!(owner.records.len(), 2);
21486 assert_eq!(owner.records[0].record.context.transaction_index, Some(1));
21487 assert_eq!(owner.records[1].record.context.transaction_index, Some(2));
21488 assert!(
21489 owner.records.iter().all(|record| matches!(
21490 record.scope,
21491 SubscriberInputScope::OwnerOnlyHandlers { .. }
21492 ))
21493 );
21494
21495 subscriber
21496 .drain_pending_backfills()
21497 .await
21498 .expect("global filter group");
21499 let global = subscriber
21500 .drain_next_scoped_batch()
21501 .expect("global ordered batch");
21502 assert_eq!(global.records.len(), 2);
21503 assert_eq!(
21504 global.records[0].record.context.block.map(|b| b.number),
21505 Some(11)
21506 );
21507 assert_eq!(
21508 global.records[1].record.context.block.map(|b| b.number),
21509 Some(12)
21510 );
21511 assert!(
21512 global
21513 .records
21514 .iter()
21515 .all(|record| record.scope.is_canonical())
21516 );
21517 assert!(matches!(
21518 global.chain_controls.as_slice(),
21519 [ChainControl::Barrier {
21520 block: Some(block),
21521 ..
21522 }] if block == &activation
21523 ));
21524 }
21525
21526 #[tokio::test(flavor = "multi_thread")]
21527 #[cfg(feature = "reactive-ws")]
21528 async fn aborting_staged_epoch_purges_only_its_buffered_delivery() {
21529 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21530 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21531 provider,
21532 SubscriberMode::PubSub,
21533 SubscriberConfig::default(),
21534 );
21535 subscriber.chain_id = Some(1);
21536 let interest = ReactiveInterest::Logs(LogInterest {
21537 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21538 local_matcher: None,
21539 route_key: None,
21540 });
21541 let owner_a = subscriber
21542 .stage_interest_owner(
21543 HandlerId::new("owner-a"),
21544 std::slice::from_ref(&interest),
21545 SubscriberOwnerStart::Live,
21546 )
21547 .unwrap();
21548 let owner_b = subscriber
21549 .stage_interest_owner(
21550 HandlerId::new("owner-b"),
21551 &[interest],
21552 SubscriberOwnerStart::Live,
21553 )
21554 .unwrap();
21555
21556 subscriber.enqueue_event(SubscriberEvent::Log {
21557 source_id: 0,
21558 log: rpc_log(false),
21559 });
21560 assert!(subscriber.abort_interest_owner(&owner_a));
21561
21562 let batch = subscriber
21563 .next_scoped_batch()
21564 .await
21565 .unwrap()
21566 .expect("shared canonical delivery remains queued");
21567 assert_eq!(batch.records.len(), 1);
21568 assert_eq!(
21569 batch.records[0].scope,
21570 SubscriberInputScope::Canonical {
21571 owners: vec![owner_b]
21572 }
21573 );
21574 }
21575
21576 #[tokio::test(flavor = "multi_thread")]
21577 #[cfg(feature = "reactive-ws")]
21578 async fn owner_backfill_dedupe_never_suppresses_canonical_delivery() {
21579 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21580 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21581 provider,
21582 SubscriberMode::PubSub,
21583 SubscriberConfig::default(),
21584 );
21585 subscriber.chain_id = Some(1);
21586 let epoch = subscriber
21587 .stage_interest_owner(
21588 HandlerId::new("owner"),
21589 &[ReactiveInterest::Logs(LogInterest {
21590 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21591 local_matcher: None,
21592 route_key: None,
21593 })],
21594 SubscriberOwnerStart::Live,
21595 )
21596 .unwrap();
21597 let log = rpc_log(false);
21598
21599 subscriber.enqueue_owner_record(
21600 log_input_record(log.clone(), InputSource::Backfill),
21601 epoch.clone(),
21602 );
21603 subscriber.enqueue_event(SubscriberEvent::Log { source_id: 0, log });
21604
21605 let batch = subscriber
21606 .next_scoped_batch()
21607 .await
21608 .unwrap()
21609 .expect("owner backfill and canonical live delivery");
21610 assert_eq!(batch.records.len(), 2);
21611 assert_eq!(
21612 batch.records[0].scope,
21613 SubscriberInputScope::OwnerOnly {
21614 owners: vec![epoch]
21615 }
21616 );
21617 assert_eq!(
21618 batch.records[1].scope,
21619 SubscriberInputScope::Canonical { owners: Vec::new() },
21620 "owner replay dedupe must not suppress the global live record"
21621 );
21622 }
21623
21624 #[tokio::test(flavor = "multi_thread")]
21625 #[cfg(feature = "reactive-polling")]
21626 async fn reconcile_fetch_drains_live_burst_beyond_output_batch_capacity() {
21627 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21628 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21629 provider,
21630 SubscriberMode::Polling,
21631 SubscriberConfig {
21632 max_batch_size: 2,
21633 ..SubscriberConfig::default()
21634 },
21635 );
21636 let interest = ReactiveInterest::Logs(LogInterest {
21637 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21638 local_matcher: None,
21639 route_key: None,
21640 });
21641 let epoch = subscriber
21642 .stage_interest_owner(
21643 HandlerId::new("owner"),
21644 std::slice::from_ref(&interest),
21645 SubscriberOwnerStart::Live,
21646 )
21647 .unwrap();
21648 subscriber.sources_dirty = false;
21649
21650 let mut duplicate = rpc_log(false);
21651 duplicate.transaction_hash = Some(B256::repeat_byte(1));
21652 duplicate.log_index = Some(0);
21653 let events = (0u8..10).map(|index| {
21654 let mut log = rpc_log(false);
21655 log.transaction_hash = Some(B256::repeat_byte(index.saturating_add(1)));
21656 log.log_index = Some(index as u64);
21657 SubscriberEvent::Log { source_id: 0, log }
21658 });
21659 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
21660 let mut streams = SubscriberStreams::new();
21661 streams.push(
21662 SubscriberStreamSource::PollingLog { filter },
21663 stream::iter(events).boxed(),
21664 );
21665 subscriber.state = AlloySubscriberState::Active(streams);
21666
21667 let mut polls = 0usize;
21668 let fetched_duplicate = duplicate.clone();
21669 let fetch = poll_fn(move |cx| {
21670 polls += 1;
21671 if polls > 10 {
21672 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>(fetched_duplicate.clone()))
21673 } else {
21674 cx.waker().wake_by_ref();
21675 std::task::Poll::Pending
21676 }
21677 });
21678 let target_epochs = HashSet::from([epoch.clone()]);
21679 let fetched_duplicate = subscriber
21680 .drive_reconcile_fetch(fetch, &target_epochs)
21681 .await
21682 .unwrap();
21683 subscriber.enqueue_owner_record_for_owners_unmerged(
21684 log_input_record(fetched_duplicate, InputSource::Backfill),
21685 vec![epoch.clone()],
21686 );
21687 subscriber.promote_reconcile_owner_records(&target_epochs);
21688
21689 assert_eq!(subscriber.pending_records.len(), 20);
21690 assert!(subscriber.pending_records.iter().take(10).all(|record| {
21691 record.scope == SubscriberInputScope::Canonical { owners: Vec::new() }
21692 }));
21693 assert!(subscriber.pending_records.iter().skip(10).all(|record| {
21694 record.scope
21695 == SubscriberInputScope::OwnerOnly {
21696 owners: vec![epoch.clone()],
21697 }
21698 }));
21699 }
21700
21701 #[tokio::test(flavor = "multi_thread")]
21702 #[cfg(feature = "reactive-polling")]
21703 async fn reconcile_fetch_waits_for_provider_when_live_topology_is_empty() {
21704 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21705 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21706 provider,
21707 SubscriberMode::Polling,
21708 SubscriberConfig::default(),
21709 );
21710 subscriber.chain_id = Some(1);
21711 subscriber.sources_dirty = false;
21712 let mut first_poll = true;
21713 let fetch = poll_fn(move |cx| {
21714 if first_poll {
21715 first_poll = false;
21716 cx.waker().wake_by_ref();
21717 std::task::Poll::Pending
21718 } else {
21719 std::task::Poll::Ready(Ok::<_, SubscriberOwnerError>("certified"))
21720 }
21721 });
21722
21723 let result = subscriber
21724 .drive_reconcile_fetch(fetch, &HashSet::new())
21725 .await
21726 .expect("an empty live topology must not be mistaken for termination");
21727 assert_eq!(result, "certified");
21728 }
21729
21730 #[tokio::test(flavor = "multi_thread")]
21731 #[cfg(all(feature = "reactive-polling", feature = "reactive-ws"))]
21732 async fn successful_owner_reconcile_seeds_its_live_filter_reconnect_anchor() {
21733 use alloy_rpc_types_eth::{Block, Header};
21734
21735 let asserter = Asserter::new();
21736 let baseline = BlockRef {
21737 number: 100,
21738 hash: B256::repeat_byte(0x64),
21739 parent_hash: Some(B256::repeat_byte(0x63)),
21740 timestamp: Some(1_700_000_100),
21741 };
21742 let through = BlockRef {
21743 number: 101,
21744 hash: B256::repeat_byte(0x65),
21745 parent_hash: Some(baseline.hash),
21746 timestamp: Some(1_700_000_101),
21747 };
21748 let rpc_block = || -> Block {
21749 Block::empty(Header {
21750 hash: through.hash,
21751 inner: alloy_consensus::Header {
21752 number: through.number,
21753 parent_hash: through.parent_hash.unwrap(),
21754 timestamp: through.timestamp.unwrap(),
21755 ..Default::default()
21756 },
21757 total_difficulty: None,
21758 size: None,
21759 })
21760 };
21761 asserter.push_success(&Some(rpc_block()));
21762 asserter.push_success(&Vec::<Log>::new());
21763 asserter.push_success(&Some(rpc_block()));
21764 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
21765 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21766 provider,
21767 SubscriberMode::PubSub,
21768 SubscriberConfig::default(),
21769 );
21770 subscriber.chain_id = Some(1);
21771 let interest = ReactiveInterest::Logs(LogInterest {
21772 provider_filter: Filter::new().address(Address::repeat_byte(0xac)),
21773 local_matcher: None,
21774 route_key: None,
21775 });
21776 let epoch = subscriber
21777 .stage_interest_owner(
21778 HandlerId::new("reconnect-anchor"),
21779 std::slice::from_ref(&interest),
21780 SubscriberOwnerStart::PostBlock(baseline),
21781 )
21782 .unwrap();
21783 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
21784 let source = SubscriberStreamSource::PubSubLog {
21785 id: subscriber.log_source_id(&filter),
21786 filter: filter.clone(),
21787 };
21788 let mut streams = SubscriberStreams::new();
21789 streams.push(source, stream::pending().boxed());
21790 subscriber.state = AlloySubscriberState::Active(streams);
21791 subscriber.sources_dirty = false;
21792
21793 subscriber
21794 .reconcile_interest_owner(&epoch, through)
21795 .await
21796 .unwrap();
21797 assert_eq!(subscriber.log_anchor(&filter), Some(through.number));
21798 assert!(asserter.read_q().is_empty());
21799 }
21800
21801 #[tokio::test(flavor = "multi_thread")]
21802 #[cfg(feature = "reactive-polling")]
21803 async fn cancelled_reconcile_retains_hidden_owner_live_delivery_for_retry() {
21804 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21805 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21806 provider,
21807 SubscriberMode::Polling,
21808 SubscriberConfig::default(),
21809 );
21810 let interest = ReactiveInterest::Logs(LogInterest {
21811 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21812 local_matcher: None,
21813 route_key: None,
21814 });
21815 let epoch = subscriber
21816 .stage_interest_owner(
21817 HandlerId::new("owner"),
21818 std::slice::from_ref(&interest),
21819 SubscriberOwnerStart::PostBlock(BlockRef {
21820 number: 100,
21821 hash: B256::repeat_byte(0x64),
21822 parent_hash: None,
21823 timestamp: None,
21824 }),
21825 )
21826 .unwrap();
21827 subscriber.sources_dirty = false;
21828
21829 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
21830 let event = SubscriberEvent::Log {
21831 source_id: 0,
21832 log: rpc_log(false),
21833 };
21834 let mut streams = SubscriberStreams::new();
21835 streams.push(
21836 SubscriberStreamSource::PollingLog { filter },
21837 stream::once(async move { event })
21838 .chain(stream::pending())
21839 .boxed(),
21840 );
21841 subscriber.state = AlloySubscriberState::Active(streams);
21842
21843 let targets = HashSet::from([epoch.clone()]);
21844 {
21845 let fetch = futures::future::pending::<Result<(), SubscriberOwnerError>>();
21846 let drive = subscriber.drive_reconcile_fetch(fetch, &targets);
21847 futures::pin_mut!(drive);
21848 poll_fn(|cx| {
21849 assert!(drive.as_mut().poll(cx).is_pending());
21850 std::task::Poll::Ready(())
21851 })
21852 .await;
21853 }
21854
21855 assert_eq!(subscriber.pending_records.len(), 1);
21856 assert_eq!(
21857 subscriber.pending_records[0].scope,
21858 SubscriberInputScope::Canonical { owners: Vec::new() },
21859 "canonical delivery commits immediately at a cancellation-safe boundary"
21860 );
21861 assert_eq!(subscriber.pending_reconcile_owner_records.len(), 1);
21862
21863 subscriber
21864 .drive_reconcile_fetch(futures::future::ready(Ok(())), &targets)
21865 .await
21866 .unwrap();
21867 subscriber.promote_reconcile_owner_records(&targets);
21868 assert!(subscriber.pending_reconcile_owner_records.is_empty());
21869 assert_eq!(subscriber.pending_records.len(), 2);
21870 assert_eq!(
21871 subscriber.pending_records[0].scope,
21872 SubscriberInputScope::Canonical { owners: Vec::new() },
21873 "canonical delivery remains target-excluded"
21874 );
21875 assert_eq!(
21876 subscriber.pending_records[1].scope,
21877 SubscriberInputScope::OwnerOnly {
21878 owners: vec![epoch]
21879 },
21880 "retry commit appends hidden owner delivery after historical catch-up"
21881 );
21882 }
21883
21884 #[tokio::test(flavor = "multi_thread")]
21885 #[cfg(feature = "reactive-ws")]
21886 async fn control_cancellation_preserves_terminated_source_reconcile_intent() {
21887 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21888 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21889 provider,
21890 SubscriberMode::PubSub,
21891 SubscriberConfig {
21892 reconnect: SubscriberReconnectConfig {
21893 initial_delay: Duration::from_secs(60),
21894 ..SubscriberReconnectConfig::default()
21895 },
21896 ..SubscriberConfig::default()
21897 },
21898 );
21899 subscriber.chain_id = Some(1);
21900 let interest = ReactiveInterest::Logs(LogInterest {
21901 provider_filter: Filter::new().address(Address::repeat_byte(0x42)),
21902 local_matcher: None,
21903 route_key: None,
21904 });
21905 let epoch = subscriber
21906 .stage_interest_owner(
21907 HandlerId::new("owner"),
21908 std::slice::from_ref(&interest),
21909 SubscriberOwnerStart::PostBlock(BlockRef {
21910 number: 7,
21911 hash: B256::repeat_byte(0x07),
21912 parent_hash: Some(B256::repeat_byte(0x06)),
21913 timestamp: Some(1_700_000_007),
21914 }),
21915 )
21916 .unwrap();
21917 subscriber.sources_dirty = false;
21918 subscriber.stream_revision = 1;
21919 let entry = subscriber
21920 .owned_interests
21921 .iter_mut()
21922 .find(|entry| entry.epoch.as_ref() == Some(&epoch))
21923 .unwrap();
21924 entry.progress = Some(SubscriberOwnerProgress {
21925 owner: epoch.clone(),
21926 through: entry.baseline.unwrap(),
21927 });
21928 entry.progress_stream_revision = Some(1);
21929
21930 let filter = log_filters(std::slice::from_ref(&interest)).pop().unwrap();
21931 let source = SubscriberStreamSource::PubSubLog {
21932 id: subscriber.log_source_id(&filter),
21933 filter,
21934 };
21935 let mut streams = SubscriberStreams::new();
21936 streams.push(
21937 source.clone(),
21938 stream::iter([SubscriberEvent::StreamTerminated(source)]).boxed(),
21939 );
21940 subscriber.state = AlloySubscriberState::Active(streams);
21941 let prior_revision = subscriber.stream_revision;
21942
21943 let mut first_poll = true;
21944 let control = poll_fn(move |cx| {
21945 if first_poll {
21946 first_poll = false;
21947 cx.waker().wake_by_ref();
21948 std::task::Poll::Pending
21949 } else {
21950 std::task::Poll::Ready("stop")
21951 }
21952 });
21953 futures::pin_mut!(control);
21954 let outcome = subscriber
21955 .next_scoped_batch_or(control.as_mut())
21956 .await
21957 .unwrap();
21958
21959 assert!(matches!(outcome, SubscriberDriverPoll::Control("stop")));
21960 assert!(subscriber.sources_dirty);
21961 assert!(subscriber.stream_revision > prior_revision);
21962 assert!(
21963 !subscriber.activate_interest_owner(&epoch),
21964 "progress certified against the terminated stream revision is stale"
21965 );
21966 }
21967
21968 #[tokio::test]
21969 #[cfg(feature = "reactive-ws")]
21970 async fn pubsub_sources_assign_stable_log_ids_before_shared_streams() {
21971 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
21972 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
21973 provider,
21974 SubscriberMode::PubSub,
21975 SubscriberConfig::default(),
21976 );
21977 subscriber.chain_id = Some(1);
21978 subscriber
21979 .register_interests(&[
21980 ReactiveInterest::Logs(LogInterest {
21981 provider_filter: Filter::new().address(Address::repeat_byte(0x01)),
21982 local_matcher: None,
21983 route_key: None,
21984 }),
21985 ReactiveInterest::Logs(LogInterest {
21986 provider_filter: Filter::new().address(Address::repeat_byte(0x02)),
21987 local_matcher: None,
21988 route_key: None,
21989 }),
21990 ReactiveInterest::PendingTransactions(PendingTxInterest::default()),
21991 ])
21992 .await
21993 .expect("register base interests");
21994
21995 let sources = subscriber.stream_sources().expect("stream sources");
21999 assert_eq!(sources.len(), 2);
22000 assert!(matches!(
22001 &sources[0],
22002 SubscriberStreamSource::PubSubLog { id: 0, .. }
22003 ));
22004 assert!(matches!(
22005 sources[1],
22006 SubscriberStreamSource::PubSubPendingHashes
22007 ));
22008
22009 let again = subscriber.stream_sources().expect("stream sources again");
22011 assert!(again[0].same_key(&sources[0]));
22012 }
22013
22014 #[tokio::test(flavor = "multi_thread")]
22015 #[cfg(feature = "reactive-ws")]
22016 async fn pubsub_stream_termination_attempts_reconnect_before_error() {
22017 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22018 let mut subscriber = AlloySubscriber::new(
22019 provider,
22020 SubscriberMode::PubSub,
22021 SubscriberConfig {
22022 reconnect: SubscriberReconnectConfig {
22023 initial_delay: Duration::ZERO,
22024 retry_delay: Duration::ZERO,
22025 max_delay: Duration::ZERO,
22026 max_attempts: Some(1),
22027 ..SubscriberReconnectConfig::default()
22028 },
22029 ..SubscriberConfig::default()
22030 },
22031 );
22032 subscriber.chain_id = Some(1);
22033 subscriber.interests = vec![ReactiveInterest::PendingTransactions(
22034 PendingTxInterest::default(),
22035 )];
22036
22037 let mut streams = SubscriberStreams::new();
22038 let source = SubscriberStreamSource::PubSubPendingHashes;
22039 streams.push(
22040 source,
22041 stream::once(async {
22042 SubscriberEvent::<Ethereum>::StreamTerminated(
22043 SubscriberStreamSource::PubSubPendingHashes,
22044 )
22045 })
22046 .boxed(),
22047 );
22048 subscriber.state = AlloySubscriberState::Active(streams);
22049
22050 let result = subscriber.next_batch().await;
22051 assert!(
22052 matches!(result, Err(SubscriberError::Provider(ref message)) if message.contains("reconnect failed after 1 attempt")),
22053 "terminated pubsub streams should attempt reconnect before surfacing failure: {result:?}"
22054 );
22055 }
22056
22057 #[tokio::test]
22058 #[cfg(feature = "reactive-ws")]
22059 async fn flashblock_stream_termination_invalidates_before_reconnect_io() {
22060 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22061 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22062 provider,
22063 SubscriberMode::PubSub,
22064 SubscriberConfig {
22065 preconfirmations: PreconfirmationMode::Required,
22066 ..SubscriberConfig::default()
22067 },
22068 )
22069 .with_provider_ref(ProviderRef::new("base-paid", 7));
22070 subscriber.chain_id = Some(8_453);
22071 subscriber.base_interests = vec![log_interest_matching_rpc_log()];
22072 subscriber.interests = subscriber.base_interests.clone();
22073 subscriber.sources_dirty = false;
22074
22075 let preview: BaseFlashblockWirePayload = serde_json::from_str(
22076 r#"{
22077 "hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
22078 "number":"0x65",
22079 "parentHash":"0x6464646464646464646464646464646464646464646464646464646464646464",
22080 "stateRoot":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
22081 "transactionsRoot":"0x1111111111111111111111111111111111111111111111111111111111111111",
22082 "timestamp":"0x6553f165",
22083 "transactions":["0x4141414141414141414141414141414141414141414141414141414141414141"]
22084 }"#,
22085 )
22086 .unwrap();
22087 let (preview, _) = subscriber.accept_base_flashblock(preview).unwrap();
22088 subscriber.latest_preconfirmation = Some(preview);
22089
22090 let mut streams = SubscriberStreams::new();
22091 streams.push(
22092 SubscriberStreamSource::BaseFlashblocks,
22093 stream::once(async {
22094 SubscriberEvent::<Ethereum>::StreamTerminated(
22095 SubscriberStreamSource::BaseFlashblocks,
22096 )
22097 })
22098 .boxed(),
22099 );
22100 subscriber.state = AlloySubscriberState::Active(streams);
22101
22102 let batch = subscriber
22103 .next_scoped_batch()
22104 .await
22105 .expect("termination handling succeeds")
22106 .expect("invalidation is delivered");
22107 assert!(batch.preconfirmation_invalidated());
22108 assert!(subscriber.latest_preconfirmation.is_none());
22109 assert_eq!(subscriber.provider_ref.as_ref().unwrap().generation, 8);
22110 assert_eq!(subscriber.pending_flashblock_reconnects.len(), 2);
22111 assert!(
22112 subscriber
22113 .pending_flashblock_reconnect_sources
22114 .iter()
22115 .any(|source| matches!(source, SubscriberStreamSource::BaseFlashblocks))
22116 );
22117 assert!(
22118 subscriber
22119 .pending_flashblock_reconnect_sources
22120 .iter()
22121 .any(|source| matches!(source, SubscriberStreamSource::BasePendingLog { .. }))
22122 );
22123 let AlloySubscriberState::Active(streams) = &subscriber.state else {
22124 panic!("subscriber remains active while reconnect is pending")
22125 };
22126 assert!(
22127 streams
22128 .entries
22129 .iter()
22130 .all(|entry| !entry.source.is_flashblocks())
22131 );
22132 }
22133
22134 #[tokio::test]
22135 #[cfg(feature = "reactive-ws")]
22136 async fn preferred_initial_flashblock_rejection_retains_canonical_streams() {
22137 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22138 let filter = Filter::new().address(Address::repeat_byte(0x42));
22139 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22140 provider,
22141 SubscriberMode::PubSub,
22142 SubscriberConfig {
22143 preconfirmations: PreconfirmationMode::Preferred,
22144 reconnect: SubscriberReconnectConfig {
22145 enabled: false,
22146 ..SubscriberReconnectConfig::default()
22147 },
22148 ..SubscriberConfig::default()
22149 },
22150 )
22151 .with_provider_ref(ProviderRef::new("base-paid", 1));
22152 subscriber.chain_id = Some(8_453);
22153 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
22154 provider_filter: filter.clone(),
22155 local_matcher: None,
22156 route_key: None,
22157 })];
22158 subscriber.interests = subscriber.base_interests.clone();
22159 subscriber.log_source_ids.insert(filter.clone(), 0);
22160 subscriber.next_log_source_id = 1;
22161
22162 let canonical_source = SubscriberStreamSource::PubSubLog {
22163 id: 0,
22164 filter: filter.clone(),
22165 };
22166 let mut streams = SubscriberStreams::new();
22167 streams.push(
22168 canonical_source.clone(),
22169 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
22170 );
22171 subscriber.state = AlloySubscriberState::Active(streams);
22172 subscriber.sources_dirty = true;
22173
22174 subscriber
22175 .ensure_streams()
22176 .await
22177 .expect("preferred Flashblocks setup degrades to canonical-only");
22178 let AlloySubscriberState::Active(streams) = &subscriber.state else {
22179 panic!("canonical stream remains active")
22180 };
22181 assert!(streams.contains_source(&canonical_source));
22182 assert!(
22183 streams
22184 .entries
22185 .iter()
22186 .all(|entry| !entry.source.is_flashblocks())
22187 );
22188 assert!(subscriber.pending_flashblock_reconnects.is_empty());
22189 assert!(!subscriber.sources_dirty);
22190 }
22191
22192 #[tokio::test]
22193 #[cfg(feature = "reactive-ws")]
22194 async fn required_initial_flashblock_rejection_remains_fail_closed() {
22195 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22196 let filter = Filter::new().address(Address::repeat_byte(0x42));
22197 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22198 provider,
22199 SubscriberMode::PubSub,
22200 SubscriberConfig {
22201 preconfirmations: PreconfirmationMode::Required,
22202 reconnect: SubscriberReconnectConfig {
22203 enabled: false,
22204 ..SubscriberReconnectConfig::default()
22205 },
22206 ..SubscriberConfig::default()
22207 },
22208 )
22209 .with_provider_ref(ProviderRef::new("base-paid", 1));
22210 subscriber.chain_id = Some(8_453);
22211 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
22212 provider_filter: filter.clone(),
22213 local_matcher: None,
22214 route_key: None,
22215 })];
22216 subscriber.interests = subscriber.base_interests.clone();
22217 subscriber.log_source_ids.insert(filter.clone(), 0);
22218 subscriber.next_log_source_id = 1;
22219
22220 let canonical_source = SubscriberStreamSource::PubSubLog {
22221 id: 0,
22222 filter: filter.clone(),
22223 };
22224 let mut streams = SubscriberStreams::new();
22225 streams.push(
22226 canonical_source.clone(),
22227 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
22228 );
22229 subscriber.state = AlloySubscriberState::Active(streams);
22230 subscriber.sources_dirty = true;
22231
22232 let error = subscriber
22233 .ensure_streams()
22234 .await
22235 .expect_err("required Flashblocks setup must fail closed");
22236 assert!(matches!(error, SubscriberError::Provider(_)));
22237 let AlloySubscriberState::Active(streams) = &subscriber.state else {
22238 panic!("the already-connected canonical stream is retained")
22239 };
22240 assert!(streams.contains_source(&canonical_source));
22241 }
22242
22243 #[tokio::test]
22244 #[cfg(feature = "reactive-ws")]
22245 async fn preferred_flashblock_termination_preserves_canonical_delivery() {
22246 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22247 let filter = Filter::new().address(Address::repeat_byte(0x42));
22248 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22249 provider,
22250 SubscriberMode::PubSub,
22251 SubscriberConfig {
22252 preconfirmations: PreconfirmationMode::Preferred,
22253 reconnect: SubscriberReconnectConfig {
22254 enabled: false,
22255 ..SubscriberReconnectConfig::default()
22256 },
22257 ..SubscriberConfig::default()
22258 },
22259 )
22260 .with_provider_ref(ProviderRef::new("base-paid", 1));
22261 subscriber.chain_id = Some(8_453);
22262 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
22263 provider_filter: filter.clone(),
22264 local_matcher: None,
22265 route_key: None,
22266 })];
22267 subscriber.interests = subscriber.base_interests.clone();
22268 subscriber.log_source_ids.insert(filter.clone(), 0);
22269 subscriber.next_log_source_id = 1;
22270 subscriber.sources_dirty = false;
22271
22272 let mut streams = SubscriberStreams::new();
22273 streams.push(
22274 SubscriberStreamSource::BaseFlashblocks,
22275 stream::once(async {
22276 SubscriberEvent::<Ethereum>::StreamTerminated(
22277 SubscriberStreamSource::BaseFlashblocks,
22278 )
22279 })
22280 .boxed(),
22281 );
22282 streams.push(
22283 SubscriberStreamSource::PubSubLog {
22284 id: 0,
22285 filter: filter.clone(),
22286 },
22287 stream::once(async {
22288 SubscriberEvent::<Ethereum>::Log {
22289 source_id: 0,
22290 log: rpc_log(false),
22291 }
22292 })
22293 .boxed(),
22294 );
22295 subscriber.state = AlloySubscriberState::Active(streams);
22296
22297 let invalidation = subscriber
22298 .next_scoped_batch()
22299 .await
22300 .expect("preferred termination does not fail")
22301 .expect("invalidation is delivered");
22302 assert!(invalidation.preconfirmation_invalidated());
22303
22304 let canonical = subscriber
22305 .next_scoped_batch()
22306 .await
22307 .expect("canonical stream remains healthy")
22308 .expect("canonical log is delivered");
22309 assert!(!canonical.preconfirmation_invalidated());
22310 assert_eq!(canonical.records().len(), 1);
22311 assert_eq!(
22312 canonical.records()[0].record.context.source,
22313 InputSource::Subscription
22314 );
22315 }
22316
22317 #[tokio::test]
22318 #[cfg(feature = "reactive-ws")]
22319 async fn preferred_flashblock_reconnect_exhaustion_preserves_canonical_delivery() {
22320 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22321 let filter = Filter::new().address(Address::repeat_byte(0x42));
22322 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22323 provider,
22324 SubscriberMode::PubSub,
22325 SubscriberConfig {
22326 preconfirmations: PreconfirmationMode::Preferred,
22327 reconnect: SubscriberReconnectConfig {
22328 enabled: false,
22329 ..SubscriberReconnectConfig::default()
22330 },
22331 ..SubscriberConfig::default()
22332 },
22333 )
22334 .with_provider_ref(ProviderRef::new("base-paid", 1));
22335 subscriber.chain_id = Some(8_453);
22336 subscriber.base_interests = vec![ReactiveInterest::Logs(LogInterest {
22337 provider_filter: filter.clone(),
22338 local_matcher: None,
22339 route_key: None,
22340 })];
22341 subscriber.interests = subscriber.base_interests.clone();
22342 subscriber.log_source_ids.insert(filter.clone(), 0);
22343 subscriber.next_log_source_id = 1;
22344 subscriber.sources_dirty = false;
22345
22346 let canonical_source = SubscriberStreamSource::PubSubLog { id: 0, filter };
22347 let mut streams = SubscriberStreams::new();
22348 streams.push(
22349 canonical_source,
22350 stream::once(async {
22351 tokio::time::sleep(Duration::from_millis(1)).await;
22352 SubscriberEvent::<Ethereum>::Log {
22353 source_id: 0,
22354 log: rpc_log(false),
22355 }
22356 })
22357 .boxed(),
22358 );
22359 subscriber.state = AlloySubscriberState::Active(streams);
22360
22361 let source = SubscriberStreamSource::BaseFlashblocks;
22362 subscriber
22363 .pending_flashblock_reconnect_sources
22364 .push(source.clone());
22365 subscriber
22366 .pending_flashblock_reconnects
22367 .push(Box::pin(async move {
22368 (
22369 source,
22370 Err(SubscriberError::Provider(
22371 "test reconnect window exhausted".to_owned(),
22372 )),
22373 )
22374 }));
22375
22376 let canonical = subscriber
22377 .next_scoped_batch()
22378 .await
22379 .expect("preferred reconnect exhaustion does not fail")
22380 .expect("canonical log is delivered");
22381 assert_eq!(canonical.records().len(), 1);
22382 assert!(subscriber.pending_flashblock_reconnects.is_empty());
22383 }
22384
22385 #[test]
22386 fn backfilled_logs_skip_recent_subscription_duplicates() {
22387 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22388 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22389 provider,
22390 SubscriberMode::PubSub,
22391 SubscriberConfig::default(),
22392 );
22393 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
22394 provider_filter: Filter::new()
22395 .address(Address::repeat_byte(0x42))
22396 .event_signature(B256::repeat_byte(0x01)),
22397 local_matcher: None,
22398 route_key: None,
22399 })];
22400
22401 let log = rpc_log(false);
22402 subscriber.enqueue_event(SubscriberEvent::Log {
22403 source_id: 0,
22404 log: log.clone(),
22405 });
22406 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
22407 source_id: 0,
22408 logs: vec![log],
22409 });
22410
22411 assert_eq!(subscriber.pending_records.len(), 1);
22412 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
22413 assert_eq!(
22414 subscriber.pending_records[0].context.source,
22415 InputSource::Subscription
22416 );
22417 }
22418
22419 #[test]
22420 fn backfilled_logs_surface_with_backfill_source() {
22421 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22426 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22427 provider,
22428 SubscriberMode::PubSub,
22429 SubscriberConfig::default(),
22430 );
22431 subscriber.interests = vec![ReactiveInterest::Logs(LogInterest {
22432 provider_filter: Filter::new()
22433 .address(Address::repeat_byte(0x42))
22434 .event_signature(B256::repeat_byte(0x01)),
22435 local_matcher: None,
22436 route_key: None,
22437 })];
22438
22439 subscriber.enqueue_event(SubscriberEvent::BackfilledLogs {
22440 source_id: 0,
22441 logs: vec![rpc_log(false)],
22442 });
22443
22444 assert_eq!(subscriber.pending_records.len(), 1);
22445 assert_eq!(
22446 subscriber.pending_records[0].context.source,
22447 InputSource::Backfill
22448 );
22449 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
22450 }
22451
22452 #[test]
22453 #[cfg(feature = "reactive-ws")]
22454 fn owner_removal_preserves_delivery_and_dedupe_state() {
22455 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22456 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22457 provider,
22458 SubscriberMode::PubSub,
22459 SubscriberConfig::default(),
22460 );
22461 subscriber
22462 .add_interest_owner(
22463 HandlerId::new("pool-a"),
22464 &[ReactiveInterest::Logs(LogInterest {
22465 provider_filter: Filter::new()
22466 .address(Address::repeat_byte(0x42))
22467 .event_signature(B256::repeat_byte(0x01)),
22468 local_matcher: None,
22469 route_key: None,
22470 })],
22471 )
22472 .expect("register pool-a owner");
22473 subscriber
22474 .add_interest_owner(
22475 HandlerId::new("pool-b"),
22476 &[ReactiveInterest::Logs(LogInterest {
22477 provider_filter: Filter::new()
22478 .address(Address::repeat_byte(0x24))
22479 .event_signature(B256::repeat_byte(0x02)),
22480 local_matcher: None,
22481 route_key: None,
22482 })],
22483 )
22484 .expect("register pool-b owner");
22485
22486 let sources = subscriber.stream_sources().expect("stream sources");
22489 subscriber.enqueue_event(SubscriberEvent::Log {
22490 source_id: 0,
22491 log: rpc_log(false),
22492 });
22493 let mut streams = SubscriberStreams::new();
22494 streams.push(
22495 sources[0].clone(),
22496 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
22497 );
22498 subscriber.state = AlloySubscriberState::Active(streams);
22499 assert_eq!(subscriber.pending_records.len(), 1);
22500 assert_eq!(subscriber.recent_input_refs.len(), 1);
22501 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
22502
22503 let removed = subscriber
22504 .remove_interest_owner(&HandlerId::new("pool-b"))
22505 .expect("pool-b should be removed");
22506
22507 assert_eq!(removed.len(), 1);
22508 assert_eq!(subscriber.pending_records.len(), 1);
22509 assert_eq!(subscriber.recent_input_refs.len(), 1);
22510 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
22511 assert!(
22512 subscriber
22513 .owner_interests(&HandlerId::new("pool-a"))
22514 .is_some()
22515 );
22516 assert!(
22517 subscriber
22518 .owner_interests(&HandlerId::new("pool-b"))
22519 .is_none()
22520 );
22521 assert_eq!(subscriber.registered_interests().len(), 1);
22522 }
22523
22524 #[test]
22525 #[cfg(feature = "reactive-ws")]
22526 fn owner_log_sources_fan_in_across_owners() {
22527 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22528 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22529 provider,
22530 SubscriberMode::PubSub,
22531 SubscriberConfig::default(),
22532 );
22533 subscriber
22534 .add_interest_owner(
22535 HandlerId::new("pool-a"),
22536 &[ReactiveInterest::Logs(LogInterest {
22537 provider_filter: Filter::new().address(Address::repeat_byte(0xa1)),
22538 local_matcher: None,
22539 route_key: None,
22540 })],
22541 )
22542 .expect("register pool-a owner");
22543
22544 let initial_sources = subscriber.stream_sources().expect("initial sources");
22545 assert_eq!(initial_sources.len(), 1);
22546 let pool_a_source = initial_sources[0].clone();
22547 assert!(matches!(
22548 &pool_a_source,
22549 SubscriberStreamSource::PubSubLog { id: 0, .. }
22550 ));
22551
22552 subscriber
22553 .add_interest_owner(
22554 HandlerId::new("pool-b"),
22555 &[ReactiveInterest::Logs(LogInterest {
22556 provider_filter: Filter::new().address(Address::repeat_byte(0xb2)),
22557 local_matcher: None,
22558 route_key: None,
22559 })],
22560 )
22561 .expect("register pool-b owner");
22562
22563 let expanded_sources = subscriber.stream_sources().expect("expanded sources");
22564 assert_eq!(
22565 expanded_sources.len(),
22566 1,
22567 "compatible owner filters should share one provider subscription"
22568 );
22569 assert!(
22570 !expanded_sources[0].same_key(&pool_a_source),
22571 "the provider-facing superset changes while owner routing remains exact"
22572 );
22573
22574 subscriber
22575 .remove_interest_owner(&HandlerId::new("pool-b"))
22576 .expect("pool-b should be removed");
22577 let trimmed_sources = subscriber.stream_sources().expect("trimmed sources");
22578 assert_eq!(trimmed_sources.len(), 1);
22579 assert!(trimmed_sources[0].same_key(&pool_a_source));
22580 }
22581
22582 #[test]
22583 #[cfg(feature = "reactive-ws")]
22584 fn provider_log_fan_in_respects_address_ceiling() {
22585 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22586 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22587 provider,
22588 SubscriberMode::PubSub,
22589 SubscriberConfig {
22590 max_log_addresses_per_subscription: 2,
22591 ..SubscriberConfig::default()
22592 },
22593 );
22594 for index in 0..5 {
22595 subscriber
22596 .add_interest_owner(
22597 HandlerId::new(format!("pool-{index}")),
22598 &[log_interest_for(index + 1)],
22599 )
22600 .expect("register pool owner");
22601 }
22602
22603 let sources = subscriber.stream_sources().expect("stream sources");
22604 assert_eq!(sources.len(), 3);
22605 let mut address_counts: Vec<_> = sources
22606 .iter()
22607 .map(|source| match source {
22608 SubscriberStreamSource::PubSubLog { filter, .. } => filter.address.iter().count(),
22609 _ => panic!("expected log source"),
22610 })
22611 .collect();
22612 address_counts.sort_unstable();
22613 assert_eq!(address_counts, vec![1, 2, 2]);
22614 }
22615
22616 #[tokio::test(flavor = "multi_thread")]
22617 #[cfg(feature = "reactive-ws")]
22618 async fn owner_backfill_seeds_reconnect_anchor_before_live_log() {
22619 let asserter = Asserter::new();
22620 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
22621 asserter.push_success(&vec![rpc_log(false)]);
22622 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
22623 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
22624 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22625 provider,
22626 SubscriberMode::PubSub,
22627 SubscriberConfig::default(),
22628 );
22629 subscriber
22630 .add_interest_owner_with_backfill(
22631 HandlerId::new("pool-a"),
22632 &[ReactiveInterest::Logs(LogInterest {
22633 provider_filter: Filter::new()
22634 .address(Address::repeat_byte(0x42))
22635 .event_signature(B256::repeat_byte(0x01)),
22636 local_matcher: None,
22637 route_key: None,
22638 })],
22639 SubscriberBackfill::range(1, 7),
22640 )
22641 .expect("register pool-a with backfill");
22642
22643 subscriber
22644 .drain_pending_backfills()
22645 .await
22646 .expect("owner backfill should drain");
22647
22648 assert_eq!(subscriber.pending_records.len(), 1);
22649 assert_eq!(subscriber.last_seen_log_blocks.get(&0), Some(&7));
22650 }
22651
22652 #[tokio::test(flavor = "multi_thread")]
22653 async fn subscriber_streams_poll_ready_sources_round_robin() {
22654 let first_hash = B256::repeat_byte(0x01);
22655 let second_hash = B256::repeat_byte(0x02);
22656 let mut streams = SubscriberStreams::new();
22657 streams.push(
22658 SubscriberStreamSource::PubSubPendingHashes,
22659 stream::iter([
22660 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
22661 SubscriberEvent::<Ethereum>::PendingHash(first_hash),
22662 ])
22663 .boxed(),
22664 );
22665 streams.push(
22666 SubscriberStreamSource::PubSubBlockHeaders,
22667 stream::once(async move { SubscriberEvent::<Ethereum>::PendingHash(second_hash) })
22668 .boxed(),
22669 );
22670
22671 assert!(matches!(
22672 streams.next().await,
22673 Some(SubscriberEvent::PendingHash(hash)) if hash == first_hash
22674 ));
22675 assert!(matches!(
22676 streams.next().await,
22677 Some(SubscriberEvent::PendingHash(hash)) if hash == second_hash
22678 ));
22679 }
22680
22681 #[tokio::test(flavor = "multi_thread")]
22682 #[cfg(feature = "reactive-ws")]
22683 async fn owner_updates_ensure_streams_without_full_reset() {
22684 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
22685 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22686 provider,
22687 SubscriberMode::PubSub,
22688 SubscriberConfig::default(),
22689 );
22690 subscriber.chain_id = Some(1);
22691 subscriber
22692 .register_interests(&[ReactiveInterest::PendingTransactions(
22693 PendingTxInterest::default(),
22694 )])
22695 .await
22696 .expect("register base pending interest");
22697 subscriber
22698 .add_interest_owner(
22699 HandlerId::new("headers"),
22700 &[ReactiveInterest::Blocks(BlockInterest::default())],
22701 )
22702 .expect("register header owner");
22703
22704 let mut streams = SubscriberStreams::new();
22705 streams.push(
22706 SubscriberStreamSource::PubSubPendingHashes,
22707 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
22708 );
22709 streams.push(
22710 SubscriberStreamSource::PubSubBlockHeaders,
22711 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
22712 );
22713 subscriber.state = AlloySubscriberState::Active(streams);
22714
22715 subscriber
22716 .remove_interest_owner(&HandlerId::new("headers"))
22717 .expect("header owner should be removed");
22718 assert!(matches!(
22719 &subscriber.state,
22720 AlloySubscriberState::Active(streams) if streams.len() == 2
22721 ));
22722
22723 subscriber
22724 .ensure_streams()
22725 .await
22726 .expect("pure removal reconciliation should not touch provider");
22727
22728 assert!(matches!(
22729 &subscriber.state,
22730 AlloySubscriberState::Active(streams)
22731 if streams.len() == 1
22732 && streams.contains_source(&SubscriberStreamSource::PubSubPendingHashes)
22733 && !streams.contains_source(&SubscriberStreamSource::PubSubBlockHeaders)
22734 ));
22735
22736 subscriber
22737 .add_interest_owner(
22738 HandlerId::new("headers"),
22739 &[ReactiveInterest::Blocks(BlockInterest::default())],
22740 )
22741 .expect("re-add header owner");
22742 assert!(matches!(
22743 &subscriber.state,
22744 AlloySubscriberState::Active(streams) if streams.len() == 1
22745 ));
22746 }
22747
22748 #[tokio::test(flavor = "multi_thread")]
22749 #[cfg(feature = "reactive-polling")]
22750 async fn ensure_streams_retains_each_successful_connection_across_later_failure() {
22751 let asserter = Asserter::new();
22752 asserter.push_success(&U256::from(1));
22753 asserter.push_failure_msg("second filter connection failed");
22754 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
22755 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22756 provider,
22757 SubscriberMode::Polling,
22758 SubscriberConfig {
22759 max_log_addresses_per_subscription: 1,
22760 ..SubscriberConfig::default()
22761 },
22762 );
22763 subscriber.chain_id = Some(1);
22764 subscriber
22765 .register_interests(&[log_interest_for(0x41), log_interest_for(0x42)])
22766 .await
22767 .expect("register two independently connected filters");
22768
22769 let error = subscriber
22770 .ensure_streams()
22771 .await
22772 .expect_err("second provider connection is forced to fail");
22773 assert!(matches!(error, SubscriberError::Provider(_)));
22774 assert!(subscriber.sources_dirty);
22775 let retained_streams = match &subscriber.state {
22776 AlloySubscriberState::Active(streams) => Some(streams.len()),
22777 AlloySubscriberState::Uninitialized | AlloySubscriberState::Empty => None,
22778 };
22779 assert_eq!(
22780 retained_streams,
22781 Some(1),
22782 "first connection must survive later error {error:?}; revision {}",
22783 subscriber.stream_revision
22784 );
22785
22786 asserter.push_success(&U256::from(2));
22787 subscriber
22788 .ensure_streams()
22789 .await
22790 .expect("retry connects only the missing source");
22791 assert!(!subscriber.sources_dirty);
22792 assert!(matches!(
22793 &subscriber.state,
22794 AlloySubscriberState::Active(streams) if streams.len() == 2
22795 ));
22796 assert!(asserter.read_q().is_empty());
22797 }
22798
22799 #[tokio::test(flavor = "multi_thread")]
22800 #[cfg(feature = "reactive-ws")]
22801 async fn cancelled_post_install_backfill_is_retried_without_reconnecting() {
22802 let asserter = Asserter::new();
22803 let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());
22804 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
22805 provider,
22806 SubscriberMode::PubSub,
22807 SubscriberConfig::default(),
22808 );
22809 subscriber.chain_id = Some(1);
22810 subscriber
22811 .register_interests(&[log_interest_for(0x43)])
22812 .await
22813 .expect("register log source");
22814 let source = subscriber
22815 .stream_sources()
22816 .expect("one desired source")
22817 .pop()
22818 .expect("log source");
22819 let SubscriberStreamSource::PubSubLog { id, .. } = source else {
22820 panic!("expected pubsub log source")
22821 };
22822 subscriber.last_seen_log_blocks.insert(id, 6);
22823
22824 {
22825 let source = SubscriberStreamSource::PubSubLog {
22826 id,
22827 filter: subscriber
22828 .log_stream_filters()
22829 .pop()
22830 .expect("provider filter"),
22831 };
22832 let interrupted = async {
22833 subscriber.install_source_stream(
22834 source.clone(),
22835 stream::pending::<SubscriberEvent<Ethereum>>().boxed(),
22836 );
22837 subscriber.queue_source_backfill(source);
22838 subscriber.sources_dirty = true;
22839 futures::future::pending::<()>().await;
22840 };
22841 futures::pin_mut!(interrupted);
22842 poll_fn(|cx| {
22843 assert!(interrupted.as_mut().poll(cx).is_pending());
22844 std::task::Poll::Ready(())
22845 })
22846 .await;
22847 }
22848
22849 assert_eq!(subscriber.pending_source_backfills.len(), 1);
22850 assert!(matches!(
22851 &subscriber.state,
22852 AlloySubscriberState::Active(streams) if streams.len() == 1
22853 ));
22854
22855 asserter.push_success(&7u64);
22856 asserter.push_success(&Vec::<Log>::new());
22857 subscriber
22858 .ensure_streams()
22859 .await
22860 .expect("retry completes only the pending historical window");
22861
22862 assert!(subscriber.pending_source_backfills.is_empty());
22863 assert!(!subscriber.sources_dirty);
22864 assert!(matches!(
22865 &subscriber.state,
22866 AlloySubscriberState::Active(streams) if streams.len() == 1
22867 ));
22868 assert!(asserter.read_q().is_empty());
22869 }
22870
22871 #[cfg(any(feature = "reactive-ws", feature = "reactive-polling"))]
22873 fn log_interest_matching_rpc_log() -> ReactiveInterest<Ethereum> {
22874 ReactiveInterest::Logs(LogInterest {
22875 provider_filter: Filter::new()
22876 .address(Address::repeat_byte(0x42))
22877 .event_signature(B256::repeat_byte(0x01)),
22878 local_matcher: None,
22879 route_key: None,
22880 })
22881 }
22882
22883 #[cfg(any(feature = "reactive-ws", feature = "reactive-polling"))]
22884 fn log_interest_for(address: u8) -> ReactiveInterest<Ethereum> {
22885 ReactiveInterest::Logs(LogInterest {
22886 provider_filter: Filter::new().address(Address::repeat_byte(address)),
22887 local_matcher: None,
22888 route_key: None,
22889 })
22890 }
22891
22892 #[tokio::test(flavor = "multi_thread")]
22895 #[cfg(feature = "reactive-ws")]
22896 async fn drain_backfill_retains_queue_entry_on_provider_error() {
22897 let asserter = Asserter::new();
22898 asserter.push_failure_msg("rate limited");
22899 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
22900 asserter.push_success(&vec![rpc_log(false)]);
22901 asserter.push_success(&Some(rpc_block(7, B256::repeat_byte(0x02))));
22902 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
22903 let mut subscriber = AlloySubscriber::new(
22904 provider,
22905 SubscriberMode::PubSub,
22906 SubscriberConfig::default(),
22907 );
22908 subscriber
22909 .add_interest_owner_with_backfill(
22910 HandlerId::new("pool"),
22911 &[log_interest_matching_rpc_log()],
22912 SubscriberBackfill::range(1, 7),
22913 )
22914 .expect("register owner with backfill");
22915 assert_eq!(subscriber.pending_backfills.len(), 1);
22916
22917 let first = subscriber.drain_pending_backfills().await;
22918 assert!(first.is_err(), "provider failure should surface");
22919 assert_eq!(
22920 subscriber.pending_backfills.len(),
22921 1,
22922 "failed fetch must leave the backfill queued for retry"
22923 );
22924 assert!(subscriber.pending_records.is_empty());
22925
22926 subscriber
22927 .drain_pending_backfills()
22928 .await
22929 .expect("retry should succeed");
22930 assert!(subscriber.pending_backfills.is_empty());
22931 assert_eq!(subscriber.pending_records.len(), 1);
22932 }
22933
22934 #[tokio::test(flavor = "multi_thread")]
22937 #[cfg(feature = "reactive-ws")]
22938 async fn drain_backfill_seeds_anchor_on_empty_window() {
22939 let asserter = Asserter::new();
22940 asserter.push_success(&Some(rpc_block(42, B256::repeat_byte(42))));
22941 asserter.push_success(&Vec::<Log>::new());
22942 asserter.push_success(&Some(rpc_block(42, B256::repeat_byte(42))));
22943 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
22944 let mut subscriber = AlloySubscriber::new(
22945 provider,
22946 SubscriberMode::PubSub,
22947 SubscriberConfig::default(),
22948 );
22949 subscriber
22950 .add_interest_owner_with_backfill(
22951 HandlerId::new("pool"),
22952 &[log_interest_matching_rpc_log()],
22953 SubscriberBackfill::range(1, 42),
22954 )
22955 .expect("register owner with backfill");
22956
22957 subscriber
22958 .drain_pending_backfills()
22959 .await
22960 .expect("empty backfill should drain");
22961
22962 assert!(subscriber.pending_records.is_empty());
22963 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
22964 .pop()
22965 .unwrap();
22966 assert_eq!(
22967 subscriber.log_anchor(&filter),
22968 Some(42),
22969 "empty window must still seed the anchor at its upper bound"
22970 );
22971 }
22972
22973 #[tokio::test(flavor = "multi_thread")]
22976 #[cfg(feature = "reactive-ws")]
22977 async fn drain_backfill_open_ended_resolves_head_and_seeds_anchor() {
22978 let asserter = Asserter::new();
22979 asserter.push_success(&100u64); asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(100))));
22981 asserter.push_success(&Vec::<Log>::new()); asserter.push_success(&Some(rpc_block(100, B256::repeat_byte(100))));
22983 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
22984 let mut subscriber = AlloySubscriber::new(
22985 provider,
22986 SubscriberMode::PubSub,
22987 SubscriberConfig::default(),
22988 );
22989 subscriber
22990 .add_interest_owner_with_backfill(
22991 HandlerId::new("pool"),
22992 &[log_interest_matching_rpc_log()],
22993 SubscriberBackfill::from_block(10),
22994 )
22995 .expect("register owner with open-ended backfill");
22996
22997 subscriber
22998 .drain_pending_backfills()
22999 .await
23000 .expect("open-ended backfill should drain");
23001
23002 let filter = log_filters(subscriber.owner_interests(&HandlerId::new("pool")).unwrap())
23003 .pop()
23004 .unwrap();
23005 assert_eq!(subscriber.log_anchor(&filter), Some(100));
23006 }
23007
23008 #[test]
23011 #[cfg(feature = "reactive-ws")]
23012 fn duplicate_filters_across_owners_map_to_single_source() {
23013 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23014 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23015 provider,
23016 SubscriberMode::PubSub,
23017 SubscriberConfig::default(),
23018 );
23019 subscriber
23020 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
23021 .expect("register pool-a");
23022 subscriber
23023 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xaa)])
23024 .expect("register pool-b with identical filter");
23025
23026 assert_eq!(
23027 subscriber.log_stream_filters().len(),
23028 1,
23029 "identical filters across owners must collapse to one"
23030 );
23031 let sources = subscriber.stream_sources().expect("stream sources");
23032 assert_eq!(sources.len(), 1);
23033 }
23034
23035 #[test]
23038 #[cfg(feature = "reactive-ws")]
23039 fn owner_removal_prunes_source_ids_and_anchors() {
23040 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23041 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23042 provider,
23043 SubscriberMode::PubSub,
23044 SubscriberConfig::default(),
23045 );
23046 subscriber
23047 .add_interest_owner(HandlerId::new("pool-a"), &[log_interest_for(0xaa)])
23048 .expect("register pool-a");
23049 subscriber
23050 .add_interest_owner(HandlerId::new("pool-b"), &[log_interest_for(0xbb)])
23051 .expect("register pool-b");
23052
23053 let _ = subscriber.stream_sources().expect("stream sources");
23055 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
23056 let filter_b = log_filters(&[log_interest_for(0xbb)]).pop().unwrap();
23057 let id_a = subscriber.log_source_id(&filter_a);
23058 let id_b = subscriber.log_source_id(&filter_b);
23059 subscriber.last_seen_log_blocks.insert(id_a, 10);
23060 subscriber.last_seen_log_blocks.insert(id_b, 20);
23061 assert_eq!(
23062 subscriber.log_source_ids.len(),
23063 3,
23064 "one provider fan-in id plus two explicitly seeded logical ids"
23065 );
23066
23067 subscriber
23068 .remove_interest_owner(&HandlerId::new("pool-b"))
23069 .expect("remove pool-b");
23070
23071 assert_eq!(
23072 subscriber.log_source_ids.len(),
23073 1,
23074 "pool-b's filter id should be retired"
23075 );
23076 assert!(subscriber.log_source_ids.contains_key(&filter_a));
23077 assert_eq!(subscriber.last_seen_log_blocks.get(&id_a), Some(&10));
23078 assert_eq!(
23079 subscriber.last_seen_log_blocks.get(&id_b),
23080 None,
23081 "pool-b's anchor should be pruned"
23082 );
23083 }
23084
23085 #[test]
23090 #[cfg(feature = "reactive-ws")]
23091 fn owner_filter_growth_queues_continuity_backfill_from_prior_anchor() {
23092 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23093 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23094 provider,
23095 SubscriberMode::PubSub,
23096 SubscriberConfig::default(),
23097 );
23098 subscriber
23099 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
23100 .expect("register amm with pool A");
23101
23102 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
23105 let id_a = subscriber.log_source_id(&filter_a);
23106 subscriber.last_seen_log_blocks.insert(id_a, 50);
23107
23108 subscriber
23111 .add_interest_owner(
23112 HandlerId::new("amm"),
23113 &[log_interest_for(0xaa), log_interest_for(0xbb)],
23114 )
23115 .expect("grow amm to pools A+B");
23116
23117 assert_eq!(
23118 subscriber.pending_backfills.len(),
23119 1,
23120 "the changed merged filter should queue exactly one continuity backfill"
23121 );
23122 let queued = &subscriber.pending_backfills[0];
23123 assert_eq!(queued.owner, Some(HandlerId::new("amm")));
23124 assert_eq!(queued.backfill.start_block(), 50);
23125 assert_eq!(
23126 queued.backfill.end_block(),
23127 None,
23128 "continuity backfill runs open-ended to the current head"
23129 );
23130 }
23131
23132 #[test]
23135 #[cfg(feature = "reactive-ws")]
23136 fn unchanged_owner_filter_does_not_queue_continuity_backfill() {
23137 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23138 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23139 provider,
23140 SubscriberMode::PubSub,
23141 SubscriberConfig::default(),
23142 );
23143 subscriber
23144 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
23145 .expect("register amm");
23146 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
23147 let id_a = subscriber.log_source_id(&filter_a);
23148 subscriber.last_seen_log_blocks.insert(id_a, 50);
23149
23150 subscriber
23151 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
23152 .expect("re-register identical interests");
23153
23154 assert!(
23155 subscriber.pending_backfills.is_empty(),
23156 "an unchanged filter shape must not queue continuity backfill"
23157 );
23158 }
23159
23160 #[test]
23164 #[cfg(feature = "reactive-ws")]
23165 fn explicit_open_ended_backfill_below_anchor_suppresses_continuity() {
23166 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23167 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23168 provider,
23169 SubscriberMode::PubSub,
23170 SubscriberConfig::default(),
23171 );
23172 subscriber
23173 .add_interest_owner(HandlerId::new("amm"), &[log_interest_for(0xaa)])
23174 .expect("register amm");
23175 let filter_a = log_filters(&[log_interest_for(0xaa)]).pop().unwrap();
23176 let id_a = subscriber.log_source_id(&filter_a);
23177 subscriber.last_seen_log_blocks.insert(id_a, 50);
23178
23179 subscriber
23181 .add_interest_owner_with_backfill(
23182 HandlerId::new("amm"),
23183 &[log_interest_for(0xaa), log_interest_for(0xbb)],
23184 SubscriberBackfill::from_block(10),
23185 )
23186 .expect("grow amm with explicit deep backfill");
23187
23188 assert_eq!(
23189 subscriber.pending_backfills.len(),
23190 1,
23191 "only the explicit backfill should be queued; continuity is subsumed"
23192 );
23193 assert_eq!(subscriber.pending_backfills[0].backfill.start_block(), 10);
23194 }
23195
23196 #[tokio::test(flavor = "multi_thread")]
23199 #[cfg(feature = "reactive-ws")]
23200 async fn ensure_streams_is_noop_when_not_dirty() {
23201 let provider = ProviderBuilder::new().connect_mocked_client(Asserter::new());
23202 let mut subscriber = AlloySubscriber::<_, Ethereum>::new(
23203 provider,
23204 SubscriberMode::PubSub,
23205 SubscriberConfig::default(),
23206 );
23207 subscriber
23209 .add_interest_owner(
23210 HandlerId::new("headers"),
23211 &[ReactiveInterest::Blocks(BlockInterest::default())],
23212 )
23213 .expect("register header owner");
23214 subscriber.state = AlloySubscriberState::Empty;
23216 subscriber.sources_dirty = false;
23217
23218 subscriber
23219 .ensure_streams()
23220 .await
23221 .expect("clean reconcile must be a no-op");
23222
23223 assert!(
23224 matches!(subscriber.state, AlloySubscriberState::Empty),
23225 "not-dirty ensure_streams must not connect new sources"
23226 );
23227 }
23228}
23229
23230fn resolve_subscriber_transport(
23231 mode: SubscriberMode,
23232) -> Result<SubscriberTransport, SubscriberError> {
23233 match mode {
23234 SubscriberMode::PubSub => {
23235 #[cfg(feature = "reactive-ws")]
23236 {
23237 Ok(SubscriberTransport::PubSub)
23238 }
23239 #[cfg(not(feature = "reactive-ws"))]
23240 {
23241 Err(SubscriberError::Unsupported(
23242 "AlloySubscriber pubsub mode requires the reactive-ws feature",
23243 ))
23244 }
23245 }
23246 SubscriberMode::Polling => {
23247 #[cfg(feature = "reactive-polling")]
23248 {
23249 Ok(SubscriberTransport::Polling)
23250 }
23251 #[cfg(not(feature = "reactive-polling"))]
23252 {
23253 Err(SubscriberError::Unsupported(
23254 "AlloySubscriber polling mode requires the reactive-polling feature",
23255 ))
23256 }
23257 }
23258 SubscriberMode::Auto => resolve_auto_subscriber_transport(),
23259 }
23260}
23261
23262fn resolve_auto_subscriber_transport() -> Result<SubscriberTransport, SubscriberError> {
23263 #[cfg(feature = "reactive-ws")]
23264 {
23265 Ok(SubscriberTransport::PubSub)
23266 }
23267
23268 #[cfg(all(not(feature = "reactive-ws"), feature = "reactive-polling"))]
23269 {
23270 Ok(SubscriberTransport::Polling)
23271 }
23272
23273 #[cfg(not(any(feature = "reactive-ws", feature = "reactive-polling")))]
23274 {
23275 Err(SubscriberError::Unsupported(
23276 "AlloySubscriber requires either reactive-ws or reactive-polling",
23277 ))
23278 }
23279}
23280
23281fn validate_subscriber_config(config: &SubscriberConfig) -> Result<(), SubscriberError> {
23282 if config.preconfirmations != PreconfirmationMode::Disabled
23283 && config.canonical_head_poll_interval.is_zero()
23284 {
23285 return Err(SubscriberError::InvalidConfig(
23286 "SubscriberConfig::canonical_head_poll_interval must be greater than zero",
23287 ));
23288 }
23289 if config.preconfirmations != PreconfirmationMode::Disabled
23290 && config.canonical_head_request_timeout.is_zero()
23291 {
23292 return Err(SubscriberError::InvalidConfig(
23293 "SubscriberConfig::canonical_head_request_timeout must be greater than zero",
23294 ));
23295 }
23296 if config.preconfirmations != PreconfirmationMode::Disabled
23297 && config.flashblock_poll_interval.is_zero()
23298 {
23299 return Err(SubscriberError::InvalidConfig(
23300 "SubscriberConfig::flashblock_poll_interval must be greater than zero",
23301 ));
23302 }
23303 if config.preconfirmations != PreconfirmationMode::Disabled
23304 && config.max_consecutive_flashblock_poll_failures == 0
23305 {
23306 return Err(SubscriberError::InvalidConfig(
23307 "SubscriberConfig::max_consecutive_flashblock_poll_failures must be greater than zero",
23308 ));
23309 }
23310 if config.preconfirmations != PreconfirmationMode::Disabled
23311 && config.max_pending_transaction_receipts_per_tick == 0
23312 {
23313 return Err(SubscriberError::InvalidConfig(
23314 "SubscriberConfig::max_pending_transaction_receipts_per_tick must be greater than zero",
23315 ));
23316 }
23317 if config.preconfirmations != PreconfirmationMode::Disabled
23318 && config.max_flashblock_rpc_requests_per_second == 0
23319 {
23320 return Err(SubscriberError::InvalidConfig(
23321 "SubscriberConfig::max_flashblock_rpc_requests_per_second must be greater than zero",
23322 ));
23323 }
23324 if config.max_batch_size == 0 {
23325 return Err(SubscriberError::InvalidConfig(
23326 "SubscriberConfig::max_batch_size must be greater than zero",
23327 ));
23328 }
23329 if config.max_log_addresses_per_subscription == 0 {
23330 return Err(SubscriberError::InvalidConfig(
23331 "SubscriberConfig::max_log_addresses_per_subscription must be greater than zero",
23332 ));
23333 }
23334 if config.max_pending_records == 0 {
23335 return Err(SubscriberError::InvalidConfig(
23336 "SubscriberConfig::max_pending_records must be greater than zero",
23337 ));
23338 }
23339 if config.max_pending_backfills == 0 {
23340 return Err(SubscriberError::InvalidConfig(
23341 "SubscriberConfig::max_pending_backfills must be greater than zero",
23342 ));
23343 }
23344 if config.max_backfill_log_bytes == 0 {
23345 return Err(SubscriberError::InvalidConfig(
23346 "SubscriberConfig::max_backfill_log_bytes must be greater than zero",
23347 ));
23348 }
23349 if config.max_reconcile_requests_in_flight == 0 {
23350 return Err(SubscriberError::InvalidConfig(
23351 "SubscriberConfig::max_reconcile_requests_in_flight must be greater than zero",
23352 ));
23353 }
23354 if config.reconnect.enabled {
23355 if config.reconnect.retry_delay > config.reconnect.max_delay {
23356 return Err(SubscriberError::InvalidConfig(
23357 "SubscriberReconnectConfig::retry_delay must be less than or equal to max_delay",
23358 ));
23359 }
23360 if matches!(config.reconnect.max_attempts, Some(0)) {
23361 return Err(SubscriberError::InvalidConfig(
23362 "SubscriberReconnectConfig::max_attempts must be greater than zero when set",
23363 ));
23364 }
23365 }
23366 Ok(())
23367}
23368
23369fn validate_supported_interests<N: Network>(
23370 mode: SubscriberMode,
23371 config: &SubscriberConfig,
23372 interests: &[ReactiveInterest<N>],
23373) -> Result<(), SubscriberError> {
23374 let transport = resolve_subscriber_transport(mode)?;
23375
23376 for interest in interests {
23377 match interest {
23378 ReactiveInterest::Logs(_) => {}
23379 ReactiveInterest::PendingTransactions(interest)
23380 if !config.hydrate_pending_transactions && interest.matches_hash_only() => {}
23381 ReactiveInterest::PendingTransactions(_) => {
23382 return Err(SubscriberError::Unsupported(
23383 "AlloySubscriber currently supports pending transaction hash interests only (full pending-tx hydration is unimplemented)",
23384 ));
23385 }
23386 ReactiveInterest::Blocks(interest) => match (transport, interest.mode) {
23387 (SubscriberTransport::PubSub, BlockInterestMode::Header) => {}
23388 (_, BlockInterestMode::FullBlock) => {
23389 return Err(SubscriberError::Unsupported(
23390 "AlloySubscriber full block streams are not implemented in this transport slice",
23391 ));
23392 }
23393 (SubscriberTransport::Polling, BlockInterestMode::Header) => {
23394 return Err(SubscriberError::Unsupported(
23395 "AlloySubscriber polling block streams are not implemented in this transport slice",
23396 ));
23397 }
23398 },
23399 }
23400 }
23401
23402 Ok(())
23403}
23404
23405fn log_filters<N: Network>(interests: &[ReactiveInterest<N>]) -> Vec<Filter> {
23406 let mut filters = Vec::new();
23407 for interest in interests {
23408 if let ReactiveInterest::Logs(interest) = interest {
23409 merge_log_subscription_filter(&mut filters, &interest.provider_filter);
23410 }
23411 }
23412 filters
23413}
23414
23415fn needs_header_block_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
23416 interests.iter().any(|interest| {
23417 matches!(
23418 interest,
23419 ReactiveInterest::Blocks(BlockInterest {
23420 mode: BlockInterestMode::Header,
23421 })
23422 )
23423 })
23424}
23425
23426fn needs_pending_hash_stream<N: Network>(interests: &[ReactiveInterest<N>]) -> bool {
23427 interests.iter().any(|interest| {
23428 matches!(
23429 interest,
23430 ReactiveInterest::PendingTransactions(interest) if interest.matches_hash_only()
23431 )
23432 })
23433}
23434
23435fn log_matches_any_interest<N: Network>(log: &Log, interests: &[ReactiveInterest<N>]) -> bool {
23436 interests.iter().any(|interest| {
23437 matches!(
23438 interest,
23439 ReactiveInterest::Logs(interest) if interest.matches(log)
23440 )
23441 })
23442}
23443
23444fn validate_owner_backfill_logs(
23445 logs: &[Log],
23446 from_block: u64,
23447 through: &BlockRef,
23448) -> Result<(), SubscriberOwnerError> {
23449 for log in logs {
23450 if log.removed {
23451 return Err(SubscriberOwnerError::InvalidBackfillLog(
23452 "removed log in canonical catch-up",
23453 ));
23454 }
23455 let number = log
23456 .block_number
23457 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
23458 "log missing block number",
23459 ))?;
23460 let hash = log
23461 .block_hash
23462 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
23463 "log missing block hash",
23464 ))?;
23465 log.transaction_hash
23466 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
23467 "log missing transaction hash",
23468 ))?;
23469 log.transaction_index
23470 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
23471 "log missing transaction index",
23472 ))?;
23473 log.log_index
23474 .ok_or(SubscriberOwnerError::InvalidBackfillLog(
23475 "log missing log index",
23476 ))?;
23477 if number < from_block || number > through.number {
23478 return Err(SubscriberOwnerError::InvalidBackfillLog(
23479 "log outside requested block range",
23480 ));
23481 }
23482 if number == through.number && hash != through.hash {
23483 return Err(SubscriberOwnerError::InvalidBackfillLog(
23484 "target-block log hash mismatch",
23485 ));
23486 }
23487 }
23488 Ok(())
23489}
23490
23491fn validate_backfill_resource_limits(
23492 logs: &[Log],
23493 max_logs: usize,
23494 max_log_bytes: usize,
23495) -> Result<usize, SubscriberError> {
23496 if logs.len() > max_logs {
23497 return Err(SubscriberError::ResourceExhausted(format!(
23498 "historical response returned {} logs, above the configured limit of {max_logs}",
23499 logs.len()
23500 )));
23501 }
23502 let bytes = logs.iter().fold(0usize, |total, log| {
23503 let fixed = 20usize + (32 * 3) + (8 * 4) + 1;
23507 total
23508 .saturating_add(fixed)
23509 .saturating_add(log.topics().len().saturating_mul(32))
23510 .saturating_add(log.inner.data.data.len())
23511 });
23512 if bytes > max_log_bytes {
23513 return Err(SubscriberError::ResourceExhausted(format!(
23514 "historical response retained approximately {bytes} log bytes, above the configured limit of {max_log_bytes}"
23515 )));
23516 }
23517 Ok(bytes)
23518}
23519
23520async fn fetch_provider_block_ref<P, N>(
23521 provider: &P,
23522 number: u64,
23523) -> Result<BlockRef, SubscriberError>
23524where
23525 P: Provider<N> + Send + Sync,
23526 N: Network,
23527{
23528 let block = provider
23529 .get_block_by_number(BlockNumberOrTag::Number(number))
23530 .await
23531 .map_err(provider_error)?
23532 .ok_or_else(|| {
23533 SubscriberError::InvalidBackfill(format!(
23534 "canonical target block {number} is unavailable"
23535 ))
23536 })?;
23537 let header = block.header();
23538 Ok(BlockRef {
23539 number: header.number(),
23540 hash: header.hash(),
23541 parent_hash: Some(header.parent_hash()),
23542 timestamp: Some(header.timestamp()),
23543 })
23544}
23545
23546fn block_ref_satisfies_expected(actual: &BlockRef, expected: &BlockRef) -> bool {
23547 actual.number == expected.number
23548 && actual.hash == expected.hash
23549 && optional_metadata_compatible(actual.parent_hash.as_ref(), expected.parent_hash.as_ref())
23550 && optional_metadata_compatible(actual.timestamp.as_ref(), expected.timestamp.as_ref())
23551}
23552
23553fn validate_owner_backfill_log_set(logs: &[Log]) -> Result<(), SubscriberOwnerError> {
23554 let mut positions = HashMap::new();
23555 let mut block_hashes = HashMap::new();
23556 let mut transaction_hashes = HashMap::new();
23557 let mut transaction_positions = HashMap::new();
23558 let mut ordering = BTreeMap::<u64, Vec<(u64, u64)>>::new();
23559 for log in logs {
23560 let number = log
23561 .block_number
23562 .expect("individual owner catch-up logs are validated before set validation");
23563 let block_hash = log
23564 .block_hash
23565 .expect("individual owner catch-up logs are validated before set validation");
23566 let transaction_hash = log
23567 .transaction_hash
23568 .expect("individual owner catch-up logs are validated before set validation");
23569 let transaction_index = log
23570 .transaction_index
23571 .expect("individual owner catch-up logs are validated before set validation");
23572 let log_index = log
23573 .log_index
23574 .expect("individual owner catch-up logs are validated before set validation");
23575 if block_hashes
23576 .insert(number, block_hash)
23577 .is_some_and(|prior| prior != block_hash)
23578 {
23579 return Err(SubscriberOwnerError::InvalidBackfillLog(
23580 "conflicting block identity in canonical catch-up",
23581 ));
23582 }
23583 if let Some(previous) = positions.insert((number, log_index), log)
23584 && previous != log
23585 {
23586 return Err(SubscriberOwnerError::InvalidBackfillLog(
23587 "conflicting logs at one canonical block position",
23588 ));
23589 }
23590 let conflicting_transaction = transaction_hashes
23591 .insert((number, transaction_index), transaction_hash)
23592 .is_some_and(|prior| prior != transaction_hash)
23593 || transaction_positions
23594 .insert((number, transaction_hash), transaction_index)
23595 .is_some_and(|prior| prior != transaction_index);
23596 if conflicting_transaction {
23597 return Err(SubscriberOwnerError::InvalidBackfillLog(
23598 "conflicting transaction identity at one canonical block position",
23599 ));
23600 }
23601 ordering
23602 .entry(number)
23603 .or_default()
23604 .push((log_index, transaction_index));
23605 }
23606 for positions in ordering.values_mut() {
23607 positions.sort_unstable();
23608 if positions.windows(2).any(|pair| pair[0].1 > pair[1].1) {
23609 return Err(SubscriberOwnerError::InvalidBackfillLog(
23610 "transaction and log positions disagree on canonical order",
23611 ));
23612 }
23613 }
23614 Ok(())
23615}
23616
23617fn merged_owner_reconcile_filters<N: Network>(
23618 plans: &[SubscriberOwnerReconcilePlan<N>],
23619 through: u64,
23620) -> Vec<SubscriberOwnerReconcileFilter> {
23621 let mut by_start = BTreeMap::<u64, Vec<Filter>>::new();
23622 for plan in plans.iter().filter(|plan| plan.from_block <= through) {
23623 let filters = by_start.entry(plan.from_block).or_default();
23624 filters.extend(
23625 log_filters(&plan.interests)
23626 .into_iter()
23627 .map(|filter| filter.from_block(plan.from_block).to_block(through)),
23628 );
23629 }
23630
23631 let mut chunks = Vec::new();
23632 for (from_block, filters) in by_start {
23633 for filters in filters.chunks(OWNER_RECONCILE_FILTERS_PER_CHUNK) {
23634 let mut merged = Vec::new();
23635 for filter in filters {
23636 merge_log_subscription_filter(&mut merged, filter);
23637 }
23638 chunks.extend(
23639 merged
23640 .into_iter()
23641 .map(|filter| SubscriberOwnerReconcileFilter { filter, from_block }),
23642 );
23643 }
23644 }
23645 chunks
23646}
23647
23648fn merged_lazy_backfill_filters(
23649 filters: &[Filter],
23650 from_block: u64,
23651 through: u64,
23652) -> Vec<SubscriberOwnerReconcileFilter> {
23653 let mut requests = Vec::new();
23654 for filters in filters.chunks(OWNER_RECONCILE_FILTERS_PER_CHUNK) {
23655 let mut merged = Vec::new();
23656 for filter in filters {
23657 merge_log_subscription_filter(
23658 &mut merged,
23659 &filter.clone().from_block(from_block).to_block(through),
23660 );
23661 }
23662 requests.extend(
23663 merged
23664 .into_iter()
23665 .map(|filter| SubscriberOwnerReconcileFilter { filter, from_block }),
23666 );
23667 }
23668 requests
23669}
23670
23671fn lazy_backfill_error(error: SubscriberOwnerError) -> SubscriberError {
23672 match error {
23673 SubscriberOwnerError::Subscriber(error) => error,
23674 error => SubscriberError::InvalidBackfill(error.to_string()),
23675 }
23676}
23677
23678fn global_backfill_barrier(backfill: SubscriberBackfill, certified: BlockRef) -> ChainControl {
23679 let mut id = b"alloy-global-backfill-v1".to_vec();
23680 id.extend_from_slice(&backfill.start_block().to_be_bytes());
23681 id.extend_from_slice(&certified.number.to_be_bytes());
23682 id.extend_from_slice(certified.hash.as_slice());
23683 ChainControl::Barrier {
23684 id,
23685 block: Some(certified),
23686 }
23687}
23688
23689async fn fetch_owner_catchup<P, N>(
23690 provider: P,
23691 filters: Vec<SubscriberOwnerReconcileFilter>,
23692 retained: Vec<BlockRef>,
23693 through: BlockRef,
23694 options: SubscriberOwnerCatchupOptions,
23695) -> Result<SubscriberOwnerCatchup, SubscriberOwnerError>
23696where
23697 P: Provider<N> + Send + Sync,
23698 N: Network,
23699{
23700 if !options.target_preverified {
23701 let _ = verify_provider_reconcile_target::<P, N>(&provider, &through).await?;
23702 }
23703 let mut certified_positions = HashSet::new();
23704 for position in retained {
23705 let target_certifies_position = position == through
23706 || (position.number.checked_add(1) == Some(through.number)
23707 && through.parent_hash == Some(position.hash));
23708 if !target_certifies_position && certified_positions.insert(position) {
23709 let _ = verify_provider_reconcile_target::<P, N>(&provider, &position).await?;
23710 }
23711 }
23712 let mut logs = Vec::new();
23713 let mut total_log_bytes = 0usize;
23714 let requests = stream::iter(filters.into_iter().map(|filter| {
23715 let provider = &provider;
23716 async move {
23717 let logs = provider
23718 .get_logs(&filter.filter)
23719 .await
23720 .map_err(provider_error)?;
23721 Ok::<_, SubscriberOwnerError>((filter.from_block, logs))
23722 }
23723 }))
23724 .buffer_unordered(options.max_requests_in_flight);
23725 futures::pin_mut!(requests);
23726 while let Some(result) = requests.next().await {
23727 let (from_block, fetched) = result?;
23728 let fetched_bytes =
23729 validate_backfill_resource_limits(&fetched, options.max_logs, options.max_log_bytes)?;
23730 validate_owner_backfill_logs(&fetched, from_block, &through)?;
23731 if logs.len().saturating_add(fetched.len()) > options.max_logs {
23732 return Err(SubscriberError::ResourceExhausted(format!(
23733 "bulk reconcile returned more than {} logs",
23734 options.max_logs
23735 ))
23736 .into());
23737 }
23738 total_log_bytes = total_log_bytes.saturating_add(fetched_bytes);
23739 if total_log_bytes > options.max_log_bytes {
23740 return Err(SubscriberError::ResourceExhausted(format!(
23741 "bulk reconcile retained approximately {total_log_bytes} log bytes, above the configured limit of {}",
23742 options.max_log_bytes
23743 ))
23744 .into());
23745 }
23746 logs.extend(fetched);
23747 }
23748 validate_owner_backfill_log_set(&logs)?;
23749 let certified = verify_provider_reconcile_target::<P, N>(&provider, &through).await?;
23750 Ok(SubscriberOwnerCatchup { logs, certified })
23751}
23752
23753async fn verify_provider_reconcile_target<P, N>(
23754 provider: &P,
23755 expected: &BlockRef,
23756) -> Result<BlockRef, SubscriberOwnerError>
23757where
23758 P: Provider<N> + Send + Sync,
23759 N: Network,
23760{
23761 let block = provider
23762 .get_block_by_number(BlockNumberOrTag::Number(expected.number))
23763 .await
23764 .map_err(provider_error)?
23765 .ok_or(SubscriberOwnerError::BlockUnavailable(expected.number))?;
23766 let header = block.header();
23767 let actual = BlockRef {
23768 number: header.number(),
23769 hash: header.hash(),
23770 parent_hash: Some(header.parent_hash()),
23771 timestamp: Some(header.timestamp()),
23772 };
23773 let exact_parent = expected
23774 .parent_hash
23775 .is_none_or(|parent| Some(parent) == actual.parent_hash);
23776 let exact_timestamp = expected
23777 .timestamp
23778 .is_none_or(|timestamp| Some(timestamp) == actual.timestamp);
23779 if actual.number != expected.number
23780 || actual.hash != expected.hash
23781 || !exact_parent
23782 || !exact_timestamp
23783 {
23784 return Err(SubscriberOwnerError::BlockMismatch {
23785 expected_number: expected.number,
23786 expected_hash: expected.hash,
23787 actual_number: actual.number,
23788 actual_hash: actual.hash,
23789 });
23790 }
23791 Ok(actual)
23792}
23793
23794fn log_input_record<N: Network>(log: Log, source: InputSource) -> ReactiveInputRecord<N> {
23795 let context = log_reactive_context(&log);
23796 ReactiveInputRecord::new(
23797 ReactiveInput::Log(log),
23798 ReactiveContext { source, ..context },
23799 )
23800}
23801
23802fn preconfirmed_log_input_record<N: Network>(
23803 log: Log,
23804 flashblock: FlashblockRef,
23805) -> ReactiveInputRecord<N> {
23806 let block = flashblock.block_ref();
23807 let provider = flashblock.provider.clone();
23808 ReactiveInputRecord::new(
23809 ReactiveInput::Log(log.clone()),
23810 ReactiveContext {
23811 chain_id: None,
23812 source: InputSource::Flashblocks,
23813 chain_status: ChainStatus::Preconfirmed {
23814 flashblock: Arc::new(flashblock),
23815 },
23816 block: Some(block),
23817 transaction_index: log.transaction_index,
23818 log_index: log.log_index,
23819 },
23820 )
23821 .with_provider(provider)
23822}
23823
23824fn log_reactive_context(log: &Log) -> ReactiveContext {
23825 let block = match (log.block_hash, log.block_number) {
23826 (Some(hash), Some(number)) => Some(BlockRef {
23827 number,
23828 hash,
23829 parent_hash: None,
23830 timestamp: log.block_timestamp,
23831 }),
23832 _ => None,
23833 };
23834
23835 let chain_status = match (&block, log.removed) {
23836 (Some(block), true) => ChainStatus::Reorged {
23837 dropped_from: *block,
23838 },
23839 (Some(block), false) => ChainStatus::Included {
23840 block: *block,
23841 confirmations: 0,
23842 },
23843 (None, _) => ChainStatus::Pending,
23844 };
23845
23846 ReactiveContext {
23847 chain_id: None,
23848 source: InputSource::Poll,
23849 chain_status,
23850 block,
23851 transaction_index: log.transaction_index,
23852 log_index: log.log_index,
23853 }
23854}
23855
23856fn block_header_input_record<N>(header: N::HeaderResponse) -> ReactiveInputRecord<N>
23857where
23858 N: Network,
23859{
23860 let block = BlockRef {
23861 number: header.number(),
23862 hash: HeaderResponseTrait::hash(&header),
23863 parent_hash: Some(header.parent_hash()),
23864 timestamp: Some(header.timestamp()),
23865 };
23866 ReactiveInputRecord::new(
23867 ReactiveInput::BlockHeader(header),
23868 ReactiveContext {
23869 chain_id: None,
23870 source: InputSource::Subscription,
23871 chain_status: ChainStatus::Included {
23872 block,
23873 confirmations: 0,
23874 },
23875 block: Some(block),
23876 transaction_index: None,
23877 log_index: None,
23878 },
23879 )
23880}
23881
23882fn pending_hash_input_record<N: Network>(
23883 hash: B256,
23884 source: InputSource,
23885) -> ReactiveInputRecord<N> {
23886 ReactiveInputRecord::new(
23887 ReactiveInput::PendingTxHash(hash),
23888 ReactiveContext {
23889 chain_id: None,
23890 source,
23891 chain_status: ChainStatus::Pending,
23892 block: None,
23893 transaction_index: None,
23894 log_index: None,
23895 },
23896 )
23897}
23898
23899#[cfg(feature = "reactive-ws")]
23900fn base_pending_log_filter(filter: &Filter) -> Result<serde_json::Value, SubscriberError> {
23901 let encoded = serde_json::to_value(filter)
23902 .map_err(|error| SubscriberError::Provider(error.to_string()))?;
23903 let serde_json::Value::Object(mut fields) = encoded else {
23904 return Err(SubscriberError::Provider(
23905 "Alloy log filter did not serialize as an object".into(),
23906 ));
23907 };
23908 fields.retain(|key, _| key == "address" || key == "topics");
23909 Ok(serde_json::Value::Object(fields))
23910}
23911
23912fn provider_error(error: impl fmt::Display) -> SubscriberError {
23913 SubscriberError::Provider(error.to_string())
23914}
23915
23916#[derive(Debug, thiserror::Error)]
23918#[non_exhaustive]
23919pub enum SubscriberError {
23920 #[error("{0}")]
23922 InvalidConfig(&'static str),
23923 #[error("{0}")]
23925 Unsupported(&'static str),
23926 #[error("subscriber chain mismatch: expected {expected}, got {actual}")]
23928 ChainMismatch {
23929 expected: u64,
23931 actual: u64,
23933 },
23934 #[error("provider error: {0}")]
23936 Provider(String),
23937 #[error("invalid canonical backfill: {0}")]
23940 InvalidBackfill(String),
23941 #[error("subscriber resource limit exceeded: {0}")]
23943 ResourceExhausted(String),
23944}