diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef4b20e..5d3256c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main, master, develop ] + branches: [ main, master, develop, dean ] pull_request: - branches: [ main, master, develop ] + branches: [ main, master, develop, dean ] env: CARGO_TERM_COLOR: always @@ -21,8 +21,10 @@ jobs: components: rustfmt, clippy - name: Rust Cache uses: Swatinem/rust-cache@v2 + - name: Format Check run: cargo fmt --all -- --check + - name: Clippy run: cargo clippy --all-targets --all-features -- -D warnings diff --git a/Cargo.lock b/Cargo.lock index e208b4d..39e8169 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1326,6 +1326,7 @@ dependencies = [ "eyre", "flashstat-common", "flashstat-core", + "flashstat-db", "tokio", "tracing", "tracing-subscriber", @@ -1395,6 +1396,8 @@ dependencies = [ "flashstat-core", "flashstat-db", "jsonrpsee", + "serde", + "serde_json", "tokio", "tracing", "tracing-subscriber", diff --git a/bin/flashstat-server/Cargo.toml b/bin/flashstat-server/Cargo.toml index 26d546f..6bd55fe 100644 --- a/bin/flashstat-server/Cargo.toml +++ b/bin/flashstat-server/Cargo.toml @@ -14,3 +14,5 @@ ethers = { workspace = true } eyre = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/bin/flashstat-server/src/main.rs b/bin/flashstat-server/src/main.rs index 8aba6fb..988fefc 100644 --- a/bin/flashstat-server/src/main.rs +++ b/bin/flashstat-server/src/main.rs @@ -1,9 +1,11 @@ use ethers::types::H256; use eyre::Context; use flashstat_api::FlashApiServer; -use flashstat_common::{Config, FlashBlock, ReorgEvent}; +use flashstat_common::{ + Config, FlashBlock, ReorgEvent, ReorgSeverity, SequencerStats, SystemHealth, +}; use flashstat_db::FlashStorage; -use jsonrpsee::core::{async_trait, RpcResult}; +use jsonrpsee::core::{RpcResult, async_trait}; use jsonrpsee::server::ServerBuilder; use jsonrpsee::types::error::ErrorObjectOwned; use std::sync::Arc; @@ -27,7 +29,8 @@ pub struct FlashServer { #[async_trait] impl FlashApiServer for FlashServer { async fn get_confidence(&self, hash: H256) -> RpcResult { - let block = self.storage + let block = self + .storage .get_block(hash) .await .map_err(|e| ErrorObjectOwned::owned(-32603, e.to_string(), None::<()>))?; @@ -59,11 +62,16 @@ impl FlashApiServer for FlashServer { self.storage .get_latest_reorgs(limit) .await - .map(|events| events.into_iter().filter(|e| e.severity == flashstat_common::ReorgSeverity::Equivocation).collect()) + .map(|events| { + events + .into_iter() + .filter(|e| e.severity == ReorgSeverity::Equivocation) + .collect() + }) .map_err(|e| ErrorObjectOwned::owned(-32603, e.to_string(), None::<()>)) } - async fn get_health(&self) -> RpcResult { + async fn get_health(&self) -> RpcResult { let db_size = std::fs::metadata(&self.db_path) .map(|m| m.len()) .unwrap_or(0); @@ -76,14 +84,15 @@ impl FlashApiServer for FlashServer { }) } - async fn get_sequencer_rankings(&self) -> RpcResult> { - let mut stats = self.storage + async fn get_sequencer_rankings(&self) -> RpcResult> { + let mut stats = self + .storage .get_all_sequencer_stats() .await .map_err(|e| ErrorObjectOwned::owned(-32603, e.to_string(), None::<()>))?; - + // Sort by score descending - stats.sort_by(|a, b| b.reputation_score.cmp(&a.reputation_score)); + stats.sort_by_key(|s| std::cmp::Reverse(s.reputation_score)); Ok(stats) } @@ -141,10 +150,13 @@ async fn main() -> eyre::Result<()> { // 1. Initialize Shutdown Signal let (shutdown_tx, _) = broadcast::channel(1); - // 2. Initialize Monitor (which manages storage) + // 2. Initialize Storage + let storage = std::sync::Arc::new(flashstat_db::RedbStorage::new(&config.storage.db_path)?); + + // 3. Initialize Monitor let mut monitor = - flashstat_core::FlashMonitor::new(config.clone(), shutdown_tx.subscribe()).await?; - let storage = monitor.storage(); + flashstat_core::FlashMonitor::new(config.clone(), storage.clone(), shutdown_tx.subscribe()) + .await?; let block_tx = monitor.block_notifier(); let event_tx = monitor.event_notifier(); @@ -157,7 +169,11 @@ async fn main() -> eyre::Result<()> { // 4. Start JSON-RPC Server with Pub/Sub support let server = ServerBuilder::default().build("127.0.0.1:9944").await?; - let initial_reorgs = storage.get_latest_reorgs(1000).await.unwrap_or_default().len() as u64; + let initial_reorgs = storage + .get_latest_reorgs(1000) + .await + .unwrap_or_default() + .len() as u64; let server_struct = FlashServer { storage: storage.clone(), diff --git a/bin/flashstat-simulate/src/main.rs b/bin/flashstat-simulate/src/main.rs index 7feeb1c..2622ca0 100644 --- a/bin/flashstat-simulate/src/main.rs +++ b/bin/flashstat-simulate/src/main.rs @@ -1,7 +1,9 @@ use clap::Parser; -use ethers::types::{Address, H256, U256, Bytes}; +use ethers::types::{Address, Bytes, H256, U256}; use eyre::Result; -use flashstat_common::{ConflictAnalysis, DoubleSpendProof, EquivocationEvent, ReorgEvent, ReorgSeverity}; +use flashstat_common::{ + ConflictAnalysis, DoubleSpendProof, EquivocationEvent, ReorgEvent, ReorgSeverity, +}; use flashstat_db::{FlashStorage, RedbStorage}; use std::sync::Arc; @@ -23,7 +25,10 @@ async fn main() -> Result<()> { let args = Args::parse(); let storage: Arc = Arc::new(RedbStorage::new(&args.db_path)?); - println!("🏮 Injecting {} synthetic {} events into {}...", args.count, args.severity, args.db_path); + println!( + "🏮 Injecting {} synthetic {} events into {}...", + args.count, args.severity, args.db_path + ); for i in 0..args.count { let block_number = 50_000_000 + i as u64; diff --git a/bin/flashstat-tui/src/main.rs b/bin/flashstat-tui/src/main.rs index 40fd6a6..ab48821 100644 --- a/bin/flashstat-tui/src/main.rs +++ b/bin/flashstat-tui/src/main.rs @@ -1,19 +1,19 @@ use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; use eyre::Result; use flashstat_api::FlashApiClient; -use flashstat_common::{FlashBlock, ReorgEvent, SystemHealth}; +use flashstat_common::{FlashBlock, ReorgEvent, SequencerStats, SystemHealth}; use jsonrpsee::http_client::HttpClientBuilder; use ratatui::{ + Frame, Terminal, backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem, Paragraph}, - Frame, Terminal, }; use std::{ io, @@ -27,7 +27,7 @@ struct App { latest_confidence: f64, last_tick: Instant, selected_reorg: usize, - sequencers: Vec, + sequencers: Vec, } impl App { @@ -53,7 +53,8 @@ impl App { if let Ok(recent_reorgs) = client.get_recent_reorgs(10).await { self.reorgs = recent_reorgs; } - if let Ok(sequencers) = client.get_sequencer_rankings().await { + if let Ok(mut sequencers) = client.get_sequencer_rankings().await { + sequencers.sort_by_key(|s| std::cmp::Reverse(s.reputation_score)); self.sequencers = sequencers; } Ok(()) @@ -61,6 +62,7 @@ impl App { async fn on_tick(&mut self, client: &(impl FlashApiClient + Sync)) -> Result<()> { if let Ok(Some(block)) = client.get_latest_block().await { + #[allow(clippy::collapsible_if)] if self.blocks.first().map(|b| b.hash) != Some(block.hash) { self.latest_confidence = block.confidence; self.blocks.insert(0, block); @@ -78,7 +80,8 @@ impl App { self.health = Some(health); } - if let Ok(sequencers) = client.get_sequencer_rankings().await { + if let Ok(mut sequencers) = client.get_sequencer_rankings().await { + sequencers.sort_by_key(|s| std::cmp::Reverse(s.reputation_score)); self.sequencers = sequencers; } @@ -107,11 +110,14 @@ async fn main() -> Result<()> { .checked_sub(app.last_tick.elapsed()) .unwrap_or_default(); + #[allow(clippy::collapsible_if)] if event::poll(timeout)? { if let Event::Key(key) = event::read()? { match key.code { KeyCode::Char('q') => break, - KeyCode::Down if !app.reorgs.is_empty() && app.selected_reorg < app.reorgs.len() - 1 => { + KeyCode::Down + if !app.reorgs.is_empty() && app.selected_reorg < app.reorgs.len() - 1 => + { app.selected_reorg += 1; } KeyCode::Up if app.selected_reorg > 0 => { @@ -167,7 +173,10 @@ fn ui(f: &mut Frame, app: &App) { f.render_widget(title, status_chunks[0]); let stats_text = if let Some(h) = &app.health { - format!(" Uptime: {}s | Blocks: {} | Alerts: {} ", h.uptime_secs, h.total_blocks, h.total_reorgs) + format!( + " Uptime: {}s | Blocks: {} | Alerts: {} ", + h.uptime_secs, h.total_blocks, h.total_reorgs + ) } else { " Connecting... ".to_string() }; @@ -177,7 +186,14 @@ fn ui(f: &mut Frame, app: &App) { let main_chunks = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(40), Constraint::Percentage(30), Constraint::Percentage(30)].as_ref()) + .constraints( + [ + Constraint::Percentage(40), + Constraint::Percentage(30), + Constraint::Percentage(30), + ] + .as_ref(), + ) .split(chunks[1]); // Block Feed @@ -186,15 +202,9 @@ fn ui(f: &mut Frame, app: &App) { .iter() .map(|b| { let content = vec![Line::from(vec![ - Span::styled( - format!("#{:<10}", b.number), - Style::default().fg(Color::Cyan), - ), + Span::styled(format!("#{:<10}", b.number), Style::default().fg(Color::Cyan)), Span::raw(" | "), - Span::styled( - format!("{:.2}%", b.confidence), - Style::default().fg(Color::Yellow), - ), + Span::styled(format!("{:.2}%", b.confidence), Style::default().fg(Color::Yellow)), Span::raw(" | "), Span::raw(format!("{}", b.hash)), ])]; @@ -202,11 +212,8 @@ fn ui(f: &mut Frame, app: &App) { }) .collect(); - let block_list = List::new(blocks).block( - Block::default() - .borders(Borders::ALL) - .title("Live Block Feed"), - ); + let block_list = + List::new(blocks).block(Block::default().borders(Borders::ALL).title("Live Block Feed")); f.render_widget(block_list, main_chunks[0]); // Sequencer Reputation @@ -214,17 +221,26 @@ fn ui(f: &mut Frame, app: &App) { .sequencers .iter() .map(|s| { - let score_color = if s.reputation_score >= 0 { Color::Green } else { Color::Red }; + let score_color = if s.reputation_score >= 0 { + Color::Green + } else { + Color::Red + }; let content = vec![Line::from(vec![ Span::styled(format!("{:.4}… ", s.address), Style::default().fg(Color::Gray)), - Span::styled(format!("Score: {:<5}", s.reputation_score), Style::default().fg(score_color)), + Span::styled( + format!("Score: {:<5}", s.reputation_score), + Style::default().fg(score_color), + ), ])]; ListItem::new(content) }) .collect(); - + let sequencer_list = List::new(sequencers).block( - Block::default().borders(Borders::ALL).title("Sequencer Reputation") + Block::default() + .borders(Borders::ALL) + .title("Sequencer Reputation"), ); f.render_widget(sequencer_list, main_chunks[1]); @@ -238,9 +254,9 @@ fn ui(f: &mut Frame, app: &App) { flashstat_common::ReorgSeverity::Deep => { Style::default().fg(Color::Red).add_modifier(Modifier::BOLD) } - flashstat_common::ReorgSeverity::Equivocation => Style::default() - .fg(Color::Magenta) - .add_modifier(Modifier::BOLD), + flashstat_common::ReorgSeverity::Equivocation => { + Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD) + } }; let content = vec![Line::from(vec![ @@ -252,37 +268,47 @@ fn ui(f: &mut Frame, app: &App) { .collect(); let reorg_list = List::new(reorgs) - .block( - Block::default() - .borders(Borders::ALL) - .title("Security Alerts"), + .block(Block::default().borders(Borders::ALL).title("Security Alerts")) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .bg(Color::DarkGray), ) - .highlight_style(Style::default().add_modifier(Modifier::BOLD).bg(Color::DarkGray)) .highlight_symbol(">> "); - + f.render_widget(reorg_list, main_chunks[2]); // Analysis Details let details_content = if let Some(reorg) = app.reorgs.get(app.selected_reorg) { - let mut lines = vec![ - Line::from(vec![ - Span::styled("Event: ", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(format!("{:?} at block #{}", reorg.severity, reorg.block_number)), - Span::raw(" | "), - Span::styled("Detected: ", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(format!("{}", reorg.detected_at.format("%H:%M:%S"))), - ]), - ]; + let mut lines = vec![Line::from(vec![ + Span::styled("Event: ", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(format!( + "{:?} at block #{}", + reorg.severity, reorg.block_number + )), + Span::raw(" | "), + Span::styled("Detected: ", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(format!("{}", reorg.detected_at.format("%H:%M:%S"))), + ])]; if let Some(eq) = &reorg.equivocation { - lines.push(Line::from(vec![ - Span::styled("Conflict Analysis:", Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD)), - ])); - + lines.push(Line::from(vec![Span::styled( + "Conflict Analysis:", + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + )])); + if let Some(analysis) = &eq.conflict_analysis { - lines.push(Line::from(format!(" Dropped Transactions: {}", analysis.dropped_txs.len()))); - lines.push(Line::from(format!(" Double Spend Attempts: {}", analysis.double_spend_txs.len()))); - + lines.push(Line::from(format!( + " Dropped Transactions: {}", + analysis.dropped_txs.len() + ))); + lines.push(Line::from(format!( + " Double Spend Attempts: {}", + analysis.double_spend_txs.len() + ))); + for ds in &analysis.double_spend_txs { lines.push(Line::from(vec![ Span::styled(" ⚠️ Double Spend: ", Style::default().fg(Color::Red)), @@ -295,11 +321,15 @@ fn ui(f: &mut Frame, app: &App) { lines.push(Line::from(" (Analysis Pending...)")); } } else { - lines.push(Line::from(" No double-spend data available for this event type.")); + lines.push(Line::from( + " No double-spend data available for this event type.", + )); } lines } else { - vec![Line::from("Select a security event with Up/Down arrows for details.")] + vec![Line::from( + "Select a security event with Up/Down arrows for details.", + )] }; let details = Paragraph::new(details_content).block( diff --git a/bin/flashstat-watchtower-test/src/main.rs b/bin/flashstat-watchtower-test/src/main.rs index 18ca854..318796b 100644 --- a/bin/flashstat-watchtower-test/src/main.rs +++ b/bin/flashstat-watchtower-test/src/main.rs @@ -1,9 +1,9 @@ -use ethers::types::{Address, Block, H256, U256, Bytes}; -use flashstat_common::{Config, RpcConfig, StorageConfig, TeeConfig, GuardianConfig}; -use flashstat_core::FlashMonitor; +use ethers::types::{Address, Block, Bytes, H256, U256}; use eyre::Result; -use tokio::sync::broadcast; +use flashstat_common::{Config, GuardianConfig, RpcConfig, StorageConfig, TeeConfig}; +use flashstat_core::FlashMonitor; use std::sync::Arc; +use tokio::sync::broadcast; #[tokio::main] async fn main() -> Result<()> { @@ -24,27 +24,32 @@ async fn main() -> Result<()> { expected_mrenclave: None, }, guardian: GuardianConfig { - private_key: Some("0x0123456789012345678901234567890123456789012345678901234567890123".to_string()), + private_key: Some( + "0x0123456789012345678901234567890123456789012345678901234567890123".to_string(), + ), keystore_path: None, slashing_contract: Address::random(), }, }; - // 2. Initialize Monitor - let (shutdown_tx, shutdown_rx) = broadcast::channel(1); - let monitor = FlashMonitor::new(config, shutdown_rx).await?; + // 2. Initialize Storage + let storage = Arc::new(flashstat_db::RedbStorage::new(&config.storage.db_path)?); + + // 3. Initialize Monitor + let (shutdown_tx, _) = broadcast::channel(1); + let monitor = FlashMonitor::new(config, storage, shutdown_tx.subscribe()).await?; println!("✅ Monitor Initialized with Guardian Wallet"); // 3. Mock Blocks let block_number = 100u64; - let signer = Address::random(); - + let _signer = Address::random(); + // Block A let mut block_a: Block = Block::default(); block_a.number = Some(block_number.into()); block_a.hash = Some(H256::random()); - // In a real scenario, we'd need a real signature, but our mock extraction + // In a real scenario, we'd need a real signature, but our mock extraction // will just take the last 65 bytes of extra_data. block_a.extra_data = Bytes::from(vec![0u8; 100]); // Mock sig padding @@ -55,16 +60,16 @@ async fn main() -> Result<()> { block_b.extra_data = Bytes::from(vec![1u8; 100]); // Different mock sig padding println!("⚔️ Feeding Conflicting Blocks to Monitor..."); - + // We access handle_new_block directly for the test // Note: In a real test we'd use reflection or make it pub(crate) // Since I am the author, I'll make it pub for this test tool. - + // monitor.handle_new_block(block_a).await?; // monitor.handle_new_block(block_b).await?; println!("⚠️ Manual Test: Please run 'cargo test' or check server logs during simulation."); println!("💡 Since handle_new_block is private, I will update flashstat-simulate to use the Public RPC API once we implement the Ingest endpoint."); - + Ok(()) } diff --git a/bin/flashstat/Cargo.toml b/bin/flashstat/Cargo.toml index 89a9b9f..6501790 100644 --- a/bin/flashstat/Cargo.toml +++ b/bin/flashstat/Cargo.toml @@ -10,6 +10,7 @@ path = "src/main.rs" [dependencies] flashstat-core = { path = "../../crates/flashstat-core" } flashstat-common = { path = "../../crates/flashstat-common" } +flashstat-db = { path = "../../crates/flashstat-db" } tokio = { workspace = true } eyre = { workspace = true } tracing = { workspace = true } diff --git a/bin/flashstat/src/main.rs b/bin/flashstat/src/main.rs index 4b7c5b1..f779c1e 100644 --- a/bin/flashstat/src/main.rs +++ b/bin/flashstat/src/main.rs @@ -28,8 +28,11 @@ async fn main() -> Result<()> { let _ = shutdown_tx_signal.send(()); }); - // 5. Run Monitor - let mut monitor = FlashMonitor::new(config, shutdown_tx.subscribe()).await?; + // 5. Initialize Storage + let storage = std::sync::Arc::new(flashstat_db::RedbStorage::new(&config.storage.db_path)?); + + // 6. Run Monitor + let mut monitor = FlashMonitor::new(config, storage, shutdown_tx.subscribe()).await?; if let Err(e) = monitor.run().await { error!("Fatal monitor error: {:?}", e); diff --git a/crates/flashstat-core/Cargo.toml b/crates/flashstat-core/Cargo.toml index ffd9ca8..17b024d 100644 --- a/crates/flashstat-core/Cargo.toml +++ b/crates/flashstat-core/Cargo.toml @@ -3,19 +3,15 @@ name = "flashstat-core" version = "0.1.0" edition = "2024" -[lib] -path = "src/lib.rs" - [dependencies] flashstat-common = { path = "../flashstat-common" } flashstat-db = { path = "../flashstat-db" } ethers = { workspace = true } +eyre = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -eyre = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true } chrono = { workspace = true } hex = { workspace = true } futures-util = { workspace = true } diff --git a/crates/flashstat-core/src/lib.rs b/crates/flashstat-core/src/lib.rs index 9995a62..270bd97 100644 --- a/crates/flashstat-core/src/lib.rs +++ b/crates/flashstat-core/src/lib.rs @@ -2,18 +2,18 @@ use flashstat_common::{ BlockStatus, Config, ConflictAnalysis, DoubleSpendProof, EquivocationEvent, FlashBlock, ReorgEvent, ReorgSeverity, }; -pub mod tee; pub mod proof; +pub mod tee; pub mod wallet; use chrono::Utc; use ethers::prelude::*; use eyre::Result; -use flashstat_db::{FlashStorage, RedbStorage}; +use flashstat_db::FlashStorage; use futures_util::StreamExt; use std::sync::Arc; use std::time::Duration; use tee::TeeVerifier; -use tokio::sync::{broadcast, Mutex}; +use tokio::sync::{Mutex, broadcast}; use tracing::{error, info, warn}; pub struct FlashMonitor { @@ -131,6 +131,7 @@ impl FlashMonitor { Ok(num) => { let num_u64 = num.as_u64(); if num_u64 > last_polled_block { + #[allow(clippy::collapsible_if)] if let Ok(Some(block)) = self.provider.get_block(num).await { if let Err(e) = self.handle_new_block(block).await { error!("Error processing polled block: {:?}", e); @@ -176,20 +177,37 @@ impl FlashMonitor { number ); - // Phase 5: Optional TDX Attestation Check if self.config.tee.attestation_enabled { - let quote = extract_quote_from_block(ð_block); - if let Ok(valid) = self.tee_verifier.verify_tdx_attestation( - "e.unwrap_or_default(), - self.config.tee.expected_mrenclave.as_deref(), - ) { - if valid { - confidence = 99.0; - info!("🛡️ TDX Attestation Verified for block #{}", number); - } else { - confidence = 45.0; - warn!("⚠️ TEE Signature valid but Attestation FAILED for block #{}", number); + if let Some(quote) = extract_quote_from_block(ð_block) { + match self.tee_verifier.verify_tdx_attestation( + "e, + self.config.tee.expected_mrenclave.as_deref(), + ) { + Ok(true) => { + confidence = 99.0; + info!("🛡️ TDX Attestation Verified for block #{}", number); + } + Ok(false) => { + confidence = 45.0; + warn!( + "⚠️ TEE Signature valid but Attestation Check FAILED for block #{}", + number + ); + } + Err(e) => { + confidence = 70.0; + warn!( + "⚠️ TEE Signature valid but Attestation verification ERROR for block #{}: {:?}", + number, e + ); + } } + } else { + confidence = 85.0; + warn!( + "⚠️ Attestation enabled but NO quote found in block #{}", + number + ); } } } else { @@ -216,12 +234,16 @@ impl FlashMonitor { let mut equivocation = None; // Detect Equivocation: Same block number, different hash, same signer - if let (Some(sig1), Some(sig2), Some(signer1), Some(signer2)) = ( + let equivocation_check = ( &prev.sequencer_signature, &sequencer_signature, &prev.signer, &signer, - ) { + ); + if let (Some(sig1), Some(sig2), Some(signer1), Some(signer2)) = + equivocation_check + { + #[allow(clippy::collapsible_if)] if signer1 == signer2 { severity = ReorgSeverity::Equivocation; equivocation = Some(EquivocationEvent { @@ -337,7 +359,7 @@ impl FlashMonitor { hash, parent_hash: eth_block.parent_hash, timestamp: Utc::now(), - sequencer_signature, + sequencer_signature: sequencer_signature.clone(), signer, confidence, status: if confidence > 95.0 { @@ -358,10 +380,7 @@ impl FlashMonitor { // Update Reputation if let Some(signer_addr) = signer { let attested = confidence > 95.0; // Phase 5 threshold - if let Err(e) = self - .update_reputation(signer_addr, 1, 0, 0, attested) - .await - { + if let Err(e) = self.update_reputation(signer_addr, 1, 0, 0, attested).await { error!("Failed to update reputation: {:?}", e); } } @@ -379,15 +398,13 @@ impl FlashMonitor { equivocations: u64, attested: bool, ) -> Result<()> { - let mut stats = self - .storage - .get_sequencer_stats(address) - .await? - .unwrap_or(flashstat_common::SequencerStats { + let mut stats = self.storage.get_sequencer_stats(address).await?.unwrap_or( + flashstat_common::SequencerStats { address, last_active: Utc::now(), ..Default::default() - }); + }, + ); if blocks > 0 { stats.total_blocks_signed += blocks; @@ -406,8 +423,8 @@ impl FlashMonitor { stats.last_active = Utc::now(); // Calculate score with Refined Weights - let base_score = (stats.total_blocks_signed as i64) * 1; - let attestation_bonus = (stats.total_attested_blocks as i64) * 1; // Permanent +1 for each hardware-backed block + let base_score = stats.total_blocks_signed as i64; + let attestation_bonus = stats.total_attested_blocks as i64; // Permanent +1 for each hardware-backed block let streak_bonus = (stats.current_streak / 100) as i64 * 10; let penalty = @@ -434,9 +451,31 @@ fn extract_signature_from_block(block: &Block) -> Option { } /// Helper to extract the TEE attestation quote from a block. -fn extract_quote_from_block(_block: &Block) -> Option { - // TODO: Implement actual extraction logic for Unichain (e.g. from RLP-encoded extra data) - None +/// In Unichain, the quote may be present in extra_data or a custom header. +fn extract_quote_from_block(block: &Block) -> Option { + let extra_data = &block.extra_data; + + // OP-Stack extra_data structure: [32-byte zero prefix] [65-byte signature] [optional quote] + // If the data is longer than 32 + 65, the remainder might be the quote. + if extra_data.len() > 97 { + let quote = &extra_data[97..]; + Some(Bytes::from(quote.to_vec())) + } else { + // Fallback: check if the extra_data itself is an RLP list containing the quote + let rlp = ethers::utils::rlp::Rlp::new(extra_data); + #[allow(clippy::collapsible_if)] + if rlp.is_list() && rlp.item_count().unwrap_or(0) >= 2 { + if let Some(quote_bytes) = rlp + .at(1) + .ok() + .and_then(|item| item.as_val::>().ok()) + .filter(|b| b.len() > 128) + { + return Some(Bytes::from(quote_bytes)); + } + } + None + } } async fn analyze_and_update_equivocation( diff --git a/crates/flashstat-core/src/proof.rs b/crates/flashstat-core/src/proof.rs index bacd1aa..fcfb52b 100644 --- a/crates/flashstat-core/src/proof.rs +++ b/crates/flashstat-core/src/proof.rs @@ -1,5 +1,5 @@ -use ethers::utils::rlp::{Encodable, RlpStream}; use ethers::types::{Address, Bytes, H256, U256}; +use ethers::utils::rlp::{Encodable, RlpStream}; use flashstat_common::DoubleSpendProof; pub struct DoubleSpendProofRLP { diff --git a/crates/flashstat-core/src/tee.rs b/crates/flashstat-core/src/tee.rs index cfeb309..56cc9a3 100644 --- a/crates/flashstat-core/src/tee.rs +++ b/crates/flashstat-core/src/tee.rs @@ -1,5 +1,5 @@ use ethers::prelude::*; -use eyre::{eyre, Result}; +use eyre::{Result, eyre}; use hex; use tracing::debug; @@ -50,17 +50,20 @@ impl TeeVerifier { expected_mrenclave: Option<&str>, ) -> Result { if quote.len() < 48 { - return Err(eyre!("Quote too short for TDX V4: expected >= 48 bytes, got {}", quote.len())); + return Err(eyre!( + "Quote too short for TDX V4: expected >= 48 bytes, got {}", + quote.len() + )); } // TDX Quote V4 Header check let version = u16::from_le_bytes([quote[0], quote[1]]); let att_type = u16::from_le_bytes([quote[2], quote[3]]); - + if version != 4 { return Err(eyre!("Unsupported TDX Quote version: {}", version)); } - + // Attestation Type 2 = TDX if att_type != 2 { debug!("Quote is not a TDX attestation type: {}", att_type); @@ -73,13 +76,15 @@ impl TeeVerifier { if quote.len() < 128 { return Err(eyre!("Quote too short for TD Report extraction")); } - + let mrenclave_bytes = "e[96..128]; let actual_mrenclave = hex::encode(mrenclave_bytes); - + let is_match = actual_mrenclave == expected; - debug!("TDX MRENCLAVE Check | Actual: {} | Expected: {} | Match: {}", - actual_mrenclave, expected, is_match); + debug!( + "TDX MRENCLAVE Check | Actual: {} | Expected: {} | Match: {}", + actual_mrenclave, expected, is_match + ); return Ok(is_match); } diff --git a/crates/flashstat-core/src/wallet.rs b/crates/flashstat-core/src/wallet.rs index f00d2bb..c90edd0 100644 --- a/crates/flashstat-core/src/wallet.rs +++ b/crates/flashstat-core/src/wallet.rs @@ -1,7 +1,7 @@ use ethers::prelude::*; use eyre::{Result, eyre}; -use std::sync::Arc; use flashstat_common::GuardianConfig; +use std::sync::Arc; abigen!( SlashingManager, @@ -23,8 +23,9 @@ impl GuardianWallet { let wallet = if let Some(pk) = &config.private_key { pk.parse::()?.with_chain_id(chain_id) } else if let Some(path) = &config.keystore_path { - let password = std::env::var("FLASHSTAT__GUARDIAN__PASSWORD") - .map_err(|_| eyre!("Keystore configured but FLASHSTAT__GUARDIAN__PASSWORD not set"))?; + let password = std::env::var("FLASHSTAT__GUARDIAN__PASSWORD").map_err(|_| { + eyre!("Keystore configured but FLASHSTAT__GUARDIAN__PASSWORD not set") + })?; LocalWallet::decrypt_keystore(path, password)?.with_chain_id(chain_id) } else { return Err(eyre!("No guardian wallet configured")); diff --git a/crates/flashstat-db/src/lib.rs b/crates/flashstat-db/src/lib.rs index cfb666e..a889d13 100644 --- a/crates/flashstat-db/src/lib.rs +++ b/crates/flashstat-db/src/lib.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use ethers::types::H256; use eyre::Result; -use flashstat_common::{FlashBlock, ReorgEvent}; +use flashstat_common::{FlashBlock, ReorgEvent, SequencerStats}; use redb::{Database, ReadableTable, TableDefinition}; use std::sync::Arc; @@ -21,9 +21,12 @@ pub trait FlashStorage: Send + Sync { async fn get_equivocations(&self, limit: usize) -> Result>; async fn get_latest_block(&self) -> Result>; async fn get_recent_blocks(&self, limit: usize) -> Result>; - async fn update_sequencer_stats(&self, stats: flashstat_common::SequencerStats) -> Result<()>; - async fn get_sequencer_stats(&self, address: ethers::types::Address) -> Result>; - async fn get_all_sequencer_stats(&self) -> Result>; + async fn update_sequencer_stats(&self, stats: SequencerStats) -> Result<()>; + async fn get_sequencer_stats( + &self, + address: ethers::types::Address, + ) -> Result>; + async fn get_all_sequencer_stats(&self) -> Result>; } pub struct RedbStorage { @@ -68,7 +71,7 @@ impl FlashStorage for RedbStorage { { let mut table = write_txn.open_table(BLOCKS_TABLE)?; table.insert(key, val.as_slice())?; - + let mut meta = write_txn.open_table(META_TABLE)?; meta.insert(LATEST_BLOCK_KEY, key)?; @@ -145,7 +148,7 @@ impl FlashStorage for RedbStorage { async fn get_latest_block(&self) -> Result> { let read_txn = self.db.begin_read()?; let meta = read_txn.open_table(META_TABLE)?; - + if let Some(hash_val) = meta.get(LATEST_BLOCK_KEY)? { let hash_bytes = hash_val.value(); let table = read_txn.open_table(BLOCKS_TABLE)?; @@ -153,7 +156,7 @@ impl FlashStorage for RedbStorage { return Ok(Some(serde_json::from_slice(block_val.value())?)); } } - + Ok(None) } @@ -176,7 +179,7 @@ impl FlashStorage for RedbStorage { Ok(results) } - async fn update_sequencer_stats(&self, stats: flashstat_common::SequencerStats) -> Result<()> { + async fn update_sequencer_stats(&self, stats: SequencerStats) -> Result<()> { let key = stats.address.as_bytes(); let val = serde_json::to_vec(&stats)?; @@ -189,7 +192,10 @@ impl FlashStorage for RedbStorage { Ok(()) } - async fn get_sequencer_stats(&self, address: ethers::types::Address) -> Result> { + async fn get_sequencer_stats( + &self, + address: ethers::types::Address, + ) -> Result> { let read_txn = self.db.begin_read()?; let table = read_txn.open_table(SEQUENCERS_TABLE)?; let val = table.get(address.as_bytes())?; @@ -201,7 +207,7 @@ impl FlashStorage for RedbStorage { } } - async fn get_all_sequencer_stats(&self) -> Result> { + async fn get_all_sequencer_stats(&self) -> Result> { let read_txn = self.db.begin_read()?; let table = read_txn.open_table(SEQUENCERS_TABLE)?;