Repository: blocksense-network/blocksense
Pull request #953: Hstaykov/feed processing to libs
Author: login: HristoStaykov
Created: 2025-02-21T12:34:15Z | Merged: 2025-03-11T10:06:17Z

Implement the necessary checks a Reporter needs to perform before participating in the second round of consensus. The sequencer provides all the votes from a quorum of reporters + their signatures that justify an aggregate update. The checks are in libs/feeds_processing/src/utils.rs:validate function.


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 31598ab1f0..5dbe9df7d8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4346,6 +4346,31 @@ dependencies = [
  "utils",
 ]
 
+[[package]]
+name = "feeds_processing"
+version = "0.1.1"
+dependencies = [
+ "alloy 0.8.3",
+ "alloy-encode-packed",
+ "alloy-primitives",
+ "anomaly_detection",
+ "anyhow",
+ "config 0.1.1",
+ "crypto",
+ "data_feeds",
+ "feed_registry",
+ "gnosis_safe",
+ "hex",
+ "once_cell",
+ "prometheus 0.1.1",
+ "ringbuf",
+ "serde 1.0.217",
+ "serde_json",
+ "tokio 1.43.0",
+ "tracing",
+ "utils",
+]
+
 [[package]]
 name = "ff"
 version = "0.13.0"
@@ -9342,6 +9367,7 @@ dependencies = [
  "envload",
  "eyre",
  "feed_registry",
+ "feeds_processing",
  "futures",
  "futures-util",
  "gnosis_safe",
diff --git a/apps/sequencer/Cargo.toml b/apps/sequencer/Cargo.toml
index 2b2766e541..e1472dc551 100644
--- a/apps/sequencer/Cargo.toml
+++ b/apps/sequencer/Cargo.toml
@@ -7,6 +7,7 @@ edition.workspace = true
 
 [dependencies]
 data_feeds = { path = "../../libs/data_feeds" }
+feeds_processing = { path = "../../libs/feeds_processing"}
 anomaly_detection = { path = "../../libs/anomaly_detection" }
 config = { path = "../../libs/config" }
 blocksense-registry = { path = "../../libs/registry" }
diff --git a/apps/sequencer/src/block_creator.rs b/apps/sequencer/src/block_creator.rs
index ec809e1156..5db93f0817 100644
--- a/apps/sequencer/src/block_creator.rs
+++ b/apps/sequencer/src/block_creator.rs
@@ -24,7 +24,7 @@ use utils::time::{current_unix_time, system_time_to_millis};
 
 use crate::feeds::feed_config_conversions::feed_config_to_block;
 use crate::sequencer_state::SequencerState;
-use crate::BatchedAggegratesToSend;
+use data_feeds::feeds_processing::BatchedAggegratesToSend;
 
 pub async fn block_creator_loop(
     sequencer_state: Data<SequencerState>,
diff --git a/apps/sequencer/src/feeds/feed_slots_processor.rs b/apps/sequencer/src/feeds/feed_slots_processor.rs
index ab1a8ef673..51b605e381 100644
--- a/apps/sequencer/src/feeds/feed_slots_processor.rs
+++ b/apps/sequencer/src/feeds/feed_slots_processor.rs
@@ -1,47 +1,29 @@
 use crate::reporters::reporter::SharedReporters;
 use crate::sequencer_state::SequencerState;
 use actix_web::web::Data;
-use anomaly_detection::ingest::anomaly_detector_aggregate;
-use config::PublishCriteria;
-use data_feeds::feeds_processing::{VotedFeedUpdate, VotedFeedUpdateWithProof};
-use eyre::{eyre, Result};
-use eyre::{Context, ContextCompat};
+use data_feeds::feeds_processing::VotedFeedUpdateWithProof;
+use eyre::{eyre, ContextCompat, Result};
 use feed_registry::aggregate::FeedAggregate;
 use feed_registry::feed_registration_cmds::ProcessorResultValue;
-use feed_registry::registry::AllFeedsReports;
-use feed_registry::registry::FeedAggregateHistory;
-use feed_registry::registry::SlotTimeTracker;
-use feed_registry::types::FeedsSlotProcessorCmds;
-use feed_registry::types::{DataFeedPayload, FeedType};
-use feed_registry::types::{FeedMetaData, Repeatability, Timestamp};
+use feed_registry::registry::{AllFeedsReports, FeedAggregateHistory, SlotTimeTracker};
+use feed_registry::types::{
+    FeedMetaData, FeedType, FeedsSlotProcessorCmds, Repeatability, Timestamp,
+};
+use feeds_processing::utils::{consume_reports, ConsumedReports};
 use prometheus::{inc_metric, metrics::FeedsMetrics};
-use ringbuf::traits::consumer::Consumer;
-use std::collections::HashMap;
 use std::sync::Arc;
 use std::time::Duration;
 use tokio::sync::mpsc;
 use tokio::sync::RwLock;
 use tracing::error;
-use tracing::warn;
 use tracing::{debug, info};
 use utils::time::current_unix_time;
 
-const AD_MIN_DATA_POINTS_THRESHOLD: usize = 100;
-
 pub struct FeedSlotsProcessor {
     name: String,
     key: u32,
 }
 
-#[derive(Debug)]
-pub struct ConsumedReports {
-    pub is_quorum_reached: bool,
-    pub skip_publishing: bool,
-    pub ad_score: Option<f64>,
-    pub result_post_to_contract: Option<VotedFeedUpdateWithProof>,
-    pub end_slot_timestamp: Timestamp,
-}
-
 impl FeedSlotsProcessor {
     pub fn new(name: String, key: u32) -> FeedSlotsProcessor {
         FeedSlotsProcessor { name, key }
@@ -84,136 +66,6 @@ impl FeedSlotsProcessor {
         res
     }
 
-    #[allow(clippy::too_many_arguments)]
-    async fn consume_reports(
-        &self,
-        reports: &HashMap<u64, DataFeedPayload>,
-        feed_type: &FeedType,
-        slot: u64,
-        quorum_percentage: f32,
-        skip_publish_if_less_then_percentage: f64,
-        always_publish_heartbeat_ms: Option<u128>,
-        end_slot_timestamp: Timestamp,
-        num_valid_reporters: usize,
-        is_oneshot: bool,
-        aggregator: FeedAggregate,
-        history: &Arc<RwLock<FeedAggregateHistory>>,
-        feed_id: u32,
-    ) -> ConsumedReports {
-        let values = self.collect_reported_values(feed_type, reports, slot);
-
-        if values.is_empty() {
-            info!("No reports found for feed: {} slot: {}!", self.name, &slot);
-            ConsumedReports {
-                is_quorum_reached: false,
-                skip_publishing: true,
-                ad_score: None,
-                result_post_to_contract: None,
-                end_slot_timestamp,
-            }
-        } else {
-            let total_votes_count = values.len() as f32;
-            let required_votes_count = quorum_percentage * 0.01f32 * (num_valid_reporters as f32);
-            let is_quorum_reached = required_votes_count <= total_votes_count;
-            let mut skip_publishing = false;
-            if !is_quorum_reached {
-                warn!(
-                    "Insufficient quorum of reports to post to contract for feed: {} slot: {}! Expected at least a quorum of {}, but received {} out of {} valid votes.",
-                    self.name, &slot, quorum_percentage, total_votes_count, num_valid_reporters
-                );
-            }
-
-            // Dispatch to concrete FeedAggregate implementation.
-            let result_post_to_contract = VotedFeedUpdate {
-                feed_id,
-                value: aggregator.aggregate(&values[..]), // Perform the concrete aggregation
-                end_slot_timestamp,
-            };
-
-            let mut proof: Vec<DataFeedPayload> = Vec::new();
-            for (_, v) in reports.iter() {
-                proof.push(v.clone());
-            }
-
-            let mut ad_score_opt: Option<f64> = None;
-
-            // Oneshot feeds have no history, so we cannot perform anomaly detection on them.
-            if !is_oneshot {
-                if let FeedType::Numerical(candidate_value) = result_post_to_contract.value {
-                    let ad_score = self
-                        .perform_anomaly_detection(history, candidate_value)
-                        .await;
-                    match ad_score {
-                        Ok(ad_score) => {
-                            info!(
-                                "AD_score for {:?} is {}",
-                                result_post_to_contract.value, ad_score
-                            );
-                            ad_score_opt = Some(ad_score)
-                        }
-                        Err(e) => {
-                            warn!("Anomaly Detection failed with error - {}", e);
-                        }
-                    }
-                    {
-                        let criteria = PublishCriteria {
-                            feed_id,
-                            skip_publish_if_less_then_percentage,
-                            always_publish_heartbeat_ms,
-                            peg_to_value: None,
-                            peg_tolerance_percentage: 0.0f64,
-                        };
-                        debug!("Get a read lock on history [feed {feed_id}]");
-                        let history_guard = history.read().await;
-                        skip_publishing =
-                            result_post_to_contract.should_skip(&criteria, &history_guard);
-                        debug!("Release the read lock on history [feed {feed_id}]");
-                    }
-                }
-            }
-            let res = ConsumedReports {
-                is_quorum_reached,
-                skip_publishing,
-                ad_score: ad_score_opt,
-                result_post_to_contract: Some(VotedFeedUpdateWithProof {
-                    update: result_post_to_contract,
-                    proof,
-                }),
-                end_slot_timestamp,
-            };
-            info!("[feed {feed_id}] result_post_to_contract = {:?}", res);
-            res
-        }
-    }
-
-    fn collect_reported_values(
-        &self,
-        expected_feed_type: &FeedType,
-        reports: &HashMap<u64, DataFeedPayload>,
-        slot: u64,
-    ) -> Vec<FeedType> {
-        let feed_id = self.key;
-        let mut values: Vec<FeedType> = vec![];
-        for kv in reports {
-            match &kv.1.result {
-                Ok(value) => {
-                    if value.same_enum_type_as(expected_feed_type) {
-                        values.push(value.clone());
-                    } else {
-                        warn!("Wrong value type reported by reporter {} for feed id {} slot {}! {} expected", kv.0, feed_id, slot, expected_feed_type.enum_type_to_string());
-                    }
-                }
-                Err(_) => {
-                    warn!(
-                        "Got error from reporter {} for feed id {} slot {}",
-                        kv.0, feed_id, slot
-                    );
-                }
-            }
-        }
-        values
-    }
-
     #[allow(clippy::too_many_arguments)]
     async fn process_end_of_slot(
         &self,
@@ -257,22 +109,22 @@ impl FeedSlotsProcessor {
                 debug!("Get a write lock on reports [feed {feed_id}]");
                 let mut reports = reports.write().await;
                 debug!("Acquired a write lock on reports [feed {feed_id}]");
-                consumed_reports = self
-                    .consume_reports(
-                        &reports.report,
-                        feed_type,
-                        slot,
-                        quorum_percentage,
-                        skip_publish_if_less_then_percentage,
-                        always_publish_heartbeat_ms,
-                        end_slot_timestamp,
-                        num_valid_reporters,
-                        is_oneshot,
-                        aggregator,
-                        history,
-                        feed_id,
-                    )
-                    .await;
+                consumed_reports = consume_reports(
+                    self.name.as_str(),
+                    &reports.report,
+                    feed_type,
+                    slot,
+                    quorum_percentage,
+                    skip_publish_if_less_then_percentage,
+                    always_publish_heartbeat_ms,
+                    end_slot_timestamp,
+                    num_valid_reporters,
+                    is_oneshot,
+                    aggregator,
+                    Some(history.clone()),
+                    feed_id,
+                )
+                .await;
 
                 reports.clear();
                 drop(reports);
@@ -348,66 +200,6 @@ impl FeedSlotsProcessor {
             .map_err(|e| eyre!("[post_to_contract]: {e}"))
     }
 
-    async fn perform_anomaly_detection(
-        &self,
-        history: &Arc<RwLock<FeedAggregateHistory>>,
-        candidate_value: f64,
-    ) -> Result<f64, eyre::Error> {
-        let feed_id = self.key;
-        let history = history.clone();
-        let anomaly_detection_future = async move {
-            debug!("Get a read lock on history [feed {feed_id}]");
-            let history_lock = history.read().await;
-
-            // The first slice is from the current read position to the end of the array
-            // The second slice represents the segment from the start of the array up to the current write position if the buffer has wrapped around
-            let heap = history_lock
-                .get(feed_id)
-                .context("Missing key from History!")?;
-            let (first, last) = heap.as_slices();
-            let history_vec: Vec<&FeedType> =
-                first.iter().chain(last.iter()).map(|h| &h.value).collect();
-            let mut numerical_vec: Vec<f64> = history_vec
-                .iter()
-                .filter_map(|feed| {
-                    if let FeedType::Numerical(value) = feed {
-                        Some(*value)
-                    } else if let FeedType::Text(_) = feed {
-                        warn!("Anomaly Detection not implemented for FeedType::Text, skipping...");
-                        None
-                    } else {
-                        warn!("Anomaly Detection does not recognize FeedType, skipping...");
-                        None
-                    }
-                })
-                .collect();
-
-            drop(history_lock);
-            debug!("Release the read lock on history [feed {feed_id}]");
-
-            numerical_vec.push(candidate_value);
-
-            // Get AD prediction only if enough data is present
-            if numerical_vec.len() > AD_MIN_DATA_POINTS_THRESHOLD {
-                debug!("Starting anomaly detection for [feed {feed_id}]");
-                anomaly_detector_aggregate(numerical_vec).map_err(|e| eyre!("{e}"))
-            } else {
-                Err(eyre!(
-                    "Skipping anomaly detection; numerical_vec.len() = {} threshold: {}",
-                    numerical_vec.len(),
-                    AD_MIN_DATA_POINTS_THRESHOLD
-                ))
-            }
-        };
-
-        tokio::task::Builder::new()
-            .name("anomaly_detection")
-            .spawn(anomaly_detection_future)
-            .context("Failed to spawn feed slots manager anomaly detection!")?
-            .await
-            .context("Failed to join feed slots manager anomaly detection!")?
-    }
-
     async fn increase_quorum_metric(
         &self,
         feed_metrics: Option<Arc<RwLock<FeedsMetrics>>>,
@@ -621,7 +413,7 @@ pub mod tests {
         let first_report_start_time = SystemTime::now();
         let always_publish_heartbeat_ms = None;
         let feed_metadata = FeedMetaData::new(
-            name,
+            name.to_string(),
             report_interval_ms,
             quorum_percentage,
             skip_publish_if_less_then_percentage,
@@ -813,7 +605,7 @@ pub mod tests {
         let first_report_start_time = SystemTime::now();
         let always_publish_heartbeat_ms = None;
         let feed_metadata = FeedMetaData::new(
-            name,
+            name.to_string(),
             report_interval_ms,
             quorum_percentage,
             skip_publish_if_less_then_percentage,
@@ -916,7 +708,7 @@ pub mod tests {
             SystemTime::now() - Duration::from_millis(report_interval_ms * 3);
         let always_publish_heartbeat_ms = None;
         let feed_metadata = FeedMetaData::new(
-            name,
+            name.to_string(),
             report_interval_ms,
             quorum_percentage,
             skip_publish_if_less_then_percentage,
@@ -1024,7 +816,7 @@ pub mod tests {
             SystemTime::now() - Duration::from_millis(report_interval_ms * 3);
         let always_publish_heartbeat_ms = None;
         let feed_metadata = FeedMetaData::new(
-            name,
+            name.to_string(),
             report_interval_ms,
             quorum_percentage,
             skip_publish_if_less_then_percentage,
@@ -1130,7 +922,7 @@ pub mod tests {
         let first_report_start_time =
             SystemTime::now() - Duration::from_millis(report_interval_ms * 3);
         let feed_metadata = FeedMetaData::new(
-            name,
+            name.to_string(),
             report_interval_ms,
             quorum_percentage,
             skip_publish_if_less_then_percentage,
diff --git a/apps/sequencer/src/feeds/feeds_slots_manager.rs b/apps/sequencer/src/feeds/feeds_slots_manager.rs
index c118490f88..0bd0c429ee 100644
--- a/apps/sequencer/src/feeds/feeds_slots_manager.rs
+++ b/apps/sequencer/src/feeds/feeds_slots_manager.rs
@@ -270,7 +270,7 @@ pub async fn register_feed_with_config(
         }
 
         let new_feed_metadata = FeedMetaData::new(
-            &new_name,
+            new_name,
             new_feed_config.report_interval_ms,
             new_feed_config.quorum_percentage,
             new_feed_config.skip_publish_if_less_then_percentage,
diff --git a/apps/sequencer/src/feeds/votes_result_sender.rs b/apps/sequencer/src/feeds/votes_result_sender.rs
index 61d6fc420c..9e195c4b0f 100644
--- a/apps/sequencer/src/feeds/votes_result_sender.rs
+++ b/apps/sequencer/src/feeds/votes_result_sender.rs
@@ -2,11 +2,13 @@ use crate::providers::eth_send_utils::{
     eth_batch_send_to_all_contracts, get_serialized_updates_for_network,
 };
 use crate::providers::provider::{GNOSIS_SAFE_CONTRACT_NAME, PRICE_FEED_CONTRACT_NAME};
-use crate::{sequencer_state::SequencerState, BatchedAggegratesToSend};
+use crate::sequencer_state::SequencerState;
 use actix_web::web::Data;
 use alloy::hex::{self, FromHex, ToHexExt};
 use alloy::providers::Provider;
+use alloy_primitives::map::HashMap;
 use alloy_primitives::{Address, Bytes, U256};
+use data_feeds::feeds_processing::BatchedAggegratesToSend;
 use feed_registry::types::Repeatability::Periodic;
 use gnosis_safe::data_types::ConsensusSecondRoundBatch;
 use gnosis_safe::utils::{create_safe_tx, generate_transaction_hash, SafeMultisig};
@@ -130,6 +132,8 @@ async fn try_send_aggregation_consensus_trigger_to_reporters(
         let feeds_metrics = sequencer_state.feeds_metrics.clone();
         let feeds_config = sequencer_state.active_feeds.clone();
 
+        let mut feeds_rounds = HashMap::new();
+
         let serialized_updates = match get_serialized_updates_for_network(
             net,
             provider,
@@ -137,6 +141,7 @@ async fn try_send_aggregation_consensus_trigger_to_reporters(
             &provider_settings,
             Some(feeds_metrics),
             feeds_config,
+            &mut feeds_rounds,
         )
         .await
         {
@@ -222,6 +227,7 @@ async fn try_send_aggregation_consensus_trigger_to_reporters(
             network: net.to_string(),
             calldata: hex::encode(serialized_updates),
             updates: updates.updates,
+            feeds_rounds,
             proofs: updates.proofs,
         };
 
diff --git a/apps/sequencer/src/http_handlers/data_feeds.rs b/apps/sequencer/src/http_handlers/data_feeds.rs
index b061a18925..ac94983bc4 100644
--- a/apps/sequencer/src/http_handlers/data_feeds.rs
+++ b/apps/sequencer/src/http_handlers/data_feeds.rs
@@ -1,7 +1,6 @@
 use actix_web::http::StatusCode;
 use alloy_primitives::{FixedBytes, PrimitiveSignature};
 use chrono::{TimeZone, Utc};
-use crypto::PublicKey;
 use eyre::Result;
 use gnosis_safe::utils::SignatureWithAddress;
 use std::str::FromStr;
@@ -22,42 +21,16 @@ use uuid::Uuid;
 use crate::feeds::feed_slots_processor::FeedSlotsProcessor;
 use crate::http_handlers::MAX_SIZE;
 use crate::sequencer_state::SequencerState;
-use crypto::verify_signature;
-use crypto::Signature;
 use feed_registry::registry::FeedAggregateHistory;
+use feed_registry::types::DataFeedPayload;
 use feed_registry::types::FeedMetaData;
-use feed_registry::types::{DataFeedPayload, FeedResult, Timestamp};
+use feeds_processing::utils::check_signature;
 use gnosis_safe::data_types::ReporterResponse;
 use prometheus::{inc_metric, inc_vec_metric};
 use std::time::{SystemTime, UNIX_EPOCH};
 use tokio::sync::{mpsc, RwLock};
 use tokio::time::Duration;
 
-pub fn check_signature(
-    signature: &Signature,
-    pub_key: &PublicKey,
-    feed_id: &str,
-    timestamp: Timestamp,
-    feed_result: &FeedResult,
-) -> bool {
-    let mut byte_buffer: Vec<u8> = feed_id
-        .as_bytes()
-        .iter()
-        .copied()
-        .chain(timestamp.to_be_bytes().to_vec())
-        .collect();
-
-    match feed_result {
-        Ok(result) => {
-            byte_buffer.extend(result.as_bytes(18));
-        }
-        Err(error) => {
-            warn!("Reported error for feed_id {} : {}", feed_id, error);
-        }
-    };
-    verify_signature(pub_key, signature, &byte_buffer)
-}
-
 async fn process_report(
     sequencer_state: &web::Data<SequencerState>,
     data_feed: DataFeedPayload,
diff --git a/apps/sequencer/src/lib.rs b/apps/sequencer/src/lib.rs
index 8c9659e1d9..9f8ab854b2 100644
--- a/apps/sequencer/src/lib.rs
+++ b/apps/sequencer/src/lib.rs
@@ -7,38 +7,3 @@ pub mod metrics_collector;
 pub mod providers;
 pub mod reporters;
 pub mod sequencer_state;
-
-use data_feeds::feeds_processing::VotedFeedUpdate;
-use feed_registry::types::DataFeedPayload;
-use std::collections::HashMap;
-use tracing::error;
-
-#[derive(Debug, Clone, Default)]
-pub struct BatchedAggegratesToSend {
-    pub block_height: u64,
-    pub updates: Vec<VotedFeedUpdate>,
-    // The key in this map is the feed id for which we provide a proof for the aggregated value.
-    pub proofs: HashMap<u32, Vec<DataFeedPayload>>,
-}
-
-impl BatchedAggegratesToSend {
-    // The updates to be sent to different networks go through multiple filters.
-    // Eventually we might need to reduce the proof to only contain records for
-    // the relevant updates. The following function does that and returns the
-    // count of removed elements.
-    pub fn normalize_proof(&mut self) -> usize {
-        let removed_elements_count = self.proofs.len() - self.updates.len();
-
-        if removed_elements_count > 0 {
-            let mut normalized_proofs = HashMap::new();
-            for update in self.updates.iter() {
-                let Some(proof) = self.proofs.remove(&update.feed_id) else {
-                    error!("Logical ERROR! no proof found for key: {}", update.feed_id);
-                    continue;
-                };
-                normalized_proofs.insert(update.feed_id, proof);
-            }
-        }
-        removed_elements_count
-    }
-}
diff --git a/apps/sequencer/src/providers/eth_send_utils.rs b/apps/sequencer/src/providers/eth_send_utils.rs
index 9b071e922c..1acfa4ee7c 100644
--- a/apps/sequencer/src/providers/eth_send_utils.rs
+++ b/apps/sequencer/src/providers/eth_send_utils.rs
@@ -6,23 +6,22 @@ use alloy::{
     providers::{Provider, ProviderBuilder},
     rpc::types::eth::TransactionRequest,
 };
-use config::FeedConfig;
-use data_feeds::feeds_processing::VotedFeedUpdate;
+use config::{FeedConfig, FeedStrideAndDecimals};
+use data_feeds::feeds_processing::{BatchedAggegratesToSend, VotedFeedUpdate};
 use eyre::{eyre, Result};
 use std::{collections::HashMap, mem, sync::Arc};
 use tokio::{sync::Mutex, sync::RwLock, time::Duration};
 use utils::to_hex_string;
 
 use crate::{
-    providers::adfs_gen_calldata::adfs_serialize_updates,
     providers::provider::{
         parse_eth_address, ProviderStatus, RpcProvider, SharedRpcProviders,
         EVENT_FEED_CONTRACT_NAME, PRICE_FEED_CONTRACT_NAME,
     },
     sequencer_state::SequencerState,
-    BatchedAggegratesToSend,
 };
 use feed_registry::types::{Repeatability, Repeatability::Periodic};
+use feeds_processing::adfs_gen_calldata::adfs_serialize_updates;
 use futures::stream::FuturesUnordered;
 use paste::paste;
 use prometheus::{inc_metric, inc_metric_by};
@@ -49,7 +48,7 @@ pub async fn deploy_contract(
 async fn legacy_serialize_updates(
     net: &str,
     updates: &BatchedAggegratesToSend,
-    feeds_config: Arc<RwLock<HashMap<u32, FeedConfig>>>,
+    feeds_config: HashMap<u32, FeedStrideAndDecimals>,
 ) -> Result<String> {
     let mut result: String = Default::default();
 
@@ -61,12 +60,12 @@ async fn legacy_serialize_updates(
     let mut num_reported_feeds = 0;
     for update in &updates.updates {
         let feed_id = update.feed_id;
-        let feed_config = feeds_config.read().await.get(&feed_id).cloned();
+        let feed_config = feeds_config.get(&feed_id);
 
         let digits_in_fraction = match &feed_config {
             Some(f) => f.decimals,
             None => {
-                warn!("Propagating result for unregistered feed! Support left for legacy one shot feeds of 32 bytes size. Decimale default to 18");
+                error!("Propagating result for unregistered feed! Support left for legacy one shot feeds of 32 bytes size. Decimal default to 18");
                 18
             }
         };
@@ -111,6 +110,7 @@ pub async fn get_serialized_updates_for_network(
     provider_settings: &config::Provider,
     feeds_metrics: Option<Arc<RwLock<FeedsMetrics>>>,
     feeds_config: Arc<RwLock<HashMap<u32, FeedConfig>>>,
+    feeds_rounds: &mut HashMap<u32, u64>,
 ) -> Result<String> {
     debug!("Acquiring a read lock on provider config for `{net}`");
     let provider = provider.lock().await;
@@ -131,14 +131,44 @@ pub async fn get_serialized_updates_for_network(
     drop(provider);
     debug!("Released a read lock on provider config for `{net}`");
 
+    let mut strides_and_decimals = HashMap::new();
+    for update in updates.updates.iter() {
+        let feed_id = update.feed_id;
+        debug!("Acquiring a read lock on feeds_config; network={net}; feed_id={feed_id}");
+        let feed_config = feeds_config.read().await.get(&feed_id).cloned();
+        debug!(
+            "Acquired and released a read lock on feeds_config; network={net}; feed_id={feed_id}"
+        );
+
+        strides_and_decimals.insert(
+            feed_id,
+            FeedStrideAndDecimals::from_feed_config(&feed_config),
+        );
+        drop(feed_config);
+    }
+
     let serialized_updates = match contract_version {
-        1 => match legacy_serialize_updates(net, updates, feeds_config).await {
-            Ok(result) => result,
-            Err(e) => eyre::bail!("Legacy serialization failed: {}!", e),
+        1 => match legacy_serialize_updates(net, updates, strides_and_decimals).await {
+            Ok(result) => {
+                debug!("legacy_serialize_updates result = {result}");
+                result
+            }
+            Err(e) => eyre::bail!("Legacy serialization failed: {e}!"),
         },
-        2 => match adfs_serialize_updates(net, updates, feeds_metrics, feeds_config).await {
-            Ok(result) => result,
-            Err(e) => eyre::bail!("ADFS serialization failed: {}!", e),
+        2 => match adfs_serialize_updates(
+            net,
+            updates,
+            feeds_metrics,
+            strides_and_decimals,
+            feeds_rounds,
+        )
+        .await
+        {
+            Ok(result) => {
+                debug!("adfs_serialize_updates result = {result}");
+                result
+            }
+            Err(e) => eyre::bail!("ADFS serialization failed: {e}!"),
         },
         _ => eyre::bail!("Unsupported contract version set for network {net}!"),
     };
@@ -158,6 +188,7 @@ pub async fn eth_batch_send_to_contract(
     transaction_retry_timeout_secs: u64,
     retry_fee_increment_fraction: f64,
 ) -> Result<(String, Vec<u32>)> {
+    let mut feeds_rounds = HashMap::new();
     let serialized_updates = get_serialized_updates_for_network(
         net.as_str(),
         &provider,
@@ -165,6 +196,7 @@ pub async fn eth_batch_send_to_contract(
         &provider_settings,
         feeds_metrics,
         feeds_config,
+        &mut feeds_rounds,
     )
     .await?;
 
@@ -604,15 +636,12 @@ mod tests {
     use alloy::primitives::{Address, TxKind};
     use alloy::rpc::types::eth::TransactionInput;
     use alloy::{node_bindings::Anvil, providers::Provider};
-    use config::{
-        get_test_config_with_multiple_providers, get_test_config_with_single_provider, AssetPair,
-    };
+    use config::{get_test_config_with_multiple_providers, get_test_config_with_single_provider};
     use config::{AllFeedsConfig, PublishCriteria};
     use data_feeds::feeds_processing::VotedFeedUpdate;
     use feed_registry::types::Repeatability::Oneshot;
     use regex::Regex;
     use std::str::FromStr;
-    use std::time::SystemTime;
     use utils::test_env::get_test_private_key_path;
 
     fn extract_address(message: &str) -> Option<String> {
@@ -623,34 +652,16 @@ mod tests {
         None
     }
 
-    fn test_feeds_config() -> Arc<RwLock<HashMap<u32, FeedConfig>>> {
+    fn test_feeds_config() -> HashMap<u32, FeedStrideAndDecimals> {
         let mut feeds_config = HashMap::new();
         feeds_config.insert(
             0,
-            FeedConfig {
-                id: 0,
-                name: "FOXY".to_string(),
-                full_name: "Foxy".to_string(),
-                description: "FOXY / USD".to_string(),
-                decimals: 18,
-                report_interval_ms: 90000,
-                quorum_percentage: 100.0,
-                skip_publish_if_less_then_percentage: 0.1,
-                always_publish_heartbeat_ms: Some(3600000),
-                _type: "Crypto".to_string(),
-                script: "CoinMarketCap".to_string(),
-                pair: AssetPair {
-                    base: "FOXY".to_string(),
-                    quote: "USD".to_string(),
-                },
-                first_report_start_time: SystemTime::now(),
-                resources: HashMap::new(),
-                value_type: "Numerical".to_string(),
-                aggregate_type: "Median".to_string(),
+            FeedStrideAndDecimals {
                 stride: 0,
+                decimals: 18,
             },
         );
-        Arc::new(RwLock::new(feeds_config))
+        feeds_config
     }
 
     #[tokio::test]
diff --git a/apps/sequencer/src/providers/mod.rs b/apps/sequencer/src/providers/mod.rs
index 8dbb6e7512..3749fb7618 100644
--- a/apps/sequencer/src/providers/mod.rs
+++ b/apps/sequencer/src/providers/mod.rs
@@ -1,3 +1,2 @@
-pub mod adfs_gen_calldata;
 pub mod eth_send_utils;
 pub mod provider;
diff --git a/apps/sequencer/src/providers/provider.rs b/apps/sequencer/src/providers/provider.rs
index d75a2b9c36..13fb735dbe 100644
--- a/apps/sequencer/src/providers/provider.rs
+++ b/apps/sequencer/src/providers/provider.rs
@@ -20,7 +20,7 @@ use config::AllFeedsConfig;
 use reqwest::{Client, Url};
 
 use config::{PublishCriteria, SequencerConfig};
-use data_feeds::feeds_processing::VotedFeedUpdate;
+use data_feeds::feeds_processing::{BatchedAggegratesToSend, VotedFeedUpdate};
 use eyre::{eyre, Result};
 use feed_registry::registry::FeedAggregateHistory;
 use feed_registry::types::FeedType;
@@ -36,7 +36,6 @@ use tokio::time::error::Elapsed;
 use tokio::time::Duration;
 use tracing::{debug, error, info, warn};
 
-use crate::BatchedAggegratesToSend;
 use std::time::Instant;
 
 pub type ProviderType = FillProvider<
diff --git a/libs/config/src/lib.rs b/libs/config/src/lib.rs
index 814bef217c..588cda69b5 100644
--- a/libs/config/src/lib.rs
+++ b/libs/config/src/lib.rs
@@ -4,7 +4,7 @@ use std::collections::HashSet;
 use std::path::Path;
 use std::time::SystemTime;
 use std::{collections::HashMap, fmt::Debug};
-use tracing::{info, trace};
+use tracing::{info, trace, warn};
 use utils::constants::{
     FEEDS_CONFIG_DIR, FEEDS_CONFIG_FILE, SEQUENCER_CONFIG_DIR, SEQUENCER_CONFIG_FILE,
 };
@@ -147,6 +147,33 @@ impl Validated for FeedConfig {
     }
 }
 
+pub struct FeedStrideAndDecimals {
+    pub stride: u16,
+    pub decimals: u8,
+}
+
+impl FeedStrideAndDecimals {
+    pub fn from_feed_config(feed_config: &Option<FeedConfig>) -> FeedStrideAndDecimals {
+        let stride = match &feed_config {
+            Some(f) => f.stride,
+            None => {
+                warn!("Propagating result for unregistered feed! Support left for legacy one shot feeds of 32 bytes size.");
+                1
+            }
+        };
+
+        let decimals = match &feed_config {
+            Some(f) => f.decimals,
+            None => {
+                warn!("Propagating result for unregistered feed! Support left for legacy one shot feeds of 32 bytes size. Decimal default to 18");
+                18
+            }
+        };
+
+        FeedStrideAndDecimals { stride, decimals }
+    }
+}
+
 #[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
 pub struct AllFeedsConfig {
     pub feeds: Vec<FeedConfig>,
diff --git a/libs/data_feeds/src/feeds_processing.rs b/libs/data_feeds/src/feeds_processing.rs
index 81576fcb3a..830b868b56 100644
--- a/libs/data_feeds/src/feeds_processing.rs
+++ b/libs/data_feeds/src/feeds_processing.rs
@@ -85,13 +85,44 @@ impl VotedFeedUpdate {
     }
 }
 
-pub const REPORT_HEX_SIZE: usize = 64;
-
 pub fn naive_packing(feed_result: &FeedType, digits_in_fraction: usize) -> Vec<u8> {
     //TODO: Return Bytes32 type
     feed_result.as_bytes(digits_in_fraction)
 }
 
+use std::collections::HashMap;
+
+#[derive(Debug, Clone, Default)]
+pub struct BatchedAggegratesToSend {
+    pub block_height: u64,
+    pub updates: Vec<VotedFeedUpdate>,
+    // The key in this map is the feed id for which we provide a proof for the aggregated value.
+    pub proofs: HashMap<u32, Vec<DataFeedPayload>>,
+}
+
+impl BatchedAggegratesToSend {
+    // The updates to be sent to different networks go through multiple filters.
+    // Eventually we might need to reduce the proof to only contain records for
+    // the relevant updates. The following function does that and returns the
+    // count of removed elements.
+    pub fn normalize_proof(&mut self) -> usize {
+        let removed_elements_count = self.proofs.len() - self.updates.len();
+
+        if removed_elements_count > 0 {
+            let mut normalized_proofs = HashMap::new();
+            for update in self.updates.iter() {
+                let Some(proof) = self.proofs.remove(&update.feed_id) else {
+                    error!("Logical ERROR! no proof found for key: {}", update.feed_id);
+                    continue;
+                };
+                normalized_proofs.insert(update.feed_id, proof);
+            }
+            self.proofs = normalized_proofs;
+        }
+        removed_elements_count
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use std::time::SystemTime;
diff --git a/libs/feed_registry/src/aggregate.rs b/libs/feed_registry/src/aggregate.rs
index 82735aedf0..374e135edb 100644
--- a/libs/feed_registry/src/aggregate.rs
+++ b/libs/feed_registry/src/aggregate.rs
@@ -87,6 +87,14 @@ impl FeedAggregate {
             }
         }
     }
+    pub fn create_from_str(aggregate_type: &str) -> anyhow::Result<Self> {
+        match aggregate_type {
+            "Median" => Ok(Self::MedianAggregator),
+            "Majority" => Ok(Self::MajorityVoteAggregator),
+            "Average" => Ok(Self::AverageAggregator),
+            _ => anyhow::bail!("Could not convert {aggregate_type} to a valid aggregator!"),
+        }
+    }
 }
 
 impl Display for FeedAggregate {
@@ -109,15 +117,6 @@ impl Debug for FeedAggregate {
     }
 }
 
-pub fn get_aggregator(aggredate_type: &str) -> FeedAggregate {
-    match aggredate_type {
-        "Median" => FeedAggregate::MedianAggregator,
-        "Majority" => FeedAggregate::MajorityVoteAggregator,
-        "Average" => FeedAggregate::AverageAggregator,
-        _ => panic!("Unknown aggregate type: {}", aggredate_type),
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/libs/feed_registry/src/registry.rs b/libs/feed_registry/src/registry.rs
index 81aede409b..f05620c84a 100644
--- a/libs/feed_registry/src/registry.rs
+++ b/libs/feed_registry/src/registry.rs
@@ -53,7 +53,7 @@ pub fn new_feeds_meta_data_reg_with_test_data() -> FeedMetaDataRegistry {
     let skip_publish_if_less_then_percentage = 0.0f32; // 0%
     let always_publish_heartbeat_ms = None;
     let fmd1 = FeedMetaData::new(
-        "DOGE/USD",
+        "DOGE/USD".to_string(),
         60000,
         60.0f32, // 60%
         skip_publish_if_less_then_percentage,
@@ -64,7 +64,7 @@ pub fn new_feeds_meta_data_reg_with_test_data() -> FeedMetaDataRegistry {
         None,
     );
     let fmd2 = FeedMetaData::new(
-        "BTS/USD",
+        "BTS/USD".to_string(),
         30000,
         60.0f32, // 60%
         skip_publish_if_less_then_percentage,
@@ -75,7 +75,7 @@ pub fn new_feeds_meta_data_reg_with_test_data() -> FeedMetaDataRegistry {
         None,
     );
     let fmd3 = FeedMetaData::new(
-        "ETH/USD",
+        "ETH/USD".to_string(),
         60000,
         60.0f32, // 60%
         skip_publish_if_less_then_percentage,
@@ -102,7 +102,7 @@ pub fn new_feeds_meta_data_reg_from_config(conf: &AllFeedsConfig) -> FeedMetaDat
         fmdr.push(
             feed.id,
             FeedMetaData::new(
-                &feed.name,
+                feed.name.clone(),
                 feed.report_interval_ms,
                 feed.quorum_percentage,
                 feed.skip_publish_if_less_then_percentage,
@@ -615,7 +615,7 @@ mod tests {
             voting_start_time,
         );
         let regular_feed = FeedMetaData::new(
-            "TestFeed",
+            "TestFeed".to_string(),
             voting_wait_duration_ms,
             10.0f32, // 10%
             1.0f32,  // 1%
diff --git a/libs/feed_registry/src/types.rs b/libs/feed_registry/src/types.rs
index a43f725f5d..e4c7eb192b 100644
--- a/libs/feed_registry/src/types.rs
+++ b/libs/feed_registry/src/types.rs
@@ -1,3 +1,4 @@
+use config::FeedConfig;
 use serde::{Deserialize, Serialize};
 use std::{
     collections::HashMap,
@@ -11,7 +12,7 @@ use tracing::debug;
 use crypto::{JsonSerializableSignature, Signature};
 use num::BigUint;
 
-use crate::aggregate::{get_aggregator, FeedAggregate};
+use crate::aggregate::FeedAggregate;
 
 #[derive(Debug, PartialEq, Copy, Clone)]
 pub enum Repeatability {
@@ -71,7 +72,7 @@ impl FeedMetaData {
 
     #[allow(clippy::too_many_arguments)]
     pub fn new(
-        name: &str,
+        name: String,
         report_interval_ms: u64, // Consider oneshot feeds.
         quorum_percentage: f32,
         skip_publish_if_less_then_percentage: f32,
@@ -82,20 +83,35 @@ impl FeedMetaData {
         processor_cmd_chan: Option<UnboundedSender<FeedsSlotProcessorCmds>>,
     ) -> FeedMetaData {
         FeedMetaData {
-            name: name.to_string(),
+            name,
             voting_repeatability: Repeatability::Periodic,
             report_interval_ms,
             quorum_percentage,
             skip_publish_if_less_then_percentage,
             always_publish_heartbeat_ms,
             first_report_start_time,
-            feed_aggregator: get_aggregator(aggregate_type.as_str()), //TODO(snikolov): This should be resolved based upon the ConsensusMetric enum sent from the reporter or directly based on the feed_id
+            feed_aggregator: FeedAggregate::create_from_str(aggregate_type.as_str())
+                .expect("Could not convert {aggregate_type} to a valid aggregator!"), //TODO(snikolov): This should be resolved based upon the ConsensusMetric enum sent from the reporter or directly based on the feed_id
             value_type,
             aggregate_type,
             processor_cmd_chan,
         }
     }
 
+    pub fn from_config(cfg: &FeedConfig) -> Self {
+        Self::new(
+            cfg.name.clone(),
+            cfg.report_interval_ms,
+            cfg.quorum_percentage,
+            cfg.skip_publish_if_less_then_percentage,
+            cfg.always_publish_heartbeat_ms,
+            cfg.first_report_start_time,
+            cfg.value_type.clone(),
+            cfg.aggregate_type.clone(),
+            None,
+        )
+    }
+
     pub fn set_processor_cmd_chan(&mut self, send_chan: UnboundedSender<FeedsSlotProcessorCmds>) {
         self.processor_cmd_chan = Some(send_chan);
     }
diff --git a/libs/feeds_processing/Cargo.toml b/libs/feeds_processing/Cargo.toml
new file mode 100644
index 0000000000..4947c7e333
--- /dev/null
+++ b/libs/feeds_processing/Cargo.toml
@@ -0,0 +1,48 @@
+[package]
+name = "feeds_processing"
+version.workspace = true
+authors.workspace = true
+license.workspace = true
+edition.workspace = true
+
+[dependencies]
+anomaly_detection = { path = "../../libs/anomaly_detection" }
+feed_registry = { path = "../feed_registry" }
+data_feeds = {path = "../data_feeds"}
+config = {path = "../config"}
+utils = {path = "../utils"}
+prometheus = {path = "../prometheus"}
+gnosis_safe = {path = "../gnosis_safe"}
+crypto = { path = "../../libs/crypto" }
+once_cell = "1.20.2"
+hex = "0.4.3"
+ringbuf = "0.4.7"
+tracing = { workspace = true }
+tokio = { workspace = true }
+alloy = { version = "0.8", features = [
+    "sol-types",
+    "contract",
+    "network",
+    "node-bindings",
+    "providers",
+    "rpc",
+    "rpc-client",
+    "rpc-types-eth",
+    "rpc-types-trace",
+    "signers",
+    "signer-keystore",
+    "signer-ledger",
+    "signer-mnemonic",
+    "signer-trezor",
+    "signer-yubihsm",
+    "transports",
+    "transport-http",
+    "transport-ipc",
+    "transport-ws",
+    "pubsub",
+] }
+alloy-primitives = "0.8.19"
+alloy-encode-packed = "0.1.1"
+anyhow = "1.0.89"
+serde_json = { workspace = true }
+serde = { workspace = true }
diff --git a/apps/sequencer/src/providers/adfs_gen_calldata.rs b/libs/feeds_processing/src/adfs_gen_calldata.rs
similarity index 79%
rename from apps/sequencer/src/providers/adfs_gen_calldata.rs
rename to libs/feeds_processing/src/adfs_gen_calldata.rs
index 881b22b251..fa91e87a85 100644
--- a/apps/sequencer/src/providers/adfs_gen_calldata.rs
+++ b/libs/feeds_processing/src/adfs_gen_calldata.rs
@@ -1,7 +1,8 @@
 use alloy::hex;
 use alloy_primitives::U256;
-use config::FeedConfig;
-use eyre::Result;
+use anyhow::Result;
+use config::FeedStrideAndDecimals;
+use data_feeds::feeds_processing::BatchedAggegratesToSend;
 use prometheus::metrics::FeedsMetrics;
 use std::cmp::max;
 use std::collections::{BTreeMap, HashMap};
@@ -9,12 +10,10 @@ use std::sync::Arc;
 use tokio::sync::RwLock;
 use utils::{from_hex_string, to_hex_string};
 
-use tracing::{debug, error, info, warn};
+use tracing::{debug, error, info};
 
 use alloy_encode_packed::{self, abi::encode_packed, SolidityDataType};
 
-use crate::BatchedAggegratesToSend;
-
 use once_cell::sync::Lazy;
 
 const MAX_HISTORY_ELEMENTS_PER_FEED: u64 = 8192;
@@ -48,7 +47,11 @@ pub async fn adfs_serialize_updates(
     net: &str,
     feed_updates: &BatchedAggegratesToSend,
     feeds_metrics: Option<Arc<RwLock<FeedsMetrics>>>,
-    feeds_config: Arc<RwLock<HashMap<u32, FeedConfig>>>,
+    strides_and_decimals: HashMap<u32, FeedStrideAndDecimals>,
+    feeds_rounds: &mut HashMap<u32, u64>, /* The rounds table for the relevant feeds. If the feeds_metrics are provided,
+                                          this map will be filled with the update count for each feed from it. If the
+                                          feeds_metrics is None, feeds_rounds will be used as the source of the updates
+                                          count. */
 ) -> Result<String> {
     let mut result = Vec::<u8>::new();
     let updates = &feed_updates.updates;
@@ -63,30 +66,15 @@ pub async fn adfs_serialize_updates(
     // Fill the value updates:
     for update in updates.iter() {
         let feed_id = update.feed_id;
-        debug!("Acquiring a read lock on feeds_config; network={net}; feed_id={feed_id}");
-        let feed_config = feeds_config.read().await.get(&feed_id).cloned();
-        debug!(
-            "Acquired and released a read lock on feeds_config; network={net}; feed_id={feed_id}"
-        );
 
-        let stride = match &feed_config {
-            Some(f) => f.stride,
+        let (stride, digits_in_fraction) = match &strides_and_decimals.get(&feed_id) {
+            Some(f) => (f.stride, f.decimals),
             None => {
-                warn!("Propagating result for unregistered feed! Support left for legacy one shot feeds of 32 bytes size.");
-                1
+                error!("Propagating result for unregistered feed! Support left for legacy one shot feeds of 32 bytes size. Decimal default to 18");
+                (1, 18)
             }
         };
 
-        let digits_in_fraction = match &feed_config {
-            Some(f) => f.decimals,
-            None => {
-                warn!("Propagating result for unregistered feed! Support left for legacy one shot feeds of 32 bytes size. Decimale default to 18");
-                18
-            }
-        };
-
-        drop(feed_config);
-
         let mut round = match &feeds_metrics {
             Some(fm) => {
                 debug!("Acquiring a read lock on feeds_metrics; network={net}; feed_id={feed_id}");
@@ -97,9 +85,13 @@ pub async fn adfs_serialize_updates(
                     .with_label_values(&[&update.feed_id.to_string(), net])
                     .get();
                 debug!("Acquired and released a read lock on feeds_metrics; network={net}; feed_id={feed_id}");
+                feeds_rounds.insert(feed_id, round);
                 round
             }
-            None => 0,
+            None => *feeds_rounds.get(&feed_id).unwrap_or({
+                error!("feeds_rounds does not contain updates count for feed_id {feed_id}. Rolling back to 0!");
+                &0
+            }),
         };
 
         round %= MAX_HISTORY_ELEMENTS_PER_FEED;
@@ -211,9 +203,6 @@ pub async fn adfs_serialize_updates(
 #[cfg(test)]
 pub mod tests {
 
-    use std::time::SystemTime;
-
-    use config::AssetPair;
     use data_feeds::feeds_processing::VotedFeedUpdate;
     use feed_registry::types::FeedType;
 
@@ -245,32 +234,6 @@ pub mod tests {
             proofs: HashMap::new(),
         };
 
-        // Helper function to create FeedConfig
-        fn create_feed_config(id: u32, stride: u16) -> FeedConfig {
-            FeedConfig {
-                id,
-                name: "BTC".to_string(),
-                full_name: "Bitcoin".to_string(),
-                description: "A Peer-to-Peer Electronic Cash System".to_string(),
-                _type: "String".to_string(),
-                decimals: 18,
-                pair: AssetPair {
-                    base: "BTC".to_string(),
-                    quote: "USD".to_string(),
-                },
-                report_interval_ms: 50_000,
-                first_report_start_time: SystemTime::now(),
-                resources: HashMap::new(),
-                quorum_percentage: 0.6,
-                skip_publish_if_less_then_percentage: 0.1,
-                always_publish_heartbeat_ms: None,
-                script: "String".to_string(),
-                value_type: "String".to_string(),
-                aggregate_type: "String".to_string(),
-                stride,
-            }
-        }
-
         // Helper function to set round metrics (number of updates for a feed for a network)
         fn set_round_metric(feeds_metrics: &mut FeedsMetrics, feed_id: &str, net: &str, val: u64) {
             feeds_metrics
@@ -295,11 +258,41 @@ pub mod tests {
         }
 
         let config = HashMap::from([
-            (1, create_feed_config(1, 1)),
-            (2, create_feed_config(2, 0)),
-            (3, create_feed_config(3, 0)),
-            (4, create_feed_config(4, 0)),
-            (5, create_feed_config(5, 0)),
+            (
+                1,
+                FeedStrideAndDecimals {
+                    stride: 1,
+                    decimals: 18,
+                },
+            ),
+            (
+                2,
+                FeedStrideAndDecimals {
+                    stride: 0,
+                    decimals: 18,
+                },
+            ),
+            (
+                3,
+                FeedStrideAndDecimals {
+                    stride: 0,
+                    decimals: 18,
+                },
+            ),
+            (
+                4,
+                FeedStrideAndDecimals {
+                    stride: 0,
+                    decimals: 18,
+                },
+            ),
+            (
+                5,
+                FeedStrideAndDecimals {
+                    stride: 0,
+                    decimals: 18,
+                },
+            ),
         ]);
 
         let feeds_metrics = static_feeds_metrics(net);
@@ -309,7 +302,8 @@ pub mod tests {
                 net,
                 &updates,
                 Some(feeds_metrics.clone()),
-                Arc::new(RwLock::new(config)),
+                config,
+                &mut HashMap::new(),
             )
             .await
             .unwrap()
diff --git a/libs/feeds_processing/src/lib.rs b/libs/feeds_processing/src/lib.rs
new file mode 100644
index 0000000000..e131cd86bd
--- /dev/null
+++ b/libs/feeds_processing/src/lib.rs
@@ -0,0 +1,2 @@
+pub mod adfs_gen_calldata;
+pub mod utils;
diff --git a/libs/feeds_processing/src/utils.rs b/libs/feeds_processing/src/utils.rs
new file mode 100644
index 0000000000..4ea0a0f1ce
--- /dev/null
+++ b/libs/feeds_processing/src/utils.rs
@@ -0,0 +1,611 @@
+use anomaly_detection::ingest::anomaly_detector_aggregate;
+use anyhow::{anyhow, Context, Result};
+use config::{FeedConfig, FeedStrideAndDecimals, PublishCriteria};
+use data_feeds::feeds_processing::{
+    BatchedAggegratesToSend, VotedFeedUpdate, VotedFeedUpdateWithProof,
+};
+use feed_registry::aggregate::FeedAggregate;
+use feed_registry::registry::FeedAggregateHistory;
+use feed_registry::types::{DataFeedPayload, FeedMetaData, FeedType, Timestamp};
+use gnosis_safe::data_types::ConsensusSecondRoundBatch;
+use gnosis_safe::utils::{create_safe_tx, generate_transaction_hash};
+use ringbuf::traits::consumer::Consumer;
+// use serde_json::from_str;
+use crate::adfs_gen_calldata::adfs_serialize_updates;
+use alloy::hex::FromHex;
+use alloy_primitives::{Address, Bytes, Uint, U256};
+use crypto::{verify_signature, PublicKey, Signature};
+use feed_registry::types::FeedResult;
+use std::collections::{HashMap, HashSet};
+use std::str::FromStr;
+use std::sync::Arc;
+use tokio::sync::RwLock;
+use tracing::{debug, info, warn};
+
+pub const AD_MIN_DATA_POINTS_THRESHOLD: usize = 100;
+
+pub fn check_signature(
+    signature: &Signature,
+    pub_key: &PublicKey,
+    feed_id: &str,
+    timestamp: Timestamp,
+    feed_result: &FeedResult,
+) -> bool {
+    let mut byte_buffer: Vec<u8> = feed_id
+        .as_bytes()
+        .iter()
+        .copied()
+        .chain(timestamp.to_be_bytes().to_vec())
+        .collect();
+
+    match feed_result {
+        Ok(result) => {
+            byte_buffer.extend(result.as_bytes(18));
+        }
+        Err(error) => {
+            warn!("Reported error for feed_id {} : {}", feed_id, error);
+        }
+    };
+    verify_signature(pub_key, signature, &byte_buffer)
+}
+
+#[derive(Debug)]
+pub struct ConsumedReports {
+    pub is_quorum_reached: bool,
+    pub skip_publishing: bool,
+    pub ad_score: Option<f64>,
+    pub result_post_to_contract: Option<VotedFeedUpdateWithProof>,
+    pub end_slot_timestamp: Timestamp,
+}
+
+#[allow(clippy::too_many_arguments)]
+pub async fn consume_reports(
+    name: &str,
+    reports: &HashMap<u64, DataFeedPayload>,
+    feed_type: &FeedType,
+    slot: u64,
+    quorum_percentage: f32,
+    skip_publish_if_less_then_percentage: f64,
+    always_publish_heartbeat_ms: Option<u128>,
+    end_slot_timestamp: Timestamp,
+    num_reporters: usize,
+    is_oneshot: bool,
+    aggregator: FeedAggregate,
+    history: Option<Arc<RwLock<FeedAggregateHistory>>>,
+    feed_id: u32,
+) -> ConsumedReports {
+    let values = collect_reported_values(feed_type, feed_id, reports, slot);
+
+    if values.is_empty() {
+        info!("No reports found for feed: {} slot: {}!", name, &slot);
+        ConsumedReports {
+            is_quorum_reached: false,
+            skip_publishing: true,
+            ad_score: None,
+            result_post_to_contract: None,
+            end_slot_timestamp,
+        }
+    } else {
+        let total_votes_count = values.len() as f32;
+        let required_votes_count = quorum_percentage * 0.01f32 * (num_reporters as f32);
+        let is_quorum_reached = required_votes_count <= total_votes_count;
+        let mut skip_publishing = false;
+        if !is_quorum_reached {
+            warn!(
+                "Insufficient quorum of reports to post to contract for feed: {} slot: {}! Expected at least a quorum of {}, but received {} out of {} valid votes.",
+                name, &slot, quorum_percentage, total_votes_count, num_reporters
+            );
+        }
+
+        // Dispatch to concrete FeedAggregate implementation.
+        let result_post_to_contract = VotedFeedUpdate {
+            feed_id,
+            value: aggregator.aggregate(&values[..]), // Perform the concrete aggregation
+            end_slot_timestamp,
+        };
+
+        let mut proof: Vec<DataFeedPayload> = Vec::new();
+        for (_, v) in reports.iter() {
+            proof.push(v.clone());
+        }
+
+        let mut ad_score_opt: Option<f64> = None;
+
+        // Oneshot feeds have no history, so we cannot perform anomaly detection on them.
+        if !is_oneshot {
+            if let Some(history) = history {
+                if let FeedType::Numerical(candidate_value) = result_post_to_contract.value {
+                    let ad_score =
+                        perform_anomaly_detection(feed_id, history.clone(), candidate_value).await;
+                    match ad_score {
+                        Ok(ad_score) => {
+                            info!(
+                                "AD_score for {:?} is {}",
+                                result_post_to_contract.value, ad_score
+                            );
+                            ad_score_opt = Some(ad_score)
+                        }
+                        Err(e) => {
+                            warn!("Anomaly Detection failed with error - {}", e);
+                        }
+                    }
+                    {
+                        let criteria = PublishCriteria {
+                            feed_id,
+                            skip_publish_if_less_then_percentage,
+                            always_publish_heartbeat_ms,
+                            peg_to_value: None,
+                            peg_tolerance_percentage: 0.0f64,
+                        };
+                        debug!("Get a read lock on history [feed {feed_id}]");
+                        let history_guard = history.read().await;
+                        skip_publishing =
+                            result_post_to_contract.should_skip(&criteria, &history_guard);
+                        debug!("Release the read lock on history [feed {feed_id}]");
+                    }
+                };
+            };
+        }
+        let res = ConsumedReports {
+            is_quorum_reached,
+            skip_publishing,
+            ad_score: ad_score_opt,
+            result_post_to_contract: Some(VotedFeedUpdateWithProof {
+                update: result_post_to_contract,
+                proof,
+            }),
+            end_slot_timestamp,
+        };
+        info!("[feed {feed_id}] result_post_to_contract = {:?}", res);
+        res
+    }
+}
+
+pub fn collect_reported_values(
+    expected_feed_type: &FeedType,
+    feed_id: u32,
+    reports: &HashMap<u64, DataFeedPayload>,
+    slot: u64,
+) -> Vec<FeedType> {
+    let mut values: Vec<FeedType> = vec![];
+    for kv in reports {
+        match &kv.1.result {
+            Ok(value) => {
+                if value.same_enum_type_as(expected_feed_type) {
+                    values.push(value.clone());
+                } else {
+                    warn!("Wrong value type reported by reporter {} for feed id {} slot {}! {} expected", kv.0, feed_id, slot, expected_feed_type.enum_type_to_string());
+                }
+            }
+            Err(_) => {
+                warn!(
+                    "Got error from reporter {} for feed id {} slot {}",
+                    kv.0, feed_id, slot
+                );
+            }
+        }
+    }
+    values
+}
+
+pub async fn perform_anomaly_detection(
+    feed_id: u32,
+    history: Arc<RwLock<FeedAggregateHistory>>,
+    candidate_value: f64,
+) -> Result<f64, anyhow::Error> {
+    let anomaly_detection_future = async move {
+        debug!("Get a read lock on history [feed {feed_id}]");
+        let history_lock = history.read().await;
+
+        // The first slice is from the current read position to the end of the array
+        // The second slice represents the segment from the start of the array up to the current write position if the buffer has wrapped around
+        let heap = history_lock
+            .get(feed_id)
+            .context("Missing key from History!")?;
+        let (first, last) = heap.as_slices();
+        let history_vec: Vec<&FeedType> =
+            first.iter().chain(last.iter()).map(|h| &h.value).collect();
+        let mut numerical_vec: Vec<f64> = history_vec
+            .iter()
+            .filter_map(|feed| {
+                if let FeedType::Numerical(value) = feed {
+                    Some(*value)
+                } else if let FeedType::Text(_) = feed {
+                    warn!("Anomaly Detection not implemented for FeedType::Text, skipping...");
+                    None
+                } else {
+                    warn!("Anomaly Detection does not recognize FeedType, skipping...");
+                    None
+                }
+            })
+            .collect();
+
+        drop(history_lock);
+        debug!("Release the read lock on history [feed {feed_id}]");
+
+        numerical_vec.push(candidate_value);
+
+        // Get AD prediction only if enough data is present
+        if numerical_vec.len() > AD_MIN_DATA_POINTS_THRESHOLD {
+            debug!("Starting anomaly detection for [feed {feed_id}]");
+            anomaly_detector_aggregate(numerical_vec).map_err(|e| anyhow!("{e}"))
+        } else {
+            Err(anyhow!(
+                "Skipping anomaly detection; numerical_vec.len() = {} threshold: {}",
+                numerical_vec.len(),
+                AD_MIN_DATA_POINTS_THRESHOLD
+            ))
+        }
+    };
+
+    tokio::task::Builder::new()
+        .name("anomaly_detection")
+        .spawn(anomaly_detection_future)
+        .context("Failed to spawn feed slots manager anomaly detection!")?
+        .await
+        .context("Failed to join feed slots manager anomaly detection!")?
+}
+
+type FeedId = u32;
+type FeedTimestamp = u64;
+type FeedIdToTimetamp = HashMap<FeedId, FeedTimestamp>;
+
+pub async fn validate_sigcheck(
+    feeds_config: Arc<RwLock<HashMap<u32, FeedConfig>>>,
+    mut batch: ConsensusSecondRoundBatch,
+    reporters_keys: HashMap<u64, PublicKey>,
+    feeds_last_updated_slots: &mut FeedIdToTimetamp,
+) -> Result<()> {
+    // Check that all the aggregated values have a corresponding set of votes
+    let feed_ids_in_updates: Vec<u32> = batch.updates.iter().map(|u| u.feed_id).collect();
+    let feed_ids_in_proof: Vec<u32> = batch.proofs.keys().cloned().collect();
+
+    if feed_ids_in_updates.len() != feed_ids_in_proof.len() {
+        anyhow::bail!("Proofs / Updates size mismatch");
+    }
+
+    let feed_ids_in_updates: HashSet<_> = feed_ids_in_updates.into_iter().collect();
+    let feed_ids_in_proof: HashSet<_> = feed_ids_in_proof.into_iter().collect();
+
+    if feed_ids_in_updates != feed_ids_in_proof {
+        anyhow::bail!("Proofs / Updates mismatch");
+    }
+
+    let mut feeds_newly_updated_slots = HashMap::<u32, u64>::new();
+
+    for update in &batch.updates {
+        let proof_for_update = batch
+            .proofs
+            .get(&update.feed_id)
+            .expect("Proofs / Updates mismatch: above checks must verify this cannot happen!");
+
+        let num_reporters = reporters_keys.len();
+
+        let feeds_config = feeds_config.read().await;
+
+        let Some(feed_config) = feeds_config.get(&update.feed_id) else {
+            anyhow::bail!("Feed ID {} has no configuration.", &update.feed_id);
+        };
+
+        let feed_metadata = FeedMetaData::from_config(feed_config);
+
+        drop(feeds_config);
+
+        // Validate that the update and all the signed votes are for the same
+        // slot based on the timestamps and feed_metadata
+        // and make sure that this update has not been previously introduced!
+
+        // Get the aggregated value's slot:
+        let update_slot = feed_metadata.get_slot(update.end_slot_timestamp);
+
+        // Make sure this slot's value was not previously proposed:
+        let update_is_relevant = feeds_last_updated_slots
+            .get(&update.feed_id)
+            .is_some_and(|&slot| slot > update_slot);
+
+        if !update_is_relevant {
+            anyhow::bail!("Reintroduction of old value: {update:?}, {proof_for_update:?}");
+        }
+
+        feeds_newly_updated_slots.insert(update.feed_id, update_slot);
+
+        // Check signatures
+        for raw_vote in proof_for_update {
+            let Some(reporter_pub_key) = reporters_keys.get(&raw_vote.payload_metadata.reporter_id)
+            else {
+                anyhow::bail!(
+                    "No public key known for reporter with ID {}!",
+                    raw_vote.payload_metadata.reporter_id
+                );
+            };
+
+            // Make sure that all votes have the same slot as the aggregated value
+            let vote_slot = feed_metadata.get_slot(raw_vote.payload_metadata.timestamp);
+            if vote_slot != update_slot {
+                anyhow::bail!(concat!(
+                    "Received a vote not corresponding to the aggregated value's slot! vote: ",
+                    "{raw_vote:?} update_slot: {update_slot} calculated vote slot: {vote_slot}"
+                ));
+            }
+
+            if !check_signature(
+                &raw_vote.payload_metadata.signature.sig,
+                reporter_pub_key,
+                raw_vote.payload_metadata.feed_id.as_str(),
+                raw_vote.payload_metadata.timestamp,
+                &raw_vote.result,
+            ) {
+                anyhow::bail!(
+                    "Signature verification failed for vote {:?} from reporter id {} with pub key {:?}",
+                    raw_vote,
+                    raw_vote.payload_metadata.reporter_id,
+                    reporter_pub_key,
+                );
+            }
+        }
+
+        let proof_for_update_mapping: HashMap<u64, DataFeedPayload> = proof_for_update
+            .iter()
+            .cloned()
+            .map(|payload| (payload.payload_metadata.reporter_id, payload))
+            .collect();
+
+        let aggregator = match FeedAggregate::create_from_str(feed_metadata.aggregate_type.as_str())
+        {
+            Ok(val) => val,
+            Err(e) => anyhow::bail!("Could not convert {} to a valid aggregator: {e}", {
+                feed_metadata.aggregate_type.as_str()
+            }),
+        };
+
+        let consumed_reports_result = consume_reports(
+            format!("Feed ID: {}", update.feed_id).as_str(),
+            &proof_for_update_mapping,
+            &FeedType::Numerical(0.0),
+            0,
+            feed_metadata.quorum_percentage,
+            feed_metadata.skip_publish_if_less_then_percentage as f64,
+            feed_metadata.always_publish_heartbeat_ms,
+            update.end_slot_timestamp,
+            num_reporters,
+            false,
+            aggregator,
+            None,
+            update.feed_id,
+        )
+        .await;
+
+        let Some(result_post_to_contract) = consumed_reports_result.result_post_to_contract else {
+            anyhow::bail!(
+                "Feed ID {}'s proof did not produce a result for sending to contract, but a value is present.",
+                &update.feed_id
+            );
+        };
+
+        if update.value != result_post_to_contract.update.value {
+            anyhow::bail!(
+                "Feed ID {}'s proof did not produce the expected result.",
+                &update.feed_id
+            );
+        }
+    }
+
+    let updates_to_serialize = BatchedAggegratesToSend {
+        block_height: batch.block_height,
+        updates: batch.updates,
+        proofs: batch.proofs,
+    };
+
+    let mut strides_and_decimals = HashMap::new();
+    for update in updates_to_serialize.updates.iter() {
+        let feed_id = update.feed_id;
+        debug!("Acquiring a read lock on feeds_config; feed_id={feed_id}");
+        let feed_config = feeds_config.read().await.get(&feed_id).cloned();
+        debug!("Acquired and released a read lock on feeds_config; feed_id={feed_id}");
+
+        strides_and_decimals.insert(
+            feed_id,
+            FeedStrideAndDecimals::from_feed_config(&feed_config),
+        );
+        drop(feed_config);
+    }
+
+    let calldata = match adfs_serialize_updates(
+        &batch.network,
+        &updates_to_serialize,
+        None,
+        strides_and_decimals,
+        &mut batch.feeds_rounds,
+    )
+    .await
+    {
+        Ok(val) => val,
+        Err(e) => anyhow::bail!("Failed to recreate calldata: {e}"),
+    };
+
+    let calldata = match Bytes::from_hex(calldata) {
+        Ok(b) => b,
+        Err(e) => {
+            anyhow::bail!("calldata is not valid hex string: {}", e);
+        }
+    };
+
+    let contract_address = match Address::from_str(batch.contract_address.as_str()) {
+        Ok(addr) => addr,
+        Err(e) => {
+            anyhow::bail!(
+                "Non valid contract address ({}) provided: {}",
+                batch.contract_address.as_str(),
+                e
+            );
+        }
+    };
+
+    let safe_address = match Address::from_str(batch.safe_address.as_str()) {
+        Ok(addr) => addr,
+        Err(e) => {
+            anyhow::bail!(
+                "Non valid safe address ({}) provided: {}",
+                batch.contract_address.as_str(),
+                e
+            );
+        }
+    };
+    let nonce = match Uint::<256, 4>::from_str(batch.nonce.as_str()) {
+        Ok(n) => n,
+        Err(e) => {
+            anyhow::bail!("Non valid nonce ({}) provided: {}", batch.nonce.as_str(), e);
+        }
+    };
+    let safe_transaction = create_safe_tx(contract_address, calldata, nonce);
+
+    let chain_id: u64 = match batch.chain_id.as_str().parse() {
+        Ok(v) => v,
+        Err(e) => {
+            anyhow::bail!("Non valid chain_id ({}) provided: {}", batch.chain_id, e);
+        }
+    };
+
+    let tx_hash =
+        generate_transaction_hash(safe_address, U256::from(chain_id), safe_transaction.clone());
+
+    let tx_hash_str = tx_hash.to_string();
+
+    if tx_hash_str != batch.tx_hash {
+        anyhow::bail!(
+            "tx_hash mismatch, recvd: {} generated: {}",
+            batch.tx_hash,
+            tx_hash_str
+        );
+    }
+
+    // Store the last updated slots per feed
+    for (key, val) in feeds_newly_updated_slots {
+        feeds_last_updated_slots.insert(key, val);
+    }
+
+    Ok(())
+}
+
+pub async fn validate(
+    feeds_config: HashMap<u32, FeedStrideAndDecimals>,
+    mut batch: ConsensusSecondRoundBatch,
+    last_votes: HashMap<u32, VotedFeedUpdate>,
+    tolerated_deviations: HashMap<u32, f64>,
+) -> Result<()> {
+    for update in &batch.updates {
+        let feed_id = update.feed_id;
+        let Some(reporter_vote) = last_votes.get(&feed_id) else {
+            anyhow::bail!("Failed to get latest vote for feed_id: {}", feed_id);
+        };
+
+        let update_aggregate_value = match update.value {
+            FeedType::Numerical(v) => v,
+            _ => anyhow::bail!(
+                "Non numeric value in update_aggregate_value for feed_id: {}",
+                feed_id
+            ),
+        };
+
+        let reporter_voted_value = match reporter_vote.value {
+            FeedType::Numerical(v) => v,
+            _ => anyhow::bail!(
+                "Non numeric value in reporter_vote for feed_id: {}",
+                feed_id
+            ),
+        };
+
+        let diff = (update_aggregate_value - reporter_voted_value).abs();
+
+        let tolerated_diff_percent = tolerated_deviations.get(&feed_id).unwrap_or(&0.01);
+
+        if reporter_voted_value.abs() < f64::EPSILON {
+            if update_aggregate_value > *tolerated_diff_percent {
+                anyhow::bail!("relative_diff {update_aggregate_value} between reporter_voted_value {reporter_voted_value} and update_aggregate_value {update_aggregate_value} is above {tolerated_diff_percent} for feed_id {feed_id}");
+            }
+        } else {
+            let relative_diff = diff / reporter_voted_value;
+
+            if relative_diff > *tolerated_diff_percent {
+                anyhow::bail!("relative_diff {relative_diff} between reporter_voted_value {reporter_voted_value} and update_aggregate_value {update_aggregate_value} is above {tolerated_diff_percent} for feed_id {feed_id}");
+            }
+        }
+    }
+
+    let updates_to_serialize = BatchedAggegratesToSend {
+        block_height: batch.block_height,
+        updates: batch.updates,
+        proofs: batch.proofs,
+    };
+
+    let calldata = match adfs_serialize_updates(
+        &batch.network,
+        &updates_to_serialize,
+        None,
+        feeds_config,
+        &mut batch.feeds_rounds,
+    )
+    .await
+    {
+        Ok(val) => val,
+        Err(e) => anyhow::bail!("Failed to recreate calldata: {e}"),
+    };
+
+    let calldata = match Bytes::from_hex(calldata) {
+        Ok(b) => b,
+        Err(e) => {
+            anyhow::bail!("calldata is not valid hex string: {}", e);
+        }
+    };
+
+    let contract_address = match Address::from_str(batch.contract_address.as_str()) {
+        Ok(addr) => addr,
+        Err(e) => {
+            anyhow::bail!(
+                "Non valid contract address ({}) provided: {}",
+                batch.contract_address.as_str(),
+                e
+            );
+        }
+    };
+
+    let safe_address = match Address::from_str(batch.safe_address.as_str()) {
+        Ok(addr) => addr,
+        Err(e) => {
+            anyhow::bail!(
+                "Non valid safe address ({}) provided: {}",
+                batch.contract_address.as_str(),
+                e
+            );
+        }
+    };
+    let nonce = match Uint::<256, 4>::from_str(batch.nonce.as_str()) {
+        Ok(n) => n,
+        Err(e) => {
+            anyhow::bail!("Non valid nonce ({}) provided: {}", batch.nonce.as_str(), e);
+        }
+    };
+    let safe_transaction = create_safe_tx(contract_address, calldata, nonce);
+
+    let chain_id: u64 = match batch.chain_id.as_str().parse() {
+        Ok(v) => v,
+        Err(e) => {
+            anyhow::bail!("Non valid chain_id ({}) provided: {}", batch.chain_id, e);
+        }
+    };
+
+    let tx_hash =
+        generate_transaction_hash(safe_address, U256::from(chain_id), safe_transaction.clone());
+
+    let tx_hash_str = tx_hash.to_string();
+
+    if tx_hash_str != batch.tx_hash {
+        anyhow::bail!(
+            "tx_hash mismatch, recvd: {} generated: {}",
+            batch.tx_hash,
+            tx_hash_str
+        );
+    }
+
+    Ok(())
+}
diff --git a/libs/gnosis_safe/src/data_types.rs b/libs/gnosis_safe/src/data_types.rs
index 4430728f60..87555b5587 100644
--- a/libs/gnosis_safe/src/data_types.rs
+++ b/libs/gnosis_safe/src/data_types.rs
@@ -16,6 +16,7 @@ pub struct ConsensusSecondRoundBatch {
     pub tx_hash: String,
     pub calldata: String, // TODO: send all data needed to validate and recreate calldata.
     pub updates: Vec<VotedFeedUpdate>,
+    pub feeds_rounds: HashMap<u32, u64>,
     pub proofs: HashMap<u32, Vec<DataFeedPayload>>,
 }
 


Reviews:
user: login: HristoStaykov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-24T13:03:05Z
user: login: smanilov
body: Looks good if you address the comments.

How did you test `validate` and `validate_sigcheck`?
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:00:32Z
user: login: HristoStaykov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:02:31Z
user: login: HristoStaykov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:16:17Z
user: login: HristoStaykov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:30:38Z
user: login: HristoStaykov
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T17:35:53Z
user: login: smanilov
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T08:49:15Z
user: login: Xearty
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T08:57:48Z

Review comments:
diff_hunk: @@ -85,13 +85,43 @@ impl VotedFeedUpdate {
     }
 }
 
-pub const REPORT_HEX_SIZE: usize = 64;
-
 pub fn naive_packing(feed_result: &FeedType, digits_in_fraction: usize) -> Vec<u8> {
     //TODO: Return Bytes32 type
     feed_result.as_bytes(digits_in_fraction)
 }
 
+use std::collections::HashMap;
+
+#[derive(Debug, Clone, Default)]
+pub struct BatchedAggegratesToSend {
+    pub block_height: u64,
+    pub updates: Vec<VotedFeedUpdate>,
+    // The key in this map is the feed id for which we provide a proof for the aggregated value.
+    pub proofs: HashMap<u32, Vec<DataFeedPayload>>,
+}
+
+impl BatchedAggegratesToSend {
+    // The updates to be sent to different networks go through multiple filters.
+    // Eventually we might need to reduce the proof to only contain records for
+    // the relevant updates. The following function does that and returns the
+    // count of removed elements.
+    pub fn normalize_proof(&mut self) -> usize {
+        let removed_elements_count = self.proofs.len() - self.updates.len();
+
+        if removed_elements_count > 0 {
+            let mut normalized_proofs = HashMap::new();
+            for update in self.updates.iter() {
+                let Some(proof) = self.proofs.remove(&update.feed_id) else {
+                    error!("Logical ERROR! no proof found for key: {}", update.feed_id);
+                    continue;
+                };
+                normalized_proofs.insert(update.feed_id, proof);
+            }
+        }
path: libs/data_feeds/src/feeds_processing.rs
user: login: HristoStaykov
body: Update the value of self.proofs to normalized_proofs.
created_at: 2025-02-24T13:03:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -85,13 +85,43 @@ impl VotedFeedUpdate {
     }
 }
 
-pub const REPORT_HEX_SIZE: usize = 64;
-
 pub fn naive_packing(feed_result: &FeedType, digits_in_fraction: usize) -> Vec<u8> {
     //TODO: Return Bytes32 type
     feed_result.as_bytes(digits_in_fraction)
 }
 
+use std::collections::HashMap;
+
+#[derive(Debug, Clone, Default)]
+pub struct BatchedAggegratesToSend {
+    pub block_height: u64,
+    pub updates: Vec<VotedFeedUpdate>,
+    // The key in this map is the feed id for which we provide a proof for the aggregated value.
+    pub proofs: HashMap<u32, Vec<DataFeedPayload>>,
+}
+
+impl BatchedAggegratesToSend {
+    // The updates to be sent to different networks go through multiple filters.
+    // Eventually we might need to reduce the proof to only contain records for
+    // the relevant updates. The following function does that and returns the
+    // count of removed elements.
+    pub fn normalize_proof(&mut self) -> usize {
+        let removed_elements_count = self.proofs.len() - self.updates.len();
+
+        if removed_elements_count > 0 {
+            let mut normalized_proofs = HashMap::new();
+            for update in self.updates.iter() {
+                let Some(proof) = self.proofs.remove(&update.feed_id) else {
+                    error!("Logical ERROR! no proof found for key: {}", update.feed_id);
+                    continue;
+                };
+                normalized_proofs.insert(update.feed_id, proof);
+            }
+        }
path: libs/data_feeds/src/feeds_processing.rs
user: login: smanilov
body: Did you leave this comment as a reminder? If so, this seems to be done. (Or maybe it is not?)
created_at: 2025-03-10T14:44:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+use anomaly_detection::ingest::anomaly_detector_aggregate;
+use anyhow::{anyhow, Context, Result};
+use config::{FeedConfig, FeedStrideAndDecimals, PublishCriteria};
+use data_feeds::feeds_processing::{
+    BatchedAggegratesToSend, VotedFeedUpdate, VotedFeedUpdateWithProof,
+};
+use feed_registry::aggregate::FeedAggregate;
+use feed_registry::registry::FeedAggregateHistory;
+use feed_registry::types::{DataFeedPayload, FeedMetaData, FeedType, Timestamp};
+use gnosis_safe::data_types::ConsensusSecondRoundBatch;
+use gnosis_safe::utils::{create_safe_tx, generate_transaction_hash};
+use ringbuf::traits::consumer::Consumer;
+// use serde_json::from_str;
+use crate::adfs_gen_calldata::adfs_serialize_updates;
+use alloy::hex::FromHex;
+use alloy_primitives::{Address, Bytes, Uint, U256};
+use crypto::{verify_signature, PublicKey, Signature};
+use feed_registry::types::FeedResult;
+use std::collections::{HashMap, HashSet};
+use std::hash::RandomState;
+use std::str::FromStr;
+use std::sync::Arc;
+use tokio::sync::RwLock;
+use tracing::{debug, info, warn};
+
+pub const AD_MIN_DATA_POINTS_THRESHOLD: usize = 100;
+
+pub fn check_signature(
+    signature: &Signature,
+    pub_key: &PublicKey,
+    feed_id: &str,
+    timestamp: Timestamp,
+    feed_result: &FeedResult,
+) -> bool {
+    let mut byte_buffer: Vec<u8> = feed_id
+        .as_bytes()
+        .iter()
+        .copied()
+        .chain(timestamp.to_be_bytes().to_vec())
+        .collect();
+
+    match feed_result {
+        Ok(result) => {
+            byte_buffer.extend(result.as_bytes(18));
+        }
+        Err(error) => {
+            warn!("Reported error for feed_id {} : {}", feed_id, error);
+        }
+    };
+    verify_signature(pub_key, signature, &byte_buffer)
+}
+
+#[derive(Debug)]
+pub struct ConsumedReports {
+    pub is_quorum_reached: bool,
+    pub skip_publishing: bool,
+    pub ad_score: Option<f64>,
+    pub result_post_to_contract: Option<VotedFeedUpdateWithProof>,
+    pub end_slot_timestamp: Timestamp,
+}
+
+#[allow(clippy::too_many_arguments)]
+pub async fn consume_reports(
+    name: &str,
+    reports: &HashMap<u64, DataFeedPayload, RandomState>,
+    feed_type: &FeedType,
+    slot: u64,
+    quorum_percentage: f32,
+    skip_publish_if_less_then_percentage: f64,
+    always_publish_heartbeat_ms: Option<u128>,
+    end_slot_timestamp: Timestamp,
+    num_reporters: usize,
+    is_oneshot: bool,
+    aggregator: FeedAggregate,
+    history: Option<Arc<RwLock<FeedAggregateHistory>>>,
+    feed_id: u32,
+) -> ConsumedReports {
+    let values = collect_repoted_values(feed_type, feed_id, reports, slot);
+
+    if values.is_empty() {
+        info!("No reports found for feed: {} slot: {}!", name, &slot);
+        ConsumedReports {
+            is_quorum_reached: false,
+            skip_publishing: true,
+            ad_score: None,
+            result_post_to_contract: None,
+            end_slot_timestamp,
+        }
+    } else {
+        let total_votes_count = values.len() as f32;
+        let required_votes_count = quorum_percentage * 0.01f32 * (num_reporters as f32);
+        let is_quorum_reached = required_votes_count <= total_votes_count;
+        let mut skip_publishing = false;
+        if !is_quorum_reached {
+            warn!(
+                "Insufficient quorum of reports to post to contract for feed: {} slot: {}! Expected at least a quorum of {}, but received {} out of {} valid votes.",
+                name, &slot, quorum_percentage, total_votes_count, num_reporters
+            );
+        }
+
+        // Dispatch to concrete FeedAggregate implementation.
+        let result_post_to_contract = VotedFeedUpdate {
+            feed_id,
+            value: aggregator.aggregate(&values[..]), // Perform the concrete aggregation
+            end_slot_timestamp,
+        };
+
+        let mut proof: Vec<DataFeedPayload> = Vec::new();
+        for (_, v) in reports.iter() {
+            proof.push(v.clone());
+        }
+
+        let mut ad_score_opt: Option<f64> = None;
+
+        // Oneshot feeds have no history, so we cannot perform anomaly detection on them.
+        if !is_oneshot {
+            if let Some(history) = history {
+                if let FeedType::Numerical(candidate_value) = result_post_to_contract.value {
+                    let ad_score =
+                        perform_anomaly_detection(feed_id, history.clone(), candidate_value).await;
+                    match ad_score {
+                        Ok(ad_score) => {
+                            info!(
+                                "AD_score for {:?} is {}",
+                                result_post_to_contract.value, ad_score
+                            );
+                            ad_score_opt = Some(ad_score)
+                        }
+                        Err(e) => {
+                            warn!("Anomaly Detection failed with error - {}", e);
+                        }
+                    }
+                    {
+                        let criteria = PublishCriteria {
+                            feed_id,
+                            skip_publish_if_less_then_percentage,
+                            always_publish_heartbeat_ms,
+                            peg_to_value: None,
+                            peg_tolerance_percentage: 0.0f64,
+                        };
+                        debug!("Get a read lock on history [feed {feed_id}]");
+                        let history_guard = history.read().await;
+                        skip_publishing =
+                            result_post_to_contract.should_skip(&criteria, &history_guard);
+                        debug!("Release the read lock on history [feed {feed_id}]");
+                    }
+                };
+            };
+        }
+        let res = ConsumedReports {
+            is_quorum_reached,
+            skip_publishing,
+            ad_score: ad_score_opt,
+            result_post_to_contract: Some(VotedFeedUpdateWithProof {
+                update: result_post_to_contract,
+                proof,
+            }),
+            end_slot_timestamp,
+        };
+        info!("[feed {feed_id}] result_post_to_contract = {:?}", res);
+        res
+    }
+}
+
+pub fn collect_repoted_values(
path: libs/feeds_processing/src/utils.rs
user: login: smanilov
body: nit: typo in the name; it was there before, but this is a good opportunity to fix this
created_at: 2025-03-10T14:57:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+use anomaly_detection::ingest::anomaly_detector_aggregate;
+use anyhow::{anyhow, Context, Result};
+use config::{FeedConfig, FeedStrideAndDecimals, PublishCriteria};
+use data_feeds::feeds_processing::{
+    BatchedAggegratesToSend, VotedFeedUpdate, VotedFeedUpdateWithProof,
+};
+use feed_registry::aggregate::FeedAggregate;
+use feed_registry::registry::FeedAggregateHistory;
+use feed_registry::types::{DataFeedPayload, FeedMetaData, FeedType, Timestamp};
+use gnosis_safe::data_types::ConsensusSecondRoundBatch;
+use gnosis_safe::utils::{create_safe_tx, generate_transaction_hash};
+use ringbuf::traits::consumer::Consumer;
+// use serde_json::from_str;
+use crate::adfs_gen_calldata::adfs_serialize_updates;
+use alloy::hex::FromHex;
+use alloy_primitives::{Address, Bytes, Uint, U256};
+use crypto::{verify_signature, PublicKey, Signature};
+use feed_registry::types::FeedResult;
+use std::collections::{HashMap, HashSet};
+use std::hash::RandomState;
+use std::str::FromStr;
+use std::sync::Arc;
+use tokio::sync::RwLock;
+use tracing::{debug, info, warn};
+
+pub const AD_MIN_DATA_POINTS_THRESHOLD: usize = 100;
+
+pub fn check_signature(
+    signature: &Signature,
+    pub_key: &PublicKey,
+    feed_id: &str,
+    timestamp: Timestamp,
+    feed_result: &FeedResult,
+) -> bool {
+    let mut byte_buffer: Vec<u8> = feed_id
+        .as_bytes()
+        .iter()
+        .copied()
+        .chain(timestamp.to_be_bytes().to_vec())
+        .collect();
+
+    match feed_result {
+        Ok(result) => {
+            byte_buffer.extend(result.as_bytes(18));
+        }
+        Err(error) => {
+            warn!("Reported error for feed_id {} : {}", feed_id, error);
+        }
+    };
+    verify_signature(pub_key, signature, &byte_buffer)
+}
+
+#[derive(Debug)]
+pub struct ConsumedReports {
+    pub is_quorum_reached: bool,
+    pub skip_publishing: bool,
+    pub ad_score: Option<f64>,
+    pub result_post_to_contract: Option<VotedFeedUpdateWithProof>,
+    pub end_slot_timestamp: Timestamp,
+}
+
+#[allow(clippy::too_many_arguments)]
+pub async fn consume_reports(
+    name: &str,
+    reports: &HashMap<u64, DataFeedPayload, RandomState>,
+    feed_type: &FeedType,
+    slot: u64,
+    quorum_percentage: f32,
+    skip_publish_if_less_then_percentage: f64,
+    always_publish_heartbeat_ms: Option<u128>,
+    end_slot_timestamp: Timestamp,
+    num_reporters: usize,
+    is_oneshot: bool,
+    aggregator: FeedAggregate,
+    history: Option<Arc<RwLock<FeedAggregateHistory>>>,
+    feed_id: u32,
+) -> ConsumedReports {
+    let values = collect_repoted_values(feed_type, feed_id, reports, slot);
+
+    if values.is_empty() {
+        info!("No reports found for feed: {} slot: {}!", name, &slot);
+        ConsumedReports {
+            is_quorum_reached: false,
+            skip_publishing: true,
+            ad_score: None,
+            result_post_to_contract: None,
+            end_slot_timestamp,
+        }
+    } else {
+        let total_votes_count = values.len() as f32;
+        let required_votes_count = quorum_percentage * 0.01f32 * (num_reporters as f32);
+        let is_quorum_reached = required_votes_count <= total_votes_count;
+        let mut skip_publishing = false;
+        if !is_quorum_reached {
+            warn!(
+                "Insufficient quorum of reports to post to contract for feed: {} slot: {}! Expected at least a quorum of {}, but received {} out of {} valid votes.",
+                name, &slot, quorum_percentage, total_votes_count, num_reporters
+            );
+        }
+
+        // Dispatch to concrete FeedAggregate implementation.
+        let result_post_to_contract = VotedFeedUpdate {
+            feed_id,
+            value: aggregator.aggregate(&values[..]), // Perform the concrete aggregation
+            end_slot_timestamp,
+        };
+
+        let mut proof: Vec<DataFeedPayload> = Vec::new();
+        for (_, v) in reports.iter() {
+            proof.push(v.clone());
+        }
+
+        let mut ad_score_opt: Option<f64> = None;
+
+        // Oneshot feeds have no history, so we cannot perform anomaly detection on them.
+        if !is_oneshot {
+            if let Some(history) = history {
+                if let FeedType::Numerical(candidate_value) = result_post_to_contract.value {
+                    let ad_score =
+                        perform_anomaly_detection(feed_id, history.clone(), candidate_value).await;
+                    match ad_score {
+                        Ok(ad_score) => {
+                            info!(
+                                "AD_score for {:?} is {}",
+                                result_post_to_contract.value, ad_score
+                            );
+                            ad_score_opt = Some(ad_score)
+                        }
+                        Err(e) => {
+                            warn!("Anomaly Detection failed with error - {}", e);
+                        }
+                    }
+                    {
+                        let criteria = PublishCriteria {
+                            feed_id,
+                            skip_publish_if_less_then_percentage,
+                            always_publish_heartbeat_ms,
+                            peg_to_value: None,
+                            peg_tolerance_percentage: 0.0f64,
+                        };
+                        debug!("Get a read lock on history [feed {feed_id}]");
+                        let history_guard = history.read().await;
+                        skip_publishing =
+                            result_post_to_contract.should_skip(&criteria, &history_guard);
+                        debug!("Release the read lock on history [feed {feed_id}]");
+                    }
+                };
+            };
+        }
+        let res = ConsumedReports {
+            is_quorum_reached,
+            skip_publishing,
+            ad_score: ad_score_opt,
+            result_post_to_contract: Some(VotedFeedUpdateWithProof {
+                update: result_post_to_contract,
+                proof,
+            }),
+            end_slot_timestamp,
+        };
+        info!("[feed {feed_id}] result_post_to_contract = {:?}", res);
+        res
+    }
+}
+
+pub fn collect_repoted_values(
+    expected_feed_type: &FeedType,
+    feed_id: u32,
+    reports: &std::collections::HashMap<u64, DataFeedPayload>,
path: libs/feeds_processing/src/utils.rs
user: login: smanilov
body: Please, `use std::collections::HashMap` and simplify type here.
created_at: 2025-03-10T14:58:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+use anomaly_detection::ingest::anomaly_detector_aggregate;
+use anyhow::{anyhow, Context, Result};
+use config::{FeedConfig, FeedStrideAndDecimals, PublishCriteria};
+use data_feeds::feeds_processing::{
+    BatchedAggegratesToSend, VotedFeedUpdate, VotedFeedUpdateWithProof,
+};
+use feed_registry::aggregate::FeedAggregate;
+use feed_registry::registry::FeedAggregateHistory;
+use feed_registry::types::{DataFeedPayload, FeedMetaData, FeedType, Timestamp};
+use gnosis_safe::data_types::ConsensusSecondRoundBatch;
+use gnosis_safe::utils::{create_safe_tx, generate_transaction_hash};
+use ringbuf::traits::consumer::Consumer;
+// use serde_json::from_str;
+use crate::adfs_gen_calldata::adfs_serialize_updates;
+use alloy::hex::FromHex;
+use alloy_primitives::{Address, Bytes, Uint, U256};
+use crypto::{verify_signature, PublicKey, Signature};
+use feed_registry::types::FeedResult;
+use std::collections::{HashMap, HashSet};
+use std::hash::RandomState;
+use std::str::FromStr;
+use std::sync::Arc;
+use tokio::sync::RwLock;
+use tracing::{debug, info, warn};
+
+pub const AD_MIN_DATA_POINTS_THRESHOLD: usize = 100;
+
+pub fn check_signature(
+    signature: &Signature,
+    pub_key: &PublicKey,
+    feed_id: &str,
+    timestamp: Timestamp,
+    feed_result: &FeedResult,
+) -> bool {
+    let mut byte_buffer: Vec<u8> = feed_id
+        .as_bytes()
+        .iter()
+        .copied()
+        .chain(timestamp.to_be_bytes().to_vec())
+        .collect();
+
+    match feed_result {
+        Ok(result) => {
+            byte_buffer.extend(result.as_bytes(18));
+        }
+        Err(error) => {
+            warn!("Reported error for feed_id {} : {}", feed_id, error);
+        }
+    };
+    verify_signature(pub_key, signature, &byte_buffer)
+}
+
+#[derive(Debug)]
+pub struct ConsumedReports {
+    pub is_quorum_reached: bool,
+    pub skip_publishing: bool,
+    pub ad_score: Option<f64>,
+    pub result_post_to_contract: Option<VotedFeedUpdateWithProof>,
+    pub end_slot_timestamp: Timestamp,
+}
+
+#[allow(clippy::too_many_arguments)]
+pub async fn consume_reports(
+    name: &str,
+    reports: &HashMap<u64, DataFeedPayload, RandomState>,
path: libs/feeds_processing/src/utils.rs
user: login: smanilov
body: RandomState is the default third type argument.

Please, remove, and remove `use std::hash::RandomState;` in this file.
created_at: 2025-03-10T14:59:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+use anomaly_detection::ingest::anomaly_detector_aggregate;
+use anyhow::{anyhow, Context, Result};
+use config::{FeedConfig, FeedStrideAndDecimals, PublishCriteria};
+use data_feeds::feeds_processing::{
+    BatchedAggegratesToSend, VotedFeedUpdate, VotedFeedUpdateWithProof,
+};
+use feed_registry::aggregate::FeedAggregate;
+use feed_registry::registry::FeedAggregateHistory;
+use feed_registry::types::{DataFeedPayload, FeedMetaData, FeedType, Timestamp};
+use gnosis_safe::data_types::ConsensusSecondRoundBatch;
+use gnosis_safe::utils::{create_safe_tx, generate_transaction_hash};
+use ringbuf::traits::consumer::Consumer;
+// use serde_json::from_str;
+use crate::adfs_gen_calldata::adfs_serialize_updates;
+use alloy::hex::FromHex;
+use alloy_primitives::{Address, Bytes, Uint, U256};
+use crypto::{verify_signature, PublicKey, Signature};
+use feed_registry::types::FeedResult;
+use std::collections::{HashMap, HashSet};
+use std::hash::RandomState;
+use std::str::FromStr;
+use std::sync::Arc;
+use tokio::sync::RwLock;
+use tracing::{debug, info, warn};
+
+pub const AD_MIN_DATA_POINTS_THRESHOLD: usize = 100;
+
+pub fn check_signature(
+    signature: &Signature,
+    pub_key: &PublicKey,
+    feed_id: &str,
+    timestamp: Timestamp,
+    feed_result: &FeedResult,
+) -> bool {
+    let mut byte_buffer: Vec<u8> = feed_id
+        .as_bytes()
+        .iter()
+        .copied()
+        .chain(timestamp.to_be_bytes().to_vec())
+        .collect();
+
+    match feed_result {
+        Ok(result) => {
+            byte_buffer.extend(result.as_bytes(18));
+        }
+        Err(error) => {
+            warn!("Reported error for feed_id {} : {}", feed_id, error);
+        }
+    };
+    verify_signature(pub_key, signature, &byte_buffer)
+}
+
+#[derive(Debug)]
+pub struct ConsumedReports {
+    pub is_quorum_reached: bool,
+    pub skip_publishing: bool,
+    pub ad_score: Option<f64>,
+    pub result_post_to_contract: Option<VotedFeedUpdateWithProof>,
+    pub end_slot_timestamp: Timestamp,
+}
+
+#[allow(clippy::too_many_arguments)]
+pub async fn consume_reports(
+    name: &str,
+    reports: &HashMap<u64, DataFeedPayload, RandomState>,
+    feed_type: &FeedType,
+    slot: u64,
+    quorum_percentage: f32,
+    skip_publish_if_less_then_percentage: f64,
+    always_publish_heartbeat_ms: Option<u128>,
+    end_slot_timestamp: Timestamp,
+    num_reporters: usize,
+    is_oneshot: bool,
+    aggregator: FeedAggregate,
+    history: Option<Arc<RwLock<FeedAggregateHistory>>>,
+    feed_id: u32,
+) -> ConsumedReports {
+    let values = collect_repoted_values(feed_type, feed_id, reports, slot);
+
+    if values.is_empty() {
+        info!("No reports found for feed: {} slot: {}!", name, &slot);
+        ConsumedReports {
+            is_quorum_reached: false,
+            skip_publishing: true,
+            ad_score: None,
+            result_post_to_contract: None,
+            end_slot_timestamp,
+        }
+    } else {
+        let total_votes_count = values.len() as f32;
+        let required_votes_count = quorum_percentage * 0.01f32 * (num_reporters as f32);
+        let is_quorum_reached = required_votes_count <= total_votes_count;
+        let mut skip_publishing = false;
+        if !is_quorum_reached {
+            warn!(
+                "Insufficient quorum of reports to post to contract for feed: {} slot: {}! Expected at least a quorum of {}, but received {} out of {} valid votes.",
+                name, &slot, quorum_percentage, total_votes_count, num_reporters
+            );
+        }
+
+        // Dispatch to concrete FeedAggregate implementation.
+        let result_post_to_contract = VotedFeedUpdate {
+            feed_id,
+            value: aggregator.aggregate(&values[..]), // Perform the concrete aggregation
+            end_slot_timestamp,
+        };
+
+        let mut proof: Vec<DataFeedPayload> = Vec::new();
+        for (_, v) in reports.iter() {
+            proof.push(v.clone());
+        }
+
+        let mut ad_score_opt: Option<f64> = None;
+
+        // Oneshot feeds have no history, so we cannot perform anomaly detection on them.
+        if !is_oneshot {
+            if let Some(history) = history {
+                if let FeedType::Numerical(candidate_value) = result_post_to_contract.value {
+                    let ad_score =
+                        perform_anomaly_detection(feed_id, history.clone(), candidate_value).await;
+                    match ad_score {
+                        Ok(ad_score) => {
+                            info!(
+                                "AD_score for {:?} is {}",
+                                result_post_to_contract.value, ad_score
+                            );
+                            ad_score_opt = Some(ad_score)
+                        }
+                        Err(e) => {
+                            warn!("Anomaly Detection failed with error - {}", e);
+                        }
+                    }
+                    {
+                        let criteria = PublishCriteria {
+                            feed_id,
+                            skip_publish_if_less_then_percentage,
+                            always_publish_heartbeat_ms,
+                            peg_to_value: None,
+                            peg_tolerance_percentage: 0.0f64,
+                        };
+                        debug!("Get a read lock on history [feed {feed_id}]");
+                        let history_guard = history.read().await;
+                        skip_publishing =
+                            result_post_to_contract.should_skip(&criteria, &history_guard);
+                        debug!("Release the read lock on history [feed {feed_id}]");
+                    }
+                };
+            };
+        }
+        let res = ConsumedReports {
+            is_quorum_reached,
+            skip_publishing,
+            ad_score: ad_score_opt,
+            result_post_to_contract: Some(VotedFeedUpdateWithProof {
+                update: result_post_to_contract,
+                proof,
+            }),
+            end_slot_timestamp,
+        };
+        info!("[feed {feed_id}] result_post_to_contract = {:?}", res);
+        res
+    }
+}
+
+pub fn collect_repoted_values(
+    expected_feed_type: &FeedType,
+    feed_id: u32,
+    reports: &std::collections::HashMap<u64, DataFeedPayload>,
+    slot: u64,
+) -> Vec<FeedType> {
+    let mut values: Vec<FeedType> = vec![];
+    for kv in reports {
+        match &kv.1.result {
+            Ok(value) => {
+                if value.same_enum_type_as(expected_feed_type) {
+                    values.push(value.clone());
+                } else {
+                    warn!("Wrong value type reported by reporter {} for feed id {} slot {}! {} expected", kv.0, feed_id, slot, expected_feed_type.enum_type_to_string());
+                }
+            }
+            Err(_) => {
+                warn!(
+                    "Got error from reporter {} for feed id {} slot {}",
+                    kv.0, feed_id, slot
+                );
+            }
+        }
+    }
+    values
+}
+
+pub async fn perform_anomaly_detection(
+    feed_id: u32,
+    history: Arc<RwLock<FeedAggregateHistory>>,
+    candidate_value: f64,
+) -> Result<f64, anyhow::Error> {
+    let anomaly_detection_future = async move {
+        debug!("Get a read lock on history [feed {feed_id}]");
+        let history_lock = history.read().await;
+
+        // The first slice is from the current read position to the end of the array
+        // The second slice represents the segment from the start of the array up to the current write position if the buffer has wrapped around
+        let heap = history_lock
+            .get(feed_id)
+            .context("Missing key from History!")?;
+        let (first, last) = heap.as_slices();
+        let history_vec: Vec<&FeedType> =
+            first.iter().chain(last.iter()).map(|h| &h.value).collect();
+        let mut numerical_vec: Vec<f64> = history_vec
+            .iter()
+            .filter_map(|feed| {
+                if let FeedType::Numerical(value) = feed {
+                    Some(*value)
+                } else if let FeedType::Text(_) = feed {
+                    warn!("Anomaly Detection not implemented for FeedType::Text, skipping...");
+                    None
+                } else {
+                    warn!("Anomaly Detection does not recognize FeedType, skipping...");
+                    None
+                }
+            })
+            .collect();
+
+        drop(history_lock);
+        debug!("Release the read lock on history [feed {feed_id}]");
+
+        numerical_vec.push(candidate_value);
+
+        // Get AD prediction only if enough data is present
+        if numerical_vec.len() > AD_MIN_DATA_POINTS_THRESHOLD {
+            debug!("Starting anomaly detection for [feed {feed_id}]");
+            anomaly_detector_aggregate(numerical_vec).map_err(|e| anyhow!("{e}"))
+        } else {
+            Err(anyhow!(
+                "Skipping anomaly detection; numerical_vec.len() = {} threshold: {}",
+                numerical_vec.len(),
+                AD_MIN_DATA_POINTS_THRESHOLD
+            ))
+        }
+    };
+
+    tokio::task::Builder::new()
+        .name("anomaly_detection")
+        .spawn(anomaly_detection_future)
+        .context("Failed to spawn feed slots manager anomaly detection!")?
+        .await
+        .context("Failed to join feed slots manager anomaly detection!")?
+}
+
+type FeedId = u32;
+type FeedTimestamp = u64;
+type FeedIdToTimetamp = HashMap<FeedId, FeedTimestamp>;
+
+pub async fn validate_sigcheck(
path: libs/feeds_processing/src/utils.rs
user: login: smanilov
body: This function is not used anywhere.

Has it been tested in any way?

Same for validate.
created_at: 2025-03-10T15:03:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,7 +72,7 @@ impl FeedMetaData {
 
     #[allow(clippy::too_many_arguments)]
     pub fn new(
-        name: &str,
+        name: String,
path: libs/feed_registry/src/types.rs
user: login: smanilov
body: Why this change? It seems completely unnecessary, given that `.to_string()` is called on `name` again.
created_at: 2025-03-10T15:18:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -270,7 +270,7 @@ pub async fn register_feed_with_config(
         }
 
         let new_feed_metadata = FeedMetaData::new(
-            &new_name,
path: apps/sequencer/src/feeds/feeds_slots_manager.rs
user: login: smanilov
body: This is the only place where a reference is taken, so perhaps just avoid the clone a few lines earlier?
created_at: 2025-03-10T15:28:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,30 +66,15 @@ pub async fn adfs_serialize_updates(
     // Fill the value updates:
     for update in updates.iter() {
         let feed_id = update.feed_id;
-        debug!("Acquiring a read lock on feeds_config; network={net}; feed_id={feed_id}");
-        let feed_config = feeds_config.read().await.get(&feed_id).cloned();
-        debug!(
-            "Acquired and released a read lock on feeds_config; network={net}; feed_id={feed_id}"
-        );
 
-        let stride = match &feed_config {
-            Some(f) => f.stride,
+        let (stride, digits_in_fraction) = match &feeds_config.get(&feed_id) {
+            Some(f) => (f.stride, f.decimals),
             None => {
-                warn!("Propagating result for unregistered feed! Support left for legacy one shot feeds of 32 bytes size.");
-                1
+                error!("Propagating result for unregistered feed! Support left for legacy one shot feeds of 32 bytes size. Decimale default to 18");
path: libs/feeds_processing/src/adfs_gen_calldata.rs
user: login: smanilov
body: typo: Decimale should be Decimal.
created_at: 2025-03-10T15:34:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,7 +47,11 @@ pub async fn adfs_serialize_updates(
     net: &str,
     feed_updates: &BatchedAggegratesToSend,
     feeds_metrics: Option<Arc<RwLock<FeedsMetrics>>>,
-    feeds_config: Arc<RwLock<HashMap<u32, FeedConfig>>>,
+    feeds_config: HashMap<u32, FeedStrideAndDecimals>,
+    feeds_rounds: &mut HashMap<u32, u64>, /* The rounds table for the relevant feeds. If the feeds_metrics are provided,
+                                          this map will be filled with the update count for each feed from it. If the
+                                          feeds_metrics is None, feeds_rounds will be used as the source of the updates
+                                          count. */
path: libs/feeds_processing/src/adfs_gen_calldata.rs
user: login: smanilov
body: In terms of implementation this is good. In terms of design, I'm not so sure.

We now have an argument, that depending on another argument is sometimes an input parameter and sometimes an output parameter.

This is a rather complicated design in my opinion. I don't have a better suggestion, though.
created_at: 2025-03-10T15:47:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,7 +47,11 @@ pub async fn adfs_serialize_updates(
     net: &str,
     feed_updates: &BatchedAggegratesToSend,
     feeds_metrics: Option<Arc<RwLock<FeedsMetrics>>>,
-    feeds_config: Arc<RwLock<HashMap<u32, FeedConfig>>>,
+    feeds_config: HashMap<u32, FeedStrideAndDecimals>,
path: libs/feeds_processing/src/adfs_gen_calldata.rs
user: login: smanilov
body: Perhaps rename parameter to `strides_and_decimals`, as it's called elsewhere. `feeds_config` is misleading, since there's a type with the same name.
created_at: 2025-03-10T15:56:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -85,13 +85,43 @@ impl VotedFeedUpdate {
     }
 }
 
-pub const REPORT_HEX_SIZE: usize = 64;
-
 pub fn naive_packing(feed_result: &FeedType, digits_in_fraction: usize) -> Vec<u8> {
     //TODO: Return Bytes32 type
     feed_result.as_bytes(digits_in_fraction)
 }
 
+use std::collections::HashMap;
+
+#[derive(Debug, Clone, Default)]
+pub struct BatchedAggegratesToSend {
+    pub block_height: u64,
+    pub updates: Vec<VotedFeedUpdate>,
+    // The key in this map is the feed id for which we provide a proof for the aggregated value.
+    pub proofs: HashMap<u32, Vec<DataFeedPayload>>,
+}
+
+impl BatchedAggegratesToSend {
+    // The updates to be sent to different networks go through multiple filters.
+    // Eventually we might need to reduce the proof to only contain records for
+    // the relevant updates. The following function does that and returns the
+    // count of removed elements.
+    pub fn normalize_proof(&mut self) -> usize {
+        let removed_elements_count = self.proofs.len() - self.updates.len();
+
+        if removed_elements_count > 0 {
+            let mut normalized_proofs = HashMap::new();
+            for update in self.updates.iter() {
+                let Some(proof) = self.proofs.remove(&update.feed_id) else {
+                    error!("Logical ERROR! no proof found for key: {}", update.feed_id);
+                    continue;
+                };
+                normalized_proofs.insert(update.feed_id, proof);
+            }
+        }
path: libs/data_feeds/src/feeds_processing.rs
user: login: HristoStaykov
body: Indeed, this was a reminder that was resolved, thanks.
created_at: 2025-03-10T16:02:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,7 +72,7 @@ impl FeedMetaData {
 
     #[allow(clippy::too_many_arguments)]
     pub fn new(
-        name: &str,
+        name: String,
path: libs/feed_registry/src/types.rs
user: login: HristoStaykov
body: I have removed the `.to_string()` call.
created_at: 2025-03-10T17:16:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,612 @@
+use anomaly_detection::ingest::anomaly_detector_aggregate;
+use anyhow::{anyhow, Context, Result};
+use config::{FeedConfig, FeedStrideAndDecimals, PublishCriteria};
+use data_feeds::feeds_processing::{
+    BatchedAggegratesToSend, VotedFeedUpdate, VotedFeedUpdateWithProof,
+};
+use feed_registry::aggregate::FeedAggregate;
+use feed_registry::registry::FeedAggregateHistory;
+use feed_registry::types::{DataFeedPayload, FeedMetaData, FeedType, Timestamp};
+use gnosis_safe::data_types::ConsensusSecondRoundBatch;
+use gnosis_safe::utils::{create_safe_tx, generate_transaction_hash};
+use ringbuf::traits::consumer::Consumer;
+// use serde_json::from_str;
+use crate::adfs_gen_calldata::adfs_serialize_updates;
+use alloy::hex::FromHex;
+use alloy_primitives::{Address, Bytes, Uint, U256};
+use crypto::{verify_signature, PublicKey, Signature};
+use feed_registry::types::FeedResult;
+use std::collections::{HashMap, HashSet};
+use std::hash::RandomState;
+use std::str::FromStr;
+use std::sync::Arc;
+use tokio::sync::RwLock;
+use tracing::{debug, info, warn};
+
+pub const AD_MIN_DATA_POINTS_THRESHOLD: usize = 100;
+
+pub fn check_signature(
+    signature: &Signature,
+    pub_key: &PublicKey,
+    feed_id: &str,
+    timestamp: Timestamp,
+    feed_result: &FeedResult,
+) -> bool {
+    let mut byte_buffer: Vec<u8> = feed_id
+        .as_bytes()
+        .iter()
+        .copied()
+        .chain(timestamp.to_be_bytes().to_vec())
+        .collect();
+
+    match feed_result {
+        Ok(result) => {
+            byte_buffer.extend(result.as_bytes(18));
+        }
+        Err(error) => {
+            warn!("Reported error for feed_id {} : {}", feed_id, error);
+        }
+    };
+    verify_signature(pub_key, signature, &byte_buffer)
+}
+
+#[derive(Debug)]
+pub struct ConsumedReports {
+    pub is_quorum_reached: bool,
+    pub skip_publishing: bool,
+    pub ad_score: Option<f64>,
+    pub result_post_to_contract: Option<VotedFeedUpdateWithProof>,
+    pub end_slot_timestamp: Timestamp,
+}
+
+#[allow(clippy::too_many_arguments)]
+pub async fn consume_reports(
+    name: &str,
+    reports: &HashMap<u64, DataFeedPayload, RandomState>,
+    feed_type: &FeedType,
+    slot: u64,
+    quorum_percentage: f32,
+    skip_publish_if_less_then_percentage: f64,
+    always_publish_heartbeat_ms: Option<u128>,
+    end_slot_timestamp: Timestamp,
+    num_reporters: usize,
+    is_oneshot: bool,
+    aggregator: FeedAggregate,
+    history: Option<Arc<RwLock<FeedAggregateHistory>>>,
+    feed_id: u32,
+) -> ConsumedReports {
+    let values = collect_repoted_values(feed_type, feed_id, reports, slot);
+
+    if values.is_empty() {
+        info!("No reports found for feed: {} slot: {}!", name, &slot);
+        ConsumedReports {
+            is_quorum_reached: false,
+            skip_publishing: true,
+            ad_score: None,
+            result_post_to_contract: None,
+            end_slot_timestamp,
+        }
+    } else {
+        let total_votes_count = values.len() as f32;
+        let required_votes_count = quorum_percentage * 0.01f32 * (num_reporters as f32);
+        let is_quorum_reached = required_votes_count <= total_votes_count;
+        let mut skip_publishing = false;
+        if !is_quorum_reached {
+            warn!(
+                "Insufficient quorum of reports to post to contract for feed: {} slot: {}! Expected at least a quorum of {}, but received {} out of {} valid votes.",
+                name, &slot, quorum_percentage, total_votes_count, num_reporters
+            );
+        }
+
+        // Dispatch to concrete FeedAggregate implementation.
+        let result_post_to_contract = VotedFeedUpdate {
+            feed_id,
+            value: aggregator.aggregate(&values[..]), // Perform the concrete aggregation
+            end_slot_timestamp,
+        };
+
+        let mut proof: Vec<DataFeedPayload> = Vec::new();
+        for (_, v) in reports.iter() {
+            proof.push(v.clone());
+        }
+
+        let mut ad_score_opt: Option<f64> = None;
+
+        // Oneshot feeds have no history, so we cannot perform anomaly detection on them.
+        if !is_oneshot {
+            if let Some(history) = history {
+                if let FeedType::Numerical(candidate_value) = result_post_to_contract.value {
+                    let ad_score =
+                        perform_anomaly_detection(feed_id, history.clone(), candidate_value).await;
+                    match ad_score {
+                        Ok(ad_score) => {
+                            info!(
+                                "AD_score for {:?} is {}",
+                                result_post_to_contract.value, ad_score
+                            );
+                            ad_score_opt = Some(ad_score)
+                        }
+                        Err(e) => {
+                            warn!("Anomaly Detection failed with error - {}", e);
+                        }
+                    }
+                    {
+                        let criteria = PublishCriteria {
+                            feed_id,
+                            skip_publish_if_less_then_percentage,
+                            always_publish_heartbeat_ms,
+                            peg_to_value: None,
+                            peg_tolerance_percentage: 0.0f64,
+                        };
+                        debug!("Get a read lock on history [feed {feed_id}]");
+                        let history_guard = history.read().await;
+                        skip_publishing =
+                            result_post_to_contract.should_skip(&criteria, &history_guard);
+                        debug!("Release the read lock on history [feed {feed_id}]");
+                    }
+                };
+            };
+        }
+        let res = ConsumedReports {
+            is_quorum_reached,
+            skip_publishing,
+            ad_score: ad_score_opt,
+            result_post_to_contract: Some(VotedFeedUpdateWithProof {
+                update: result_post_to_contract,
+                proof,
+            }),
+            end_slot_timestamp,
+        };
+        info!("[feed {feed_id}] result_post_to_contract = {:?}", res);
+        res
+    }
+}
+
+pub fn collect_repoted_values(
+    expected_feed_type: &FeedType,
+    feed_id: u32,
+    reports: &std::collections::HashMap<u64, DataFeedPayload>,
+    slot: u64,
+) -> Vec<FeedType> {
+    let mut values: Vec<FeedType> = vec![];
+    for kv in reports {
+        match &kv.1.result {
+            Ok(value) => {
+                if value.same_enum_type_as(expected_feed_type) {
+                    values.push(value.clone());
+                } else {
+                    warn!("Wrong value type reported by reporter {} for feed id {} slot {}! {} expected", kv.0, feed_id, slot, expected_feed_type.enum_type_to_string());
+                }
+            }
+            Err(_) => {
+                warn!(
+                    "Got error from reporter {} for feed id {} slot {}",
+                    kv.0, feed_id, slot
+                );
+            }
+        }
+    }
+    values
+}
+
+pub async fn perform_anomaly_detection(
+    feed_id: u32,
+    history: Arc<RwLock<FeedAggregateHistory>>,
+    candidate_value: f64,
+) -> Result<f64, anyhow::Error> {
+    let anomaly_detection_future = async move {
+        debug!("Get a read lock on history [feed {feed_id}]");
+        let history_lock = history.read().await;
+
+        // The first slice is from the current read position to the end of the array
+        // The second slice represents the segment from the start of the array up to the current write position if the buffer has wrapped around
+        let heap = history_lock
+            .get(feed_id)
+            .context("Missing key from History!")?;
+        let (first, last) = heap.as_slices();
+        let history_vec: Vec<&FeedType> =
+            first.iter().chain(last.iter()).map(|h| &h.value).collect();
+        let mut numerical_vec: Vec<f64> = history_vec
+            .iter()
+            .filter_map(|feed| {
+                if let FeedType::Numerical(value) = feed {
+                    Some(*value)
+                } else if let FeedType::Text(_) = feed {
+                    warn!("Anomaly Detection not implemented for FeedType::Text, skipping...");
+                    None
+                } else {
+                    warn!("Anomaly Detection does not recognize FeedType, skipping...");
+                    None
+                }
+            })
+            .collect();
+
+        drop(history_lock);
+        debug!("Release the read lock on history [feed {feed_id}]");
+
+        numerical_vec.push(candidate_value);
+
+        // Get AD prediction only if enough data is present
+        if numerical_vec.len() > AD_MIN_DATA_POINTS_THRESHOLD {
+            debug!("Starting anomaly detection for [feed {feed_id}]");
+            anomaly_detector_aggregate(numerical_vec).map_err(|e| anyhow!("{e}"))
+        } else {
+            Err(anyhow!(
+                "Skipping anomaly detection; numerical_vec.len() = {} threshold: {}",
+                numerical_vec.len(),
+                AD_MIN_DATA_POINTS_THRESHOLD
+            ))
+        }
+    };
+
+    tokio::task::Builder::new()
+        .name("anomaly_detection")
+        .spawn(anomaly_detection_future)
+        .context("Failed to spawn feed slots manager anomaly detection!")?
+        .await
+        .context("Failed to join feed slots manager anomaly detection!")?
+}
+
+type FeedId = u32;
+type FeedTimestamp = u64;
+type FeedIdToTimetamp = HashMap<FeedId, FeedTimestamp>;
+
+pub async fn validate_sigcheck(
path: libs/feeds_processing/src/utils.rs
user: login: HristoStaykov
body: Checking the signatures was the reporter's initial method to confirm that a quorum had been reached before participating in the second round of consensus. Later, we decided to use a deviation-based check, where the reporter only verifies that the aggregated value is within an acceptable deviation of its own vote. Both functions are yet to be tested.
created_at: 2025-03-10T17:30:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,7 +47,11 @@ pub async fn adfs_serialize_updates(
     net: &str,
     feed_updates: &BatchedAggegratesToSend,
     feeds_metrics: Option<Arc<RwLock<FeedsMetrics>>>,
-    feeds_config: Arc<RwLock<HashMap<u32, FeedConfig>>>,
+    feeds_config: HashMap<u32, FeedStrideAndDecimals>,
+    feeds_rounds: &mut HashMap<u32, u64>, /* The rounds table for the relevant feeds. If the feeds_metrics are provided,
+                                          this map will be filled with the update count for each feed from it. If the
+                                          feeds_metrics is None, feeds_rounds will be used as the source of the updates
+                                          count. */
path: libs/feeds_processing/src/adfs_gen_calldata.rs
user: login: HristoStaykov
body: I agree that it is a bit complicated, but we need to be able to call this function both from within the sequencer (which manages the feed round buffer offsets) and from within the reporter (which receives those offsets from the sequencer and uses the provided values as input).
created_at: 2025-03-10T17:35:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,7 +72,7 @@ impl FeedMetaData {
 
     #[allow(clippy::too_many_arguments)]
     pub fn new(
-        name: &str,
+        name: String,
path: libs/feed_registry/src/types.rs
user: login: Xearty
body: It's better for a function to take ownership of a String if it needs an owned String in its body
created_at: 2025-03-11T08:57:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-11T08:52:14Z
body: ### 🚀 Deployment Links of Blocksense Network websites:

| Website | Latest Update | Commit |
|---------------------|----------|----------------------------------------------------------------------|
| 🌱 [Documentation](https://b047883f.blocksense.pages.dev) | 11.03.2025 10:52:12 | 1bf9ce7a1b9e2a3ac7fe9faea9149f2c045e25bd |
<!-- Sticky Pull Request Comment -->
author_association: NONE
user: login: Xearty
created_at: 2025-03-11T08:54:45Z
body: @smanilov Some of the comments have been resolved in https://github.com/blocksense-network/blocksense/pull/980
author_association: CONTRIBUTOR
user: login: smanilov
created_at: 2025-03-11T09:58:45Z
body: > @smanilov Some of the comments have been resolved in #980

Sweet! Which ones?
author_association: CONTRIBUTOR