Added some crazy AI generated ideas, as a brainstorming exercise.
This commit is contained in:
802
examples/oracle_network.rs
Normal file
802
examples/oracle_network.rs
Normal file
@@ -0,0 +1,802 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// A comprehensive example of a decentralized oracle network using BFT-CRDTs
|
||||
/// that eliminates the need for consensus on price updates.
|
||||
|
||||
// ==== Core Types ====
|
||||
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OracleId(pub String);
|
||||
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AssetPair(pub String);
|
||||
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AttestationId(pub String);
|
||||
|
||||
/// A price attestation from an oracle
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PriceAttestation {
|
||||
pub id: AttestationId,
|
||||
pub oracle_id: OracleId,
|
||||
pub asset_pair: AssetPair,
|
||||
pub price: u128,
|
||||
pub confidence: u8, // 0-100
|
||||
pub timestamp: u64,
|
||||
pub sources: Vec<DataSource>,
|
||||
pub proof: AttestationProof,
|
||||
pub signature: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Data source information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DataSource {
|
||||
pub name: String,
|
||||
pub price: u128,
|
||||
pub volume: u128,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
/// Proof that the price data is authentic
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum AttestationProof {
|
||||
/// TLS proof from HTTPS API
|
||||
TlsProof {
|
||||
server_cert: Vec<u8>,
|
||||
response_hash: Vec<u8>,
|
||||
timestamp: u64,
|
||||
},
|
||||
/// Signed data from WebSocket feed
|
||||
SignedFeed {
|
||||
exchange_signature: Vec<u8>,
|
||||
sequence_number: u64,
|
||||
},
|
||||
/// On-chain proof (e.g., from DEX)
|
||||
OnChainProof {
|
||||
chain_id: String,
|
||||
block_number: u64,
|
||||
transaction_hash: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Oracle reputation and performance metrics
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OracleMetrics {
|
||||
pub oracle_id: OracleId,
|
||||
pub total_attestations: u64,
|
||||
pub average_deviation: f64,
|
||||
pub uptime_percentage: f64,
|
||||
pub last_submission: u64,
|
||||
pub quality_score: f64,
|
||||
}
|
||||
|
||||
/// The main CRDT structure for the oracle network
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OracleNetworkCRDT {
|
||||
/// All price attestations
|
||||
attestations: HashMap<AttestationId, PriceAttestation>,
|
||||
|
||||
/// Index by asset pair and time for efficient queries
|
||||
price_index: BTreeMap<(AssetPair, u64), Vec<AttestationId>>,
|
||||
|
||||
/// Oracle performance metrics
|
||||
oracle_metrics: HashMap<OracleId, OracleMetrics>,
|
||||
|
||||
/// Detected anomalies and disputes
|
||||
anomalies: HashMap<AttestationId, AnomalyReport>,
|
||||
|
||||
/// Network parameters
|
||||
params: NetworkParams,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NetworkParams {
|
||||
pub min_oracle_stake: u128,
|
||||
pub max_price_age: Duration,
|
||||
pub outlier_threshold: f64,
|
||||
pub min_sources: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AnomalyReport {
|
||||
pub attestation_id: AttestationId,
|
||||
pub report_type: AnomalyType,
|
||||
pub reporter: OracleId,
|
||||
pub evidence: String,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum AnomalyType {
|
||||
OutlierPrice { deviation_percentage: f64 },
|
||||
InvalidProof,
|
||||
StaleData { age_seconds: u64 },
|
||||
SuspiciousPattern,
|
||||
}
|
||||
|
||||
impl OracleNetworkCRDT {
|
||||
pub fn new(params: NetworkParams) -> Self {
|
||||
Self {
|
||||
attestations: HashMap::new(),
|
||||
price_index: BTreeMap::new(),
|
||||
oracle_metrics: HashMap::new(),
|
||||
anomalies: HashMap::new(),
|
||||
params,
|
||||
}
|
||||
}
|
||||
|
||||
/// Submit a new price attestation to the network
|
||||
pub fn submit_attestation(
|
||||
&mut self,
|
||||
attestation: PriceAttestation,
|
||||
) -> Result<(), String> {
|
||||
// Validate attestation
|
||||
self.validate_attestation(&attestation)?;
|
||||
|
||||
// Check for duplicate
|
||||
if self.attestations.contains_key(&attestation.id) {
|
||||
return Ok(()); // Idempotent
|
||||
}
|
||||
|
||||
// Add to main storage
|
||||
let id = attestation.id.clone();
|
||||
let asset_pair = attestation.asset_pair.clone();
|
||||
let timestamp = attestation.timestamp;
|
||||
let oracle_id = attestation.oracle_id.clone();
|
||||
|
||||
self.attestations.insert(id.clone(), attestation);
|
||||
|
||||
// Update index
|
||||
self.price_index
|
||||
.entry((asset_pair, timestamp))
|
||||
.or_insert_with(Vec::new)
|
||||
.push(id);
|
||||
|
||||
// Update oracle metrics
|
||||
self.update_oracle_metrics(&oracle_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate an attestation before accepting it
|
||||
fn validate_attestation(&self, attestation: &PriceAttestation) -> Result<(), String> {
|
||||
// Check timestamp is reasonable
|
||||
let now = Self::timestamp();
|
||||
if attestation.timestamp > now + 60 {
|
||||
return Err("Attestation timestamp is in the future".to_string());
|
||||
}
|
||||
if attestation.timestamp < now - self.params.max_price_age.as_secs() {
|
||||
return Err("Attestation is too old".to_string());
|
||||
}
|
||||
|
||||
// Verify minimum sources
|
||||
if attestation.sources.len() < self.params.min_sources {
|
||||
return Err("Insufficient data sources".to_string());
|
||||
}
|
||||
|
||||
// Verify signature (placeholder - real implementation would verify cryptographically)
|
||||
if attestation.signature.is_empty() {
|
||||
return Err("Missing signature".to_string());
|
||||
}
|
||||
|
||||
// Validate proof
|
||||
match &attestation.proof {
|
||||
AttestationProof::TlsProof { timestamp, .. } => {
|
||||
if *timestamp < attestation.timestamp - 300 {
|
||||
return Err("TLS proof too old".to_string());
|
||||
}
|
||||
}
|
||||
AttestationProof::SignedFeed { .. } => {
|
||||
// Verify exchange signature in real implementation
|
||||
}
|
||||
AttestationProof::OnChainProof { .. } => {
|
||||
// Verify on-chain data in real implementation
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get aggregated price for an asset pair within a time window
|
||||
pub fn get_aggregate_price(
|
||||
&self,
|
||||
asset_pair: &AssetPair,
|
||||
time_window: Duration,
|
||||
) -> Option<AggregatedPrice> {
|
||||
let now = Self::timestamp();
|
||||
let start_time = now.saturating_sub(time_window.as_secs());
|
||||
|
||||
// Collect all attestations in time window
|
||||
let mut attestations = Vec::new();
|
||||
|
||||
for ((pair, timestamp), attestation_ids) in self.price_index.range(
|
||||
(asset_pair.clone(), start_time)..=(asset_pair.clone(), now)
|
||||
) {
|
||||
if pair == asset_pair {
|
||||
for id in attestation_ids {
|
||||
if let Some(attestation) = self.attestations.get(id) {
|
||||
attestations.push(attestation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if attestations.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Calculate aggregated price
|
||||
self.aggregate_prices(attestations, now)
|
||||
}
|
||||
|
||||
/// Aggregate multiple price attestations into a single price
|
||||
fn aggregate_prices(
|
||||
&self,
|
||||
attestations: Vec<&PriceAttestation>,
|
||||
current_time: u64,
|
||||
) -> Option<AggregatedPrice> {
|
||||
let mut weighted_prices = Vec::new();
|
||||
|
||||
for attestation in &attestations {
|
||||
// Calculate weight based on:
|
||||
// 1. Oracle quality score
|
||||
// 2. Attestation confidence
|
||||
// 3. Recency
|
||||
let oracle_score = self.oracle_metrics
|
||||
.get(&attestation.oracle_id)
|
||||
.map(|m| m.quality_score)
|
||||
.unwrap_or(0.5);
|
||||
|
||||
let confidence_weight = attestation.confidence as f64 / 100.0;
|
||||
|
||||
let age = current_time.saturating_sub(attestation.timestamp);
|
||||
let recency_weight = 1.0 / (1.0 + (age as f64 / 300.0)); // 5-minute half-life
|
||||
|
||||
let total_weight = oracle_score * confidence_weight * recency_weight;
|
||||
|
||||
weighted_prices.push((attestation.price, total_weight));
|
||||
}
|
||||
|
||||
// Remove outliers
|
||||
let filtered_prices = self.remove_outliers(weighted_prices);
|
||||
|
||||
if filtered_prices.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Calculate weighted average
|
||||
let total_weight: f64 = filtered_prices.iter().map(|(_, w)| w).sum();
|
||||
let weighted_sum: f64 = filtered_prices
|
||||
.iter()
|
||||
.map(|(price, weight)| *price as f64 * weight)
|
||||
.sum();
|
||||
|
||||
let average_price = (weighted_sum / total_weight) as u128;
|
||||
|
||||
// Calculate confidence metrics
|
||||
let prices: Vec<u128> = filtered_prices.iter().map(|(p, _)| *p).collect();
|
||||
let std_dev = self.calculate_std_dev(&prices, average_price);
|
||||
let confidence = self.calculate_confidence(&prices, std_dev, average_price);
|
||||
|
||||
Some(AggregatedPrice {
|
||||
price: average_price,
|
||||
confidence,
|
||||
num_sources: filtered_prices.len(),
|
||||
std_deviation: std_dev,
|
||||
timestamp: current_time,
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove statistical outliers from price data
|
||||
fn remove_outliers(&self, mut prices: Vec<(u128, f64)>) -> Vec<(u128, f64)> {
|
||||
if prices.len() < 3 {
|
||||
return prices; // Not enough data to detect outliers
|
||||
}
|
||||
|
||||
// Sort by price
|
||||
prices.sort_by_key(|(price, _)| *price);
|
||||
|
||||
// Calculate IQR (Interquartile Range)
|
||||
let q1_idx = prices.len() / 4;
|
||||
let q3_idx = 3 * prices.len() / 4;
|
||||
let q1 = prices[q1_idx].0;
|
||||
let q3 = prices[q3_idx].0;
|
||||
let iqr = q3.saturating_sub(q1);
|
||||
|
||||
// Filter out outliers (prices outside 1.5 * IQR)
|
||||
let lower_bound = q1.saturating_sub(iqr * 3 / 2);
|
||||
let upper_bound = q3.saturating_add(iqr * 3 / 2);
|
||||
|
||||
prices
|
||||
.into_iter()
|
||||
.filter(|(price, _)| *price >= lower_bound && *price <= upper_bound)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Calculate standard deviation
|
||||
fn calculate_std_dev(&self, prices: &[u128], mean: u128) -> u128 {
|
||||
if prices.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let variance: f64 = prices
|
||||
.iter()
|
||||
.map(|price| {
|
||||
let diff = if *price > mean {
|
||||
(*price - mean) as f64
|
||||
} else {
|
||||
(mean - *price) as f64
|
||||
};
|
||||
diff * diff
|
||||
})
|
||||
.sum::<f64>() / prices.len() as f64;
|
||||
|
||||
variance.sqrt() as u128
|
||||
}
|
||||
|
||||
/// Calculate confidence score based on data quality
|
||||
fn calculate_confidence(&self, prices: &[u128], std_dev: u128, mean: u128) -> u8 {
|
||||
// Base confidence on:
|
||||
// 1. Number of sources
|
||||
// 2. Standard deviation relative to mean
|
||||
// 3. Agreement between sources
|
||||
|
||||
let num_sources_score = (prices.len().min(10) * 10) as u8;
|
||||
|
||||
let deviation_ratio = if mean > 0 {
|
||||
(std_dev as f64) / (mean as f64)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let deviation_score = if deviation_ratio < 0.01 {
|
||||
100
|
||||
} else if deviation_ratio < 0.05 {
|
||||
80
|
||||
} else if deviation_ratio < 0.10 {
|
||||
60
|
||||
} else {
|
||||
40
|
||||
};
|
||||
|
||||
(num_sources_score.min(100) + deviation_score) / 2
|
||||
}
|
||||
|
||||
/// Update oracle performance metrics
|
||||
fn update_oracle_metrics(&mut self, oracle_id: &OracleId) {
|
||||
let attestations: Vec<_> = self.attestations
|
||||
.values()
|
||||
.filter(|a| &a.oracle_id == oracle_id)
|
||||
.collect();
|
||||
|
||||
if attestations.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let metrics = self.oracle_metrics
|
||||
.entry(oracle_id.clone())
|
||||
.or_insert(OracleMetrics {
|
||||
oracle_id: oracle_id.clone(),
|
||||
total_attestations: 0,
|
||||
average_deviation: 0.0,
|
||||
uptime_percentage: 100.0,
|
||||
last_submission: 0,
|
||||
quality_score: 0.5,
|
||||
});
|
||||
|
||||
metrics.total_attestations = attestations.len() as u64;
|
||||
metrics.last_submission = attestations
|
||||
.iter()
|
||||
.map(|a| a.timestamp)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
// Calculate quality score based on historical performance
|
||||
// (simplified - real implementation would track accuracy over time)
|
||||
metrics.quality_score = 0.5 + (metrics.total_attestations.min(100) as f64 / 200.0);
|
||||
}
|
||||
|
||||
/// Report an anomaly in an attestation
|
||||
pub fn report_anomaly(
|
||||
&mut self,
|
||||
attestation_id: AttestationId,
|
||||
reporter: OracleId,
|
||||
anomaly_type: AnomalyType,
|
||||
evidence: String,
|
||||
) -> Result<(), String> {
|
||||
if !self.attestations.contains_key(&attestation_id) {
|
||||
return Err("Attestation not found".to_string());
|
||||
}
|
||||
|
||||
let report = AnomalyReport {
|
||||
attestation_id: attestation_id.clone(),
|
||||
report_type: anomaly_type,
|
||||
reporter,
|
||||
evidence,
|
||||
timestamp: Self::timestamp(),
|
||||
};
|
||||
|
||||
self.anomalies.insert(attestation_id, report);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get oracle reputation score
|
||||
pub fn get_oracle_reputation(&self, oracle_id: &OracleId) -> Option<OracleReputation> {
|
||||
let metrics = self.oracle_metrics.get(oracle_id)?;
|
||||
|
||||
// Count anomalies reported against this oracle
|
||||
let anomaly_count = self.anomalies
|
||||
.values()
|
||||
.filter(|report| {
|
||||
self.attestations
|
||||
.get(&report.attestation_id)
|
||||
.map(|a| &a.oracle_id == oracle_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.count();
|
||||
|
||||
Some(OracleReputation {
|
||||
oracle_id: oracle_id.clone(),
|
||||
quality_score: metrics.quality_score,
|
||||
total_attestations: metrics.total_attestations,
|
||||
anomaly_reports: anomaly_count as u64,
|
||||
last_active: metrics.last_submission,
|
||||
})
|
||||
}
|
||||
|
||||
/// Merge another CRDT instance
|
||||
pub fn merge(&mut self, other: &Self) {
|
||||
// Merge attestations
|
||||
for (id, attestation) in &other.attestations {
|
||||
if !self.attestations.contains_key(id) {
|
||||
let _ = self.submit_attestation(attestation.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Merge anomaly reports
|
||||
for (id, report) in &other.anomalies {
|
||||
self.anomalies.entry(id.clone()).or_insert(report.clone());
|
||||
}
|
||||
|
||||
// Recalculate metrics after merge
|
||||
for oracle_id in other.oracle_metrics.keys() {
|
||||
self.update_oracle_metrics(oracle_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn timestamp() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
}
|
||||
|
||||
/// Aggregated price result
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AggregatedPrice {
|
||||
pub price: u128,
|
||||
pub confidence: u8,
|
||||
pub num_sources: usize,
|
||||
pub std_deviation: u128,
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
/// Oracle reputation information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OracleReputation {
|
||||
pub oracle_id: OracleId,
|
||||
pub quality_score: f64,
|
||||
pub total_attestations: u64,
|
||||
pub anomaly_reports: u64,
|
||||
pub last_active: u64,
|
||||
}
|
||||
|
||||
/// Example oracle client implementation
|
||||
pub struct OracleClient {
|
||||
oracle_id: OracleId,
|
||||
network: OracleNetworkCRDT,
|
||||
data_sources: Vec<Box<dyn DataSourceClient>>,
|
||||
}
|
||||
|
||||
trait DataSourceClient {
|
||||
fn fetch_price(&self, asset_pair: &AssetPair) -> Result<DataSource, String>;
|
||||
}
|
||||
|
||||
impl OracleClient {
|
||||
pub async fn submit_price(&mut self, asset_pair: AssetPair) -> Result<(), String> {
|
||||
// Fetch prices from multiple sources
|
||||
let mut sources = Vec::new();
|
||||
for client in &self.data_sources {
|
||||
match client.fetch_price(&asset_pair) {
|
||||
Ok(source) => sources.push(source),
|
||||
Err(_) => continue, // Skip failed sources
|
||||
}
|
||||
}
|
||||
|
||||
if sources.len() < self.network.params.min_sources {
|
||||
return Err("Insufficient data sources available".to_string());
|
||||
}
|
||||
|
||||
// Calculate aggregate price from sources
|
||||
let total_volume: u128 = sources.iter().map(|s| s.volume).sum();
|
||||
let weighted_sum: u128 = sources
|
||||
.iter()
|
||||
.map(|s| s.price * s.volume)
|
||||
.sum();
|
||||
let price = weighted_sum / total_volume;
|
||||
|
||||
// Calculate confidence based on source agreement
|
||||
let prices: Vec<u128> = sources.iter().map(|s| s.price).collect();
|
||||
let std_dev = self.network.calculate_std_dev(&prices, price);
|
||||
let confidence = if std_dev < price / 100 { 95 } else { 80 };
|
||||
|
||||
// Create attestation
|
||||
let attestation = PriceAttestation {
|
||||
id: AttestationId(format!("{}_{}", self.oracle_id.0, Self::timestamp())),
|
||||
oracle_id: self.oracle_id.clone(),
|
||||
asset_pair,
|
||||
price,
|
||||
confidence,
|
||||
timestamp: Self::timestamp(),
|
||||
sources,
|
||||
proof: AttestationProof::SignedFeed {
|
||||
exchange_signature: vec![1, 2, 3], // Placeholder
|
||||
sequence_number: Self::timestamp(),
|
||||
},
|
||||
signature: vec![4, 5, 6], // Placeholder
|
||||
};
|
||||
|
||||
// Submit to network
|
||||
self.network.submit_attestation(attestation)
|
||||
}
|
||||
|
||||
fn timestamp() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_oracle_network() {
|
||||
let params = NetworkParams {
|
||||
min_oracle_stake: 1000,
|
||||
max_price_age: Duration::from_secs(3600),
|
||||
outlier_threshold: 0.1,
|
||||
min_sources: 2,
|
||||
};
|
||||
|
||||
let mut network = OracleNetworkCRDT::new(params);
|
||||
|
||||
// Create test oracles
|
||||
let oracle1 = OracleId("oracle1".to_string());
|
||||
let oracle2 = OracleId("oracle2".to_string());
|
||||
let oracle3 = OracleId("oracle3".to_string());
|
||||
|
||||
let eth_usd = AssetPair("ETH/USD".to_string());
|
||||
|
||||
// Submit prices from multiple oracles
|
||||
let attestation1 = PriceAttestation {
|
||||
id: AttestationId("att1".to_string()),
|
||||
oracle_id: oracle1.clone(),
|
||||
asset_pair: eth_usd.clone(),
|
||||
price: 2500_000_000, // $2500 with 6 decimals
|
||||
confidence: 95,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
sources: vec![
|
||||
DataSource {
|
||||
name: "Binance".to_string(),
|
||||
price: 2501_000_000,
|
||||
volume: 1000_000_000,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
},
|
||||
DataSource {
|
||||
name: "Coinbase".to_string(),
|
||||
price: 2499_000_000,
|
||||
volume: 800_000_000,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
},
|
||||
],
|
||||
proof: AttestationProof::SignedFeed {
|
||||
exchange_signature: vec![1, 2, 3],
|
||||
sequence_number: 1,
|
||||
},
|
||||
signature: vec![4, 5, 6],
|
||||
};
|
||||
|
||||
network.submit_attestation(attestation1).unwrap();
|
||||
|
||||
// Submit from oracle 2
|
||||
let attestation2 = PriceAttestation {
|
||||
id: AttestationId("att2".to_string()),
|
||||
oracle_id: oracle2.clone(),
|
||||
asset_pair: eth_usd.clone(),
|
||||
price: 2502_000_000,
|
||||
confidence: 90,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
sources: vec![
|
||||
DataSource {
|
||||
name: "Kraken".to_string(),
|
||||
price: 2502_000_000,
|
||||
volume: 500_000_000,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
},
|
||||
DataSource {
|
||||
name: "Gemini".to_string(),
|
||||
price: 2502_000_000,
|
||||
volume: 300_000_000,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
},
|
||||
],
|
||||
proof: AttestationProof::SignedFeed {
|
||||
exchange_signature: vec![7, 8, 9],
|
||||
sequence_number: 2,
|
||||
},
|
||||
signature: vec![10, 11, 12],
|
||||
};
|
||||
|
||||
network.submit_attestation(attestation2).unwrap();
|
||||
|
||||
// Submit outlier price from oracle 3
|
||||
let attestation3 = PriceAttestation {
|
||||
id: AttestationId("att3".to_string()),
|
||||
oracle_id: oracle3.clone(),
|
||||
asset_pair: eth_usd.clone(),
|
||||
price: 3000_000_000, // Outlier price
|
||||
confidence: 50,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
sources: vec![
|
||||
DataSource {
|
||||
name: "Unknown".to_string(),
|
||||
price: 3000_000_000,
|
||||
volume: 100_000_000,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
},
|
||||
DataSource {
|
||||
name: "Sketchy".to_string(),
|
||||
price: 3000_000_000,
|
||||
volume: 50_000_000,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
},
|
||||
],
|
||||
proof: AttestationProof::SignedFeed {
|
||||
exchange_signature: vec![13, 14, 15],
|
||||
sequence_number: 3,
|
||||
},
|
||||
signature: vec![16, 17, 18],
|
||||
};
|
||||
|
||||
network.submit_attestation(attestation3).unwrap();
|
||||
|
||||
// Get aggregated price
|
||||
let aggregated = network
|
||||
.get_aggregate_price(ð_usd, Duration::from_secs(300))
|
||||
.unwrap();
|
||||
|
||||
// Should filter out the outlier
|
||||
assert!(aggregated.price > 2490_000_000 && aggregated.price < 2510_000_000);
|
||||
assert!(aggregated.confidence > 80);
|
||||
assert_eq!(aggregated.num_sources, 2); // Outlier filtered out
|
||||
|
||||
// Report anomaly
|
||||
network
|
||||
.report_anomaly(
|
||||
AttestationId("att3".to_string()),
|
||||
oracle1.clone(),
|
||||
AnomalyType::OutlierPrice {
|
||||
deviation_percentage: 20.0,
|
||||
},
|
||||
"Price deviates significantly from market".to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check oracle reputation
|
||||
let reputation = network.get_oracle_reputation(&oracle3).unwrap();
|
||||
assert_eq!(reputation.anomaly_reports, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_crdt_merge() {
|
||||
let params = NetworkParams {
|
||||
min_oracle_stake: 1000,
|
||||
max_price_age: Duration::from_secs(3600),
|
||||
outlier_threshold: 0.1,
|
||||
min_sources: 2,
|
||||
};
|
||||
|
||||
let mut network1 = OracleNetworkCRDT::new(params.clone());
|
||||
let mut network2 = OracleNetworkCRDT::new(params);
|
||||
|
||||
let oracle1 = OracleId("oracle1".to_string());
|
||||
let btc_usd = AssetPair("BTC/USD".to_string());
|
||||
|
||||
// Submit to network1
|
||||
let attestation1 = PriceAttestation {
|
||||
id: AttestationId("att1".to_string()),
|
||||
oracle_id: oracle1.clone(),
|
||||
asset_pair: btc_usd.clone(),
|
||||
price: 45000_000_000,
|
||||
confidence: 95,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
sources: vec![
|
||||
DataSource {
|
||||
name: "Binance".to_string(),
|
||||
price: 45000_000_000,
|
||||
volume: 2000_000_000,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
},
|
||||
DataSource {
|
||||
name: "Coinbase".to_string(),
|
||||
price: 45000_000_000,
|
||||
volume: 1500_000_000,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
},
|
||||
],
|
||||
proof: AttestationProof::OnChainProof {
|
||||
chain_id: "ethereum".to_string(),
|
||||
block_number: 18000000,
|
||||
transaction_hash: vec![1, 2, 3],
|
||||
},
|
||||
signature: vec![4, 5, 6],
|
||||
};
|
||||
|
||||
network1.submit_attestation(attestation1).unwrap();
|
||||
|
||||
// Submit different attestation to network2
|
||||
let attestation2 = PriceAttestation {
|
||||
id: AttestationId("att2".to_string()),
|
||||
oracle_id: oracle1.clone(),
|
||||
asset_pair: btc_usd.clone(),
|
||||
price: 45100_000_000,
|
||||
confidence: 90,
|
||||
timestamp: OracleNetworkCRDT::timestamp() + 1,
|
||||
sources: vec![
|
||||
DataSource {
|
||||
name: "Kraken".to_string(),
|
||||
price: 45100_000_000,
|
||||
volume: 1000_000_000,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
},
|
||||
DataSource {
|
||||
name: "Gemini".to_string(),
|
||||
price: 45100_000_000,
|
||||
volume: 800_000_000,
|
||||
timestamp: OracleNetworkCRDT::timestamp(),
|
||||
},
|
||||
],
|
||||
proof: AttestationProof::SignedFeed {
|
||||
exchange_signature: vec![7, 8, 9],
|
||||
sequence_number: 100,
|
||||
},
|
||||
signature: vec![10, 11, 12],
|
||||
};
|
||||
|
||||
network2.submit_attestation(attestation2).unwrap();
|
||||
|
||||
// Merge networks
|
||||
network1.merge(&network2);
|
||||
network2.merge(&network1);
|
||||
|
||||
// Both should have same data
|
||||
assert_eq!(network1.attestations.len(), 2);
|
||||
assert_eq!(network2.attestations.len(), 2);
|
||||
|
||||
// Both should calculate same aggregate price
|
||||
let price1 = network1
|
||||
.get_aggregate_price(&btc_usd, Duration::from_secs(300))
|
||||
.unwrap();
|
||||
let price2 = network2
|
||||
.get_aggregate_price(&btc_usd, Duration::from_secs(300))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(price1.price, price2.price);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user