1use alloy_network::{Ethereum, Network};
2use alloy_primitives::{Address, B256, I256, U256};
3use evm_fork_cache::reactive::{ReactiveBatchReport, ReactiveReport, ReportTag};
4
5use crate::{
6 AggregatorChange, AggregatorLayoutEvidence, ChainlinkFeedProvider, FeedId, FeedRegistration,
7 FeedSource, ORACLE_LEGACY_ANSWER_UPDATED_KIND, ORACLE_SIGNAL_NAMESPACE, OracleBlockRef,
8 OracleError, OracleFeedReadinessReport, OraclePrice, OracleRegistry, OracleRoundStatus,
9 OracleSignalKind, OracleSnapshot, OracleValueSource, OracleValueStatus, RoundData,
10 registry::snapshot_from_proxy_read, state::EventSnapshotInput,
11};
12
13#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct OracleUpdate {
16 pub id: FeedId,
18 pub proxy: Address,
20 pub aggregator: Address,
22 pub round: RoundData,
24 pub block_number: Option<u64>,
26 pub log_index: Option<u64>,
28 pub value_status: OracleValueStatus,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct OraclePriceUpdate {
35 pub id: FeedId,
37 pub proxy: Address,
39 pub aggregator: Address,
41 pub label: Option<String>,
43 pub base: Option<String>,
45 pub quote: Option<String>,
47 pub raw_answer: I256,
49 pub decimals: u8,
51 pub event_round_id: U256,
53 pub started_at: u64,
55 pub updated_at: u64,
57 pub block_number: Option<u64>,
59 pub block_hash: Option<B256>,
61 pub log_index: Option<u64>,
63 pub round_status: OracleRoundStatus,
65 pub value_status: OracleValueStatus,
67 pub source: OracleValueSource,
69}
70
71impl OraclePriceUpdate {
72 fn round(&self) -> RoundData {
73 RoundData {
74 round_id: self.event_round_id,
75 answer: self.raw_answer,
76 started_at: self.started_at,
77 updated_at: self.updated_at,
78 answered_in_round: self.event_round_id,
79 }
80 }
81}
82
83#[non_exhaustive]
85#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
86pub enum OracleReconciliationKind {
87 #[default]
91 Proxy,
92 DerivedProtocolRead,
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct OracleReconciliationRequest {
102 pub id: FeedId,
104 pub proxy: Address,
106 pub aggregator: Option<Address>,
108 pub event_round: RoundData,
110 pub block_number: Option<u64>,
112 pub block_hash: Option<B256>,
114 pub log_index: Option<u64>,
116 pub kind: OracleReconciliationKind,
118}
119
120impl OracleReconciliationRequest {
121 pub fn block_ref(&self) -> Option<OracleBlockRef> {
123 Some(OracleBlockRef {
124 number: self.block_number?,
125 hash: self.block_hash?,
126 })
127 }
128}
129
130#[derive(Clone, Debug, PartialEq, Eq)]
132pub struct OraclePriceConfirmed {
133 pub id: FeedId,
135 pub proxy: Address,
137 pub aggregator: Option<Address>,
139 pub label: Option<String>,
141 pub base: Option<String>,
143 pub quote: Option<String>,
145 pub raw_answer: I256,
147 pub decimals: u8,
149 pub event_round_id: U256,
151 pub updated_at: u64,
153 pub block_number: Option<u64>,
155 pub block_hash: Option<B256>,
157 pub log_index: Option<u64>,
159 pub round_status: OracleRoundStatus,
161 pub value_status: OracleValueStatus,
163 pub source: OracleValueSource,
165 pub proxy_round: RoundData,
167}
168
169#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct OraclePriceCorrected {
172 pub id: FeedId,
174 pub proxy: Address,
176 pub aggregator: Option<Address>,
178 pub label: Option<String>,
180 pub base: Option<String>,
182 pub quote: Option<String>,
184 pub event_answer: I256,
186 pub raw_answer: I256,
188 pub decimals: u8,
190 pub event_round_id: U256,
192 pub updated_at: u64,
194 pub block_number: Option<u64>,
196 pub block_hash: Option<B256>,
198 pub log_index: Option<u64>,
200 pub round_status: OracleRoundStatus,
202 pub value_status: OracleValueStatus,
204 pub source: OracleValueSource,
206 pub corrected_round: RoundData,
208}
209
210#[derive(Clone, Debug, PartialEq, Eq)]
212pub struct OraclePriceStale {
213 pub id: FeedId,
215 pub proxy: Address,
217 pub aggregator: Option<Address>,
219 pub label: Option<String>,
221 pub base: Option<String>,
223 pub quote: Option<String>,
225 pub raw_answer: I256,
227 pub decimals: u8,
229 pub event_round_id: U256,
231 pub updated_at: u64,
233 pub block_number: Option<u64>,
235 pub block_hash: Option<B256>,
237 pub log_index: Option<u64>,
239 pub round_status: OracleRoundStatus,
241 pub value_status: OracleValueStatus,
243 pub source: OracleValueSource,
245 pub proxy_round: RoundData,
247}
248
249#[non_exhaustive]
251#[derive(Clone, Debug, PartialEq, Eq)]
252pub enum OracleHookEvent {
253 PriceUpdate(OraclePriceUpdate),
255 PriceConfirmed(OraclePriceConfirmed),
257 PriceCorrected(OraclePriceCorrected),
259 PriceStale(OraclePriceStale),
261 AggregatorChanged(AggregatorChange),
263}
264
265pub type OracleAggregatorChanged = AggregatorChange;
267
268#[derive(Clone, Debug, PartialEq, Eq)]
270pub struct OracleReconciliationResult {
271 pub request: OracleReconciliationRequest,
273 pub hooks: Vec<OracleHookEvent>,
275}
276
277#[derive(Clone, Debug, Default, PartialEq, Eq)]
279pub struct ReconcileReport {
280 pub checked_feeds: usize,
282 pub feed_statuses: Vec<OracleFeedReadinessReport>,
284 pub changed_feeds: Vec<FeedId>,
286 pub aggregator_changes: Vec<AggregatorChange>,
288}
289
290#[derive(Clone, Debug, PartialEq, Eq)]
293pub struct DerivedReconcileFailure {
294 pub request: OracleReconciliationRequest,
296 pub error: OracleError,
298}
299
300#[derive(Clone, Debug, Default, PartialEq, Eq)]
302pub struct DerivedReconcileReport {
303 pub reconciled: Vec<OracleReconciliationResult>,
305 pub failed: Vec<DerivedReconcileFailure>,
307}
308
309#[derive(Clone, Debug)]
311pub struct OracleTracker {
312 registry: OracleRegistry,
313 pending_reconciliations: Vec<OracleReconciliationRequest>,
314}
315
316impl OracleTracker {
317 pub fn new(registry: OracleRegistry) -> Self {
319 Self {
320 registry,
321 pending_reconciliations: Vec::new(),
322 }
323 }
324
325 pub fn from_registrations_at_timestamp(
327 feeds: Vec<(FeedRegistration, RoundData)>,
328 now_timestamp: u64,
329 ) -> Result<Self, OracleError> {
330 let mut registry = OracleRegistry::new_at_timestamp(now_timestamp);
331 for (registration, round) in feeds {
332 let id = registration.id.clone();
333 if registry.registrations_map().contains_key(&id) {
334 return Err(OracleError::DuplicateFeedId(id.to_string()));
335 }
336 if registry.id_by_proxy(registration.proxy).is_some() {
337 return Err(OracleError::DuplicateProxy(registration.proxy));
338 }
339
340 let snapshot = OracleSnapshot::proxy_read(
341 id.clone(),
342 registration.proxy,
343 registration.current_aggregator,
344 registration.metadata.clone(),
345 round,
346 now_timestamp,
347 ®istration.staleness,
348 );
349 registry
350 .registrations_map_mut()
351 .insert(id.clone(), registration.clone());
352 registry
353 .snapshots_by_proxy_mut()
354 .insert(registration.proxy, snapshot);
355 registry.record_proxy_id(registration.proxy, id);
356 }
357 Ok(Self {
358 registry,
359 pending_reconciliations: Vec::new(),
360 })
361 }
362
363 pub fn registrations(&self) -> impl Iterator<Item = FeedRegistration> + '_ {
368 self.registry.registrations()
369 }
370
371 pub fn registrations_iter(&self) -> impl Iterator<Item = &FeedRegistration> {
373 self.registry.registrations_iter()
374 }
375
376 pub fn feed_readiness(&self) -> Vec<OracleFeedReadinessReport> {
378 self.registry.feed_readiness()
379 }
380
381 pub fn insert_seeded_registration(
383 &mut self,
384 registration: FeedRegistration,
385 round: RoundData,
386 ) -> Result<(), OracleError> {
387 self.registry
388 .insert_seeded_registration(registration, round)
389 }
390
391 pub fn remove_by_id(&mut self, id: FeedId) -> Option<FeedRegistration> {
393 let registration = self.registry.remove_registration_by_id(id)?;
394 self.clear_pending_for_proxy(registration.proxy);
395 Some(registration)
396 }
397
398 pub fn remove_by_proxy(&mut self, proxy: Address) -> Option<FeedRegistration> {
400 let registration = self.registry.remove_registration_by_proxy(proxy)?;
401 self.clear_pending_for_proxy(registration.proxy);
402 Some(registration)
403 }
404
405 pub fn now_timestamp(&self) -> u64 {
407 self.registry.now_timestamp()
408 }
409
410 pub fn latest(&self, proxy: Address) -> Option<&OracleSnapshot> {
412 self.registry.latest(proxy)
413 }
414
415 pub fn registration_for_proxy(&self, proxy: Address) -> Option<&FeedRegistration> {
417 let id = self.registry.id_by_proxy(proxy)?;
418 self.registry.registrations_map().get(id)
419 }
420
421 pub fn price(&self, id: impl AsRef<str>) -> Result<OraclePrice, OracleError> {
428 let registration = self
429 .registration_by_id_str(id.as_ref())
430 .ok_or(OracleError::FeedNotFound)?;
431 self.price_for_registration(registration)
432 }
433
434 pub fn price_by_id(&self, id: FeedId) -> Result<OraclePrice, OracleError> {
441 let registration = self
442 .registry
443 .registrations_map()
444 .get(&id)
445 .ok_or(OracleError::FeedNotFound)?;
446 self.price_for_registration(registration)
447 }
448
449 pub fn price_by_proxy(&self, proxy: Address) -> Result<OraclePrice, OracleError> {
456 let id = self
457 .registry
458 .id_by_proxy(proxy)
459 .cloned()
460 .ok_or(OracleError::FeedNotFound)?;
461 self.price_by_id(id)
462 }
463
464 pub fn latest_round(&self, id: impl AsRef<str>) -> Result<RoundData, OracleError> {
471 Ok(self.price(id)?.round_data())
472 }
473
474 pub fn latest_round_by_proxy(&self, proxy: Address) -> Result<RoundData, OracleError> {
476 Ok(self.price_by_proxy(proxy)?.round_data())
477 }
478
479 pub fn pending_reconciliations(&self) -> &[OracleReconciliationRequest] {
481 &self.pending_reconciliations
482 }
483
484 pub fn apply_batch_report<N: Network>(
495 &mut self,
496 report: &ReactiveBatchReport<N>,
497 ) -> Result<(), OracleError> {
498 let rich_price_update_keys = report
503 .applied
504 .iter()
505 .flat_map(|applied| applied.hook_signals.iter())
506 .filter(|signal| {
507 signal.namespace.as_ref() == ORACLE_SIGNAL_NAMESPACE
508 && signal.kind.as_ref() == OracleSignalKind::PriceUpdate.as_str()
509 })
510 .filter_map(|signal| signal.payload.as_ref()?.downcast_ref::<OraclePriceUpdate>())
511 .map(|update| (update.proxy, update.event_round_id))
512 .collect::<std::collections::BTreeSet<_>>();
513
514 for applied in &report.applied {
515 for signal in &applied.hook_signals {
516 if signal.namespace.as_ref() != ORACLE_SIGNAL_NAMESPACE {
517 continue;
518 }
519
520 let Some(payload) = signal.payload.as_ref() else {
521 continue;
522 };
523
524 match signal.kind.as_ref() {
525 kind if kind == OracleSignalKind::PriceUpdate.as_str() => {
526 if let Some(update) = payload.downcast_ref::<OraclePriceUpdate>() {
527 self.apply_price_update_with_tags(update.clone(), &signal.labels)?;
528 }
529 }
530 kind if kind == ORACLE_LEGACY_ANSWER_UPDATED_KIND => {
531 if let Some(update) = payload.downcast_ref::<OracleUpdate>()
532 && !rich_price_update_keys
533 .contains(&(update.proxy, update.round.round_id))
534 {
535 self.apply_update(update.clone())?;
536 }
537 }
538 _ => {}
539 }
540 }
541 }
542
543 if report.reports.iter().any(|report| {
544 matches!(
545 report.as_ref(),
546 ReactiveReport::Reorg(reorg)
550 if reorg.dropped.is_some() || !reorg.dropped_blocks.is_empty()
551 )
552 }) {
553 self.mark_event_snapshots_unknown();
554 }
555
556 Ok(())
557 }
558
559 pub async fn reconcile<P: ChainlinkFeedProvider>(
561 &mut self,
562 provider: &P,
563 ) -> Result<ReconcileReport, OracleError> {
564 let registrations: Vec<_> = self.registry.registrations().collect();
565 let mut report = ReconcileReport::default();
566
567 for mut registration in registrations {
568 report.checked_feeds += 1;
569 if !registration.source.supports_proxy_reconciliation() {
570 continue;
571 }
572 let mut new_source = None;
573 let (round, new_aggregator, new_layout) = if matches!(
574 registration.source,
575 FeedSource::AaveSynchronicityPegToBase { .. }
576 ) {
577 let reconciled =
578 reconcile_current_aave_synchronicity_peg_to_base(self, provider, ®istration)
579 .await?;
580 new_source = reconciled.new_source;
581 (
582 reconciled.proxy_round,
583 reconciled.new_aggregator,
584 reconciled.new_layout,
585 )
586 } else {
587 let read_proxy = registration.source.read_proxy(registration.proxy);
588 let round = registration
589 .source
590 .normalize_round(provider.latest_round_data(read_proxy).await?);
591 let new_aggregator = provider
592 .aggregator(read_proxy)
593 .await
594 .unwrap_or(registration.current_aggregator);
595 let new_layout = self
596 .registry
597 .detect_aggregator_layout(provider, new_aggregator, None)
598 .await;
599 (round, new_aggregator, new_layout)
600 };
601 let old_aggregator = registration.current_aggregator;
602 let snapshot = snapshot_from_proxy_read(
603 ®istration,
604 round,
605 new_aggregator,
606 self.registry.now_timestamp(),
607 );
608
609 let changed = self
610 .registry
611 .latest(registration.proxy)
612 .is_none_or(|old| old != &snapshot);
613 if changed {
614 report.changed_feeds.push(registration.id.clone());
615 }
616
617 if old_aggregator != new_aggregator {
618 report.aggregator_changes.push(AggregatorChange {
619 id: registration.id.clone(),
620 proxy: registration.proxy,
621 old: old_aggregator,
622 new: new_aggregator,
623 });
624 }
625
626 registration.current_aggregator = new_aggregator;
627 registration.aggregator_layout = new_layout.clone();
628 if let Some(stored) = self
629 .registry
630 .registrations_map_mut()
631 .get_mut(®istration.id)
632 {
633 stored.current_aggregator = new_aggregator;
634 stored.aggregator_layout = new_layout;
635 if let Some(new_source) = new_source {
636 stored.source = new_source;
637 }
638 }
639 self.registry.replace_snapshot(snapshot);
640 self.clear_pending_for_proxy(registration.proxy);
641 }
642
643 report.feed_statuses = self
644 .registry
645 .registrations_iter()
646 .map(|registration| OracleFeedReadinessReport {
647 id: Some(registration.id.clone()),
648 proxy: registration.proxy,
649 status: registration.status,
650 reason: None,
651 })
652 .collect();
653
654 Ok(report)
655 }
656
657 pub(crate) fn registration_by_id_str(&self, id: &str) -> Option<&FeedRegistration> {
659 self.registry.registrations_map().get(id)
660 }
661
662 fn price_for_registration(
663 &self,
664 registration: &FeedRegistration,
665 ) -> Result<OraclePrice, OracleError> {
666 let snapshot = self
667 .registry
668 .latest(registration.proxy)
669 .ok_or(OracleError::FeedNotFound)?;
670 Ok(OraclePrice::from_snapshot(snapshot, registration))
671 }
672
673 fn apply_update(&mut self, update: OracleUpdate) -> Result<(), OracleError> {
674 let Some(registration) = self.registry.registrations_map().get(&update.id).cloned() else {
675 return Ok(());
676 };
677
678 if !registration
679 .source
680 .accepts_event_from(registration.current_aggregator, update.aggregator)
681 {
682 return Ok(());
683 }
684
685 let snapshot = OracleSnapshot::event(EventSnapshotInput {
686 id: update.id.clone(),
687 proxy: update.proxy,
688 aggregator: Some(update.aggregator),
689 metadata: registration.metadata,
690 round: update.round,
691 now_timestamp: self.registry.now_timestamp(),
692 staleness: ®istration.staleness,
693 block_number: update.block_number,
694 block_hash: None,
695 value_status: update.value_status,
696 source: registration.source.event_value_source(),
697 });
698 if let Some(kind) = Self::reconciliation_kind(®istration.source) {
699 self.queue_reconciliation(OracleReconciliationRequest {
700 id: update.id.clone(),
701 proxy: update.proxy,
702 aggregator: Some(update.aggregator),
703 event_round: snapshot.round.clone(),
704 block_number: update.block_number,
705 block_hash: None,
706 log_index: update.log_index,
707 kind,
708 });
709 }
710 self.registry.replace_snapshot(snapshot);
711 Ok(())
712 }
713
714 fn apply_price_update_with_tags(
715 &mut self,
716 update: OraclePriceUpdate,
717 labels: &[ReportTag],
718 ) -> Result<(), OracleError> {
719 let Some(mut registration) = self.registry.registrations_map().get(&update.id).cloned()
720 else {
721 return Ok(());
722 };
723
724 if let Some(new_source) = pyth_source_from_event_tags(®istration.source, labels) {
725 if let Some(stored) = self.registry.registrations_map_mut().get_mut(&update.id) {
726 stored.source = new_source.clone();
727 }
728 registration.source = new_source;
729 }
730
731 if !registration
732 .source
733 .accepts_event_from(registration.current_aggregator, update.aggregator)
734 {
735 return Ok(());
736 }
737
738 let event_round = update.round();
739 if update.value_status == OracleValueStatus::Unknown {
740 self.mark_matching_event_snapshot_unknown(
741 update.proxy,
742 &event_round,
743 update.block_number,
744 update.block_hash,
745 );
746 return Ok(());
747 }
748
749 let snapshot = OracleSnapshot::event(EventSnapshotInput {
750 id: update.id.clone(),
751 proxy: update.proxy,
752 aggregator: Some(update.aggregator),
753 metadata: registration.metadata,
754 round: event_round.clone(),
755 now_timestamp: self.registry.now_timestamp(),
756 staleness: ®istration.staleness,
757 block_number: update.block_number,
758 block_hash: update.block_hash,
759 value_status: update.value_status,
760 source: update.source,
761 });
762 if let Some(kind) = Self::reconciliation_kind(®istration.source) {
763 self.queue_reconciliation(OracleReconciliationRequest {
764 id: update.id.clone(),
765 proxy: update.proxy,
766 aggregator: Some(update.aggregator),
767 event_round,
768 block_number: update.block_number,
769 block_hash: update.block_hash,
770 log_index: update.log_index,
771 kind,
772 });
773 }
774 self.registry.replace_snapshot(snapshot);
775 Ok(())
776 }
777
778 fn mark_matching_event_snapshot_unknown(
779 &mut self,
780 proxy: Address,
781 event_round: &RoundData,
782 block_number: Option<u64>,
783 block_hash: Option<B256>,
784 ) {
785 let Some(snapshot) = self.registry.snapshots_by_proxy_mut().get_mut(&proxy) else {
786 return;
787 };
788 if !is_event_originated_source(snapshot.source) {
789 return;
790 }
791 if !proxy_round_matches_event(&snapshot.round, event_round) {
792 return;
793 }
794 if block_number.is_some() && snapshot.block_number != block_number {
795 return;
796 }
797 if block_hash.is_some() && snapshot.block_hash != block_hash {
798 return;
799 }
800
801 snapshot.mark_unknown();
802 self.clear_pending_for_proxy(proxy);
803 }
804
805 fn mark_event_snapshots_unknown(&mut self) {
806 let mut unknown_proxies = Vec::new();
807 for snapshot in self.registry.snapshots_by_proxy_mut().values_mut() {
808 if is_event_originated_source(snapshot.source) {
809 unknown_proxies.push(snapshot.proxy);
810 snapshot.mark_unknown();
811 }
812 }
813 for proxy in unknown_proxies {
814 self.clear_pending_for_proxy(proxy);
815 }
816 }
817
818 pub fn reconcile_derived_pending_with<R: crate::OracleDerivedReader>(
839 &mut self,
840 reader: &mut R,
841 ) -> DerivedReconcileReport {
842 let requests: Vec<OracleReconciliationRequest> = self
843 .pending_reconciliations
844 .iter()
845 .filter(|request| request.kind == OracleReconciliationKind::DerivedProtocolRead)
846 .cloned()
847 .collect();
848 let mut report = DerivedReconcileReport::default();
849 for request in requests {
850 let Some(registration) = self.registry.registrations_map().get(&request.id).cloned()
851 else {
852 report.failed.push(DerivedReconcileFailure {
853 request,
854 error: OracleError::FeedNotFound,
855 });
856 continue;
857 };
858 let answer = match reader.read_derived_value(®istration) {
859 Ok(answer) => answer,
860 Err(error) => {
861 report
862 .failed
863 .push(DerivedReconcileFailure { request, error });
864 continue;
865 }
866 };
867 let proxy_round = if answer == request.event_round.answer {
872 request.event_round.clone()
873 } else {
874 let now_timestamp = self.registry.now_timestamp();
875 RoundData {
876 round_id: request.event_round.round_id,
877 answer,
878 started_at: now_timestamp,
879 updated_at: now_timestamp,
880 answered_in_round: request.event_round.answered_in_round,
881 }
882 };
883 let apply = self.apply_reconciled_request(
887 &request,
888 proxy_round,
889 registration.current_aggregator,
890 registration.aggregator_layout.clone(),
891 None,
892 true,
893 );
894 match apply {
895 Ok(hooks) => report
896 .reconciled
897 .push(OracleReconciliationResult { request, hooks }),
898 Err(error) => report
899 .failed
900 .push(DerivedReconcileFailure { request, error }),
901 }
902 }
903 report
904 }
905
906 fn queue_reconciliation(&mut self, request: OracleReconciliationRequest) {
907 self.clear_pending_for_proxy(request.proxy);
908 self.pending_reconciliations.push(request);
909 }
910
911 fn reconciliation_kind(source: &FeedSource) -> Option<OracleReconciliationKind> {
915 if source.supports_proxy_reconciliation() {
916 Some(OracleReconciliationKind::Proxy)
917 } else if source.supports_derived_reconciliation() {
918 Some(OracleReconciliationKind::DerivedProtocolRead)
919 } else {
920 None
921 }
922 }
923
924 fn clear_pending_for_proxy(&mut self, proxy: Address) {
925 self.pending_reconciliations
926 .retain(|request| request.proxy != proxy);
927 }
928
929 fn complete_pending_reconciliation(&mut self, request: &OracleReconciliationRequest) {
930 self.pending_reconciliations
931 .retain(|pending| pending != request);
932 }
933
934 fn apply_reconciled_request(
935 &mut self,
936 request: &OracleReconciliationRequest,
937 proxy_round: RoundData,
938 new_aggregator: Option<Address>,
939 new_layout: Option<AggregatorLayoutEvidence>,
940 new_source: Option<FeedSource>,
941 round_is_normalized: bool,
942 ) -> Result<Vec<OracleHookEvent>, OracleError> {
943 let Some(registration) = self.registry.registrations_map().get(&request.id).cloned() else {
944 return Err(OracleError::FeedNotFound);
945 };
946
947 let proxy_round = if round_is_normalized {
948 proxy_round
949 } else {
950 registration.source.normalize_round(proxy_round)
951 };
952 let old_aggregator = registration.current_aggregator;
953 let mut snapshot = snapshot_from_proxy_read(
954 ®istration,
955 proxy_round.clone(),
956 new_aggregator,
957 self.registry.now_timestamp(),
958 );
959 let value_status =
960 reconciliation_value_status(&snapshot.round_status, request, &proxy_round);
961 snapshot.set_value_status(value_status);
962
963 let mut hooks = Vec::new();
964 if old_aggregator != new_aggregator {
965 hooks.push(OracleHookEvent::AggregatorChanged(AggregatorChange {
966 id: registration.id.clone(),
967 proxy: registration.proxy,
968 old: old_aggregator,
969 new: new_aggregator,
970 }));
971 }
972
973 match value_status {
974 OracleValueStatus::Confirmed => {
975 hooks.push(OracleHookEvent::PriceConfirmed(OraclePriceConfirmed {
976 id: registration.id.clone(),
977 proxy: registration.proxy,
978 aggregator: new_aggregator,
979 label: registration.label.clone(),
980 base: registration.base.clone(),
981 quote: registration.quote.clone(),
982 raw_answer: proxy_round.answer,
983 decimals: registration.metadata.decimals,
984 event_round_id: request.event_round.round_id,
985 updated_at: request.event_round.updated_at,
986 block_number: request.block_number,
987 block_hash: request.block_hash,
988 log_index: request.log_index,
989 round_status: snapshot.round_status.clone(),
990 value_status,
991 source: OracleValueSource::Proxy,
992 proxy_round: proxy_round.clone(),
993 }));
994 }
995 OracleValueStatus::Corrected => {
996 hooks.push(OracleHookEvent::PriceCorrected(OraclePriceCorrected {
997 id: registration.id.clone(),
998 proxy: registration.proxy,
999 aggregator: new_aggregator,
1000 label: registration.label.clone(),
1001 base: registration.base.clone(),
1002 quote: registration.quote.clone(),
1003 event_answer: request.event_round.answer,
1004 raw_answer: proxy_round.answer,
1005 decimals: registration.metadata.decimals,
1006 event_round_id: request.event_round.round_id,
1007 updated_at: request.event_round.updated_at,
1008 block_number: request.block_number,
1009 block_hash: request.block_hash,
1010 log_index: request.log_index,
1011 round_status: snapshot.round_status.clone(),
1012 value_status,
1013 source: OracleValueSource::Proxy,
1014 corrected_round: proxy_round.clone(),
1015 }));
1016 }
1017 OracleValueStatus::EventPending
1018 | OracleValueStatus::RequiresRepair
1019 | OracleValueStatus::Unknown => {}
1020 }
1021
1022 if matches!(snapshot.round_status, OracleRoundStatus::Stale { .. }) {
1023 hooks.push(OracleHookEvent::PriceStale(OraclePriceStale {
1024 id: registration.id.clone(),
1025 proxy: registration.proxy,
1026 aggregator: new_aggregator,
1027 label: registration.label.clone(),
1028 base: registration.base.clone(),
1029 quote: registration.quote.clone(),
1030 raw_answer: proxy_round.answer,
1031 decimals: registration.metadata.decimals,
1032 event_round_id: request.event_round.round_id,
1033 updated_at: request.event_round.updated_at,
1034 block_number: request.block_number,
1035 block_hash: request.block_hash,
1036 log_index: request.log_index,
1037 round_status: snapshot.round_status.clone(),
1038 value_status,
1039 source: OracleValueSource::Proxy,
1040 proxy_round: proxy_round.clone(),
1041 }));
1042 }
1043
1044 if let Some(stored) = self
1045 .registry
1046 .registrations_map_mut()
1047 .get_mut(®istration.id)
1048 {
1049 stored.current_aggregator = new_aggregator;
1050 stored.aggregator_layout = new_layout;
1051 if let Some(new_source) = new_source {
1052 stored.source = new_source;
1053 }
1054 }
1055 self.registry.replace_snapshot(snapshot);
1056 self.complete_pending_reconciliation(request);
1057 Ok(hooks)
1058 }
1059}
1060
1061#[derive(Clone, Debug, Default)]
1063pub struct OracleReconciler {
1064 queue: std::collections::VecDeque<OracleReconciliationRequest>,
1065}
1066
1067impl OracleReconciler {
1068 pub fn enqueue(&mut self, request: OracleReconciliationRequest) {
1070 self.queue.push_back(request);
1071 }
1072
1073 pub async fn reconcile_next<P: ChainlinkFeedProvider>(
1075 &mut self,
1076 tracker: &mut OracleTracker,
1077 provider: &P,
1078 ) -> Result<Option<OracleReconciliationResult>, OracleError> {
1079 let request = loop {
1080 let Some(front) = self.queue.front().cloned() else {
1081 return Ok(None);
1082 };
1083 if front.kind == OracleReconciliationKind::DerivedProtocolRead {
1084 self.queue.pop_front();
1088 continue;
1089 }
1090 break front;
1091 };
1092
1093 let Some(registration) = tracker
1094 .registry
1095 .registrations_map()
1096 .get(&request.id)
1097 .cloned()
1098 else {
1099 return Err(OracleError::FeedNotFound);
1100 };
1101 if !registration.source.supports_proxy_reconciliation() {
1102 self.queue.pop_front();
1103 return Ok(Some(OracleReconciliationResult {
1104 request,
1105 hooks: Vec::new(),
1106 }));
1107 }
1108 let block_ref = request.block_ref();
1109 let reconciled = if matches!(
1110 registration.source,
1111 FeedSource::AaveSynchronicityPegToBase { .. }
1112 ) {
1113 reconcile_aave_synchronicity_peg_to_base(
1114 tracker,
1115 provider,
1116 ®istration,
1117 &request,
1118 block_ref,
1119 )
1120 .await?
1121 } else {
1122 reconcile_single_proxy_source(tracker, provider, ®istration, &request, block_ref)
1123 .await?
1124 };
1125 let hooks = tracker.apply_reconciled_request(
1126 &request,
1127 reconciled.proxy_round,
1128 reconciled.new_aggregator,
1129 reconciled.new_layout,
1130 reconciled.new_source,
1131 reconciled.round_is_normalized,
1132 )?;
1133 self.queue.pop_front();
1134
1135 Ok(Some(OracleReconciliationResult { request, hooks }))
1136 }
1137}
1138
1139struct ReconciledSourceRead {
1140 proxy_round: RoundData,
1141 new_aggregator: Option<Address>,
1142 new_layout: Option<AggregatorLayoutEvidence>,
1143 new_source: Option<FeedSource>,
1144 round_is_normalized: bool,
1145}
1146
1147async fn reconcile_single_proxy_source<P: ChainlinkFeedProvider>(
1148 tracker: &mut OracleTracker,
1149 provider: &P,
1150 registration: &FeedRegistration,
1151 request: &OracleReconciliationRequest,
1152 block_ref: Option<OracleBlockRef>,
1153) -> Result<ReconciledSourceRead, OracleError> {
1154 let read_proxy = registration.source.read_proxy(registration.proxy);
1155 let proxy_round = provider.latest_round_data_at(read_proxy, block_ref).await?;
1156 let new_aggregator = provider
1157 .aggregator_at(read_proxy, block_ref)
1158 .await
1159 .unwrap_or_else(|_| {
1160 tracker
1161 .current_aggregator(&request.id)
1162 .or(request.aggregator)
1163 });
1164 let new_layout = tracker
1165 .detect_aggregator_layout(provider, new_aggregator, block_ref)
1166 .await;
1167 Ok(ReconciledSourceRead {
1168 proxy_round,
1169 new_aggregator,
1170 new_layout,
1171 new_source: None,
1172 round_is_normalized: false,
1173 })
1174}
1175
1176async fn reconcile_aave_synchronicity_peg_to_base<P: ChainlinkFeedProvider>(
1177 tracker: &mut OracleTracker,
1178 provider: &P,
1179 registration: &FeedRegistration,
1180 request: &OracleReconciliationRequest,
1181 block_ref: Option<OracleBlockRef>,
1182) -> Result<ReconciledSourceRead, OracleError> {
1183 let FeedSource::AaveSynchronicityPegToBase {
1184 asset_to_peg_proxy,
1185 asset_to_peg_aggregator,
1186 peg_to_base_proxy,
1187 peg_to_base_aggregator,
1188 ..
1189 } = registration.source.clone()
1190 else {
1191 unreachable!("caller checked source family")
1192 };
1193
1194 let asset_round = provider
1195 .latest_round_data_at(asset_to_peg_proxy, block_ref)
1196 .await?;
1197 let peg_round = provider
1198 .latest_round_data_at(peg_to_base_proxy, block_ref)
1199 .await?;
1200 let new_asset_aggregator = provider
1201 .aggregator_at(asset_to_peg_proxy, block_ref)
1202 .await
1203 .unwrap_or(Some(asset_to_peg_aggregator));
1204 let new_peg_aggregator = provider
1205 .aggregator_at(peg_to_base_proxy, block_ref)
1206 .await
1207 .unwrap_or(Some(peg_to_base_aggregator));
1208 let changed_dependency_round = if new_peg_aggregator == request.aggregator {
1209 &peg_round
1210 } else {
1211 &asset_round
1212 };
1213 let derived_answer = registration
1214 .source
1215 .normalize_dependency_answers(asset_round.answer, peg_round.answer)
1216 .ok_or_else(|| {
1217 OracleError::Config(crate::error::OracleConfigError::Other(
1218 "source is not an Aave peg-to-base synchronicity source".to_string(),
1219 ))
1220 })?;
1221 let proxy_round = RoundData {
1222 round_id: changed_dependency_round.round_id,
1223 answer: derived_answer,
1224 started_at: changed_dependency_round.started_at,
1225 updated_at: changed_dependency_round.updated_at,
1226 answered_in_round: changed_dependency_round.answered_in_round,
1227 };
1228 let new_source = match (new_asset_aggregator, new_peg_aggregator) {
1229 (Some(asset_aggregator), Some(peg_aggregator)) => {
1230 registration.source.with_synchronicity_dependency_state(
1231 asset_aggregator,
1232 asset_round.answer,
1233 peg_aggregator,
1234 peg_round.answer,
1235 )
1236 }
1237 _ => None,
1238 };
1239 let new_layout = tracker
1240 .detect_aggregator_layout(provider, new_asset_aggregator, block_ref)
1241 .await;
1242
1243 Ok(ReconciledSourceRead {
1244 proxy_round,
1245 new_aggregator: new_asset_aggregator,
1246 new_layout,
1247 new_source,
1248 round_is_normalized: true,
1249 })
1250}
1251
1252async fn reconcile_current_aave_synchronicity_peg_to_base<P: ChainlinkFeedProvider>(
1253 tracker: &mut OracleTracker,
1254 provider: &P,
1255 registration: &FeedRegistration,
1256) -> Result<ReconciledSourceRead, OracleError> {
1257 let FeedSource::AaveSynchronicityPegToBase {
1258 asset_to_peg_proxy,
1259 asset_to_peg_aggregator,
1260 peg_to_base_proxy,
1261 peg_to_base_aggregator,
1262 ..
1263 } = registration.source.clone()
1264 else {
1265 unreachable!("caller checked source family")
1266 };
1267
1268 let asset_round = provider.latest_round_data(asset_to_peg_proxy).await?;
1269 let peg_round = provider.latest_round_data(peg_to_base_proxy).await?;
1270 let new_asset_aggregator = provider
1271 .aggregator(asset_to_peg_proxy)
1272 .await
1273 .unwrap_or(Some(asset_to_peg_aggregator));
1274 let new_peg_aggregator = provider
1275 .aggregator(peg_to_base_proxy)
1276 .await
1277 .unwrap_or(Some(peg_to_base_aggregator));
1278 let representative_round = if peg_round.updated_at >= asset_round.updated_at {
1279 &peg_round
1280 } else {
1281 &asset_round
1282 };
1283 let derived_answer = registration
1284 .source
1285 .normalize_dependency_answers(asset_round.answer, peg_round.answer)
1286 .ok_or_else(|| {
1287 OracleError::Config(crate::error::OracleConfigError::Other(
1288 "source is not an Aave peg-to-base synchronicity source".to_string(),
1289 ))
1290 })?;
1291 let proxy_round = RoundData {
1292 round_id: representative_round.round_id,
1293 answer: derived_answer,
1294 started_at: representative_round.started_at,
1295 updated_at: representative_round.updated_at,
1296 answered_in_round: representative_round.answered_in_round,
1297 };
1298 let new_source = match (new_asset_aggregator, new_peg_aggregator) {
1299 (Some(asset_aggregator), Some(peg_aggregator)) => {
1300 registration.source.with_synchronicity_dependency_state(
1301 asset_aggregator,
1302 asset_round.answer,
1303 peg_aggregator,
1304 peg_round.answer,
1305 )
1306 }
1307 _ => None,
1308 };
1309 let new_layout = tracker
1310 .detect_aggregator_layout(provider, new_asset_aggregator, None)
1311 .await;
1312
1313 Ok(ReconciledSourceRead {
1314 proxy_round,
1315 new_aggregator: new_asset_aggregator,
1316 new_layout,
1317 new_source,
1318 round_is_normalized: true,
1319 })
1320}
1321
1322impl OracleTracker {
1323 async fn detect_aggregator_layout<P: ChainlinkFeedProvider>(
1324 &mut self,
1325 provider: &P,
1326 aggregator: Option<Address>,
1327 block: Option<OracleBlockRef>,
1328 ) -> Option<AggregatorLayoutEvidence> {
1329 self.registry
1330 .detect_aggregator_layout(provider, aggregator, block)
1331 .await
1332 }
1333
1334 fn current_aggregator(&self, id: &FeedId) -> Option<Address> {
1335 self.registry
1336 .registrations_map()
1337 .get(id)
1338 .and_then(|registration| registration.current_aggregator)
1339 }
1340}
1341
1342fn reconciliation_value_status(
1343 round_status: &OracleRoundStatus,
1344 request: &OracleReconciliationRequest,
1345 proxy_round: &RoundData,
1346) -> OracleValueStatus {
1347 match round_status {
1348 OracleRoundStatus::Unknown => OracleValueStatus::RequiresRepair,
1349 OracleRoundStatus::Fresh
1350 | OracleRoundStatus::Stale { .. }
1351 | OracleRoundStatus::IncompleteRound
1352 | OracleRoundStatus::InvalidAnswer
1353 if proxy_round_matches_event(proxy_round, &request.event_round) =>
1354 {
1355 OracleValueStatus::Confirmed
1356 }
1357 OracleRoundStatus::Fresh
1358 | OracleRoundStatus::Stale { .. }
1359 | OracleRoundStatus::IncompleteRound
1360 | OracleRoundStatus::InvalidAnswer => OracleValueStatus::Corrected,
1361 }
1362}
1363
1364fn is_event_originated_source(source: OracleValueSource) -> bool {
1365 matches!(
1366 source,
1367 OracleValueSource::Event | OracleValueSource::Derived
1368 )
1369}
1370
1371fn proxy_round_matches_event(proxy_round: &RoundData, event_round: &RoundData) -> bool {
1372 proxy_round.round_id == event_round.round_id
1373 && proxy_round.answer == event_round.answer
1374 && proxy_round.updated_at == event_round.updated_at
1375}
1376
1377fn pyth_source_from_event_tags(source: &FeedSource, labels: &[ReportTag]) -> Option<FeedSource> {
1378 let (_pyth, price_id, current_expo, current_conf) = source.pyth_source()?;
1379 let mut expo = current_expo;
1380 let mut conf = current_conf;
1381 let mut saw_pyth_label = false;
1382
1383 for label in labels {
1384 match label.key.as_str() {
1385 "pyth_expo" => {
1386 if let Ok(value) = label.value.parse::<i32>() {
1387 expo = value;
1388 saw_pyth_label = true;
1389 }
1390 }
1391 "pyth_conf" => {
1392 if let Ok(value) = label.value.parse::<u64>() {
1393 conf = value;
1394 saw_pyth_label = true;
1395 }
1396 }
1397 _ => {}
1398 }
1399 }
1400
1401 saw_pyth_label.then(|| {
1402 source
1403 .with_pyth_event_metadata(price_id, expo, conf)
1404 .expect("pyth_source returned Some for Pyth source")
1405 })
1406}
1407
1408#[allow(dead_code)]
1409fn _ethereum_batch_report_type_is_supported(_: &ReactiveBatchReport<Ethereum>) {}