1use std::{
2 collections::{HashMap, HashSet},
3 time::Duration,
4};
5
6use async_trait::async_trait;
7use rand::Rng;
8use thiserror::Error;
9use tokio::{
10 select,
11 sync::{
12 mpsc::{channel, error::SendError, Receiver, Sender},
13 oneshot,
14 },
15 task::JoinHandle,
16 time::{sleep, timeout, Instant},
17};
18use tracing::{debug, error, info, instrument, warn};
19use tycho_common::{
20 models::{
21 blockchain::{
22 BlockAggregatedChanges, DCIUpdate, EntryPointWithTracingParams, TracingResult,
23 },
24 contract::Account,
25 protocol::{ProtocolComponent, ProtocolComponentState},
26 Chain, ExtractorIdentity,
27 },
28 Bytes,
29};
30
31use crate::{
32 deltas::{DeltasClient, SubscriptionOptions},
33 feed::{
34 component_tracker::{ComponentFilter, ComponentTracker},
35 BlockHeader, HeaderLike,
36 },
37 rpc::{
38 RPCClient, RPCError, SnapshotParameters, TracedEntryPointsPaginatedParams,
39 RPC_CLIENT_CONCURRENCY,
40 },
41 DeltasError,
42};
43
44pub const DEFAULT_SNAPSHOT_JITTER: Duration = Duration::from_secs(60);
46pub const MIN_SNAPSHOT_JITTER: Duration = Duration::from_secs(5);
48
49#[derive(Error, Debug)]
50pub enum SynchronizerError {
51 #[error("RPC error: {0}")]
53 RPCError(#[from] RPCError),
54
55 #[error("{0}")]
57 ChannelError(String),
58
59 #[error("Timeout error: {0}")]
61 Timeout(String),
62
63 #[error("Failed to close synchronizer: {0}")]
65 CloseError(String),
66
67 #[error("Connection error: {0}")]
69 ConnectionError(String),
70
71 #[error("Connection closed")]
73 ConnectionClosed,
74
75 #[error("Internal error: {0}")]
77 Internal(String),
78}
79
80pub type SyncResult<T> = Result<T, SynchronizerError>;
81pub type SyncError = (SynchronizerError, Option<oneshot::Receiver<()>>);
82
83impl SynchronizerError {
84 pub fn is_transient(&self) -> bool {
92 match self {
93 SynchronizerError::RPCError(e) => matches!(
94 e,
95 RPCError::HttpClient(_, _) |
96 RPCError::RateLimited(_) |
97 RPCError::ServerUnreachable(_) |
98 RPCError::StaleBlock(_)
99 ),
100 SynchronizerError::Timeout(_) |
101 SynchronizerError::ConnectionError(_) |
102 SynchronizerError::ConnectionClosed => true,
103 _ => false,
104 }
105 }
106}
107
108impl<T> From<SendError<T>> for SynchronizerError {
109 fn from(err: SendError<T>) -> Self {
110 SynchronizerError::ChannelError(format!("Failed to send message: {err}"))
111 }
112}
113
114impl From<DeltasError> for SynchronizerError {
115 fn from(err: DeltasError) -> Self {
116 match err {
117 DeltasError::NotConnected => SynchronizerError::ConnectionClosed,
118 _ => SynchronizerError::ConnectionError(err.to_string()),
119 }
120 }
121}
122
123pub struct ProtocolStateSynchronizer<R: RPCClient, D: DeltasClient> {
124 extractor_id: ExtractorIdentity,
125 retrieve_balances: bool,
126 rpc_client: R,
127 deltas_client: D,
128 max_retries: u64,
129 retry_cooldown: Duration,
130 include_snapshots: bool,
131 component_tracker: ComponentTracker<R>,
132 last_synced_block: Option<BlockHeader>,
133 timeout: u64,
134 include_tvl: bool,
135 compression: bool,
136 partial_blocks: bool,
137 uses_dci: bool,
138 snapshot_jitter: Duration,
139 snapshot_tasks: Vec<SnapshotTask>,
143 buffered_deltas: Vec<BlockAggregatedChanges>,
147 snapshot_queue: HashMap<String, SnapshotStatus>,
151}
152
153#[derive(Clone, PartialEq, Debug)]
154pub struct ComponentWithState {
155 pub state: ProtocolComponentState,
156 pub component: ProtocolComponent,
157 pub component_tvl: Option<f64>,
158 pub entrypoints: Vec<(EntryPointWithTracingParams, TracingResult)>,
159}
160
161#[derive(Clone, PartialEq, Debug, Default)]
162pub struct Snapshot {
163 pub states: HashMap<String, ComponentWithState>,
164 pub vm_storage: HashMap<Bytes, Account>,
165}
166
167impl Snapshot {
168 fn extend(&mut self, other: Snapshot) {
169 self.states.extend(other.states);
170 self.vm_storage.extend(other.vm_storage);
171 }
172
173 pub fn get_states(&self) -> &HashMap<String, ComponentWithState> {
174 &self.states
175 }
176
177 pub fn get_vm_storage(&self) -> &HashMap<Bytes, Account> {
178 &self.vm_storage
179 }
180}
181
182#[derive(Clone, PartialEq, Debug, Default)]
183pub struct StateSyncMessage<H>
184where
185 H: HeaderLike,
186{
187 pub header: H,
189 pub snapshots: Snapshot,
191 pub deltas: Option<BlockAggregatedChanges>,
195 pub removed_components: HashMap<String, ProtocolComponent>,
197}
198
199impl<H> StateSyncMessage<H>
200where
201 H: HeaderLike,
202{
203 pub fn merge(mut self, other: Self) -> Self {
204 self.removed_components
206 .retain(|k, _| !other.snapshots.states.contains_key(k));
207 self.snapshots
208 .states
209 .retain(|k, _| !other.removed_components.contains_key(k));
210
211 self.snapshots.extend(other.snapshots);
212 let deltas = match (self.deltas, other.deltas) {
213 (Some(l), Some(r)) => Some(l.merge(r)),
214 (None, Some(r)) => Some(r),
215 (Some(l), None) => Some(l),
216 (None, None) => None,
217 };
218 self.removed_components
219 .extend(other.removed_components);
220 Self {
221 header: other.header,
222 snapshots: self.snapshots,
223 deltas,
224 removed_components: self.removed_components,
225 }
226 }
227}
228
229#[derive(Debug, Clone, PartialEq)]
231enum SnapshotStatus {
232 Deferred,
234 InFlight,
236 RetryNext,
238 Blacklisted,
240}
241
242struct SnapshotFetchResult {
243 components: HashMap<String, ProtocolComponent>,
244 contract_ids: HashSet<Bytes>,
245 dci_update: DCIUpdate,
246 snapshot: Snapshot,
247 snapshot_block: u64,
248}
249
250struct SnapshotTask {
251 component_ids: Vec<String>,
252 snapshot_block: u64,
253 receiver: oneshot::Receiver<Result<SnapshotFetchResult, SynchronizerError>>,
254}
255
256pub struct SynchronizerTaskHandle {
261 join_handle: JoinHandle<()>,
262 close_tx: oneshot::Sender<()>,
263}
264
265impl SynchronizerTaskHandle {
274 pub fn new(join_handle: JoinHandle<()>, close_tx: oneshot::Sender<()>) -> Self {
275 Self { join_handle, close_tx }
276 }
277
278 pub fn split(self) -> (JoinHandle<()>, oneshot::Sender<()>) {
284 (self.join_handle, self.close_tx)
285 }
286}
287
288#[async_trait]
289pub trait StateSynchronizer: Send + Sync + 'static {
290 async fn initialize(&mut self) -> SyncResult<()>;
291 async fn start(
294 mut self,
295 ) -> (SynchronizerTaskHandle, Receiver<SyncResult<StateSyncMessage<BlockHeader>>>);
296}
297
298struct FetchSnapshotParams {
299 chain: Chain,
300 protocol_system: String,
301 block_number: u64,
302 uses_dci: bool,
303 retrieve_balances: bool,
304 include_tvl: bool,
305}
306
307async fn fetch_snapshot<R: RPCClient>(
313 rpc_client: &R,
314 components: HashMap<String, ProtocolComponent>,
315 mut contract_ids: HashSet<Bytes>,
316 params: &FetchSnapshotParams,
317) -> Result<(Snapshot, DCIUpdate, HashSet<Bytes>), SynchronizerError> {
318 if components.is_empty() {
319 return Ok((Snapshot::default(), DCIUpdate::default(), contract_ids));
320 }
321
322 let component_ids: Vec<String> = components.keys().cloned().collect();
323
324 let (dci_update, entrypoints_result) = if params.uses_dci {
325 let result = rpc_client
326 .get_traced_entry_points_paginated(TracedEntryPointsPaginatedParams::new(
327 params.chain,
328 ¶ms.protocol_system,
329 component_ids.clone(),
330 RPC_CLIENT_CONCURRENCY,
331 ))
332 .await?;
333 let dci_contracts: HashSet<Bytes> = result
334 .values()
335 .flat_map(|traces| {
336 traces
337 .iter()
338 .flat_map(|(_, tr)| tr.accessed_slots.keys().cloned())
339 })
340 .collect();
341 contract_ids.extend(dci_contracts);
342 let eps = result.clone();
343 let dci: DCIUpdate = result.into();
344 (dci, eps)
345 } else {
346 (DCIUpdate::default(), HashMap::new())
347 };
348
349 let contract_ids_vec: Vec<Bytes> = contract_ids.iter().cloned().collect();
350 let request = SnapshotParameters::new(
351 params.chain,
352 ¶ms.protocol_system,
353 &components,
354 &contract_ids_vec,
355 params.block_number,
356 )
357 .entrypoints(&entrypoints_result)
358 .include_balances(params.retrieve_balances)
359 .include_tvl(params.include_tvl);
360
361 let snapshot = rpc_client
362 .get_snapshots(&request, None, RPC_CLIENT_CONCURRENCY)
363 .await?;
364
365 Ok((snapshot, dci_update, contract_ids))
366}
367
368async fn fetch_snapshot_background<R: RPCClient>(
371 rpc_client: R,
372 component_ids: Vec<String>,
373 params: FetchSnapshotParams,
374) -> Result<SnapshotFetchResult, SynchronizerError> {
375 if component_ids.is_empty() {
376 return Ok(SnapshotFetchResult {
377 components: HashMap::new(),
378 contract_ids: HashSet::new(),
379 dci_update: DCIUpdate::default(),
380 snapshot: Snapshot::default(),
381 snapshot_block: params.block_number,
382 });
383 }
384
385 let request = crate::rpc::ProtocolComponentsParams::new(params.chain, ¶ms.protocol_system)
386 .with_component_ids(component_ids);
387 let components: HashMap<String, ProtocolComponent> = rpc_client
388 .get_protocol_components(request)
389 .await?
390 .into_data()
391 .into_iter()
392 .map(|pc| (pc.id.clone(), pc))
393 .collect();
394
395 let contract_ids: HashSet<Bytes> = components
396 .values()
397 .flat_map(|c| c.contract_addresses.iter().cloned())
398 .collect();
399
400 let snapshot_block = params.block_number;
401 let (snapshot, dci_update, contract_ids) =
402 fetch_snapshot(&rpc_client, components.clone(), contract_ids, ¶ms).await?;
403
404 Ok(SnapshotFetchResult { components, contract_ids, dci_update, snapshot, snapshot_block })
405}
406
407impl<R, D> ProtocolStateSynchronizer<R, D>
408where
409 R: RPCClient + Clone + Send + Sync + 'static,
412 D: DeltasClient + Clone + Send + Sync + 'static,
413{
414 #[allow(clippy::too_many_arguments)]
416 pub fn new(
417 extractor_id: ExtractorIdentity,
418 retrieve_balances: bool,
419 component_filter: ComponentFilter,
420 max_retries: u64,
421 retry_cooldown: Duration,
422 include_snapshots: bool,
423 include_tvl: bool,
424 compression: bool,
425 rpc_client: R,
426 deltas_client: D,
427 timeout: u64,
428 ) -> Self {
429 Self {
430 extractor_id: extractor_id.clone(),
431 retrieve_balances,
432 rpc_client: rpc_client.clone(),
433 include_snapshots,
434 deltas_client,
435 component_tracker: ComponentTracker::new(
436 extractor_id.chain,
437 extractor_id.name.as_str(),
438 component_filter,
439 rpc_client,
440 ),
441 max_retries,
442 retry_cooldown,
443 last_synced_block: None,
444 timeout,
445 include_tvl,
446 compression,
447 partial_blocks: false,
448 uses_dci: false,
449 snapshot_jitter: DEFAULT_SNAPSHOT_JITTER,
450 snapshot_tasks: Vec::new(),
451 buffered_deltas: Vec::new(),
452 snapshot_queue: HashMap::new(),
453 }
454 }
455
456 pub fn with_dci(mut self, uses_dci: bool) -> Self {
459 self.uses_dci = uses_dci;
460 self
461 }
462
463 pub fn with_partial_blocks(mut self, partial_blocks: bool) -> Self {
465 self.partial_blocks = partial_blocks;
466 self
467 }
468
469 pub fn with_snapshot_jitter(mut self, jitter: Duration) -> Self {
475 if jitter < MIN_SNAPSHOT_JITTER {
476 warn!(
477 extractor_id=%self.extractor_id,
478 requested_ms=jitter.as_millis(),
479 min_ms=MIN_SNAPSHOT_JITTER.as_millis(),
480 "Snapshot jitter is below the minimum; using the minimum"
481 );
482 }
483 self.snapshot_jitter = jitter.max(MIN_SNAPSHOT_JITTER);
484 self
485 }
486
487 #[instrument(skip(self, block_tx, end_rx), fields(extractor_id = %self.extractor_id))]
503 async fn state_sync(
504 &mut self,
505 block_tx: &mut Sender<SyncResult<StateSyncMessage<BlockHeader>>>,
506 mut end_rx: oneshot::Receiver<()>,
507 snapshot_delay: Duration,
508 ) -> Result<(), Box<SyncError>> {
509 let subscription_options = SubscriptionOptions::new()
511 .with_state(self.include_snapshots)
512 .with_compression(self.compression)
513 .with_partial_blocks(self.partial_blocks);
514 let (subscription_id, mut msg_rx) = match self
515 .deltas_client
516 .subscribe(self.extractor_id.clone(), subscription_options)
517 .await
518 {
519 Ok(result) => result,
520 Err(e) => return Err(Box::new((e.into(), Some(end_rx)))),
521 };
522
523 let result = async {
524 info!("Waiting for deltas...");
525 let mut last_block_number: Option<u64> = None;
527
528 const MAX_STALE_RETRIES: u32 = 5;
534 let mut stale_retries: u32 = 0;
535 let mut snapshot_not_before: Option<Instant> = None;
538 let (msg, header) = 'init: loop {
539 let mut warned_waiting_for_new_block = false;
540 let mut warned_skipping_synced = false;
541 let mut first_msg = loop {
542 let msg = select! {
543 deltas_result = timeout(Duration::from_secs(self.timeout), msg_rx.recv()) => {
544 deltas_result
545 .map_err(|_| {
546 SynchronizerError::Timeout(format!(
547 "First deltas took longer than {t}s to arrive",
548 t = self.timeout
549 ))
550 })?
551 .ok_or_else(|| {
552 SynchronizerError::ConnectionError(
553 "Deltas channel closed before first message".to_string(),
554 )
555 })?
556 },
557 _ = &mut end_rx => {
558 info!("Received close signal while waiting for first deltas");
559 return Ok(());
560 }
561 };
562
563 let incoming: BlockHeader = (&msg).into();
564
565 let is_new_block_candidate = if self.partial_blocks {
569 match msg.partial_block_index {
570 None => {
571 last_block_number = Some(incoming.number);
573 true
574 }
575 Some(current_partial_idx) => {
576 let is_new_block = last_block_number
577 .map(|prev_block| incoming.number > prev_block)
578 .unwrap_or(false);
579
580 if !warned_waiting_for_new_block {
581 info!(
582 extractor=%self.extractor_id,
583 block=incoming.number,
584 partial_idx=current_partial_idx,
585 "Syncing. Waiting for new block to start"
586 );
587 warned_waiting_for_new_block = true;
588 }
589 last_block_number = Some(incoming.number);
590 is_new_block
591 }
592 }
593 } else {
594 true };
596
597 if !is_new_block_candidate {
598 continue;
599 }
600
601 if let Some(current) = &self.last_synced_block {
603 if current.number >= incoming.number && !self.is_next_expected(&incoming) {
604 if !warned_skipping_synced {
605 info!(extractor=%self.extractor_id, from=incoming.number, to=current.number, "Syncing. Skipping already synced block");
606 warned_skipping_synced = true;
607 }
608 continue;
609 }
610 }
611 match snapshot_not_before {
612 Some(not_before) if Instant::now() < not_before => continue,
613 None if !snapshot_delay.is_zero() && !self.is_next_expected(&incoming) => {
614 info!(
615 extractor=%self.extractor_id,
616 delay_ms=snapshot_delay.as_millis(),
617 "Syncing. Delaying snapshot request"
618 );
619 snapshot_not_before = Some(Instant::now() + snapshot_delay);
620 continue;
621 }
622 _ => {}
623 }
624 break msg;
625 };
626
627 self.filter_deltas(&mut first_msg);
628
629 info!(height = first_msg.get_block().number, "First deltas received");
631 let header: BlockHeader = (&first_msg).into();
632 let deltas_msg = StateSyncMessage {
633 header: header.clone(),
634 snapshots: Default::default(),
635 deltas: Some(first_msg),
636 removed_components: Default::default(),
637 };
638
639 if !self.is_next_expected(&header) {
641 info!("Retrieving snapshot");
642 let snapshot_header = if self.partial_blocks && header.number > 0 {
645 BlockHeader {
646 number: header.number - 1,
647 hash: header.parent_hash.clone(),
648 ..Default::default()
649 }
650 } else {
651 BlockHeader { revert: false, ..header.clone() }
652 };
653 let component_ids =
654 self.component_tracker.get_tracked_component_ids();
655 let init_snapshot = if !self.include_snapshots ||
656 component_ids.is_empty()
657 {
658 Snapshot::default()
659 } else {
660 let components: HashMap<_, _> = self
662 .component_tracker
663 .components
664 .iter()
665 .filter(|(id, _)| component_ids.contains(id))
666 .map(|(k, v)| (k.clone(), v.clone()))
667 .collect();
668 let contract_ids: HashSet<Bytes> = self
669 .component_tracker
670 .get_contracts_by_component(&component_ids)
671 .into_iter()
672 .collect();
673 let fetch_params = FetchSnapshotParams {
674 chain: self.extractor_id.chain,
675 protocol_system: self.extractor_id.name.clone(),
676 block_number: snapshot_header.number,
677 uses_dci: self.uses_dci,
678 retrieve_balances: self.retrieve_balances,
679 include_tvl: self.include_tvl,
680 };
681 match fetch_snapshot(
682 &self.rpc_client,
683 components,
684 contract_ids,
685 &fetch_params,
686 )
687 .await
688 {
689 Ok((snap, dci_update, _)) => {
690 self.component_tracker
691 .process_entrypoints(&dci_update);
692 snap
693 }
694 Err(SynchronizerError::RPCError(
695 crate::rpc::RPCError::StaleBlock(reason),
696 )) => {
697 stale_retries += 1;
698 if stale_retries > MAX_STALE_RETRIES {
699 return Err(SynchronizerError::RPCError(
700 crate::rpc::RPCError::StaleBlock(reason),
701 ));
702 }
703 warn!(
708 block = header.number,
709 stale_retries,
710 %reason,
711 "Snapshot block is outside server retention \
712 window; waiting for a more recent block"
713 );
714 continue 'init;
715 }
716 Err(e) => return Err(e),
717 }
718 };
719 let n_components = self.component_tracker.components.len();
720 let n_snapshots = init_snapshot.states.len();
721 info!(
722 n_components,
723 n_snapshots,
724 "Initial snapshot retrieved, starting delta message feed"
725 );
726 let snapshot_msg = StateSyncMessage {
727 header: snapshot_header,
728 snapshots: init_snapshot,
729 deltas: None,
730 removed_components: HashMap::new(),
731 };
732 break 'init (snapshot_msg.merge(deltas_msg), header);
733 } else {
734 break 'init (deltas_msg, header);
735 }
736 };
737
738 block_tx.send(Ok(msg)).await?;
739 self.last_synced_block = Some(header);
740 loop {
741 select! {
742 deltas_opt = msg_rx.recv() => {
743 if let Some(mut deltas) = deltas_opt {
744 let header: BlockHeader = (&deltas).into();
745 debug!(block_number=?header.number, "Received delta message");
746
747 if !self.snapshot_tasks.is_empty() {
749 self.buffered_deltas.push(deltas.clone());
750 }
751
752 let background_snapshots = self.drain_completed_snapshots();
753
754 if self.snapshot_tasks.is_empty() {
756 self.buffered_deltas.clear();
757 } else {
758 let oldest_pending_block = self
759 .snapshot_tasks
760 .iter()
761 .map(|p| p.snapshot_block)
762 .min()
763 .unwrap_or(u64::MAX);
764 self.buffered_deltas
765 .retain(|d| d.block.number > oldest_pending_block);
766 }
767
768 let (snapshots, removed_components) = {
769 let (to_add, to_remove) =
770 self.component_tracker.filter_updated_components(&deltas);
771
772 let retry_ids: Vec<String> = self
778 .snapshot_queue
779 .iter()
780 .filter(|(_, s)| matches!(s, SnapshotStatus::RetryNext))
781 .map(|(id, _)| id.clone())
782 .collect();
783 for id in &retry_ids {
784 self.snapshot_queue.remove(id);
785 }
786
787 let truly_new: Vec<String> = {
792 let mut seen = HashSet::new();
793 to_add
794 .iter()
795 .chain(retry_ids.iter())
796 .filter(|id| {
797 !self.component_tracker
798 .components
799 .contains_key(id.as_str())
800 && !self
801 .snapshot_queue
802 .contains_key(id.as_str())
803 && seen.insert(id.as_str())
804 })
805 .cloned()
806 .collect()
807 };
808
809 if self.partial_blocks {
810 let is_new_block = self
811 .last_synced_block
812 .as_ref()
813 .map(|b| header.number > b.number)
814 .unwrap_or(true);
815
816 let has_deferred = self
817 .snapshot_queue
818 .values()
819 .any(|s| matches!(s, SnapshotStatus::Deferred));
820 if is_new_block && has_deferred && header.number > 0 {
821 let to_fire: Vec<String> = self
825 .snapshot_queue
826 .iter()
827 .filter(|(_, s)| matches!(s, SnapshotStatus::Deferred))
828 .map(|(id, _)| id.clone())
829 .collect();
830 for id in &to_fire {
831 self.snapshot_queue.remove(id);
832 }
833 let snapshot_header = BlockHeader {
834 number: header.number - 1,
835 hash: header.parent_hash.clone(),
836 ..Default::default()
837 };
838 debug!(
839 components = ?to_fire,
840 extractor = %self.extractor_id.name,
841 snapshot_block = header.number - 1,
842 "snapshot_deferred_to_background"
843 );
844 self.spawn_snapshot_task(
845 to_fire,
846 snapshot_header,
847 &deltas,
848 );
849 }
850
851 for id in truly_new {
854 self.snapshot_queue
855 .insert(id, SnapshotStatus::Deferred);
856 }
857 } else if !truly_new.is_empty() {
858 debug!(
859 components = ?truly_new,
860 extractor = %self.extractor_id.name,
861 block_number = ?header.number,
862 "snapshot_deferred_to_background"
863 );
864 let snapshot_header =
865 BlockHeader { revert: false, ..header.clone() };
866 self.spawn_snapshot_task(truly_new, snapshot_header, &deltas);
867 }
868
869 let snapshots = background_snapshots;
870
871 let removed_components = if !to_remove.is_empty() {
872 self.component_tracker.stop_tracking(&to_remove)
873 } else {
874 Default::default()
875 };
876
877 (snapshots, removed_components)
878 };
879
880 self.component_tracker.process_entrypoints(&deltas.dci_update);
882
883 self.filter_deltas(&mut deltas);
885 let n_changes = deltas.n_changes();
886
887 let next = StateSyncMessage {
888 header: header.clone(),
889 snapshots,
890 deltas: Some(deltas),
891 removed_components,
892 };
893 block_tx.send(Ok(next)).await?;
894 self.last_synced_block = Some(header.clone());
895
896 debug!(block_number=?header.number, n_changes, "Finished processing delta message");
897 } else {
898 return Err(SynchronizerError::ConnectionError("Deltas channel closed".to_string()));
899 }
900 },
901 _ = &mut end_rx => {
902 info!("Received close signal during state_sync");
903 return Ok(());
904 }
905 }
906 }
907 }.await;
908
909 warn!(last_synced_block = ?&self.last_synced_block, "Deltas processing ended.");
911 let _ = self
913 .deltas_client
914 .unsubscribe(subscription_id)
915 .await
916 .map_err(|err| {
917 warn!(err=?err, "Unsubscribing from deltas on cleanup failed!");
918 });
919
920 match result {
923 Ok(()) => Ok(()), Err(e) => {
925 Err(Box::new((e, Some(end_rx))))
929 }
930 }
931 }
932
933 fn apply_deltas_to_snapshot(
936 &self,
937 snapshot: &mut Snapshot,
938 snapshot_block: u64,
939 contract_ids: &HashSet<Bytes>,
940 ) {
941 for delta in &self.buffered_deltas {
942 if delta.block.number <= snapshot_block {
943 continue;
944 }
945 for (component_id, state_delta) in &delta.state_deltas {
946 if let Some(cws) = snapshot.states.get_mut(component_id) {
947 cws.state.attributes.extend(
948 state_delta
949 .updated_attributes
950 .iter()
951 .map(|(k, v)| (k.clone(), v.clone())),
952 );
953 for key in &state_delta.deleted_attributes {
954 cws.state.attributes.remove(key);
955 }
956 }
957 }
958 for (component_id, token_balances) in &delta.component_balances {
959 if let Some(cws) = snapshot.states.get_mut(component_id) {
960 for (token, bal) in token_balances {
961 cws.state
962 .balances
963 .insert(token.clone(), bal.balance.clone());
964 }
965 }
966 }
967 for (address, account_delta) in &delta.account_deltas {
968 if contract_ids.contains(address) {
969 if let Some(account) = snapshot.vm_storage.get_mut(address) {
970 account.slots.extend(
971 account_delta
972 .slots
973 .iter()
974 .filter_map(|(k, v)| {
975 v.as_ref()
976 .map(|v| (k.clone(), v.clone()))
977 }),
978 );
979 if let Some(balance) = &account_delta.balance {
980 account.native_balance = balance.clone();
981 }
982 if let Some(code) = account_delta.code() {
983 account.code = code.clone();
984 }
985 }
986 }
987 }
988 }
989 }
990
991 fn spawn_snapshot_task(
995 &mut self,
996 component_ids: Vec<String>,
997 snapshot_header: BlockHeader,
998 current_delta: &BlockAggregatedChanges,
999 ) {
1000 let snapshot_block = snapshot_header.number;
1001
1002 if self.snapshot_tasks.is_empty() {
1003 self.buffered_deltas
1004 .push(current_delta.clone());
1005 }
1006
1007 let (tx, rx) = oneshot::channel();
1008 let rpc = self.rpc_client.clone();
1009 let bg_params = FetchSnapshotParams {
1010 chain: self.extractor_id.chain,
1011 protocol_system: self.extractor_id.name.clone(),
1012 block_number: snapshot_block,
1013 uses_dci: self.uses_dci,
1014 retrieve_balances: self.retrieve_balances,
1015 include_tvl: self.include_tvl,
1016 };
1017 let ids = component_ids.clone();
1018 tokio::spawn(async move {
1019 let _ = tx.send(fetch_snapshot_background(rpc, ids, bg_params).await);
1020 });
1021 for id in &component_ids {
1022 self.snapshot_queue
1023 .insert(id.clone(), SnapshotStatus::InFlight);
1024 }
1025 self.snapshot_tasks
1026 .push(SnapshotTask { component_ids, snapshot_block, receiver: rx });
1027 }
1028
1029 fn drain_completed_snapshots(&mut self) -> Snapshot {
1032 let mut result = Snapshot::default();
1033 let pending = std::mem::take(&mut self.snapshot_tasks);
1034
1035 for mut p in pending {
1036 match p.receiver.try_recv() {
1037 Ok(Ok(fetch_result)) => {
1038 debug!(
1039 components = ?p.component_ids,
1040 extractor = %self.extractor_id.name,
1041 "snapshot_background_ready"
1042 );
1043 for id in &p.component_ids {
1044 self.snapshot_queue.remove(id);
1045 }
1046 let new_component_ids: Vec<String> = fetch_result
1047 .components
1048 .keys()
1049 .cloned()
1050 .collect();
1051 self.component_tracker
1052 .components
1053 .extend(fetch_result.components);
1054 self.component_tracker
1055 .process_entrypoints(&fetch_result.dci_update);
1056 self.component_tracker
1057 .update_contracts(new_component_ids);
1058 let mut snapshot = fetch_result.snapshot;
1059 self.apply_deltas_to_snapshot(
1060 &mut snapshot,
1061 fetch_result.snapshot_block,
1062 &fetch_result.contract_ids,
1063 );
1064 result.extend(snapshot);
1065 }
1066 Ok(Err(e)) => {
1067 if e.is_transient() {
1068 warn!(
1069 components = ?p.component_ids,
1070 extractor = %self.extractor_id.name,
1071 err = %e,
1072 "Background snapshot fetch failed transiently; will retry next block"
1073 );
1074 for id in &p.component_ids {
1075 self.snapshot_queue
1076 .insert(id.clone(), SnapshotStatus::RetryNext);
1077 }
1078 } else {
1079 warn!(
1080 components = ?p.component_ids,
1081 extractor = %self.extractor_id.name,
1082 err = %e,
1083 "Background snapshot fetch failed permanently; \
1084 components blacklisted until restart"
1085 );
1086 for id in &p.component_ids {
1087 self.snapshot_queue
1088 .insert(id.clone(), SnapshotStatus::Blacklisted);
1089 }
1090 }
1091 }
1092 Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {
1093 self.snapshot_tasks.push(p);
1094 }
1095 Err(tokio::sync::oneshot::error::TryRecvError::Closed) => {
1096 warn!(
1097 components = ?p.component_ids,
1098 extractor = %self.extractor_id.name,
1099 "Background snapshot task dropped before sending result"
1100 );
1101 for id in &p.component_ids {
1102 self.snapshot_queue.remove(id);
1103 }
1104 }
1105 }
1106 }
1107
1108 result
1109 }
1110
1111 fn is_next_expected(&self, incoming: &BlockHeader) -> bool {
1112 if let Some(block) = self.last_synced_block.as_ref() {
1113 return incoming.parent_hash == block.hash;
1114 }
1115 false
1116 }
1117 fn filter_deltas(&self, deltas: &mut BlockAggregatedChanges) {
1118 deltas.filter_by_component(|id| {
1119 self.component_tracker
1120 .components
1121 .contains_key(id)
1122 });
1123 deltas.filter_by_contract(|id| {
1124 self.component_tracker
1125 .contracts
1126 .contains(id)
1127 });
1128 }
1129}
1130
1131#[async_trait]
1132impl<R, D> StateSynchronizer for ProtocolStateSynchronizer<R, D>
1133where
1134 R: RPCClient + Clone + Send + Sync + 'static,
1135 D: DeltasClient + Clone + Send + Sync + 'static,
1136{
1137 async fn initialize(&mut self) -> SyncResult<()> {
1138 info!("Retrieving relevant protocol components");
1139 self.component_tracker
1140 .initialise_components()
1141 .await?;
1142 info!(
1143 n_components = self.component_tracker.components.len(),
1144 n_contracts = self.component_tracker.contracts.len(),
1145 extractor = %self.extractor_id,
1146 "Finished retrieving components",
1147 );
1148
1149 Ok(())
1150 }
1151
1152 async fn start(
1153 mut self,
1154 ) -> (SynchronizerTaskHandle, Receiver<SyncResult<StateSyncMessage<BlockHeader>>>) {
1155 let (mut tx, rx) = channel(15);
1156 let (end_tx, end_rx) = oneshot::channel::<()>();
1157
1158 let jh = tokio::spawn(async move {
1159 let mut retry_count = 0;
1160 let mut current_end_rx = end_rx;
1161 let mut final_error = None;
1162
1163 while retry_count < self.max_retries {
1164 info!(extractor_id=%&self.extractor_id, retry_count, "(Re)starting synchronization loop");
1165
1166 let prev_block = self
1167 .last_synced_block
1168 .as_ref()
1169 .map(|h| h.number);
1170 let requests_snapshot = self.include_snapshots &&
1174 !self
1175 .component_tracker
1176 .components
1177 .is_empty();
1178 let snapshot_delay = if prev_block.is_some() && requests_snapshot {
1179 rand::thread_rng().gen_range(Duration::ZERO..=self.snapshot_jitter)
1180 } else {
1181 Duration::ZERO
1182 };
1183 let res = self
1184 .state_sync(&mut tx, current_end_rx, snapshot_delay)
1185 .await;
1186 let made_progress = self
1187 .last_synced_block
1188 .as_ref()
1189 .map(|h| h.number) >
1190 prev_block;
1191 match res {
1192 Ok(()) => {
1193 info!(
1194 extractor_id=%&self.extractor_id,
1195 retry_count,
1196 "State synchronization exited cleanly"
1197 );
1198 return;
1199 }
1200 Err(e) => {
1201 let (e, maybe_end_rx) = *e;
1202 warn!(
1203 extractor_id=%&self.extractor_id,
1204 retry_count,
1205 error=%e,
1206 "State synchronization errored!"
1207 );
1208
1209 if let Some(recovered_end_rx) = maybe_end_rx {
1211 current_end_rx = recovered_end_rx;
1212
1213 if let SynchronizerError::ConnectionClosed = e {
1214 error!(
1216 "Websocket connection closed. State synchronization exiting."
1217 );
1218 let _ = tx.send(Err(e)).await;
1219 return;
1220 } else {
1221 final_error = Some(e);
1223 }
1224 } else {
1225 info!(extractor_id=%&self.extractor_id, "Received close signal, exiting.");
1227 return;
1228 }
1229 }
1230 }
1231 if made_progress {
1235 retry_count = 0;
1236 } else {
1237 retry_count += 1;
1238 sleep(self.retry_cooldown).await;
1239 }
1240 }
1241 if let Some(e) = final_error {
1242 warn!(extractor_id=%&self.extractor_id, retry_count, error=%e, "Max retries exceeded");
1243 let _ = tx.send(Err(e)).await;
1244 }
1245 });
1246
1247 let handle = SynchronizerTaskHandle::new(jh, end_tx);
1248 (handle, rx)
1249 }
1250}
1251
1252#[cfg(test)]
1253mod test {
1254 use std::{collections::HashSet, sync::Arc};
1273
1274 use tycho_common::models::{
1275 blockchain::{
1276 AddressStorageLocation, Block, BlockAggregatedChanges, DCIUpdate, EntryPoint,
1277 EntryPointWithTracingParams, RPCTracerParams, TracingParams, TracingResult,
1278 },
1279 protocol::{ProtocolComponent, ProtocolComponentState},
1280 token::Token,
1281 Chain,
1282 };
1283 use uuid::Uuid;
1284
1285 use super::*;
1286 use crate::{
1287 deltas::MockDeltasClient,
1288 rpc::{MockRPCClient, Page},
1289 DeltasError, RPCError,
1290 };
1291
1292 struct ArcRPCClient<T>(Arc<T>);
1294
1295 impl<T> Clone for ArcRPCClient<T> {
1297 fn clone(&self) -> Self {
1298 ArcRPCClient(self.0.clone())
1299 }
1300 }
1301
1302 #[async_trait]
1303 impl<T> RPCClient for ArcRPCClient<T>
1304 where
1305 T: RPCClient + Sync + Send + 'static,
1306 {
1307 async fn get_tokens(
1308 &self,
1309 params: crate::rpc::TokensParams,
1310 ) -> Result<crate::rpc::Page<Vec<Token>>, RPCError> {
1311 self.0.get_tokens(params).await
1312 }
1313
1314 async fn get_contract_state(
1315 &self,
1316 params: crate::rpc::ContractStateParams,
1317 ) -> Result<crate::rpc::Page<Vec<Account>>, RPCError> {
1318 self.0.get_contract_state(params).await
1319 }
1320
1321 async fn get_protocol_components(
1322 &self,
1323 params: crate::rpc::ProtocolComponentsParams,
1324 ) -> Result<crate::rpc::Page<Vec<ProtocolComponent>>, RPCError> {
1325 self.0
1326 .get_protocol_components(params)
1327 .await
1328 }
1329
1330 async fn get_protocol_states(
1331 &self,
1332 params: crate::rpc::ProtocolStatesParams,
1333 ) -> Result<crate::rpc::Page<Vec<ProtocolComponentState>>, RPCError> {
1334 self.0.get_protocol_states(params).await
1335 }
1336
1337 async fn get_protocol_systems(
1338 &self,
1339 params: crate::rpc::ProtocolSystemsParams,
1340 ) -> Result<crate::rpc::Page<crate::rpc::ProtocolSystems>, RPCError> {
1341 self.0
1342 .get_protocol_systems(params)
1343 .await
1344 }
1345
1346 async fn get_component_tvl(
1347 &self,
1348 params: crate::rpc::ComponentTvlParams,
1349 ) -> Result<crate::rpc::Page<HashMap<String, f64>>, RPCError> {
1350 self.0.get_component_tvl(params).await
1351 }
1352
1353 async fn get_traced_entry_points(
1354 &self,
1355 params: crate::rpc::TracedEntryPointsParams,
1356 ) -> Result<
1357 crate::rpc::Page<HashMap<String, Vec<(EntryPointWithTracingParams, TracingResult)>>>,
1358 RPCError,
1359 > {
1360 self.0
1361 .get_traced_entry_points(params)
1362 .await
1363 }
1364
1365 async fn get_snapshots<'a>(
1366 &self,
1367 request: &SnapshotParameters<'a>,
1368 chunk_size: Option<usize>,
1369 concurrency: usize,
1370 ) -> Result<Snapshot, RPCError> {
1371 self.0
1372 .get_snapshots(request, chunk_size, concurrency)
1373 .await
1374 }
1375
1376 fn compression(&self) -> bool {
1377 self.0.compression()
1378 }
1379 }
1380
1381 struct ArcDeltasClient<T>(Arc<T>);
1383
1384 impl<T> Clone for ArcDeltasClient<T> {
1386 fn clone(&self) -> Self {
1387 ArcDeltasClient(self.0.clone())
1388 }
1389 }
1390
1391 #[async_trait]
1392 impl<T> DeltasClient for ArcDeltasClient<T>
1393 where
1394 T: DeltasClient + Sync + Send + 'static,
1395 {
1396 async fn subscribe(
1397 &self,
1398 extractor_id: tycho_common::models::ExtractorIdentity,
1399 options: SubscriptionOptions,
1400 ) -> Result<(Uuid, Receiver<BlockAggregatedChanges>), DeltasError> {
1401 self.0
1402 .subscribe(extractor_id, options)
1403 .await
1404 }
1405
1406 async fn unsubscribe(&self, subscription_id: Uuid) -> Result<(), DeltasError> {
1407 self.0
1408 .unsubscribe(subscription_id)
1409 .await
1410 }
1411
1412 async fn connect(&self) -> Result<JoinHandle<Result<(), DeltasError>>, DeltasError> {
1413 self.0.connect().await
1414 }
1415
1416 async fn close(&self) -> Result<(), DeltasError> {
1417 self.0.close().await
1418 }
1419 }
1420
1421 fn with_mocked_clients(
1422 native: bool,
1423 include_tvl: bool,
1424 rpc_client: Option<MockRPCClient>,
1425 deltas_client: Option<MockDeltasClient>,
1426 ) -> ProtocolStateSynchronizer<ArcRPCClient<MockRPCClient>, ArcDeltasClient<MockDeltasClient>>
1427 {
1428 let rpc_client = ArcRPCClient(Arc::new(rpc_client.unwrap_or_default()));
1429 let deltas_client = ArcDeltasClient(Arc::new(deltas_client.unwrap_or_default()));
1430
1431 ProtocolStateSynchronizer::new(
1432 ExtractorIdentity::new(Chain::Ethereum, "uniswap-v2"),
1433 native,
1434 ComponentFilter::with_tvl_range(50.0, 50.0),
1435 1,
1436 Duration::from_secs(0),
1437 true,
1438 include_tvl,
1439 true, rpc_client,
1441 deltas_client,
1442 10_u64,
1443 )
1444 }
1445
1446 fn state_snapshot_native() -> Vec<ProtocolComponentState> {
1447 vec![ProtocolComponentState {
1448 component_id: "Component1".to_string(),
1449 attributes: HashMap::new(),
1450 balances: HashMap::new(),
1451 }]
1452 }
1453
1454 fn make_mock_client() -> MockRPCClient {
1455 let mut m = MockRPCClient::new();
1456 m.expect_compression()
1457 .return_const(false);
1458 m
1459 }
1460
1461 #[test_log::test(tokio::test)]
1462 async fn test_get_snapshots_native() {
1463 let header = BlockHeader::default();
1464 let mut rpc = make_mock_client();
1465 let component = ProtocolComponent { id: "Component1".to_string(), ..Default::default() };
1466
1467 let component_clone = component.clone();
1468 rpc.expect_get_snapshots()
1469 .returning(move |_request, _chunk_size, _concurrency| {
1470 Ok(Snapshot {
1471 states: state_snapshot_native()
1472 .into_iter()
1473 .map(|state| {
1474 (
1475 state.component_id.clone(),
1476 ComponentWithState {
1477 state,
1478 component: component_clone.clone(),
1479 entrypoints: vec![],
1480 component_tvl: None,
1481 },
1482 )
1483 })
1484 .collect(),
1485 vm_storage: HashMap::new(),
1486 })
1487 });
1488
1489 let mut state_sync = with_mocked_clients(true, false, Some(rpc), None);
1490 state_sync
1491 .component_tracker
1492 .components
1493 .insert("Component1".to_string(), component.clone());
1494 let components_arg = ["Component1".to_string()];
1495 let exp = StateSyncMessage {
1496 header: header.clone(),
1497 snapshots: Snapshot {
1498 states: state_snapshot_native()
1499 .into_iter()
1500 .map(|state| {
1501 (
1502 state.component_id.clone(),
1503 ComponentWithState {
1504 state,
1505 component: component.clone(),
1506 entrypoints: vec![],
1507 component_tvl: None,
1508 },
1509 )
1510 })
1511 .collect(),
1512 vm_storage: HashMap::new(),
1513 },
1514 deltas: None,
1515 removed_components: Default::default(),
1516 };
1517
1518 let req_ids: Vec<String> = components_arg.to_vec();
1519 let components: HashMap<_, _> = state_sync
1520 .component_tracker
1521 .components
1522 .iter()
1523 .filter(|(id, _)| req_ids.contains(id))
1524 .map(|(k, v)| (k.clone(), v.clone()))
1525 .collect();
1526 let contract_ids: HashSet<Bytes> = state_sync
1527 .component_tracker
1528 .get_contracts_by_component(&req_ids)
1529 .into_iter()
1530 .collect();
1531 let params = FetchSnapshotParams {
1532 chain: Chain::Ethereum,
1533 protocol_system: "uniswap-v2".to_string(),
1534 block_number: header.number,
1535 uses_dci: false,
1536 retrieve_balances: true,
1537 include_tvl: false,
1538 };
1539 let (snapshot, _, _) =
1540 fetch_snapshot(&state_sync.rpc_client, components, contract_ids, ¶ms)
1541 .await
1542 .expect("Retrieving snapshot failed");
1543 let snap = StateSyncMessage {
1544 header: header.clone(),
1545 snapshots: snapshot,
1546 deltas: None,
1547 removed_components: Default::default(),
1548 };
1549
1550 assert_eq!(snap, exp);
1551 }
1552
1553 #[test_log::test(tokio::test)]
1554 async fn test_get_snapshots_native_with_tvl() {
1555 let header = BlockHeader::default();
1556 let mut rpc = make_mock_client();
1557 let component = ProtocolComponent { id: "Component1".to_string(), ..Default::default() };
1558
1559 let component_clone = component.clone();
1560 rpc.expect_get_snapshots()
1561 .returning(move |_request, _chunk_size, _concurrency| {
1562 Ok(Snapshot {
1563 states: state_snapshot_native()
1564 .into_iter()
1565 .map(|state| {
1566 (
1567 state.component_id.clone(),
1568 ComponentWithState {
1569 state,
1570 component: component_clone.clone(),
1571 component_tvl: Some(100.0),
1572 entrypoints: vec![],
1573 },
1574 )
1575 })
1576 .collect(),
1577 vm_storage: HashMap::new(),
1578 })
1579 });
1580
1581 let mut state_sync = with_mocked_clients(true, true, Some(rpc), None);
1582 state_sync
1583 .component_tracker
1584 .components
1585 .insert("Component1".to_string(), component.clone());
1586 let components_arg = ["Component1".to_string()];
1587 let exp = StateSyncMessage {
1588 header: header.clone(),
1589 snapshots: Snapshot {
1590 states: state_snapshot_native()
1591 .into_iter()
1592 .map(|state| {
1593 (
1594 state.component_id.clone(),
1595 ComponentWithState {
1596 state,
1597 component: component.clone(),
1598 component_tvl: Some(100.0),
1599 entrypoints: vec![],
1600 },
1601 )
1602 })
1603 .collect(),
1604 vm_storage: HashMap::new(),
1605 },
1606 deltas: None,
1607 removed_components: Default::default(),
1608 };
1609
1610 let req_ids: Vec<String> = components_arg.to_vec();
1611 let components: HashMap<_, _> = state_sync
1612 .component_tracker
1613 .components
1614 .iter()
1615 .filter(|(id, _)| req_ids.contains(id))
1616 .map(|(k, v)| (k.clone(), v.clone()))
1617 .collect();
1618 let contract_ids: HashSet<Bytes> = state_sync
1619 .component_tracker
1620 .get_contracts_by_component(&req_ids)
1621 .into_iter()
1622 .collect();
1623 let params = FetchSnapshotParams {
1624 chain: Chain::Ethereum,
1625 protocol_system: "uniswap-v2".to_string(),
1626 block_number: header.number,
1627 uses_dci: false,
1628 retrieve_balances: true,
1629 include_tvl: true,
1630 };
1631 let (snapshot, _, _) =
1632 fetch_snapshot(&state_sync.rpc_client, components, contract_ids, ¶ms)
1633 .await
1634 .expect("Retrieving snapshot failed");
1635 let snap = StateSyncMessage {
1636 header: header.clone(),
1637 snapshots: snapshot,
1638 deltas: None,
1639 removed_components: Default::default(),
1640 };
1641
1642 assert_eq!(snap, exp);
1643 }
1644
1645 fn state_snapshot_vm() -> Vec<Account> {
1646 vec![
1647 Account::new(
1648 Chain::default(),
1649 Bytes::from("0x0badc0ffee"),
1650 String::new(),
1651 HashMap::new(),
1652 Bytes::default(),
1653 HashMap::new(),
1654 Bytes::default(),
1655 Bytes::default(),
1656 Bytes::default(),
1657 Bytes::default(),
1658 None,
1659 ),
1660 Account::new(
1661 Chain::default(),
1662 Bytes::from("0xbabe42"),
1663 String::new(),
1664 HashMap::new(),
1665 Bytes::default(),
1666 HashMap::new(),
1667 Bytes::default(),
1668 Bytes::default(),
1669 Bytes::default(),
1670 Bytes::default(),
1671 None,
1672 ),
1673 ]
1674 }
1675
1676 fn traced_entry_point_response(
1677 ) -> HashMap<String, Vec<(EntryPointWithTracingParams, TracingResult)>> {
1678 HashMap::from([(
1679 "Component1".to_string(),
1680 vec![(
1681 EntryPointWithTracingParams {
1682 entry_point: EntryPoint {
1683 external_id: "entrypoint_a".to_string(),
1684 target: Bytes::from("0x0badc0ffee"),
1685 signature: "sig()".to_string(),
1686 },
1687 params: TracingParams::RPCTracer(RPCTracerParams {
1688 caller: Some(Bytes::from("0x0badc0ffee")),
1689 calldata: Bytes::from("0x0badc0ffee"),
1690 state_overrides: None,
1691 prune_addresses: None,
1692 }),
1693 },
1694 TracingResult {
1695 retriggers: HashSet::from([(
1696 Bytes::from("0x0badc0ffee"),
1697 AddressStorageLocation::new(Bytes::from("0x0badc0ffee"), 12),
1698 )]),
1699 accessed_slots: HashMap::from([(
1700 Bytes::from("0x0badc0ffee"),
1701 HashSet::from([Bytes::from("0xbadbeef0")]),
1702 )]),
1703 },
1704 )],
1705 )])
1706 }
1707
1708 #[test_log::test(tokio::test)]
1709 async fn test_get_snapshots_vm() {
1710 let header = BlockHeader::default();
1711 let mut rpc = make_mock_client();
1712
1713 let traced_ep_response = traced_entry_point_response();
1714 rpc.expect_get_snapshots()
1715 .returning(move |_request, _chunk_size, _concurrency| {
1716 let vm_storage_accounts = state_snapshot_vm();
1717 Ok(Snapshot {
1718 states: [(
1719 "Component1".to_string(),
1720 ComponentWithState {
1721 state: ProtocolComponentState {
1722 component_id: "Component1".to_string(),
1723 attributes: HashMap::new(),
1724 balances: HashMap::new(),
1725 },
1726 component: ProtocolComponent {
1727 id: "Component1".to_string(),
1728 contract_addresses: vec![
1729 Bytes::from("0x0badc0ffee"),
1730 Bytes::from("0xbabe42"),
1731 ],
1732 ..Default::default()
1733 },
1734 component_tvl: None,
1735 entrypoints: traced_ep_response
1736 .get("Component1")
1737 .cloned()
1738 .unwrap_or_default(),
1739 },
1740 )]
1741 .into_iter()
1742 .collect(),
1743 vm_storage: vm_storage_accounts
1744 .into_iter()
1745 .map(|account| (account.address.clone(), account))
1746 .collect(),
1747 })
1748 });
1749
1750 let mut state_sync = with_mocked_clients(false, false, Some(rpc), None);
1751 let component = ProtocolComponent {
1752 id: "Component1".to_string(),
1753 contract_addresses: vec![Bytes::from("0x0badc0ffee"), Bytes::from("0xbabe42")],
1754 ..Default::default()
1755 };
1756 state_sync
1757 .component_tracker
1758 .components
1759 .insert("Component1".to_string(), component.clone());
1760 let components_arg = ["Component1".to_string()];
1761 let exp = StateSyncMessage {
1762 header: header.clone(),
1763 snapshots: Snapshot {
1764 states: [(
1765 component.id.clone(),
1766 ComponentWithState {
1767 state: ProtocolComponentState {
1768 component_id: "Component1".to_string(),
1769 attributes: HashMap::new(),
1770 balances: HashMap::new(),
1771 },
1772 component: component.clone(),
1773 component_tvl: None,
1774 entrypoints: traced_entry_point_response()
1775 .remove("Component1")
1776 .unwrap_or_default(),
1777 },
1778 )]
1779 .into_iter()
1780 .collect(),
1781 vm_storage: state_snapshot_vm()
1782 .into_iter()
1783 .map(|account| (account.address.clone(), account))
1784 .collect(),
1785 },
1786 deltas: None,
1787 removed_components: Default::default(),
1788 };
1789
1790 let req_ids: Vec<String> = components_arg.to_vec();
1791 let components: HashMap<_, _> = state_sync
1792 .component_tracker
1793 .components
1794 .iter()
1795 .filter(|(id, _)| req_ids.contains(id))
1796 .map(|(k, v)| (k.clone(), v.clone()))
1797 .collect();
1798 let contract_ids: HashSet<Bytes> = state_sync
1799 .component_tracker
1800 .get_contracts_by_component(&req_ids)
1801 .into_iter()
1802 .collect();
1803 let params = FetchSnapshotParams {
1804 chain: Chain::Ethereum,
1805 protocol_system: "uniswap-v2".to_string(),
1806 block_number: header.number,
1807 uses_dci: false,
1808 retrieve_balances: false,
1809 include_tvl: false,
1810 };
1811 let (snapshot, _, _) =
1812 fetch_snapshot(&state_sync.rpc_client, components, contract_ids, ¶ms)
1813 .await
1814 .expect("Retrieving snapshot failed");
1815 let snap = StateSyncMessage {
1816 header: header.clone(),
1817 snapshots: snapshot,
1818 deltas: None,
1819 removed_components: Default::default(),
1820 };
1821
1822 assert_eq!(snap, exp);
1823 }
1824
1825 #[test_log::test(tokio::test)]
1826 async fn test_get_snapshots_vm_with_tvl() {
1827 let header = BlockHeader::default();
1828 let mut rpc = make_mock_client();
1829 let component = ProtocolComponent {
1830 id: "Component1".to_string(),
1831 contract_addresses: vec![Bytes::from("0x0badc0ffee"), Bytes::from("0xbabe42")],
1832 ..Default::default()
1833 };
1834
1835 let component_clone = component.clone();
1836 rpc.expect_get_snapshots()
1837 .returning(move |_request, _chunk_size, _concurrency| {
1838 let vm_storage_accounts = state_snapshot_vm();
1839 Ok(Snapshot {
1840 states: [(
1841 "Component1".to_string(),
1842 ComponentWithState {
1843 state: ProtocolComponentState {
1844 component_id: "Component1".to_string(),
1845 attributes: HashMap::new(),
1846 balances: HashMap::new(),
1847 },
1848 component: component_clone.clone(),
1849 component_tvl: Some(100.0),
1850 entrypoints: vec![],
1851 },
1852 )]
1853 .into_iter()
1854 .collect(),
1855 vm_storage: vm_storage_accounts
1856 .into_iter()
1857 .map(|account| (account.address.clone(), account))
1858 .collect(),
1859 })
1860 });
1861
1862 let mut state_sync = with_mocked_clients(false, true, Some(rpc), None);
1863 state_sync
1864 .component_tracker
1865 .components
1866 .insert("Component1".to_string(), component.clone());
1867 let components_arg = ["Component1".to_string()];
1868 let exp = StateSyncMessage {
1869 header: header.clone(),
1870 snapshots: Snapshot {
1871 states: [(
1872 component.id.clone(),
1873 ComponentWithState {
1874 state: ProtocolComponentState {
1875 component_id: "Component1".to_string(),
1876 attributes: HashMap::new(),
1877 balances: HashMap::new(),
1878 },
1879 component: component.clone(),
1880 component_tvl: Some(100.0),
1881 entrypoints: vec![],
1882 },
1883 )]
1884 .into_iter()
1885 .collect(),
1886 vm_storage: state_snapshot_vm()
1887 .into_iter()
1888 .map(|account| (account.address.clone(), account))
1889 .collect(),
1890 },
1891 deltas: None,
1892 removed_components: Default::default(),
1893 };
1894
1895 let req_ids: Vec<String> = components_arg.to_vec();
1896 let components: HashMap<_, _> = state_sync
1897 .component_tracker
1898 .components
1899 .iter()
1900 .filter(|(id, _)| req_ids.contains(id))
1901 .map(|(k, v)| (k.clone(), v.clone()))
1902 .collect();
1903 let contract_ids: HashSet<Bytes> = state_sync
1904 .component_tracker
1905 .get_contracts_by_component(&req_ids)
1906 .into_iter()
1907 .collect();
1908 let params = FetchSnapshotParams {
1909 chain: Chain::Ethereum,
1910 protocol_system: "uniswap-v2".to_string(),
1911 block_number: header.number,
1912 uses_dci: false,
1913 retrieve_balances: false,
1914 include_tvl: true,
1915 };
1916 let (snapshot, _, _) =
1917 fetch_snapshot(&state_sync.rpc_client, components, contract_ids, ¶ms)
1918 .await
1919 .expect("Retrieving snapshot failed");
1920 let snap = StateSyncMessage {
1921 header: header.clone(),
1922 snapshots: snapshot,
1923 deltas: None,
1924 removed_components: Default::default(),
1925 };
1926
1927 assert_eq!(snap, exp);
1928 }
1929
1930 #[test_log::test(tokio::test)]
1934 async fn test_get_snapshots_filters_to_requested_components_only() {
1935 let header = BlockHeader::default();
1936 let mut rpc = make_mock_client();
1937
1938 let component1 = ProtocolComponent { id: "Component1".to_string(), ..Default::default() };
1940 let component2 = ProtocolComponent { id: "Component2".to_string(), ..Default::default() };
1941 let component3 = ProtocolComponent { id: "Component3".to_string(), ..Default::default() };
1942
1943 let component2_clone = component2.clone();
1944
1945 rpc.expect_get_snapshots()
1947 .withf(
1948 |request: &SnapshotParameters,
1949 _chunk_size: &Option<usize>,
1950 _concurrency: &usize| {
1951 request.components.len() == 1 &&
1953 request
1954 .components
1955 .contains_key("Component2")
1956 },
1957 )
1958 .times(1)
1959 .returning(move |_request, _chunk_size, _concurrency| {
1960 Ok(Snapshot {
1961 states: [(
1962 "Component2".to_string(),
1963 ComponentWithState {
1964 state: ProtocolComponentState {
1965 component_id: "Component2".to_string(),
1966 attributes: HashMap::new(),
1967 balances: HashMap::new(),
1968 },
1969 component: component2_clone.clone(),
1970 entrypoints: vec![],
1971 component_tvl: None,
1972 },
1973 )]
1974 .into_iter()
1975 .collect(),
1976 vm_storage: HashMap::new(),
1977 })
1978 });
1979
1980 let mut state_sync = with_mocked_clients(true, false, Some(rpc), None);
1981
1982 state_sync
1984 .component_tracker
1985 .components
1986 .insert("Component1".to_string(), component1.clone());
1987 state_sync
1988 .component_tracker
1989 .components
1990 .insert("Component2".to_string(), component2.clone());
1991 state_sync
1992 .component_tracker
1993 .components
1994 .insert("Component3".to_string(), component3.clone());
1995
1996 let components_arg = ["Component2".to_string()];
1998 let req_ids: Vec<String> = components_arg.to_vec();
1999 let components: HashMap<_, _> = state_sync
2000 .component_tracker
2001 .components
2002 .iter()
2003 .filter(|(id, _)| req_ids.contains(id))
2004 .map(|(k, v)| (k.clone(), v.clone()))
2005 .collect();
2006 let contract_ids: HashSet<Bytes> = state_sync
2007 .component_tracker
2008 .get_contracts_by_component(&req_ids)
2009 .into_iter()
2010 .collect();
2011 let params = FetchSnapshotParams {
2012 chain: Chain::Ethereum,
2013 protocol_system: "uniswap-v2".to_string(),
2014 block_number: header.number,
2015 uses_dci: false,
2016 retrieve_balances: true,
2017 include_tvl: false,
2018 };
2019 let (snapshot, _, _) =
2020 fetch_snapshot(&state_sync.rpc_client, components, contract_ids, ¶ms)
2021 .await
2022 .expect("Retrieving snapshot failed");
2023
2024 assert_eq!(snapshot.states.len(), 1);
2026 assert!(snapshot
2027 .states
2028 .contains_key("Component2"));
2029 assert!(!snapshot
2030 .states
2031 .contains_key("Component1"));
2032 assert!(!snapshot
2033 .states
2034 .contains_key("Component3"));
2035 }
2036
2037 fn mock_clients_for_state_sync(
2038 bg_done: Option<Arc<tokio::sync::Notify>>,
2039 ) -> (MockRPCClient, MockDeltasClient, Sender<BlockAggregatedChanges>) {
2040 let mut rpc_client = make_mock_client();
2041 rpc_client
2044 .expect_get_protocol_components()
2045 .withf(|params: &crate::rpc::ProtocolComponentsParams| {
2046 params
2047 .component_ids()
2048 .is_some_and(|ids| ids.contains(&"Component3".to_string()))
2049 })
2050 .returning(|_| {
2051 Ok(Page::new(
2053 vec![
2054 ProtocolComponent { id: "Component3".to_string(), ..Default::default() },
2056 ],
2057 1,
2058 0,
2059 100,
2060 ))
2061 });
2062 rpc_client
2064 .expect_get_snapshots()
2065 .withf(
2066 |request: &SnapshotParameters,
2067 _chunk_size: &Option<usize>,
2068 _concurrency: &usize| {
2069 request
2070 .components
2071 .contains_key("Component3")
2072 },
2073 )
2074 .returning(move |_request, _chunk_size, _concurrency| {
2075 let snap = Ok(Snapshot {
2076 states: [(
2077 "Component3".to_string(),
2078 ComponentWithState {
2079 state: ProtocolComponentState::new(
2080 "Component3",
2081 Default::default(),
2082 Default::default(),
2083 ),
2084 component: ProtocolComponent {
2085 id: "Component3".to_string(),
2086 ..Default::default()
2087 },
2088 component_tvl: Some(1000.0),
2089 entrypoints: vec![],
2090 },
2091 )]
2092 .into_iter()
2093 .collect(),
2094 vm_storage: HashMap::new(),
2095 });
2096 if let Some(n) = &bg_done {
2097 n.notify_one();
2098 }
2099 snap
2100 });
2101
2102 rpc_client
2104 .expect_get_protocol_components()
2105 .returning(|_| {
2106 Ok(Page::new(
2108 vec![
2109 ProtocolComponent { id: "Component1".to_string(), ..Default::default() },
2111 ProtocolComponent { id: "Component2".to_string(), ..Default::default() },
2113 ],
2115 2,
2116 0,
2117 100,
2118 ))
2119 });
2120
2121 rpc_client
2122 .expect_get_snapshots()
2123 .returning(|_request, _chunk_size, _concurrency| {
2124 Ok(Snapshot {
2125 states: [
2126 (
2127 "Component1".to_string(),
2128 ComponentWithState {
2129 state: ProtocolComponentState::new(
2130 "Component1",
2131 Default::default(),
2132 Default::default(),
2133 ),
2134 component: ProtocolComponent {
2135 id: "Component1".to_string(),
2136 ..Default::default()
2137 },
2138 component_tvl: Some(100.0),
2139 entrypoints: vec![],
2140 },
2141 ),
2142 (
2143 "Component2".to_string(),
2144 ComponentWithState {
2145 state: ProtocolComponentState::new(
2146 "Component2",
2147 Default::default(),
2148 Default::default(),
2149 ),
2150 component: ProtocolComponent {
2151 id: "Component2".to_string(),
2152 ..Default::default()
2153 },
2154 component_tvl: Some(0.0),
2155 entrypoints: vec![],
2156 },
2157 ),
2158 ]
2159 .into_iter()
2160 .collect(),
2161 vm_storage: HashMap::new(),
2162 })
2163 });
2164
2165 rpc_client
2167 .expect_get_traced_entry_points()
2168 .returning(|_| Ok(Page::new(HashMap::new(), 0, 0, 0)));
2169
2170 let mut deltas_client = MockDeltasClient::new();
2172 let (tx, rx) = channel(1);
2173 deltas_client
2174 .expect_subscribe()
2175 .return_once(move |_, _| {
2176 Ok((Uuid::default(), rx))
2178 });
2179
2180 deltas_client
2182 .expect_unsubscribe()
2183 .return_once(|_| Ok(()));
2184
2185 (rpc_client, deltas_client, tx)
2186 }
2187
2188 #[test_log::test(tokio::test)]
2196 async fn test_state_sync() {
2197 let bg_done = Arc::new(tokio::sync::Notify::new());
2198 let (rpc_client, deltas_client, tx) = mock_clients_for_state_sync(Some(bg_done.clone()));
2199 let deltas = [
2200 BlockAggregatedChanges {
2201 extractor: "uniswap-v2".to_string(),
2202 chain: Chain::Ethereum,
2203 block: Block {
2204 number: 1,
2205 hash: Bytes::from("0x01"),
2206 parent_hash: Bytes::from("0x00"),
2207 chain: Chain::Ethereum,
2208 ts: Default::default(),
2209 },
2210 revert: false,
2211 dci_update: DCIUpdate {
2212 new_entrypoints: HashMap::from([(
2213 "Component1".to_string(),
2214 HashSet::from([EntryPoint {
2215 external_id: "entrypoint_a".to_string(),
2216 target: Bytes::from("0x0badc0ffee"),
2217 signature: "sig()".to_string(),
2218 }]),
2219 )]),
2220 new_entrypoint_params: HashMap::from([(
2221 "entrypoint_a".to_string(),
2222 HashSet::from([(
2223 TracingParams::RPCTracer(RPCTracerParams {
2224 caller: Some(Bytes::from("0x0badc0ffee")),
2225 calldata: Bytes::from("0x0badc0ffee"),
2226 state_overrides: None,
2227 prune_addresses: None,
2228 }),
2229 "Component1".to_string(),
2230 )]),
2231 )]),
2232 trace_results: HashMap::from([(
2233 "entrypoint_a".to_string(),
2234 TracingResult {
2235 retriggers: HashSet::from([(
2236 Bytes::from("0x0badc0ffee"),
2237 AddressStorageLocation::new(Bytes::from("0x0badc0ffee"), 12),
2238 )]),
2239 accessed_slots: HashMap::from([(
2240 Bytes::from("0x0badc0ffee"),
2241 HashSet::from([Bytes::from("0xbadbeef0")]),
2242 )]),
2243 },
2244 )]),
2245 },
2246 ..Default::default()
2247 },
2248 BlockAggregatedChanges {
2249 extractor: "uniswap-v2".to_string(),
2250 chain: Chain::Ethereum,
2251 block: Block {
2252 number: 2,
2253 hash: Bytes::from("0x02"),
2254 parent_hash: Bytes::from("0x01"),
2255 chain: Chain::Ethereum,
2256 ts: Default::default(),
2257 },
2258 revert: false,
2259 component_tvl: [
2260 ("Component1".to_string(), 100.0),
2261 ("Component2".to_string(), 0.0),
2262 ("Component3".to_string(), 1000.0),
2263 ]
2264 .into_iter()
2265 .collect(),
2266 ..Default::default()
2267 },
2268 BlockAggregatedChanges {
2271 extractor: "uniswap-v2".to_string(),
2272 chain: Chain::Ethereum,
2273 block: Block {
2274 number: 3,
2275 hash: Bytes::from("0x03"),
2276 parent_hash: Bytes::from("0x02"),
2277 chain: Chain::Ethereum,
2278 ts: Default::default(),
2279 },
2280 revert: false,
2281 ..Default::default()
2282 },
2283 ];
2284 let mut state_sync = with_mocked_clients(true, true, Some(rpc_client), Some(deltas_client));
2285 state_sync
2286 .initialize()
2287 .await
2288 .expect("Init failed");
2289
2290 let (handle, mut rx) = state_sync.start().await;
2292 let (jh, close_tx) = handle.split();
2293 tx.send(deltas[0].clone())
2294 .await
2295 .expect("deltas channel msg 0 closed!");
2296 let first_msg = timeout(Duration::from_millis(200), rx.recv())
2297 .await
2298 .expect("waiting for first state msg timed out!")
2299 .expect("state sync block sender closed!");
2300 tx.send(deltas[1].clone())
2301 .await
2302 .expect("deltas channel msg 1 closed!");
2303 let second_msg = timeout(Duration::from_millis(200), rx.recv())
2304 .await
2305 .expect("waiting for second state msg timed out!")
2306 .expect("state sync block sender closed!");
2307 bg_done.notified().await;
2309 tx.send(deltas[2].clone())
2310 .await
2311 .expect("deltas channel msg 2 closed!");
2312 let third_msg = timeout(Duration::from_millis(200), rx.recv())
2313 .await
2314 .expect("waiting for third state msg timed out!")
2315 .expect("state sync block sender closed!");
2316 let _ = close_tx.send(());
2317 jh.await
2318 .expect("state sync task panicked!");
2319
2320 let exp1 = StateSyncMessage {
2322 header: BlockHeader {
2323 number: 1,
2324 hash: Bytes::from("0x01"),
2325 parent_hash: Bytes::from("0x00"),
2326 revert: false,
2327 ..Default::default()
2328 },
2329 snapshots: Snapshot {
2330 states: [
2331 (
2332 "Component1".to_string(),
2333 ComponentWithState {
2334 state: ProtocolComponentState::new(
2335 "Component1",
2336 Default::default(),
2337 Default::default(),
2338 ),
2339 component: ProtocolComponent {
2340 id: "Component1".to_string(),
2341 ..Default::default()
2342 },
2343 component_tvl: Some(100.0),
2344 entrypoints: vec![],
2345 },
2346 ),
2347 (
2348 "Component2".to_string(),
2349 ComponentWithState {
2350 state: ProtocolComponentState::new(
2351 "Component2",
2352 Default::default(),
2353 Default::default(),
2354 ),
2355 component: ProtocolComponent {
2356 id: "Component2".to_string(),
2357 ..Default::default()
2358 },
2359 component_tvl: Some(0.0),
2360 entrypoints: vec![],
2361 },
2362 ),
2363 ]
2364 .into_iter()
2365 .collect(),
2366 vm_storage: HashMap::new(),
2367 },
2368 deltas: Some(deltas[0].clone()),
2369 removed_components: Default::default(),
2370 };
2371
2372 let exp2 = StateSyncMessage {
2375 header: BlockHeader {
2376 number: 2,
2377 hash: Bytes::from("0x02"),
2378 parent_hash: Bytes::from("0x01"),
2379 revert: false,
2380 ..Default::default()
2381 },
2382 snapshots: Snapshot::default(),
2383 deltas: Some(BlockAggregatedChanges {
2384 extractor: "uniswap-v2".to_string(),
2385 chain: Chain::Ethereum,
2386 block: Block {
2387 number: 2,
2388 hash: Bytes::from("0x02"),
2389 parent_hash: Bytes::from("0x01"),
2390 chain: Chain::Ethereum,
2391 ts: Default::default(),
2392 },
2393 revert: false,
2394 component_tvl: [
2395 ("Component1".to_string(), 100.0),
2397 ]
2398 .into_iter()
2399 .collect(),
2400 ..Default::default()
2401 }),
2402 removed_components: [(
2404 "Component2".to_string(),
2405 ProtocolComponent { id: "Component2".to_string(), ..Default::default() },
2406 )]
2407 .into_iter()
2408 .collect(),
2409 };
2410
2411 let exp3 = StateSyncMessage {
2413 header: BlockHeader {
2414 number: 3,
2415 hash: Bytes::from("0x03"),
2416 parent_hash: Bytes::from("0x02"),
2417 revert: false,
2418 ..Default::default()
2419 },
2420 snapshots: Snapshot {
2421 states: [(
2422 "Component3".to_string(),
2423 ComponentWithState {
2424 state: ProtocolComponentState::new(
2425 "Component3",
2426 Default::default(),
2427 Default::default(),
2428 ),
2429 component: ProtocolComponent {
2430 id: "Component3".to_string(),
2431 ..Default::default()
2432 },
2433 component_tvl: Some(1000.0),
2434 entrypoints: vec![],
2435 },
2436 )]
2437 .into_iter()
2438 .collect(),
2439 vm_storage: HashMap::new(),
2440 },
2441 deltas: Some(deltas[2].clone()),
2442 removed_components: Default::default(),
2443 };
2444 assert_eq!(first_msg.unwrap(), exp1);
2445 assert_eq!(second_msg.unwrap(), exp2);
2446 assert_eq!(third_msg.unwrap(), exp3);
2447 }
2448
2449 #[test_log::test(tokio::test)]
2450 async fn test_state_sync_with_tvl_range() {
2451 let remove_tvl_threshold = 5.0;
2452 let add_tvl_threshold = 7.0;
2453 let bg_done = Arc::new(tokio::sync::Notify::new());
2454
2455 let mut rpc_client = make_mock_client();
2456 let mut deltas_client = MockDeltasClient::new();
2457
2458 rpc_client
2459 .expect_get_protocol_components()
2460 .withf(|params: &crate::rpc::ProtocolComponentsParams| {
2461 params
2462 .component_ids()
2463 .is_some_and(|ids| ids.contains(&"Component3".to_string()))
2464 })
2465 .returning(|_| {
2466 Ok(Page::new(
2467 vec![ProtocolComponent { id: "Component3".to_string(), ..Default::default() }],
2468 1,
2469 0,
2470 100,
2471 ))
2472 });
2473 let bg_done_clone = bg_done.clone();
2475 rpc_client
2476 .expect_get_snapshots()
2477 .withf(
2478 |request: &SnapshotParameters,
2479 _chunk_size: &Option<usize>,
2480 _concurrency: &usize| {
2481 request
2482 .components
2483 .contains_key("Component3")
2484 },
2485 )
2486 .returning(move |_request, _chunk_size, _concurrency| {
2487 let snap = Ok(Snapshot {
2488 states: [(
2489 "Component3".to_string(),
2490 ComponentWithState {
2491 state: ProtocolComponentState::new(
2492 "Component3",
2493 Default::default(),
2494 Default::default(),
2495 ),
2496 component: ProtocolComponent {
2497 id: "Component3".to_string(),
2498 ..Default::default()
2499 },
2500 component_tvl: Some(10.0),
2501 entrypoints: vec![],
2502 },
2503 )]
2504 .into_iter()
2505 .collect(),
2506 vm_storage: HashMap::new(),
2507 });
2508 bg_done_clone.notify_one();
2509 snap
2510 });
2511
2512 rpc_client
2514 .expect_get_protocol_components()
2515 .returning(|_| {
2516 Ok(Page::new(
2517 vec![
2518 ProtocolComponent { id: "Component1".to_string(), ..Default::default() },
2519 ProtocolComponent { id: "Component2".to_string(), ..Default::default() },
2520 ],
2521 2,
2522 0,
2523 100,
2524 ))
2525 });
2526
2527 rpc_client
2529 .expect_get_snapshots()
2530 .returning(|_request, _chunk_size, _concurrency| {
2531 Ok(Snapshot {
2532 states: [
2533 (
2534 "Component1".to_string(),
2535 ComponentWithState {
2536 state: ProtocolComponentState::new(
2537 "Component1",
2538 Default::default(),
2539 Default::default(),
2540 ),
2541 component: ProtocolComponent {
2542 id: "Component1".to_string(),
2543 ..Default::default()
2544 },
2545 component_tvl: Some(6.0),
2546 entrypoints: vec![],
2547 },
2548 ),
2549 (
2550 "Component2".to_string(),
2551 ComponentWithState {
2552 state: ProtocolComponentState::new(
2553 "Component2",
2554 Default::default(),
2555 Default::default(),
2556 ),
2557 component: ProtocolComponent {
2558 id: "Component2".to_string(),
2559 ..Default::default()
2560 },
2561 component_tvl: Some(2.0),
2562 entrypoints: vec![],
2563 },
2564 ),
2565 ]
2566 .into_iter()
2567 .collect(),
2568 vm_storage: HashMap::new(),
2569 })
2570 });
2571
2572 rpc_client
2574 .expect_get_traced_entry_points()
2575 .returning(|_| Ok(Page::new(HashMap::new(), 0, 0, 0)));
2576
2577 let (tx, rx) = channel(1);
2578 deltas_client
2579 .expect_subscribe()
2580 .return_once(move |_, _| Ok((Uuid::default(), rx)));
2581
2582 deltas_client
2584 .expect_unsubscribe()
2585 .return_once(|_| Ok(()));
2586
2587 let mut state_sync = ProtocolStateSynchronizer::new(
2588 ExtractorIdentity::new(Chain::Ethereum, "uniswap-v2"),
2589 true,
2590 ComponentFilter::with_tvl_range(remove_tvl_threshold, add_tvl_threshold),
2591 1,
2592 Duration::from_secs(0),
2593 true,
2594 true,
2595 true,
2596 ArcRPCClient(Arc::new(rpc_client)),
2597 ArcDeltasClient(Arc::new(deltas_client)),
2598 10_u64,
2599 );
2600 state_sync
2601 .initialize()
2602 .await
2603 .expect("Init failed");
2604
2605 let deltas = [
2607 BlockAggregatedChanges {
2608 extractor: "uniswap-v2".to_string(),
2609 chain: Chain::Ethereum,
2610 block: Block {
2611 number: 1,
2612 hash: Bytes::from("0x01"),
2613 parent_hash: Bytes::from("0x00"),
2614 chain: Chain::Ethereum,
2615 ts: Default::default(),
2616 },
2617 revert: false,
2618 ..Default::default()
2619 },
2620 BlockAggregatedChanges {
2621 extractor: "uniswap-v2".to_string(),
2622 chain: Chain::Ethereum,
2623 block: Block {
2624 number: 2,
2625 hash: Bytes::from("0x02"),
2626 parent_hash: Bytes::from("0x01"),
2627 chain: Chain::Ethereum,
2628 ts: Default::default(),
2629 },
2630 revert: false,
2631 component_tvl: [
2632 ("Component1".to_string(), 6.0), ("Component2".to_string(), 2.0), ("Component3".to_string(), 10.0), ]
2636 .into_iter()
2637 .collect(),
2638 ..Default::default()
2639 },
2640 BlockAggregatedChanges {
2642 extractor: "uniswap-v2".to_string(),
2643 chain: Chain::Ethereum,
2644 block: Block {
2645 number: 3,
2646 hash: Bytes::from("0x03"),
2647 parent_hash: Bytes::from("0x02"),
2648 chain: Chain::Ethereum,
2649 ts: Default::default(),
2650 },
2651 revert: false,
2652 ..Default::default()
2653 },
2654 ];
2655
2656 let (handle, mut rx) = state_sync.start().await;
2657 let (jh, close_tx) = handle.split();
2658
2659 tx.send(deltas[0].clone())
2661 .await
2662 .expect("deltas channel msg 0 closed!");
2663
2664 let _ = timeout(Duration::from_millis(200), rx.recv())
2666 .await
2667 .expect("waiting for first state msg timed out!")
2668 .expect("state sync block sender closed!");
2669
2670 tx.send(deltas[1].clone())
2673 .await
2674 .expect("deltas channel msg 1 closed!");
2675 let second_msg = timeout(Duration::from_millis(200), rx.recv())
2676 .await
2677 .expect("waiting for second state msg timed out!")
2678 .expect("state sync block sender closed!")
2679 .expect("no error");
2680
2681 bg_done.notified().await;
2683
2684 tx.send(deltas[2].clone())
2685 .await
2686 .expect("deltas channel msg 2 closed!");
2687 let third_msg = timeout(Duration::from_millis(200), rx.recv())
2688 .await
2689 .expect("waiting for third state msg timed out!")
2690 .expect("state sync block sender closed!")
2691 .expect("no error");
2692
2693 let _ = close_tx.send(());
2694 jh.await
2695 .expect("state sync task panicked!");
2696
2697 let expected_second_msg = StateSyncMessage {
2699 header: BlockHeader {
2700 number: 2,
2701 hash: Bytes::from("0x02"),
2702 parent_hash: Bytes::from("0x01"),
2703 revert: false,
2704 ..Default::default()
2705 },
2706 snapshots: Snapshot::default(),
2707 deltas: Some(BlockAggregatedChanges {
2708 extractor: "uniswap-v2".to_string(),
2709 chain: Chain::Ethereum,
2710 block: Block {
2711 number: 2,
2712 hash: Bytes::from("0x02"),
2713 parent_hash: Bytes::from("0x01"),
2714 chain: Chain::Ethereum,
2715 ts: Default::default(),
2716 },
2717 revert: false,
2718 component_tvl: [
2719 ("Component1".to_string(), 6.0), ]
2723 .into_iter()
2724 .collect(),
2725 ..Default::default()
2726 }),
2727 removed_components: [(
2728 "Component2".to_string(),
2729 ProtocolComponent { id: "Component2".to_string(), ..Default::default() },
2730 )]
2731 .into_iter()
2732 .collect(),
2733 };
2734
2735 let expected_third_msg = StateSyncMessage {
2737 header: BlockHeader {
2738 number: 3,
2739 hash: Bytes::from("0x03"),
2740 parent_hash: Bytes::from("0x02"),
2741 revert: false,
2742 ..Default::default()
2743 },
2744 snapshots: Snapshot {
2745 states: [(
2746 "Component3".to_string(),
2747 ComponentWithState {
2748 state: ProtocolComponentState::new(
2749 "Component3",
2750 Default::default(),
2751 Default::default(),
2752 ),
2753 component: ProtocolComponent {
2754 id: "Component3".to_string(),
2755 ..Default::default()
2756 },
2757 component_tvl: Some(10.0),
2758 entrypoints: vec![],
2759 },
2760 )]
2761 .into_iter()
2762 .collect(),
2763 vm_storage: HashMap::new(),
2764 },
2765 deltas: Some(deltas[2].clone()),
2766 removed_components: Default::default(),
2767 };
2768
2769 assert_eq!(second_msg, expected_second_msg);
2770 assert_eq!(third_msg, expected_third_msg);
2771 }
2772
2773 #[test_log::test(tokio::test)]
2774 async fn test_public_close_api_functionality() {
2775 let mut rpc_client = make_mock_client();
2782 let mut deltas_client = MockDeltasClient::new();
2783
2784 rpc_client
2786 .expect_get_protocol_components()
2787 .returning(|_| Ok(Page::new(vec![], 0, 0, 0)));
2788
2789 let (_tx, rx) = channel(1);
2791 deltas_client
2792 .expect_subscribe()
2793 .return_once(move |_, _| Ok((Uuid::default(), rx)));
2794
2795 deltas_client
2797 .expect_unsubscribe()
2798 .return_once(|_| Ok(()));
2799
2800 let mut state_sync = ProtocolStateSynchronizer::new(
2801 ExtractorIdentity::new(Chain::Ethereum, "test-protocol"),
2802 true,
2803 ComponentFilter::with_tvl_range(0.0, 0.0),
2804 5, Duration::from_secs(0),
2806 true,
2807 false,
2808 true,
2809 ArcRPCClient(Arc::new(rpc_client)),
2810 ArcDeltasClient(Arc::new(deltas_client)),
2811 10000_u64, );
2813
2814 state_sync
2815 .initialize()
2816 .await
2817 .expect("Init should succeed");
2818
2819 let (handle, _rx) = state_sync.start().await;
2821 let (jh, close_tx) = handle.split();
2822
2823 tokio::time::sleep(Duration::from_millis(100)).await;
2825
2826 close_tx
2828 .send(())
2829 .expect("Should be able to send close signal");
2830 jh.await.expect("Task should not panic");
2832 }
2833
2834 #[test_log::test(tokio::test)]
2835 async fn test_cleanup_runs_when_state_sync_processing_errors() {
2836 let mut rpc_client = make_mock_client();
2841 let mut deltas_client = MockDeltasClient::new();
2842
2843 rpc_client
2845 .expect_get_protocol_components()
2846 .returning(|_| Ok(Page::new(vec![], 0, 0, 0)));
2847
2848 rpc_client
2850 .expect_get_protocol_states()
2851 .returning(|_| {
2852 Err(RPCError::ParseResponse("Test error during snapshot retrieval".to_string()))
2853 });
2854
2855 let (tx, rx) = channel(10);
2857 deltas_client
2858 .expect_subscribe()
2859 .return_once(move |_, _| {
2860 let delta = BlockAggregatedChanges {
2862 extractor: "test".to_string(),
2863 chain: Chain::Ethereum,
2864 block: Block {
2865 hash: Bytes::from("0x0123"),
2866 number: 1,
2867 parent_hash: Bytes::from("0x0000"),
2868 chain: Chain::Ethereum,
2869 ts: chrono::DateTime::from_timestamp(1234567890, 0)
2870 .unwrap()
2871 .naive_utc(),
2872 },
2873 revert: false,
2874 new_protocol_components: [(
2876 "new_component".to_string(),
2877 ProtocolComponent { id: "new_component".to_string(), ..Default::default() },
2878 )]
2879 .into_iter()
2880 .collect(),
2881 component_tvl: [("new_component".to_string(), 100.0)]
2882 .into_iter()
2883 .collect(),
2884 ..Default::default()
2885 };
2886
2887 tokio::spawn(async move {
2888 let _ = tx.send(delta).await;
2889 });
2891
2892 Ok((Uuid::default(), rx))
2893 });
2894
2895 deltas_client
2897 .expect_unsubscribe()
2898 .return_once(|_| Ok(()));
2899
2900 let mut state_sync = ProtocolStateSynchronizer::new(
2901 ExtractorIdentity::new(Chain::Ethereum, "test-protocol"),
2902 true,
2903 ComponentFilter::with_tvl_range(0.0, 1000.0), 1,
2905 Duration::from_secs(0),
2906 true,
2907 false,
2908 true,
2909 ArcRPCClient(Arc::new(rpc_client)),
2910 ArcDeltasClient(Arc::new(deltas_client)),
2911 5000_u64,
2912 );
2913
2914 state_sync
2915 .initialize()
2916 .await
2917 .expect("Init should succeed");
2918
2919 state_sync.last_synced_block = Some(BlockHeader {
2921 hash: Bytes::from("0x0badc0ffee"),
2922 number: 42,
2923 parent_hash: Bytes::from("0xbadbeef0"),
2924 revert: false,
2925 timestamp: 123456789,
2926 partial_block_index: None,
2927 });
2928
2929 let (mut block_tx, _block_rx) = channel(10);
2931
2932 let (_end_tx, end_rx) = oneshot::channel::<()>();
2934 let result = state_sync
2935 .state_sync(&mut block_tx, end_rx, Duration::ZERO)
2936 .await;
2937 assert!(result.is_err(), "state_sync should have errored during processing");
2939
2940 }
2943
2944 #[test_log::test(tokio::test)]
2945 async fn test_close_signal_while_waiting_for_first_deltas() {
2946 let mut rpc_client = make_mock_client();
2950 let mut deltas_client = MockDeltasClient::new();
2951
2952 rpc_client
2953 .expect_get_protocol_components()
2954 .returning(|_| Ok(Page::new(vec![], 0, 0, 0)));
2955
2956 let (_tx, rx) = channel(1);
2957 deltas_client
2958 .expect_subscribe()
2959 .return_once(move |_, _| Ok((Uuid::default(), rx)));
2960
2961 deltas_client
2962 .expect_unsubscribe()
2963 .return_once(|_| Ok(()));
2964
2965 let mut state_sync = ProtocolStateSynchronizer::new(
2966 ExtractorIdentity::new(Chain::Ethereum, "test-protocol"),
2967 true,
2968 ComponentFilter::with_tvl_range(0.0, 0.0),
2969 1,
2970 Duration::from_secs(0),
2971 true,
2972 true,
2973 false,
2974 ArcRPCClient(Arc::new(rpc_client)),
2975 ArcDeltasClient(Arc::new(deltas_client)),
2976 10000_u64,
2977 );
2978
2979 state_sync
2980 .initialize()
2981 .await
2982 .expect("Init should succeed");
2983
2984 let (mut block_tx, _block_rx) = channel(10);
2985 let (end_tx, end_rx) = oneshot::channel::<()>();
2986
2987 let state_sync_handle = tokio::spawn(async move {
2989 state_sync
2990 .state_sync(&mut block_tx, end_rx, Duration::ZERO)
2991 .await
2992 });
2993
2994 tokio::time::sleep(Duration::from_millis(100)).await;
2996
2997 let _ = end_tx.send(());
2999
3000 let result = state_sync_handle
3002 .await
3003 .expect("Task should not panic");
3004 assert!(result.is_ok(), "state_sync should exit cleanly when closed: {result:?}");
3005
3006 println!("SUCCESS: Close signal handled correctly while waiting for first deltas");
3007 }
3008
3009 #[test_log::test(tokio::test)]
3010 async fn test_close_signal_during_main_processing_loop() {
3011 let mut rpc_client = make_mock_client();
3017 let mut deltas_client = MockDeltasClient::new();
3018
3019 rpc_client
3021 .expect_get_protocol_components()
3022 .returning(|_| Ok(Page::new(vec![], 0, 0, 0)));
3023
3024 rpc_client
3026 .expect_get_protocol_states()
3027 .returning(|_| Ok(Page::new(vec![], 0, 0, 0)));
3028
3029 rpc_client
3030 .expect_get_component_tvl()
3031 .returning(|_| Ok(Page::new(HashMap::new(), 0, 0, 0)));
3032
3033 rpc_client
3034 .expect_get_traced_entry_points()
3035 .returning(|_| Ok(Page::new(HashMap::new(), 0, 0, 0)));
3036
3037 let (tx, rx) = channel(10);
3039 deltas_client
3040 .expect_subscribe()
3041 .return_once(move |_, _| {
3042 let first_delta = BlockAggregatedChanges {
3044 extractor: "test".to_string(),
3045 chain: Chain::Ethereum,
3046 block: Block {
3047 hash: Bytes::from("0x0123"),
3048 number: 1,
3049 parent_hash: Bytes::from("0x0000"),
3050 chain: Chain::Ethereum,
3051 ts: chrono::DateTime::from_timestamp(1234567890, 0)
3052 .unwrap()
3053 .naive_utc(),
3054 },
3055 revert: false,
3056 ..Default::default()
3057 };
3058
3059 tokio::spawn(async move {
3060 let _ = tx.send(first_delta).await;
3061 tokio::time::sleep(Duration::from_secs(30)).await;
3064 });
3065
3066 Ok((Uuid::default(), rx))
3067 });
3068
3069 deltas_client
3070 .expect_unsubscribe()
3071 .return_once(|_| Ok(()));
3072
3073 let mut state_sync = ProtocolStateSynchronizer::new(
3074 ExtractorIdentity::new(Chain::Ethereum, "test-protocol"),
3075 true,
3076 ComponentFilter::with_tvl_range(0.0, 1000.0),
3077 1,
3078 Duration::from_secs(0),
3079 true,
3080 false,
3081 true,
3082 ArcRPCClient(Arc::new(rpc_client)),
3083 ArcDeltasClient(Arc::new(deltas_client)),
3084 10000_u64,
3085 );
3086
3087 state_sync
3088 .initialize()
3089 .await
3090 .expect("Init should succeed");
3091
3092 let (mut block_tx, mut block_rx) = channel(10);
3093 let (end_tx, end_rx) = oneshot::channel::<()>();
3094
3095 let state_sync_handle = tokio::spawn(async move {
3097 state_sync
3098 .state_sync(&mut block_tx, end_rx, Duration::ZERO)
3099 .await
3100 });
3101
3102 let first_snapshot = block_rx
3104 .recv()
3105 .await
3106 .expect("Should receive first snapshot")
3107 .expect("Synchronizer error");
3108 assert!(
3109 !first_snapshot
3110 .snapshots
3111 .states
3112 .is_empty() ||
3113 first_snapshot.deltas.is_some()
3114 );
3115 let _ = end_tx.send(());
3117
3118 let result = state_sync_handle
3120 .await
3121 .expect("Task should not panic");
3122 assert!(
3123 result.is_ok(),
3124 "state_sync should exit cleanly when closed after first message: {result:?}"
3125 );
3126 }
3127
3128 #[test_log::test(tokio::test)]
3129 async fn test_max_retries_exceeded_error_propagation() {
3130 let mut rpc_client = make_mock_client();
3134 let mut deltas_client = MockDeltasClient::new();
3135
3136 rpc_client
3138 .expect_get_protocol_components()
3139 .returning(|_| Ok(Page::new(vec![], 0, 0, 0)));
3140
3141 deltas_client
3144 .expect_subscribe()
3145 .returning(|_, _| {
3146 Err(DeltasError::NotConnected)
3148 });
3149
3150 deltas_client
3152 .expect_unsubscribe()
3153 .returning(|_| Ok(()))
3154 .times(0..=5);
3155
3156 let mut state_sync = ProtocolStateSynchronizer::new(
3158 ExtractorIdentity::new(Chain::Ethereum, "test-protocol"),
3159 true,
3160 ComponentFilter::with_tvl_range(0.0, 1000.0),
3161 2, Duration::from_millis(10), true,
3164 false,
3165 true,
3166 ArcRPCClient(Arc::new(rpc_client)),
3167 ArcDeltasClient(Arc::new(deltas_client)),
3168 1000_u64,
3169 );
3170
3171 state_sync
3172 .initialize()
3173 .await
3174 .expect("Init should succeed");
3175
3176 let (handle, mut rx) = state_sync.start().await;
3178 let (jh, _close_tx) = handle.split();
3179
3180 let res = tokio::time::timeout(Duration::from_millis(100), rx.recv())
3181 .await
3182 .expect("responsds in time")
3183 .expect("channel open");
3184
3185 if let Err(err) = res {
3187 assert!(
3188 matches!(err, SynchronizerError::ConnectionClosed),
3189 "Expected ConnectionClosed error, got: {:?}",
3190 err
3191 );
3192 } else {
3193 panic!("Expected an error")
3194 }
3195
3196 let task_result = tokio::time::timeout(Duration::from_secs(2), jh).await;
3198 assert!(task_result.is_ok(), "Synchronizer task should complete after max retries");
3199 }
3200
3201 #[test_log::test(tokio::test)]
3202 async fn test_restart_after_healthy_run_skips_cooldown() {
3203 let mut rpc_client = make_mock_client();
3204 rpc_client
3205 .expect_get_protocol_components()
3206 .returning(|_| Ok(Page::new(vec![], 0, 0, 0)));
3207
3208 let mut deltas_client = MockDeltasClient::new();
3209 let (resubscribed_tx, resubscribed_rx) = oneshot::channel();
3210 let mut resubscribed_tx = Some(resubscribed_tx);
3211 let mut open_senders = Vec::new();
3212 let mut calls = 0;
3213 deltas_client
3214 .expect_subscribe()
3215 .returning(move |_, _| {
3216 calls += 1;
3217 let (tx, rx) = channel(10);
3218 if calls == 1 {
3219 let block = Block { number: 1, ..Default::default() };
3221 tx.try_send(BlockAggregatedChanges { block, ..Default::default() })
3222 .unwrap();
3223 } else {
3224 if let Some(notify) = resubscribed_tx.take() {
3225 let _ = notify.send(std::time::Instant::now());
3226 }
3227 open_senders.push(tx);
3228 }
3229 Ok((Uuid::default(), rx))
3230 });
3231 deltas_client
3232 .expect_unsubscribe()
3233 .returning(|_| Ok(()));
3234
3235 let cooldown = Duration::from_secs(5);
3236 let mut state_sync = ProtocolStateSynchronizer::new(
3237 ExtractorIdentity::new(Chain::Ethereum, "test-protocol"),
3238 true,
3239 ComponentFilter::with_tvl_range(0.0, 1000.0),
3240 3,
3241 cooldown,
3242 false,
3243 false,
3244 true,
3245 ArcRPCClient(Arc::new(rpc_client)),
3246 ArcDeltasClient(Arc::new(deltas_client)),
3247 10_u64,
3248 );
3249 state_sync
3250 .initialize()
3251 .await
3252 .expect("Init should succeed");
3253
3254 let started = std::time::Instant::now();
3255 let (handle, _rx) = state_sync.start().await;
3256 let resubscribed_at = timeout(cooldown * 2, resubscribed_rx)
3257 .await
3258 .expect("synchronizer should resubscribe")
3259 .unwrap();
3260 let (_jh, close_tx) = handle.split();
3261 let _ = close_tx.send(());
3262
3263 assert!(resubscribed_at - started < Duration::from_secs(1));
3264 }
3265
3266 #[test_log::test(tokio::test)]
3267 async fn test_is_next_expected() {
3268 let mut state_sync = with_mocked_clients(true, false, None, None);
3272
3273 let incoming_header = BlockHeader {
3275 number: 100,
3276 hash: Bytes::from("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
3277 parent_hash: Bytes::from(
3278 "0x0000000000000000000000000000000000000000000000000000000000000000",
3279 ),
3280 revert: false,
3281 timestamp: 123456789,
3282 partial_block_index: None,
3283 };
3284 assert!(
3285 !state_sync.is_next_expected(&incoming_header),
3286 "Should return false when no previous block is set"
3287 );
3288
3289 let previous_header = BlockHeader {
3291 number: 99,
3292 hash: Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000000"),
3293 parent_hash: Bytes::from(
3294 "0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
3295 ),
3296 revert: false,
3297 timestamp: 123456788,
3298 partial_block_index: None,
3299 };
3300 state_sync.last_synced_block = Some(previous_header.clone());
3301
3302 assert!(
3303 state_sync.is_next_expected(&incoming_header),
3304 "Should return true when incoming parent_hash matches previous hash"
3305 );
3306
3307 let non_matching_header = BlockHeader {
3309 number: 100,
3310 hash: Bytes::from("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"),
3311 parent_hash: Bytes::from(
3312 "0x1111111111111111111111111111111111111111111111111111111111111111",
3313 ), revert: false,
3315 timestamp: 123456789,
3316 partial_block_index: None,
3317 };
3318 assert!(
3319 !state_sync.is_next_expected(&non_matching_header),
3320 "Should return false when incoming parent_hash doesn't match previous hash"
3321 );
3322 }
3323
3324 #[test_log::test(tokio::test)]
3325 async fn test_synchronizer_restart_skip_snapshot_on_expected_block() {
3326 let mut rpc_client = make_mock_client();
3330 let mut deltas_client = MockDeltasClient::new();
3331
3332 rpc_client
3334 .expect_get_protocol_components()
3335 .returning(|_| {
3336 Ok(Page::new(
3337 vec![ProtocolComponent { id: "Component1".to_string(), ..Default::default() }],
3338 1,
3339 0,
3340 100,
3341 ))
3342 });
3343
3344 let (tx, rx) = channel(10);
3346 deltas_client
3347 .expect_subscribe()
3348 .return_once(move |_, _| {
3349 let expected_next_delta = BlockAggregatedChanges {
3350 extractor: "uniswap-v2".to_string(),
3351 chain: Chain::Ethereum,
3352 block: Block {
3353 hash: Bytes::from(
3354 "0x0000000000000000000000000000000000000000000000000000000000000002",
3355 ), number: 2,
3357 parent_hash: Bytes::from(
3358 "0x0000000000000000000000000000000000000000000000000000000000000001",
3359 ), chain: Chain::Ethereum,
3361 ts: chrono::DateTime::from_timestamp(1234567890, 0)
3362 .unwrap()
3363 .naive_utc(),
3364 },
3365 revert: false,
3366 ..Default::default()
3367 };
3368
3369 tokio::spawn(async move {
3370 let _ = tx.send(expected_next_delta).await;
3371 });
3372
3373 Ok((Uuid::default(), rx))
3374 });
3375
3376 deltas_client
3377 .expect_unsubscribe()
3378 .return_once(|_| Ok(()));
3379
3380 let mut state_sync = ProtocolStateSynchronizer::new(
3381 ExtractorIdentity::new(Chain::Ethereum, "uniswap-v2"),
3382 true,
3383 ComponentFilter::with_tvl_range(0.0, 1000.0),
3384 1,
3385 Duration::from_secs(0),
3386 true, false,
3388 true,
3389 ArcRPCClient(Arc::new(rpc_client)),
3390 ArcDeltasClient(Arc::new(deltas_client)),
3391 10000_u64,
3392 );
3393
3394 state_sync
3396 .initialize()
3397 .await
3398 .expect("Init should succeed");
3399
3400 state_sync.last_synced_block = Some(BlockHeader {
3402 number: 1,
3403 hash: Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000001"), parent_hash: Bytes::from(
3405 "0x0000000000000000000000000000000000000000000000000000000000000000",
3406 ),
3407 revert: false,
3408 timestamp: 123456789,
3409 partial_block_index: None,
3410 });
3411
3412 let (mut block_tx, mut block_rx) = channel(10);
3413 let (end_tx, end_rx) = oneshot::channel::<()>();
3414
3415 let state_sync_handle = tokio::spawn(async move {
3417 state_sync
3418 .state_sync(&mut block_tx, end_rx, Duration::ZERO)
3419 .await
3420 });
3421
3422 let result_msg = timeout(Duration::from_millis(200), block_rx.recv())
3424 .await
3425 .expect("Should receive message within timeout")
3426 .expect("Channel should be open")
3427 .expect("Should not be an error");
3428
3429 let _ = end_tx.send(());
3431
3432 let _ = state_sync_handle
3434 .await
3435 .expect("Task should not panic");
3436
3437 assert!(result_msg.deltas.is_some(), "Should contain deltas");
3440 assert!(
3441 result_msg.snapshots.states.is_empty(),
3442 "Should not contain snapshots when next expected block is received"
3443 );
3444
3445 if let Some(deltas) = &result_msg.deltas {
3447 assert_eq!(deltas.block.number, 2);
3448 assert_eq!(
3449 deltas.block.hash,
3450 Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000002")
3451 );
3452 assert_eq!(
3453 deltas.block.parent_hash,
3454 Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000001")
3455 );
3456 }
3457 }
3458
3459 #[test_log::test(tokio::test)]
3460 async fn test_skip_previously_processed_messages() {
3461 let mut rpc_client = make_mock_client();
3465 let mut deltas_client = MockDeltasClient::new();
3466
3467 rpc_client
3469 .expect_get_protocol_components()
3470 .returning(|_| {
3471 Ok(Page::new(
3472 vec![ProtocolComponent { id: "Component1".to_string(), ..Default::default() }],
3473 1,
3474 0,
3475 100,
3476 ))
3477 });
3478
3479 rpc_client
3481 .expect_get_protocol_states()
3482 .returning(|_| {
3483 Ok(Page::new(
3484 vec![ProtocolComponentState::new(
3485 "Component1",
3486 Default::default(),
3487 Default::default(),
3488 )],
3489 1,
3490 0,
3491 100,
3492 ))
3493 });
3494
3495 rpc_client
3496 .expect_get_component_tvl()
3497 .returning(|_| {
3498 Ok(Page::new(
3499 [("Component1".to_string(), 100.0)]
3500 .into_iter()
3501 .collect(),
3502 1,
3503 0,
3504 100,
3505 ))
3506 });
3507
3508 rpc_client
3509 .expect_get_traced_entry_points()
3510 .returning(|_| Ok(Page::new(HashMap::new(), 0, 0, 0)));
3511
3512 let (tx, rx) = channel(10);
3514 deltas_client
3515 .expect_subscribe()
3516 .return_once(move |_, _| {
3517 let old_messages = vec![
3519 BlockAggregatedChanges {
3520 extractor: "uniswap-v2".to_string(),
3521 chain: Chain::Ethereum,
3522 block: Block {
3523 hash: Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000003"),
3524 number: 3,
3525 parent_hash: Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000002"),
3526 chain: Chain::Ethereum,
3527 ts: chrono::DateTime::from_timestamp(1234567890, 0).unwrap().naive_utc(),
3528 },
3529 revert: false,
3530 ..Default::default()
3531 },
3532 BlockAggregatedChanges {
3533 extractor: "uniswap-v2".to_string(),
3534 chain: Chain::Ethereum,
3535 block: Block {
3536 hash: Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000004"),
3537 number: 4,
3538 parent_hash: Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000003"),
3539 chain: Chain::Ethereum,
3540 ts: chrono::DateTime::from_timestamp(1234567891, 0).unwrap().naive_utc(),
3541 },
3542 revert: false,
3543 ..Default::default()
3544 },
3545 BlockAggregatedChanges {
3546 extractor: "uniswap-v2".to_string(),
3547 chain: Chain::Ethereum,
3548 block: Block {
3549 hash: Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000005"),
3550 number: 5,
3551 parent_hash: Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000004"),
3552 chain: Chain::Ethereum,
3553 ts: chrono::DateTime::from_timestamp(1234567892, 0).unwrap().naive_utc(),
3554 },
3555 revert: false,
3556 ..Default::default()
3557 },
3558 BlockAggregatedChanges {
3560 extractor: "uniswap-v2".to_string(),
3561 chain: Chain::Ethereum,
3562 block: Block {
3563 hash: Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000006"),
3564 number: 6,
3565 parent_hash: Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000005"),
3566 chain: Chain::Ethereum,
3567 ts: chrono::DateTime::from_timestamp(1234567893, 0).unwrap().naive_utc(),
3568 },
3569 revert: false,
3570 ..Default::default()
3571 },
3572 ];
3573
3574 tokio::spawn(async move {
3575 for message in old_messages {
3576 let _ = tx.send(message).await;
3577 tokio::time::sleep(Duration::from_millis(10)).await;
3578 }
3579 });
3580
3581 Ok((Uuid::default(), rx))
3582 });
3583
3584 deltas_client
3585 .expect_unsubscribe()
3586 .return_once(|_| Ok(()));
3587
3588 let mut state_sync = ProtocolStateSynchronizer::new(
3589 ExtractorIdentity::new(Chain::Ethereum, "uniswap-v2"),
3590 true,
3591 ComponentFilter::with_tvl_range(0.0, 1000.0),
3592 1,
3593 Duration::from_secs(0),
3594 true,
3595 true,
3596 true,
3597 ArcRPCClient(Arc::new(rpc_client)),
3598 ArcDeltasClient(Arc::new(deltas_client)),
3599 10000_u64,
3600 );
3601
3602 state_sync
3604 .initialize()
3605 .await
3606 .expect("Init should succeed");
3607
3608 state_sync.last_synced_block = Some(BlockHeader {
3609 number: 5,
3610 hash: Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000005"),
3611 parent_hash: Bytes::from(
3612 "0x0000000000000000000000000000000000000000000000000000000000000004",
3613 ),
3614 revert: false,
3615 timestamp: 1234567892,
3616 partial_block_index: None,
3617 });
3618
3619 let (mut block_tx, mut block_rx) = channel(10);
3620 let (end_tx, end_rx) = oneshot::channel::<()>();
3621
3622 let state_sync_handle = tokio::spawn(async move {
3624 state_sync
3625 .state_sync(&mut block_tx, end_rx, Duration::ZERO)
3626 .await
3627 });
3628
3629 let result_msg = timeout(Duration::from_millis(500), block_rx.recv())
3631 .await
3632 .expect("Should receive message within timeout")
3633 .expect("Channel should be open")
3634 .expect("Should not be an error");
3635
3636 let _ = end_tx.send(());
3638
3639 let _ = state_sync_handle
3641 .await
3642 .expect("Task should not panic");
3643
3644 assert!(result_msg.deltas.is_some(), "Should contain deltas");
3646 if let Some(deltas) = &result_msg.deltas {
3647 assert_eq!(
3648 deltas.block.number, 6,
3649 "Should only process block 6, skipping earlier blocks"
3650 );
3651 assert_eq!(
3652 deltas.block.hash,
3653 Bytes::from("0x0000000000000000000000000000000000000000000000000000000000000006")
3654 );
3655 }
3656
3657 match timeout(Duration::from_millis(50), block_rx.recv()).await {
3660 Err(_) => {
3661 }
3663 Ok(Some(Err(_))) => {
3664 }
3666 Ok(Some(Ok(_))) => {
3667 panic!("Should not receive additional messages - old blocks should be skipped");
3668 }
3669 Ok(None) => {
3670 }
3672 }
3673 }
3674
3675 fn header_of(changes: &BlockAggregatedChanges) -> BlockHeader {
3676 changes.into()
3677 }
3678
3679 fn mock_rpc_recording_snapshot_blocks() -> (MockRPCClient, Arc<std::sync::Mutex<Vec<u64>>>) {
3681 let mut rpc_client = make_mock_client();
3682 rpc_client
3683 .expect_get_protocol_components()
3684 .returning(|_| {
3685 Ok(Page::new(
3686 vec![ProtocolComponent { id: "Component1".to_string(), ..Default::default() }],
3687 1,
3688 0,
3689 100,
3690 ))
3691 });
3692 let snapshot_blocks = Arc::new(std::sync::Mutex::new(Vec::new()));
3693 let recorded = snapshot_blocks.clone();
3694 rpc_client
3695 .expect_get_snapshots()
3696 .returning(move |request, _chunk_size, _concurrency| {
3697 recorded
3698 .lock()
3699 .unwrap()
3700 .push(request.block_number);
3701 Ok(Snapshot::default())
3702 });
3703 (rpc_client, snapshot_blocks)
3704 }
3705
3706 fn mock_deltas_emitting(blocks: Vec<u64>) -> MockDeltasClient {
3708 let mut deltas_client = MockDeltasClient::new();
3709 let (tx, rx) = channel(128);
3710 deltas_client
3711 .expect_subscribe()
3712 .return_once(move |_, _| {
3713 tokio::spawn(async move {
3714 for block in blocks {
3715 let _ = tx
3716 .send(make_block_changes(block, None))
3717 .await;
3718 sleep(Duration::from_millis(50)).await;
3719 }
3720 });
3721 Ok((Uuid::default(), rx))
3722 });
3723 deltas_client
3724 .expect_unsubscribe()
3725 .return_once(|_| Ok(()));
3726 deltas_client
3727 }
3728
3729 #[test_log::test(tokio::test(start_paused = true))]
3730 async fn test_snapshot_delay_discards_deltas_until_it_ends() {
3731 let (rpc_client, snapshot_blocks) = mock_rpc_recording_snapshot_blocks();
3732 let deltas_client = mock_deltas_emitting((10..=20).collect());
3733 let mut state_sync =
3734 with_mocked_clients(true, false, Some(rpc_client), Some(deltas_client));
3735 state_sync
3736 .initialize()
3737 .await
3738 .expect("Init should succeed");
3739 state_sync.last_synced_block = Some(header_of(&make_block_changes(5, None)));
3740
3741 let (mut block_tx, mut block_rx) = channel(10);
3742 let (end_tx, end_rx) = oneshot::channel::<()>();
3743 let handle = tokio::spawn(async move {
3744 state_sync
3745 .state_sync(&mut block_tx, end_rx, Duration::from_millis(300))
3746 .await
3747 });
3748
3749 let first = timeout(Duration::from_secs(1), block_rx.recv())
3750 .await
3751 .expect("Should receive first message")
3752 .expect("Channel should be open")
3753 .expect("Should not be an error");
3754 let second = timeout(Duration::from_secs(1), block_rx.recv())
3755 .await
3756 .expect("Should receive second message")
3757 .expect("Channel should be open")
3758 .expect("Should not be an error");
3759 let _ = end_tx.send(());
3760 let _ = handle
3761 .await
3762 .expect("Task should not panic");
3763
3764 let snapshot_blocks = snapshot_blocks.lock().unwrap().clone();
3766 assert_eq!(snapshot_blocks.len(), 1, "Should request one snapshot");
3767 assert!(
3768 snapshot_blocks[0] >= 16,
3769 "Snapshot at block {} is inside the delay",
3770 snapshot_blocks[0]
3771 );
3772 assert_eq!(first.header.number, snapshot_blocks[0]);
3773 assert_eq!(second.header.number, snapshot_blocks[0] + 1);
3774 }
3775
3776 #[test_log::test(tokio::test(start_paused = true))]
3777 async fn test_snapshot_delay_skipped_when_next_block_expected() {
3778 let mut rpc_client = make_mock_client();
3779 rpc_client
3780 .expect_get_protocol_components()
3781 .returning(|_| {
3782 Ok(Page::new(
3783 vec![ProtocolComponent { id: "Component1".to_string(), ..Default::default() }],
3784 1,
3785 0,
3786 100,
3787 ))
3788 });
3789 rpc_client
3790 .expect_get_snapshots()
3791 .never();
3792 let deltas_client = mock_deltas_emitting(vec![10, 11]);
3793 let mut state_sync =
3794 with_mocked_clients(true, false, Some(rpc_client), Some(deltas_client));
3795 state_sync
3796 .initialize()
3797 .await
3798 .expect("Init should succeed");
3799 state_sync.last_synced_block = Some(header_of(&make_block_changes(9, None)));
3800
3801 let (mut block_tx, mut block_rx) = channel(10);
3802 let (end_tx, end_rx) = oneshot::channel::<()>();
3803 let handle = tokio::spawn(async move {
3804 state_sync
3805 .state_sync(&mut block_tx, end_rx, Duration::from_millis(300))
3806 .await
3807 });
3808
3809 let msg = timeout(Duration::from_millis(10), block_rx.recv())
3810 .await
3811 .expect("Should receive the next expected block at once")
3812 .expect("Channel should be open")
3813 .expect("Should not be an error");
3814 let _ = end_tx.send(());
3815 let _ = handle
3816 .await
3817 .expect("Task should not panic");
3818
3819 assert_eq!(msg.header.number, 10);
3820 assert!(msg.snapshots.states.is_empty());
3821 }
3822
3823 #[test_log::test(tokio::test(start_paused = true))]
3824 async fn test_first_start_has_no_snapshot_delay() {
3825 let (rpc_client, snapshot_blocks) = mock_rpc_recording_snapshot_blocks();
3826 let deltas_client = mock_deltas_emitting(vec![1]);
3827 let mut state_sync =
3828 with_mocked_clients(true, false, Some(rpc_client), Some(deltas_client));
3829 assert_eq!(state_sync.snapshot_jitter, DEFAULT_SNAPSHOT_JITTER);
3830 state_sync
3831 .initialize()
3832 .await
3833 .expect("Init should succeed");
3834
3835 let (handle, mut block_rx) = state_sync.start().await;
3836 let (jh, close_tx) = handle.split();
3837
3838 let msg = timeout(Duration::from_millis(10), block_rx.recv())
3839 .await
3840 .expect("Should receive the first block at once")
3841 .expect("Channel should be open")
3842 .expect("Should not be an error");
3843 let _ = close_tx.send(());
3844 jh.await.expect("Task should not panic");
3845
3846 assert_eq!(msg.header.number, 1);
3847 assert_eq!(*snapshot_blocks.lock().unwrap(), vec![1]);
3848 }
3849
3850 #[test]
3851 fn test_snapshot_jitter_has_a_minimum() {
3852 let state_sync = with_mocked_clients(true, false, None, None);
3853 let state_sync = state_sync.with_snapshot_jitter(Duration::ZERO);
3854 assert_eq!(state_sync.snapshot_jitter, MIN_SNAPSHOT_JITTER);
3855
3856 let state_sync = state_sync.with_snapshot_jitter(Duration::from_secs(10));
3857 assert_eq!(state_sync.snapshot_jitter, Duration::from_secs(10));
3858 }
3859
3860 #[test_log::test(tokio::test(start_paused = true))]
3861 async fn test_retry_before_first_block_has_no_snapshot_delay() {
3862 let (rpc_client, snapshot_blocks) = mock_rpc_recording_snapshot_blocks();
3863 let mut deltas_client = MockDeltasClient::new();
3864 deltas_client
3865 .expect_subscribe()
3866 .times(1)
3867 .returning(|_, _| Err(DeltasError::ServerError("unavailable".to_string())));
3868 let (tx, rx) = channel(128);
3869 deltas_client
3870 .expect_subscribe()
3871 .return_once(move |_, _| {
3872 tokio::spawn(async move {
3873 for block in 1..=5 {
3874 let _ = tx
3875 .send(make_block_changes(block, None))
3876 .await;
3877 sleep(Duration::from_millis(50)).await;
3878 }
3879 });
3880 Ok((Uuid::default(), rx))
3881 });
3882 deltas_client
3883 .expect_unsubscribe()
3884 .return_once(|_| Ok(()));
3885 let mut state_sync =
3886 with_mocked_clients(true, false, Some(rpc_client), Some(deltas_client));
3887 state_sync.max_retries = 2;
3888 state_sync
3889 .initialize()
3890 .await
3891 .expect("Init should succeed");
3892
3893 let (handle, mut block_rx) = state_sync.start().await;
3894 let (jh, close_tx) = handle.split();
3895
3896 let msg = timeout(Duration::from_secs(1), block_rx.recv())
3897 .await
3898 .expect("Should receive the first block")
3899 .expect("Channel should be open")
3900 .expect("Should not be an error");
3901 let _ = close_tx.send(());
3902 jh.await.expect("Task should not panic");
3903
3904 assert_eq!(msg.header.number, 1);
3905 assert_eq!(*snapshot_blocks.lock().unwrap(), vec![1]);
3906 }
3907
3908 async fn first_block_after_restart(
3910 mut state_sync: ProtocolStateSynchronizer<
3911 ArcRPCClient<MockRPCClient>,
3912 ArcDeltasClient<MockDeltasClient>,
3913 >,
3914 ) -> u64 {
3915 state_sync
3916 .initialize()
3917 .await
3918 .expect("Init should succeed");
3919 state_sync.last_synced_block = Some(header_of(&make_block_changes(5, None)));
3920
3921 let (handle, mut block_rx) = state_sync.start().await;
3922 let (jh, close_tx) = handle.split();
3923 let msg = timeout(Duration::from_millis(10), block_rx.recv())
3924 .await
3925 .expect("Should receive the first block at once")
3926 .expect("Channel should be open")
3927 .expect("Should not be an error");
3928 let _ = close_tx.send(());
3929 jh.await.expect("Task should not panic");
3930 msg.header.number
3931 }
3932
3933 #[test_log::test(tokio::test(start_paused = true))]
3934 async fn test_no_state_restart_has_no_snapshot_delay() {
3935 let mut rpc_client = make_mock_client();
3936 rpc_client
3937 .expect_get_protocol_components()
3938 .returning(|_| {
3939 Ok(Page::new(
3940 vec![ProtocolComponent { id: "Component1".to_string(), ..Default::default() }],
3941 1,
3942 0,
3943 100,
3944 ))
3945 });
3946 rpc_client
3947 .expect_get_snapshots()
3948 .never();
3949 let deltas_client = mock_deltas_emitting((10..=14).collect());
3950 let mut state_sync =
3951 with_mocked_clients(true, false, Some(rpc_client), Some(deltas_client));
3952 state_sync.include_snapshots = false;
3953
3954 assert_eq!(first_block_after_restart(state_sync).await, 10);
3955 }
3956
3957 #[test_log::test(tokio::test(start_paused = true))]
3958 async fn test_restart_without_components_has_no_snapshot_delay() {
3959 let mut rpc_client = make_mock_client();
3960 rpc_client
3961 .expect_get_protocol_components()
3962 .returning(|_| Ok(Page::new(vec![], 0, 0, 100)));
3963 rpc_client
3964 .expect_get_snapshots()
3965 .never();
3966 let deltas_client = mock_deltas_emitting((10..=14).collect());
3967 let state_sync = with_mocked_clients(true, false, Some(rpc_client), Some(deltas_client));
3968
3969 assert_eq!(first_block_after_restart(state_sync).await, 10);
3970 }
3971
3972 fn make_block_changes(block_num: u64, partial_idx: Option<u32>) -> BlockAggregatedChanges {
3973 let hash = Bytes::from(vec![block_num as u8; 32]);
3975 let parent_hash = Bytes::from(vec![block_num.saturating_sub(1) as u8; 32]);
3976 BlockAggregatedChanges {
3977 extractor: "uniswap-v2".to_string(),
3978 chain: Chain::Ethereum,
3979 block: Block {
3980 number: block_num,
3981 hash,
3982 parent_hash,
3983 chain: Chain::Ethereum,
3984 ts: Default::default(),
3985 },
3986 revert: false,
3987 partial_block_index: partial_idx,
3988 ..Default::default()
3989 }
3990 }
3991
3992 #[test_log::test(tokio::test)]
3994 async fn test_partial_mode_accepts_full_block_as_first_message() {
3995 let (rpc_client, deltas_client, tx) = mock_clients_for_state_sync(None);
3996 let mut state_sync = with_mocked_clients(true, true, Some(rpc_client), Some(deltas_client))
3997 .with_partial_blocks(true);
3998 state_sync
3999 .initialize()
4000 .await
4001 .expect("Init failed");
4002
4003 let (handle, mut block_rx) = state_sync.start().await;
4004 let (jh, close_tx) = handle.split();
4005
4006 tx.send(make_block_changes(1, None))
4008 .await
4009 .unwrap();
4010
4011 let msg = timeout(Duration::from_millis(100), block_rx.recv())
4013 .await
4014 .expect("Should receive message")
4015 .expect("Channel open")
4016 .expect("No error");
4017
4018 assert_eq!(msg.header.number, 1, "Should use block 1 (full block)");
4019 assert_eq!(msg.header.partial_block_index, None, "Should be a full block");
4020
4021 let _ = close_tx.send(());
4022 jh.await.expect("Task should not panic");
4023 }
4024
4025 #[test_log::test(tokio::test)]
4027 async fn test_partial_mode_detects_block_number_increase() {
4028 let (rpc_client, deltas_client, tx) = mock_clients_for_state_sync(None);
4029 let mut state_sync = with_mocked_clients(true, true, Some(rpc_client), Some(deltas_client))
4030 .with_partial_blocks(true);
4031 state_sync
4032 .initialize()
4033 .await
4034 .expect("Init failed");
4035
4036 let (handle, mut block_rx) = state_sync.start().await;
4037 let (jh, close_tx) = handle.split();
4038
4039 tx.send(make_block_changes(1, Some(0)))
4041 .await
4042 .unwrap();
4043 tx.send(make_block_changes(1, Some(3)))
4044 .await
4045 .unwrap();
4046
4047 match timeout(Duration::from_millis(50), block_rx.recv()).await {
4049 Err(_) => { }
4050 Ok(_) => panic!("Should not receive message while waiting for new block"),
4051 }
4052
4053 tx.send(make_block_changes(2, Some(5)))
4056 .await
4057 .unwrap();
4058
4059 let msg = timeout(Duration::from_millis(100), block_rx.recv())
4061 .await
4062 .expect("Should receive message")
4063 .expect("Channel open")
4064 .expect("No error");
4065
4066 assert_eq!(msg.header.number, 2, "Should use block 2 (block number increased)");
4067 assert_eq!(msg.header.partial_block_index, Some(5));
4068
4069 let _ = close_tx.send(());
4070 jh.await.expect("Task should not panic");
4071 }
4072
4073 #[test_log::test(tokio::test)]
4075 async fn test_partial_mode_skips_already_synced_blocks() {
4076 let (rpc_client, deltas_client, tx) = mock_clients_for_state_sync(None);
4077 let mut state_sync = with_mocked_clients(true, true, Some(rpc_client), Some(deltas_client))
4078 .with_partial_blocks(true);
4079 state_sync
4080 .initialize()
4081 .await
4082 .expect("Init failed");
4083
4084 state_sync.last_synced_block = Some(BlockHeader {
4086 number: 5,
4087 hash: Bytes::from("0x05"),
4088 parent_hash: Bytes::from("0x04"),
4089 revert: false,
4090 timestamp: 0,
4091 partial_block_index: None,
4092 });
4093 state_sync.snapshot_jitter = Duration::ZERO;
4095
4096 let (handle, mut block_rx) = state_sync.start().await;
4097 let (jh, close_tx) = handle.split();
4098
4099 tx.send(make_block_changes(3, Some(2)))
4101 .await
4102 .unwrap();
4103
4104 tx.send(make_block_changes(4, Some(0)))
4106 .await
4107 .unwrap();
4108
4109 match timeout(Duration::from_millis(50), block_rx.recv()).await {
4111 Err(_) => { }
4112 Ok(_) => panic!("Should skip block 4 because it's already synced"),
4113 }
4114
4115 tx.send(make_block_changes(5, Some(3)))
4118 .await
4119 .unwrap();
4120 tx.send(make_block_changes(6, Some(0)))
4122 .await
4123 .unwrap();
4124
4125 let msg = timeout(Duration::from_millis(100), block_rx.recv())
4126 .await
4127 .expect("Should receive message")
4128 .expect("Channel open")
4129 .expect("No error");
4130
4131 assert_eq!(msg.header.number, 6, "Should use block 6 (after last synced)");
4132
4133 let _ = close_tx.send(());
4134 jh.await.expect("Task should not panic");
4135 }
4136
4137 #[test_log::test(tokio::test)]
4138 async fn test_get_snapshots_skips_entrypoints_when_not_dci() {
4139 let header = BlockHeader::default();
4140 let mut rpc = make_mock_client();
4141 let component = ProtocolComponent { id: "Component1".to_string(), ..Default::default() };
4142
4143 let component_clone = component.clone();
4144 rpc.expect_get_snapshots()
4145 .returning(move |_request, _chunk_size, _concurrency| {
4146 Ok(Snapshot {
4147 states: [(
4148 "Component1".to_string(),
4149 ComponentWithState {
4150 state: ProtocolComponentState::new(
4151 "Component1",
4152 Default::default(),
4153 Default::default(),
4154 ),
4155 component: component_clone.clone(),
4156 entrypoints: vec![],
4157 component_tvl: None,
4158 },
4159 )]
4160 .into_iter()
4161 .collect(),
4162 vm_storage: HashMap::new(),
4163 })
4164 });
4165
4166 rpc.expect_get_traced_entry_points()
4168 .never();
4169
4170 let mut state_sync = with_mocked_clients(true, false, Some(rpc), None);
4171 state_sync
4173 .component_tracker
4174 .components
4175 .insert("Component1".to_string(), component);
4176
4177 let components_arg = ["Component1".to_string()];
4178 let req_ids: Vec<String> = components_arg.to_vec();
4179 let components: HashMap<_, _> = state_sync
4180 .component_tracker
4181 .components
4182 .iter()
4183 .filter(|(id, _)| req_ids.contains(id))
4184 .map(|(k, v)| (k.clone(), v.clone()))
4185 .collect();
4186 let contract_ids: HashSet<Bytes> = state_sync
4187 .component_tracker
4188 .get_contracts_by_component(&req_ids)
4189 .into_iter()
4190 .collect();
4191 let params = FetchSnapshotParams {
4192 chain: Chain::Ethereum,
4193 protocol_system: "uniswap-v2".to_string(),
4194 block_number: header.number,
4195 uses_dci: false,
4196 retrieve_balances: true,
4197 include_tvl: false,
4198 };
4199 let (snapshot, _, _) =
4200 fetch_snapshot(&state_sync.rpc_client, components, contract_ids, ¶ms)
4201 .await
4202 .expect("Retrieving snapshot failed");
4203
4204 assert!(snapshot
4205 .states
4206 .contains_key("Component1"));
4207 }
4208
4209 #[test_log::test(tokio::test)]
4210 async fn test_get_snapshots_fetches_entrypoints_when_dci() {
4211 let header = BlockHeader::default();
4212 let mut rpc = make_mock_client();
4213 let component = ProtocolComponent { id: "Component1".to_string(), ..Default::default() };
4214
4215 let component_clone = component.clone();
4216 rpc.expect_get_snapshots()
4217 .returning(move |_request, _chunk_size, _concurrency| {
4218 Ok(Snapshot {
4219 states: [(
4220 "Component1".to_string(),
4221 ComponentWithState {
4222 state: ProtocolComponentState::new(
4223 "Component1",
4224 Default::default(),
4225 Default::default(),
4226 ),
4227 component: component_clone.clone(),
4228 entrypoints: vec![],
4229 component_tvl: None,
4230 },
4231 )]
4232 .into_iter()
4233 .collect(),
4234 vm_storage: HashMap::new(),
4235 })
4236 });
4237
4238 rpc.expect_get_traced_entry_points()
4240 .times(1)
4241 .returning(|_| Ok(Page::new(HashMap::new(), 0, 0, 0)));
4242
4243 let mut state_sync = with_mocked_clients(true, false, Some(rpc), None).with_dci(true);
4244 state_sync
4245 .component_tracker
4246 .components
4247 .insert("Component1".to_string(), component);
4248
4249 let components_arg = ["Component1".to_string()];
4250 let req_ids: Vec<String> = components_arg.to_vec();
4251 let components: HashMap<_, _> = state_sync
4252 .component_tracker
4253 .components
4254 .iter()
4255 .filter(|(id, _)| req_ids.contains(id))
4256 .map(|(k, v)| (k.clone(), v.clone()))
4257 .collect();
4258 let contract_ids: HashSet<Bytes> = state_sync
4259 .component_tracker
4260 .get_contracts_by_component(&req_ids)
4261 .into_iter()
4262 .collect();
4263 let params = FetchSnapshotParams {
4264 chain: Chain::Ethereum,
4265 protocol_system: "uniswap-v2".to_string(),
4266 block_number: header.number,
4267 uses_dci: true,
4268 retrieve_balances: true,
4269 include_tvl: false,
4270 };
4271 let (snapshot, _, _) =
4272 fetch_snapshot(&state_sync.rpc_client, components, contract_ids, ¶ms)
4273 .await
4274 .expect("Retrieving snapshot failed");
4275
4276 assert!(snapshot
4277 .states
4278 .contains_key("Component1"));
4279 }
4280
4281 #[test_log::test(tokio::test)]
4292 async fn test_partial_mode_defers_brand_new_component_snapshot_to_next_block() {
4293 use std::time::Duration;
4294
4295 use tokio::{sync::mpsc::channel, time::timeout};
4296
4297 let bg_done = Arc::new(tokio::sync::Notify::new());
4298 let mut rpc_client = make_mock_client();
4299 rpc_client
4301 .expect_get_protocol_components()
4302 .withf(|params: &crate::rpc::ProtocolComponentsParams| {
4303 params
4304 .component_ids()
4305 .is_some_and(|ids| ids.contains(&"BrandNew".to_string()))
4306 })
4307 .returning(|_| {
4308 Ok(Page::new(
4309 vec![
4310 ProtocolComponent { id: "BrandNew".to_string(), ..Default::default() },
4311 ProtocolComponent { id: "Preexisting".to_string(), ..Default::default() },
4312 ],
4313 2,
4314 0,
4315 100,
4316 ))
4317 });
4318 rpc_client
4320 .expect_get_protocol_components()
4321 .returning(|_| {
4322 Ok(Page::new(
4323 vec![
4324 ProtocolComponent { id: "Component1".to_string(), ..Default::default() },
4325 ProtocolComponent { id: "Component2".to_string(), ..Default::default() },
4326 ],
4327 2,
4328 0,
4329 100,
4330 ))
4331 });
4332 let bg_done_clone = bg_done.clone();
4334 rpc_client
4335 .expect_get_snapshots()
4336 .withf(
4337 |request: &SnapshotParameters,
4338 _chunk_size: &Option<usize>,
4339 _concurrency: &usize| {
4340 request.block_number == 2 &&
4341 (request
4342 .components
4343 .contains_key("BrandNew") ||
4344 request
4345 .components
4346 .contains_key("Preexisting"))
4347 },
4348 )
4349 .returning(move |_request, _chunk_size, _concurrency| {
4350 let snap = Ok(Snapshot {
4351 states: [
4352 (
4353 "BrandNew".to_string(),
4354 ComponentWithState {
4355 state: ProtocolComponentState::new(
4356 "BrandNew",
4357 Default::default(),
4358 Default::default(),
4359 ),
4360 component: ProtocolComponent {
4361 id: "BrandNew".to_string(),
4362 ..Default::default()
4363 },
4364 component_tvl: Some(100.0),
4365 entrypoints: vec![],
4366 },
4367 ),
4368 (
4369 "Preexisting".to_string(),
4370 ComponentWithState {
4371 state: ProtocolComponentState::new(
4372 "Preexisting",
4373 Default::default(),
4374 Default::default(),
4375 ),
4376 component: ProtocolComponent {
4377 id: "Preexisting".to_string(),
4378 ..Default::default()
4379 },
4380 component_tvl: Some(75.0),
4381 entrypoints: vec![],
4382 },
4383 ),
4384 ]
4385 .into_iter()
4386 .collect(),
4387 vm_storage: HashMap::new(),
4388 });
4389 bg_done_clone.notify_one();
4390 snap
4391 });
4392 rpc_client
4394 .expect_get_snapshots()
4395 .returning(|_request, _chunk_size, _concurrency| {
4396 Ok(Snapshot {
4397 states: [
4398 (
4399 "Component1".to_string(),
4400 ComponentWithState {
4401 state: ProtocolComponentState::new(
4402 "Component1",
4403 Default::default(),
4404 Default::default(),
4405 ),
4406 component: ProtocolComponent {
4407 id: "Component1".to_string(),
4408 ..Default::default()
4409 },
4410 component_tvl: Some(100.0),
4411 entrypoints: vec![],
4412 },
4413 ),
4414 (
4415 "Component2".to_string(),
4416 ComponentWithState {
4417 state: ProtocolComponentState::new(
4418 "Component2",
4419 Default::default(),
4420 Default::default(),
4421 ),
4422 component: ProtocolComponent {
4423 id: "Component2".to_string(),
4424 ..Default::default()
4425 },
4426 component_tvl: Some(0.0),
4427 entrypoints: vec![],
4428 },
4429 ),
4430 ]
4431 .into_iter()
4432 .collect(),
4433 vm_storage: HashMap::new(),
4434 })
4435 });
4436 rpc_client
4437 .expect_get_traced_entry_points()
4438 .returning(|_| Ok(Page::new(HashMap::new(), 0, 0, 0)));
4439
4440 let mut deltas_client = MockDeltasClient::new();
4441 let (tx, rx) = channel(4);
4442 deltas_client
4443 .expect_subscribe()
4444 .return_once(move |_, _| Ok((Uuid::default(), rx)));
4445 deltas_client
4446 .expect_unsubscribe()
4447 .return_once(|_| Ok(()));
4448
4449 let mut state_sync = with_mocked_clients(true, true, Some(rpc_client), Some(deltas_client))
4450 .with_partial_blocks(true);
4451 state_sync
4452 .initialize()
4453 .await
4454 .expect("Init failed");
4455
4456 let (handle, mut block_rx) = state_sync.start().await;
4457 let (jh, close_tx) = handle.split();
4458
4459 tx.send(make_block_changes(1, None))
4461 .await
4462 .unwrap();
4463 let _msg1 = timeout(Duration::from_millis(200), block_rx.recv())
4464 .await
4465 .expect("Should receive initial + block 1")
4466 .expect("Channel open")
4467 .expect("No error");
4468
4469 let mut block2 = make_block_changes(2, Some(2));
4472 block2.new_protocol_components = HashMap::from([(
4473 "BrandNew".to_string(),
4474 ProtocolComponent { id: "BrandNew".to_string(), ..Default::default() },
4475 )]);
4476 block2.component_tvl =
4477 HashMap::from([("BrandNew".to_string(), 100.0), ("Preexisting".to_string(), 75.0)]);
4478 tx.send(block2).await.unwrap();
4479 let msg2 = timeout(Duration::from_millis(200), block_rx.recv())
4480 .await
4481 .expect("Should receive block 2")
4482 .expect("Channel open")
4483 .expect("No error");
4484
4485 assert!(
4486 !msg2
4487 .snapshots
4488 .states
4489 .contains_key("Preexisting"),
4490 "Preexisting should still be deferred in block 2, not yet snapshotted; got: {:?}",
4491 msg2.snapshots
4492 .states
4493 .keys()
4494 .collect::<Vec<_>>()
4495 );
4496 assert!(
4497 !msg2
4498 .snapshots
4499 .states
4500 .contains_key("BrandNew"),
4501 "BrandNew should still be deferred in block 2, not yet snapshotted"
4502 );
4503
4504 tx.send(make_block_changes(3, Some(1)))
4507 .await
4508 .unwrap();
4509 let msg3 = timeout(Duration::from_millis(200), block_rx.recv())
4510 .await
4511 .expect("Should receive block 3")
4512 .expect("Channel open")
4513 .expect("No error");
4514
4515 assert_eq!(msg3.header.number, 3);
4516 assert_eq!(msg3.header.partial_block_index, Some(1));
4517 assert!(
4518 !msg3
4519 .snapshots
4520 .states
4521 .contains_key("BrandNew"),
4522 "BrandNew task just fired; snapshot not yet available in msg3"
4523 );
4524 assert!(
4525 !msg3
4526 .snapshots
4527 .states
4528 .contains_key("Preexisting"),
4529 "Preexisting task just fired; snapshot not yet available in msg3"
4530 );
4531
4532 bg_done.notified().await;
4534
4535 tx.send(make_block_changes(4, Some(0)))
4537 .await
4538 .unwrap();
4539 let msg4 = timeout(Duration::from_millis(200), block_rx.recv())
4540 .await
4541 .expect("Should receive block 4")
4542 .expect("Channel open")
4543 .expect("No error");
4544
4545 assert_eq!(msg4.header.number, 4);
4546 assert_eq!(msg4.header.partial_block_index, Some(0));
4547 assert!(
4548 msg4.snapshots
4549 .states
4550 .contains_key("BrandNew"),
4551 "BrandNew snapshot should be in msg4 after background task drains; got keys: {:?}",
4552 msg4.snapshots
4553 .states
4554 .keys()
4555 .collect::<Vec<_>>()
4556 );
4557 assert!(
4558 msg4.snapshots
4559 .states
4560 .contains_key("Preexisting"),
4561 "Preexisting snapshot should be in msg4 after background task drains; got keys: {:?}",
4562 msg4.snapshots
4563 .states
4564 .keys()
4565 .collect::<Vec<_>>()
4566 );
4567
4568 let _ = close_tx.send(());
4569 jh.await.expect("Task should not panic");
4570 }
4571
4572 #[test]
4576 fn test_apply_deltas_to_snapshot() {
4577 use tycho_common::models::{
4578 contract::{Account, AccountDelta},
4579 protocol::{ComponentBalance, ProtocolComponentStateDelta},
4580 ChangeType,
4581 };
4582
4583 let contract_addr = Bytes::from("0xc0ffee");
4584 let token_addr = Bytes::from("0xdeadbeef");
4585
4586 let mut snapshot = Snapshot {
4589 states: [(
4590 "comp1".to_string(),
4591 ComponentWithState {
4592 state: ProtocolComponentState::new(
4593 "comp1",
4594 [
4595 ("keep".to_string(), Bytes::from("0x01")),
4596 ("delete_me".to_string(), Bytes::from("0x02")),
4597 ]
4598 .into_iter()
4599 .collect(),
4600 [(token_addr.clone(), Bytes::from("0x64"))]
4601 .into_iter()
4602 .collect(),
4603 ),
4604 component: ProtocolComponent::default(),
4605 component_tvl: None,
4606 entrypoints: vec![],
4607 },
4608 )]
4609 .into_iter()
4610 .collect(),
4611 vm_storage: [(
4612 contract_addr.clone(),
4613 Account {
4614 chain: Chain::Ethereum,
4615 address: contract_addr.clone(),
4616 title: String::new(),
4617 slots: [(Bytes::from("0x01"), Bytes::from("0xaa"))]
4618 .into_iter()
4619 .collect(),
4620 native_balance: Bytes::from("0x10"),
4621 token_balances: HashMap::new(),
4622 code: Bytes::from("0x0a0b"),
4623 code_hash: Default::default(),
4624 balance_modify_tx: Default::default(),
4625 code_modify_tx: Default::default(),
4626 creation_tx: None,
4627 },
4628 )]
4629 .into_iter()
4630 .collect(),
4631 };
4632
4633 let skipped_delta = BlockAggregatedChanges {
4636 block: Block { number: 5, ..Default::default() },
4637 state_deltas: [(
4638 "comp1".to_string(),
4639 ProtocolComponentStateDelta::new(
4640 "comp1",
4641 [("keep".to_string(), Bytes::from("0xff"))]
4642 .into_iter()
4643 .collect(),
4644 HashSet::new(),
4645 ),
4646 )]
4647 .into_iter()
4648 .collect(),
4649 ..Default::default()
4650 };
4651 let applied_delta = BlockAggregatedChanges {
4652 block: Block { number: 6, ..Default::default() },
4653 state_deltas: [(
4654 "comp1".to_string(),
4655 ProtocolComponentStateDelta::new(
4656 "comp1",
4657 [("keep".to_string(), Bytes::from("0x99"))]
4658 .into_iter()
4659 .collect(),
4660 ["delete_me".to_string()]
4661 .into_iter()
4662 .collect(),
4663 ),
4664 )]
4665 .into_iter()
4666 .collect(),
4667 component_balances: [(
4668 "comp1".to_string(),
4669 [(
4670 token_addr.clone(),
4671 ComponentBalance::new(
4672 token_addr.clone(),
4673 Bytes::from("0xc8"),
4674 200.0,
4675 Default::default(),
4676 "comp1",
4677 ),
4678 )]
4679 .into_iter()
4680 .collect(),
4681 )]
4682 .into_iter()
4683 .collect(),
4684 account_deltas: [(
4685 contract_addr.clone(),
4686 AccountDelta::new(
4687 Chain::Ethereum,
4688 contract_addr.clone(),
4689 [
4690 (Bytes::from("0x01"), Some(Bytes::from("0xbb"))),
4691 (Bytes::from("0x02"), Some(Bytes::from("0xcc"))),
4692 ]
4693 .into_iter()
4694 .collect(),
4695 Some(Bytes::from("0x20")),
4696 Some(Bytes::from("0x0c0d")),
4697 ChangeType::Update,
4698 ),
4699 )]
4700 .into_iter()
4701 .collect(),
4702 ..Default::default()
4703 };
4704
4705 let mut sync = with_mocked_clients(true, false, None, None);
4706 sync.buffered_deltas = vec![skipped_delta, applied_delta];
4707
4708 let contract_ids: HashSet<Bytes> = [contract_addr.clone()]
4709 .into_iter()
4710 .collect();
4711 sync.apply_deltas_to_snapshot(&mut snapshot, 5, &contract_ids);
4712
4713 let comp = &snapshot.states["comp1"].state;
4714
4715 assert_eq!(comp.attributes["keep"], Bytes::from("0x99"));
4717 assert!(!comp
4719 .attributes
4720 .contains_key("delete_me"));
4721 assert_eq!(comp.balances[&token_addr], Bytes::from("0xc8"));
4723
4724 let account = &snapshot.vm_storage[&contract_addr];
4725 assert_eq!(account.slots[&Bytes::from("0x01")], Bytes::from("0xbb"));
4727 assert_eq!(account.slots[&Bytes::from("0x02")], Bytes::from("0xcc"));
4728 assert_eq!(account.native_balance, Bytes::from("0x20"));
4730 assert_eq!(account.code, Bytes::from("0x0c0d"));
4732 }
4733}