Spaces:
Sleeping
Sleeping
| """ | |
| BSV Blockchain Client for Semantic Scalpel | |
| Provides blockchain anchoring for inference provenance: | |
| - Simulated mode: Generate realistic TXIDs for demo | |
| - Live mode: Real BSV transactions via SimpleBSV API | |
| Created by Bryan Daugherty | |
| SmartLedger Blockchain Solutions Inc | |
| """ | |
| import hashlib | |
| import json | |
| import secrets | |
| import time | |
| from dataclasses import dataclass, field | |
| from datetime import datetime | |
| from typing import Any, Dict, Optional | |
| from enum import Enum | |
| try: | |
| import httpx | |
| HAS_HTTPX = True | |
| except ImportError: | |
| HAS_HTTPX = False | |
| from config import get_config, BSVMode | |
| class TransactionStatus(str, Enum): | |
| PENDING = "pending" | |
| PROCESSING = "processing" | |
| COMPLETED = "completed" | |
| FAILED = "failed" | |
| SIMULATED = "simulated" | |
| class BSVTransaction: | |
| """Result of a BSV blockchain operation.""" | |
| success: bool | |
| txid: Optional[str] = None | |
| status: TransactionStatus = TransactionStatus.PENDING | |
| query_hash: str = "" | |
| anchor_mode: str = "async" | |
| fee_satoshis: int = 0 | |
| error: Optional[str] = None | |
| timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat() + "Z") | |
| is_simulated: bool = True | |
| def whatsonchain_url(self) -> Optional[str]: | |
| """Get WhatsOnChain explorer URL.""" | |
| if self.txid: | |
| return f"https://whatsonchain.com/tx/{self.txid}" | |
| return None | |
| def to_dict(self) -> Dict[str, Any]: | |
| return { | |
| "success": self.success, | |
| "txid": self.txid, | |
| "status": self.status.value, | |
| "query_hash": self.query_hash, | |
| "anchor_mode": self.anchor_mode, | |
| "fee_satoshis": self.fee_satoshis, | |
| "error": self.error, | |
| "timestamp": self.timestamp, | |
| "is_simulated": self.is_simulated, | |
| "whatsonchain_url": self.whatsonchain_url, | |
| } | |
| class BSVClient: | |
| """ | |
| BSV Blockchain client with simulated and live modes. | |
| Usage: | |
| client = BSVClient() | |
| # Anchor a prediction | |
| result = client.anchor_prediction( | |
| text="The White House announced...", | |
| prediction="Presidential administration", | |
| confidence=0.95, | |
| mode="async" | |
| ) | |
| print(f"TXID: {result.txid}") | |
| print(f"Verify: {result.whatsonchain_url}") | |
| """ | |
| def __init__(self): | |
| self.config = get_config() | |
| self._session = None | |
| if self.config.bsv_mode == BSVMode.LIVE and HAS_HTTPX: | |
| self._init_session() | |
| def _init_session(self): | |
| """Initialize HTTP session for live BSV API.""" | |
| if not self.config.bsv_api_key: | |
| return | |
| self._session = httpx.Client( | |
| timeout=30.0, | |
| headers={ | |
| "Authorization": f"Bearer {self.config.bsv_api_key}", | |
| "Content-Type": "application/json", | |
| "User-Agent": "SemanticScalpel-BSV/1.0.0", | |
| } | |
| ) | |
| def _compute_query_hash( | |
| self, | |
| text: str, | |
| prediction: str, | |
| confidence: float, | |
| ) -> str: | |
| """Compute deterministic hash for a query.""" | |
| # Include hour to allow some caching but prevent replay attacks | |
| content = f"{text}|{prediction}|{confidence:.4f}|{datetime.utcnow().strftime('%Y-%m-%d-%H')}" | |
| return hashlib.sha256(content.encode()).hexdigest() | |
| def _generate_simulated_txid(self) -> str: | |
| """Generate a realistic-looking BSV transaction ID.""" | |
| return secrets.token_hex(32) | |
| # ------------------------------------------------------------------------- | |
| # Simulated Mode | |
| # ------------------------------------------------------------------------- | |
| def _anchor_simulated( | |
| self, | |
| query_hash: str, | |
| mode: str, | |
| payload: Dict, | |
| ) -> BSVTransaction: | |
| """Create a simulated BSV transaction for demo purposes.""" | |
| # Simulate processing time based on mode | |
| if mode == "sync": | |
| time.sleep(0.1) # Simulate brief delay | |
| return BSVTransaction( | |
| success=True, | |
| txid=self._generate_simulated_txid(), | |
| status=TransactionStatus.SIMULATED, | |
| query_hash=query_hash, | |
| anchor_mode=mode, | |
| fee_satoshis=200, # Typical BSV transaction fee | |
| is_simulated=True, | |
| ) | |
| # ------------------------------------------------------------------------- | |
| # Live Mode | |
| # ------------------------------------------------------------------------- | |
| def _anchor_live( | |
| self, | |
| query_hash: str, | |
| mode: str, | |
| payload: Dict, | |
| ) -> BSVTransaction: | |
| """Anchor to real BSV blockchain via SimpleBSV API.""" | |
| if not self._session: | |
| return BSVTransaction( | |
| success=False, | |
| error="BSV session not initialized (missing API key?)", | |
| query_hash=query_hash, | |
| anchor_mode=mode, | |
| is_simulated=False, | |
| ) | |
| # Build anchor payload | |
| anchor_data = { | |
| "protocol": "SmartLedger-Inference-v1", | |
| "provider": "SmartLedger Blockchain Solutions Inc", | |
| "model": "semantic-scalpel-v1", | |
| "query_hash": query_hash, | |
| "timestamp": datetime.utcnow().isoformat() + "Z", | |
| "nist_control": "AU-10", | |
| **payload, | |
| } | |
| try: | |
| # Determine endpoint and params | |
| url = f"{self.config.bsv_api_url}/publish/json" | |
| if mode == "sync" or self.config.bsv_sync_mode: | |
| url += "?wait=true" | |
| response = self._session.post( | |
| url, | |
| json={"json": anchor_data, "signWithAIP": True}, | |
| ) | |
| if response.status_code == 401: | |
| return BSVTransaction( | |
| success=False, | |
| error="Unauthorized: Invalid BSV API key", | |
| query_hash=query_hash, | |
| anchor_mode=mode, | |
| is_simulated=False, | |
| ) | |
| if response.status_code >= 400: | |
| data = response.json() if response.content else {} | |
| return BSVTransaction( | |
| success=False, | |
| error=data.get("error", f"HTTP {response.status_code}"), | |
| query_hash=query_hash, | |
| anchor_mode=mode, | |
| is_simulated=False, | |
| ) | |
| data = response.json() | |
| return BSVTransaction( | |
| success=data.get("success", False), | |
| txid=data.get("txid"), | |
| status=TransactionStatus(data.get("status", "pending")), | |
| query_hash=query_hash, | |
| anchor_mode=mode, | |
| fee_satoshis=data.get("fee", 0), | |
| is_simulated=False, | |
| ) | |
| except httpx.TimeoutException: | |
| return BSVTransaction( | |
| success=False, | |
| error="BSV API timeout", | |
| query_hash=query_hash, | |
| anchor_mode=mode, | |
| is_simulated=False, | |
| ) | |
| except Exception as e: | |
| return BSVTransaction( | |
| success=False, | |
| error=str(e), | |
| query_hash=query_hash, | |
| anchor_mode=mode, | |
| is_simulated=False, | |
| ) | |
| # ------------------------------------------------------------------------- | |
| # Public API | |
| # ------------------------------------------------------------------------- | |
| def anchor_prediction( | |
| self, | |
| text: str, | |
| prediction: str, | |
| confidence: float, | |
| mode: str = "async", | |
| metadata: Optional[Dict] = None, | |
| ) -> BSVTransaction: | |
| """ | |
| Anchor a prediction to BSV blockchain. | |
| Args: | |
| text: Input text that was analyzed | |
| prediction: Model's prediction | |
| confidence: Confidence score (0-1) | |
| mode: Anchor mode (sync, async, batch) | |
| metadata: Optional additional metadata | |
| Returns: | |
| BSVTransaction with TXID on success | |
| """ | |
| # Compute query hash | |
| query_hash = self._compute_query_hash(text, prediction, confidence) | |
| # Build payload (hash only - no raw text on chain) | |
| payload = { | |
| "prediction_hash": hashlib.sha256(prediction.encode()).hexdigest()[:16], | |
| "confidence_bucket": "high" if confidence >= 0.85 else "medium" if confidence >= 0.70 else "low", | |
| } | |
| if metadata: | |
| payload["metadata"] = metadata | |
| # Route to appropriate handler | |
| if self.config.bsv_mode == BSVMode.LIVE: | |
| return self._anchor_live(query_hash, mode, payload) | |
| else: | |
| return self._anchor_simulated(query_hash, mode, payload) | |
| def health_check(self) -> Dict[str, Any]: | |
| """Check BSV API health (live mode only).""" | |
| if self.config.bsv_mode != BSVMode.LIVE or not self._session: | |
| return { | |
| "status": "simulated", | |
| "mode": self.config.bsv_mode.value, | |
| "message": "BSV in simulated mode - no live connection", | |
| } | |
| try: | |
| response = self._session.get(f"{self.config.bsv_api_url}/health") | |
| if response.status_code == 200: | |
| data = response.json() | |
| return { | |
| "status": "healthy", | |
| "mode": "live", | |
| **data, | |
| } | |
| return { | |
| "status": "degraded", | |
| "mode": "live", | |
| "http_status": response.status_code, | |
| } | |
| except Exception as e: | |
| return { | |
| "status": "error", | |
| "mode": "live", | |
| "error": str(e), | |
| } | |
| def get_mode_info(self) -> Dict[str, Any]: | |
| """Get current BSV mode information.""" | |
| return { | |
| "mode": self.config.bsv_mode.value, | |
| "is_live": self.config.bsv_mode == BSVMode.LIVE, | |
| "api_url": self.config.bsv_api_url if self.config.bsv_mode == BSVMode.LIVE else None, | |
| "has_api_key": bool(self.config.bsv_api_key), | |
| "sync_mode": self.config.bsv_sync_mode, | |
| } | |
| # Global client instance | |
| _client: Optional[BSVClient] = None | |
| def get_bsv_client() -> BSVClient: | |
| """Get or create the global BSV client instance.""" | |
| global _client | |
| if _client is None: | |
| _client = BSVClient() | |
| return _client | |