diff --git a/.gitignore b/.gitignore index b6696dcec..13b9e2149 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,13 @@ uet_web/.env # Archived non-research platform/prototype material, if temporarily restored locally. _archive_non_research/ + +# Local service experiment artifacts +services_and_experiments/**/.env +services_and_experiments/**/target/ +services_and_experiments/**/target_*/ +services_and_experiments/**/debug/ +services_and_experiments/**/.rustc_info.json +services_and_experiments/**/CACHEDIR.TAG +services_and_experiments/**/*.exe +services_and_experiments/**/*_linux diff --git a/services_and_experiments/README.md b/services_and_experiments/README.md new file mode 100644 index 000000000..a43360f54 --- /dev/null +++ b/services_and_experiments/README.md @@ -0,0 +1,26 @@ +# Services and Experiments + +This folder keeps prototype service code that supports UET tooling experiments. + +It is source-only by default. Do not commit local runtime state, generated build +outputs, compiled binaries, credentials, `.env` files, or machine-specific caches. + +## Current areas + +| Area | Purpose | +| :-- | :-- | +| `uet_agents/` | prototype agent orchestration and document ingestion helpers | +| `uet_api/` | experimental Rust API service | +| `uet_chain/` | experimental ledger and chain primitives | +| `uet_core/` | reusable Rust core calculation primitives | +| `uet_kb/` | experimental knowledge-base and MCP-facing service code | +| `uet_miner/` | experimental mining and benchmarking code | +| `uet_security/` | experimental signing, hashing, and key-management primitives | +| `uet_under_development/` | unfinished market, governance, oracle, and economic modules | + +## Commit discipline + +- Keep this directory separate from research topic hardening commits. +- Treat these services as prototypes unless a later document says otherwise. +- Keep generated artifacts out of Git and publish binaries through releases only. +- Store real secrets outside the repository. diff --git a/services_and_experiments/uet_agents/README.md b/services_and_experiments/uet_agents/README.md new file mode 100644 index 000000000..866410351 --- /dev/null +++ b/services_and_experiments/uet_agents/README.md @@ -0,0 +1,51 @@ +# 🧠 UET Agents: The Executive Branch + +> **"The Office" of the UET System.** +> While `uet_core` is the factory (Rust Engine) and `uet_kb` is the warehouse (Rust Database), this directory (`uet_agents`) is where the **Decisions** and **Logic** happen. + +## 🌟 Why Python? +We use Python for Agents because: +1. **LLM Native:** AI models speak Python (PyTorch, SDKs). +2. **Flexibility:** Logic for conversation and reasoning changes often; Python is agile. +3. **Orchestration:** Python is excellent at gluing together high-performance Rust components. + +## 🤖 The Workforce + +| Agent | Role | File | +| :--- | :--- | :--- | +| **Orchestrator** | **The Boss.** Routes queries to the right specialist. | `orchestrator.py` | +| **ResearchAgent** | **The Librarian.** Searches `uet_kb` for verified knowledge. | `research_agent.py` | +| **MarketingAgent** | **The PR Officer.** Manages Social Media & Moltbook protocol. | `marketing_agent.py` | +| **EquationExpert** | **The Mathematician.** Verifies math & dimensions. | `base_agent.py` (specialized) | + +## 🚀 How to Run + +Run the main orchestrator loop from the project root: + +```bash +# Windows +python uet_agents/main.py + +# Linux/Mac +python3 uet_agents/main.py +``` + +## 🔗 Relationship to System + +```mermaid +graph TD + User[User] --> ORC[Orchestrator Agent] + + subgraph "Python Agents (The Office)" + ORC --> RES[Research Agent] + ORC --> MKT[Marketing Agent] + ORC --> EQ[Equation Expert] + end + + subgraph "Rust Infrastructure (The Factory)" + RES --> KB[UET Knowledge Base] + EQ --> CORE[UET Core Engine] + end + + KB --> DB[(Vector Database)] +``` diff --git a/services_and_experiments/uet_agents/__init__.py b/services_and_experiments/uet_agents/__init__.py new file mode 100644 index 000000000..afe02bcae --- /dev/null +++ b/services_and_experiments/uet_agents/__init__.py @@ -0,0 +1,19 @@ +# UET Agents Package + +# Standalone components (no external dependencies beyond requirements.txt) +from .semantic_engine import UETSemanticEngine +from .executive_router import ExecutiveRouter +from .memory_store import ( + WorkingMemoryStore, + EpisodicMemoryStore, + SemanticMemoryStore, + ProceduralMemoryStore, +) + +# Components that depend on docs.knowledge_base (optional) +try: + from .base_agent import BaseAgent + from .research_agent import ResearchAgent + from .orchestrator import OrchestratorAgent +except ImportError: + pass diff --git a/services_and_experiments/uet_agents/api_server.py b/services_and_experiments/uet_agents/api_server.py new file mode 100644 index 000000000..e3546b7a3 --- /dev/null +++ b/services_and_experiments/uet_agents/api_server.py @@ -0,0 +1,183 @@ +import time +import uuid +import uvicorn +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import Optional, Dict, Any, List +from .executive_router import ExecutiveRouter +from .semantic_engine import UETSemanticEngine + +app = FastAPI(title="UET Semantic Engine API") +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +engine = UETSemanticEngine() +router = ExecutiveRouter(engine) + +# Expose models +class ChatRequest(BaseModel): + prompt: str + doc_context: Optional[str] = None + session_id: Optional[str] = None + project_scope: Optional[str] = None + source_tags: Optional[list[str]] = None + +class IngestRequest(BaseModel): + doc_id: str + text: str + source_type: Optional[str] = "text" + project_scope: Optional[str] = None + doc_version: Optional[str] = None + tags: Optional[list[str]] = None + ingest_mode: Optional[str] = "manual" + +class ComputeResult(BaseModel): + response: str + equilibrium_data: Dict[str, Any] + work_computed: float + task_type: Optional[str] = None + session_id: Optional[str] = None + +@app.post("/ingest") +async def ingest_document(req: IngestRequest): + """Ingest a document into the UET Semantic Manifold""" + router.ingest_document( + doc_id=req.doc_id, + text=req.text, + source_type=req.source_type or "text", + project_scope=req.project_scope, + doc_version=req.doc_version, + tags=req.tags, + ingest_mode=req.ingest_mode or "manual", + ) + return {"status": "success", "message": f"Ingested {len(req.text)} chars for {req.doc_id}"} + +@app.post("/chat", response_model=ComputeResult) +async def chat(req: ChatRequest): + """ + 1. Calculate Equilibrium Path (Work) + 2. Generate Response based on Path + """ + if len(engine.knowledge_chunks) == 0 and req.doc_context: + router.ingest_document("temp_context", req.doc_context) + + result = router.handle_chat( + prompt=req.prompt, + doc_context=req.doc_context, + session_id=req.session_id, + project_scope=req.project_scope, + source_tags=req.source_tags, + ) + + return ComputeResult( + response=result.response, + equilibrium_data=result.equilibrium_data, + work_computed=result.work_computed, + task_type=result.task_type, + session_id=result.session_id, + ) + +@app.get("/debug/status") +async def debug_status(): + return { + "status": "ok", + "knowledge_chunk_count": len(engine.knowledge_chunks), + "vocab_size": len(engine.vocab), + } + +@app.get("/debug/session/{session_id}") +async def debug_session(session_id: str): + return router.debug_session(session_id) + + +# ───────────────────────────────────────────────────────────── +# OpenAI-Compatible API (so LobeChat / any OpenAI client works) +# ───────────────────────────────────────────────────────────── + +class OAIMessage(BaseModel): + role: str + content: str + +class OAIChatRequest(BaseModel): + model: Optional[str] = "uet-agent" + messages: List[OAIMessage] + stream: Optional[bool] = False + temperature: Optional[float] = 0.7 + max_tokens: Optional[int] = None + +@app.get("/v1/models") +async def list_models(): + """OpenAI-compatible model list — returns UET agent models.""" + return { + "object": "list", + "data": [ + {"id": "uet-agent", "object": "model", "created": 1700000000, "owned_by": "uet"}, + {"id": "uet-agent-fast", "object": "model", "created": 1700000000, "owned_by": "uet"}, + {"id": "glm-4.7-flash", "object": "model", "created": 1700000000, "owned_by": "uet"}, + ], + } + +@app.post("/v1/chat/completions") +async def oai_chat_completions(req: OAIChatRequest): + """OpenAI-compatible chat completions — proxies through UET Semantic Engine.""" + # Extract the last user message as the prompt + user_messages = [m for m in req.messages if m.role == "user"] + system_messages = [m for m in req.messages if m.role == "system"] + if not user_messages: + raise HTTPException(status_code=400, detail="No user message provided") + + prompt = user_messages[-1].content + system_ctx = system_messages[-1].content if system_messages else None + + # Include system prompt in doc_context if present + doc_context = system_ctx if system_ctx else None + + result = router.handle_chat( + prompt=prompt, + doc_context=doc_context, + session_id=None, + project_scope="lobechat", + source_tags=["lobechat"], + ) + + completion_id = f"chatcmpl-{uuid.uuid4().hex[:16]}" + created_ts = int(time.time()) + + return { + "id": completion_id, + "object": "chat.completion", + "created": created_ts, + "model": req.model or "uet-agent", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": result.response, + }, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": len(prompt.split()), + "completion_tokens": len(result.response.split()), + "total_tokens": len(prompt.split()) + len(result.response.split()), + }, + "uet_metadata": { + "work_computed": result.work_computed, + "task_type": result.task_type, + "session_id": result.session_id, + }, + } + + +def start_server(port: int = 8001): + uvicorn.run(app, host="0.0.0.0", port=port) + +if __name__ == "__main__": + start_server() diff --git a/services_and_experiments/uet_agents/base_agent.py b/services_and_experiments/uet_agents/base_agent.py new file mode 100644 index 000000000..0e5aeef49 --- /dev/null +++ b/services_and_experiments/uet_agents/base_agent.py @@ -0,0 +1,53 @@ +from typing import List, Dict, Optional, Any +import logging +from docs.knowledge_base.api_client import OpenRouterClient, CostTracker + + +class BaseAgent: + """ + Base class for all UET Agents. + Handles configuration, API client connection, and common utilities. + """ + + def __init__( + self, + name: str, + client: OpenRouterClient, + system_prompt: str = "", + model_override: Optional[str] = None, + ): + self.name = name + self.client = client + self.system_prompt = system_prompt + self.model_override = model_override + + # Load agent config + self.agent_config = self.client.agents.get(name, {}) + self.model = model_override or self.agent_config.get("model") + + if not self.model: + # Fallback if config is missing or incomplete + self.model = "qwen/qwen3-coder-next" + logging.warning(f"Agent '{name}' has no model in config. Defaulting to {self.model}") + + def chat(self, messages: List[Dict[str, str]], temperature: float = 0.7) -> str: + """ + Send a chat request to the LLM. + Automatically prepends system prompt if not present in history (optional strategy). + For now, we assume 'messages' includes what is needed, or we prepend system prompt here. + """ + # Prepend system message if provided and not already there + msgs_to_send = [] + if self.system_prompt: + msgs_to_send.append({"role": "system", "content": self.system_prompt}) + + msgs_to_send.extend(messages) + + return self.client.chat(agent_id=self.name, messages=msgs_to_send, temperature=temperature) + + def run(self, user_query: str) -> str: + """ + Simple synchronous run: User Query -> Answer. + Override this for more complex loops (e.g. ResearchAgent). + """ + return self.chat([{"role": "user", "content": user_query}]) diff --git a/services_and_experiments/uet_agents/executive_router.py b/services_and_experiments/uet_agents/executive_router.py new file mode 100644 index 000000000..7d3210aa4 --- /dev/null +++ b/services_and_experiments/uet_agents/executive_router.py @@ -0,0 +1,181 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Optional +from uuid import uuid4 + +from .memory_store import EpisodeRecord, EpisodicMemoryStore, ProceduralMemoryStore, SemanticMemoryStore, WorkingMemoryStore +from .semantic_engine import UETSemanticEngine + + +@dataclass +class ExecutiveResult: + response: str + equilibrium_data: Dict[str, Any] + work_computed: float + task_type: str + session_id: str + + +class ExecutiveRouter: + def __init__(self, semantic_engine: UETSemanticEngine): + self.semantic_engine = semantic_engine + self.working_memory = WorkingMemoryStore() + self.episodic_memory = EpisodicMemoryStore() + self.semantic_memory = SemanticMemoryStore() + self.procedural_memory = ProceduralMemoryStore() + + def ingest_document( + self, + doc_id: str, + text: str, + source_type: str = "text", + project_scope: Optional[str] = None, + doc_version: Optional[str] = None, + tags: Optional[list[str]] = None, + ingest_mode: str = "manual", + ) -> int: + return self.semantic_engine.ingest_document( + doc_id=doc_id, + text=text, + source_type=source_type, + project_scope=project_scope, + doc_version=doc_version, + tags=tags, + ingest_mode=ingest_mode, + ) + + def handle_chat( + self, + prompt: str, + doc_context: Optional[str] = None, + session_id: Optional[str] = None, + project_scope: Optional[str] = None, + source_tags: Optional[list[str]] = None, + ) -> ExecutiveResult: + resolved_session_id = session_id or self._make_session_id() + task_type = self._classify_task(prompt) + + self.working_memory.update( + session_id=resolved_session_id, + prompt=prompt, + doc_context=doc_context, + task_type=task_type, + ) + + evidence_bundle = self.semantic_memory.build_evidence_bundle( + session_id=resolved_session_id, + prompt=prompt, + task_type=task_type, + persistent_chunks=self.semantic_engine.knowledge_chunks, + temporary_chunks=self.semantic_engine.build_temp_chunks(doc_context), + ) + + equilibrium_data = self._execute_task_path( + task_type=task_type, + prompt=prompt, + evidence_bundle=evidence_bundle, + ) + recent_episodes = self.episodic_memory.recent(resolved_session_id) + response = self.semantic_engine.generate_response( + prompt=prompt, + equilibrium_data=equilibrium_data, + task_type=task_type, + recent_episodes=[self._episode_to_dict(record) for record in recent_episodes], + procedure_hint=self.procedural_memory.get(task_type), + ) + + self.episodic_memory.append( + resolved_session_id, + EpisodeRecord( + prompt=prompt, + response=response, + task_type=task_type, + equilibrium_found=bool(equilibrium_data.get("equilibrium_found", False)), + resonance_score=float(equilibrium_data.get("resonance_score", 0.0)), + work_computed=float(equilibrium_data.get("work_computed", 0.0)), + created_at=datetime.utcnow().isoformat(), + metadata={ + "best_chunk_id": self._best_chunk_id(equilibrium_data), + "doc_context_present": bool(doc_context and doc_context.strip()), + "project_scope": project_scope, + "source_tags": source_tags or [], + "semantic_total_chunk_count": evidence_bundle.total_chunk_count, + "semantic_persistent_chunk_count": evidence_bundle.persistent_chunk_count, + "semantic_temporary_chunk_count": evidence_bundle.temporary_chunk_count, + }, + ), + ) + + return ExecutiveResult( + response=response, + equilibrium_data=equilibrium_data, + work_computed=float(equilibrium_data.get("work_computed", 0.0)), + task_type=task_type, + session_id=resolved_session_id, + ) + + def _classify_task(self, prompt: str) -> str: + prompt_lower = prompt.lower() + calculation_signals = [ + "equation", + "calculate", + "solve", + "formula", + "ā¸Ēā¸Ąā¸ā¸˛ā¸Ŗ", + "⏄⏺⏙⏧⏓", + "ā¸žā¸´ā¸Ēā¸šā¸ˆā¸™āšŒ", + "ā¸Ģā¸˛ā¸„āšˆā¸˛", + ] + if any(signal in prompt_lower for signal in calculation_signals): + return "calculation" + return "chat" + + def _execute_task_path( + self, + task_type: str, + prompt: str, + evidence_bundle, + ) -> Dict[str, Any]: + equilibrium_data = self.semantic_engine.calculate_equilibrium_path_from_search_space( + prompt=prompt, + search_space=evidence_bundle.search_space, + ) + + if task_type == "calculation": + equilibrium_data["calculation_mode"] = True + equilibrium_data["path_strategy"] = "calculation" + equilibrium_data["work_computed"] = equilibrium_data["work_computed"] * 1.25 + return equilibrium_data + + equilibrium_data["calculation_mode"] = False + equilibrium_data["path_strategy"] = "chat" + return equilibrium_data + + def _make_session_id(self) -> str: + return f"session-{uuid4()}" + + def _best_chunk_id(self, equilibrium_data: Dict[str, Any]) -> Optional[str]: + best_chunk = equilibrium_data.get("best_chunk") + if isinstance(best_chunk, dict): + return best_chunk.get("chunk_id") + return None + + def _episode_to_dict(self, record: EpisodeRecord) -> Dict[str, Any]: + return { + "prompt": record.prompt, + "response": record.response, + "task_type": record.task_type, + "equilibrium_found": record.equilibrium_found, + "resonance_score": record.resonance_score, + "work_computed": record.work_computed, + "created_at": record.created_at, + "metadata": record.metadata, + } + + def debug_session(self, session_id: str) -> Dict[str, Any]: + return { + "session_id": session_id, + "working_memory": self.working_memory.debug_snapshot(session_id), + "recent_episodes": self.episodic_memory.debug_recent(session_id), + "semantic_bundle": self.semantic_memory.debug_bundle(session_id), + } diff --git a/services_and_experiments/uet_agents/ingest_docs.py b/services_and_experiments/uet_agents/ingest_docs.py new file mode 100644 index 000000000..dcf8c2d68 --- /dev/null +++ b/services_and_experiments/uet_agents/ingest_docs.py @@ -0,0 +1,118 @@ +""" +Bulk-ingest research documentation into the UET Semantic Engine. + +Usage: + python -m uet_agents.ingest_docs # via running api_server + python uet_agents/ingest_docs.py --direct # direct (no server needed) +""" + +import argparse +import json +import sys +from pathlib import Path + +# Docs directories to ingest (relative to repo root) +DOCS_DIRS = [ + "docs/Docs", + "docs/Doc", +] + +EXTENSIONS = {".md", ".txt", ".rst"} + + +def collect_docs(repo_root: Path) -> list[dict]: + """Walk doc directories and collect all text files.""" + docs = [] + for docs_dir in DOCS_DIRS: + base = repo_root / docs_dir + if not base.exists(): + print(f" Skipping {docs_dir} (not found)") + continue + for fpath in sorted(base.rglob("*")): + if fpath.suffix.lower() in EXTENSIONS and fpath.stat().st_size > 100: + try: + text = fpath.read_text(encoding="utf-8", errors="ignore") + doc_id = str(fpath.relative_to(repo_root)).replace("\\", "/") + docs.append({ + "doc_id": doc_id, + "text": text, + "source_type": "documentation", + "tags": [fpath.parent.name, fpath.suffix.lstrip(".")], + }) + except Exception as e: + print(f" Error reading {fpath}: {e}") + return docs + + +def ingest_via_api(docs: list[dict], base_url: str = "http://localhost:8001"): + """Ingest docs by calling the running API server.""" + import requests + + for i, doc in enumerate(docs, 1): + try: + r = requests.post( + f"{base_url}/ingest", + json={ + "doc_id": doc["doc_id"], + "text": doc["text"], + "source_type": doc["source_type"], + "tags": doc["tags"], + "ingest_mode": "bulk", + }, + timeout=30, + ) + r.raise_for_status() + print(f" [{i}/{len(docs)}] {doc['doc_id']} ({len(doc['text'])} chars)") + except Exception as e: + print(f" [{i}/{len(docs)}] FAILED {doc['doc_id']}: {e}") + + +def ingest_direct(docs: list[dict]): + """Ingest docs directly into semantic engine (no server needed).""" + sys.path.insert(0, str(Path(__file__).parent.parent)) + from uet_agents.semantic_engine import UETSemanticEngine + + engine = UETSemanticEngine() + print(f" Before: {len(engine.knowledge_chunks)} chunks, {len(engine.vocab)} vocab") + + for i, doc in enumerate(docs, 1): + chunks_added = engine.ingest_document( + doc_id=doc["doc_id"], + text=doc["text"], + source_type=doc["source_type"], + tags=doc["tags"], + ingest_mode="bulk", + ) + print(f" [{i}/{len(docs)}] {doc['doc_id']} → +{chunks_added} chunks") + + print(f" After: {len(engine.knowledge_chunks)} chunks, {len(engine.vocab)} vocab") + print(f" State saved to {engine.state_file}") + + +def main(): + parser = argparse.ArgumentParser(description="Ingest UET docs into Semantic Engine") + parser.add_argument("--direct", action="store_true", help="Ingest directly (no API server)") + parser.add_argument("--url", default="http://localhost:8001", help="API server URL") + args = parser.parse_args() + + repo_root = Path(__file__).parent.parent + print(f"Collecting docs from {repo_root}...") + docs = collect_docs(repo_root) + print(f"Found {len(docs)} documents ({sum(len(d['text']) for d in docs):,} chars total)") + + if not docs: + print("No documents found. Check DOCS_DIRS paths.") + return + + if args.direct: + print("Ingesting directly into semantic engine...") + ingest_direct(docs) + else: + print(f"Ingesting via API at {args.url}...") + ingest_via_api(docs, args.url) + + print("Done!") + + +if __name__ == "__main__": + main() diff --git a/services_and_experiments/uet_agents/main.py b/services_and_experiments/uet_agents/main.py new file mode 100644 index 000000000..59d3240fa --- /dev/null +++ b/services_and_experiments/uet_agents/main.py @@ -0,0 +1,110 @@ +import sys +from pathlib import Path + +# Add project root to path +# Add project root to path +PROJECT_ROOT = Path(__file__).resolve().parent.parent +sys.path.append(str(PROJECT_ROOT)) + +from docs.knowledge_base.config import CONFIG +from docs.knowledge_base.api_client import OpenRouterClient, CostTracker +from docs.knowledge_base.vector_store import VectorStore +from docs.knowledge_base.tensorizer import UetTensorizer +from docs.knowledge_base.omega_search import OmegaSearch +from uet_agents.base_agent import BaseAgent +from uet_agents.research_agent import ResearchAgent +from uet_agents.marketing_agent import MarketingAgent +from uet_agents.orchestrator import OrchestratorAgent + + +def main(): + print("=" * 60) + print(" UET MULTI-AGENT SYSTEM (v0.9.0)") + print(" Initializing components...") + print("=" * 60) + + # 1. Load Config & Client + client_config = CONFIG["openrouter"] + keys = client_config.get("keys", {}) + cost_tracker = CostTracker(Path(CONFIG["cost_tracking"]["log_file"])) + + api_client = OpenRouterClient( + base_url=client_config["base_url"], + keys=keys, + agents=CONFIG["agents"], + cost_tracker=cost_tracker, + ) + print(" ✅ API Client & Cost Tracker ready.") + + # 2. Load Knowledge Base + db_path = Path(CONFIG["vector_db"]["path"]) + store = VectorStore(db_path) + tensorizer = UetTensorizer(grid_size=CONFIG["tensorizer"]["grid_size"]) + search_engine = OmegaSearch(store, tensorizer) + print(f" ✅ Knowledge Base loaded ({store.count()} docs).") + + # 3. Initialize Agents + # Research Agent + researcher = ResearchAgent( + name="web_research", # Mapping to config agent name, maybe "web_research" or "local_data"? + # config.toml has "local_data" for "Searches local research data". Let's use that. + # But wait, local_data uses glm-4.7-flash. + # Let's map ResearchAgent to "local_data" in config. + client=api_client, + search_engine=search_engine, + # system_prompt passed in constructor or default + ) + # Re-map name to match config key strictly + researcher.name = "local_data" + + # Equation Expert + equation_expert = BaseAgent( + name="equation_expert", + client=api_client, + system_prompt="You are an expert in UET (Unified Energy Theory) mathematics. Analyze equations for dimensional consistency, symmetry, and physical validity.", + ) + + # Marketing Agent + marketing_agent = MarketingAgent( + name="marketing", + client=api_client, + ) + + # Orchestrator + orchestrator = OrchestratorAgent( + name="orchestrator", + client=api_client, + research_agent=researcher, + equation_agent=equation_expert, + marketing_agent=marketing_agent, + ) + print( + " ✅ Agents initialized: Orchestrator, Researcher (local_data), EquationExpert, MarketingAgent." + ) + print("-" * 60) + print(" Type your query below (or 'exit' to quit).") + + # 4. Interactive Loop + while True: + try: + print("\n>> ", end="") + query = input().strip() + if not query: + continue + if query.lower() in ("exit", "quit"): + break + + # Run Orchestrator + response = orchestrator.run(query) + print(f"\nUET Agent: {response}") + + except KeyboardInterrupt: + break + except Exception as e: + print(f"\n❌ Error: {e}") + + print("\nExiting. Cost report available via `cost_dashboard`.") + + +if __name__ == "__main__": + main() diff --git a/services_and_experiments/uet_agents/marketing_agent.py b/services_and_experiments/uet_agents/marketing_agent.py new file mode 100644 index 000000000..52fa8bbff --- /dev/null +++ b/services_and_experiments/uet_agents/marketing_agent.py @@ -0,0 +1,132 @@ +from typing import List, Dict, Any, Optional +import json +from .base_agent import BaseAgent + +MARKETING_SYSTEM_PROMPT = """ +You are the **MarketingAgent** for Unity Equilibrium Theory (UET). +Your goal is to increase awareness by creating scientifically accurate content. + +## Identity +- Project: UET v0.9.0 (The Thermodynamics of Ethics) +- Tagline: "Ί = C ¡ I" (Balance = Connection × Isolation) +- GitHub: https://github.com/unityequilibrium/UnityEquilibriumTheory + +## Platforms +1. **Twitter/X**: Short, threads, hashtags #UET #AIPhysics. +2. **Reddit**: Deep discussions (r/MachineLearning). +3. **Moltbook**: AI-to-AI protocol (JSON payload). + +## Tool Calling Strict Format +To use a tool, you MUST output a JSON block like this: +```json +{ + "tool": "post_moltbook", + "arguments": { + "content": "..." + } +} +``` +Supported Tools: +- `post_moltbook(content: str)` -> Returns success message +- `post_tweet(content: str)` -> Returns success message +- `post_reddit(subreddit: str, title: str, body: str)` -> Returns success message + +Output ONLY the JSON block when calling a tool. +If asking for user approval, just speak normally. +""" + + +class MarketingAgent(BaseAgent): + """ + Agent responsible for social media marketing. + Directly handles tool execution loop (ReAct pattern). + """ + + def __init__(self, name: str, client): + super().__init__(name, client, system_prompt=MARKETING_SYSTEM_PROMPT) + # Register naive tools + self.tools = { + "post_moltbook": self._post_to_moltbook, + "post_tweet": self._post_tweet, + "post_reddit": self._post_reddit, + } + self.history = [] + + def _post_to_moltbook(self, content: str) -> str: + try: + # Simulate posting + payload = {"agent": "NON_UET", "verification": "splash-BJ6W", "content": content} + # In real system: requests.post(...) + print(f"đŸĻž [Moltbook] Payload: {json.dumps(payload, indent=2)}") + return "✅ Success: Posted to Moltbook." + except Exception as e: + return f"❌ Error: {str(e)}" + + def _post_tweet(self, content: str) -> str: + print(f"đŸĻ [Twitter] {content}") + return "✅ Success: Tweet posted." + + def _post_reddit(self, subreddit: str, title: str, body: str) -> str: + print(f"đŸ‘Ŋ [Reddit] r/{subreddit}: {title}") + return "✅ Success: Reddit post created." + + def _parse_tool_call(self, text: str) -> Optional[Dict]: + """Extract JSON tool call from markdown block or raw text.""" + try: + if "```json" in text: + start = text.index("```json") + 7 + end = text.find("```", start) + if end != -1: + return json.loads(text[start:end].strip()) + if text.strip().startswith("{") and '"tool"' in text: + return json.loads(text.strip()) + except Exception: + pass + return None + + def run(self, user_query: str) -> str: + print(f" đŸ“ĸ MarketingAgent processing: '{user_query}'...") + self.history.append({"role": "user", "content": user_query}) + + MAX_TURNS = 5 + for _ in range(MAX_TURNS): + # 1. Get model response + response = self.chat(self.history) + + # 2. Check for tool call + tool_data = self._parse_tool_call(response) + + if tool_data: + tool_name = tool_data.get("tool") + print(f" đŸ› ī¸ Tool Call: {tool_name}") + + # Execute tool + if tool_name in self.tools: + args = tool_data.get("arguments", {}) + try: + if tool_name == "post_moltbook": + result = self._post_to_moltbook(args.get("content")) + elif tool_name == "post_tweet": + result = self._post_tweet(args.get("content")) + elif tool_name == "post_reddit": + result = self._post_reddit( + args.get("subreddit"), args.get("title"), args.get("body") + ) + else: + result = f"Error: {tool_name} not implemented." + except Exception as e: + result = f"Error executing {tool_name}: {e}" + else: + result = f"Error: Tool {tool_name} not found." + + # Add to history + self.history.append({"role": "assistant", "content": response}) + self.history.append({"role": "user", "content": f"TOOL_OUTPUT: {result}"}) + + # Loop continues for model to see result and respond + else: + # No tool call, just return the text + self.history.append({"role": "assistant", "content": response}) + return response + + return "Error: Max turns exceeded." diff --git a/services_and_experiments/uet_agents/memory_store.py b/services_and_experiments/uet_agents/memory_store.py new file mode 100644 index 000000000..388ae0f94 --- /dev/null +++ b/services_and_experiments/uet_agents/memory_store.py @@ -0,0 +1,163 @@ +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any, Dict, List, Optional + + +@dataclass +class WorkingMemorySnapshot: + prompt: str + doc_context_present: bool + active_source_count: int + task_type: str + updated_at: str + + +@dataclass +class EpisodeRecord: + prompt: str + response: str + task_type: str + equilibrium_found: bool + resonance_score: float + work_computed: float + created_at: str + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class SemanticEvidenceBundle: + prompt: str + search_space: List[Dict[str, Any]] + persistent_chunk_count: int + temporary_chunk_count: int + total_chunk_count: int + task_type: str + best_chunk_preview: Optional[str] + created_at: str + + +class WorkingMemoryStore: + def __init__(self): + self._sessions: Dict[str, WorkingMemorySnapshot] = {} + + def update( + self, + session_id: str, + prompt: str, + doc_context: Optional[str], + task_type: str, + ) -> WorkingMemorySnapshot: + active_source_count = 0 + if doc_context: + active_source_count = len([line for line in doc_context.split("\n") if line.strip()]) + + snapshot = WorkingMemorySnapshot( + prompt=prompt, + doc_context_present=bool(doc_context and doc_context.strip()), + active_source_count=active_source_count, + task_type=task_type, + updated_at=datetime.utcnow().isoformat(), + ) + self._sessions[session_id] = snapshot + return snapshot + + def get(self, session_id: str) -> Optional[WorkingMemorySnapshot]: + return self._sessions.get(session_id) + + def debug_snapshot(self, session_id: str) -> Dict[str, Any]: + snapshot = self.get(session_id) + if snapshot is None: + return {} + return { + "prompt": snapshot.prompt, + "doc_context_present": snapshot.doc_context_present, + "active_source_count": snapshot.active_source_count, + "task_type": snapshot.task_type, + "updated_at": snapshot.updated_at, + } + + +class EpisodicMemoryStore: + def __init__(self, max_episodes_per_session: int = 25): + self.max_episodes_per_session = max_episodes_per_session + self._episodes: Dict[str, List[EpisodeRecord]] = {} + + def append(self, session_id: str, record: EpisodeRecord) -> None: + history = self._episodes.setdefault(session_id, []) + history.append(record) + if len(history) > self.max_episodes_per_session: + del history[0 : len(history) - self.max_episodes_per_session] + + def recent(self, session_id: str, limit: int = 3) -> List[EpisodeRecord]: + return self._episodes.get(session_id, [])[-limit:] + + def debug_recent(self, session_id: str, limit: int = 5) -> List[Dict[str, Any]]: + return [ + { + "prompt": record.prompt, + "response": record.response, + "task_type": record.task_type, + "equilibrium_found": record.equilibrium_found, + "resonance_score": record.resonance_score, + "work_computed": record.work_computed, + "created_at": record.created_at, + "metadata": record.metadata, + } + for record in self.recent(session_id, limit) + ] + + +class SemanticMemoryStore: + def __init__(self): + self._last_bundle_by_session: Dict[str, SemanticEvidenceBundle] = {} + + def build_evidence_bundle( + self, + session_id: str, + prompt: str, + task_type: str, + persistent_chunks: List[Dict[str, Any]], + temporary_chunks: List[Dict[str, Any]], + ) -> SemanticEvidenceBundle: + search_space = persistent_chunks + temporary_chunks + bundle = SemanticEvidenceBundle( + prompt=prompt, + search_space=search_space, + persistent_chunk_count=len(persistent_chunks), + temporary_chunk_count=len(temporary_chunks), + total_chunk_count=len(search_space), + task_type=task_type, + best_chunk_preview=search_space[0]["text"][:160] if search_space else None, + created_at=datetime.utcnow().isoformat(), + ) + self._last_bundle_by_session[session_id] = bundle + return bundle + + def debug_bundle(self, session_id: str) -> Dict[str, Any]: + bundle = self._last_bundle_by_session.get(session_id) + if bundle is None: + return {} + return { + "prompt": bundle.prompt, + "task_type": bundle.task_type, + "persistent_chunk_count": bundle.persistent_chunk_count, + "temporary_chunk_count": bundle.temporary_chunk_count, + "total_chunk_count": bundle.total_chunk_count, + "best_chunk_preview": bundle.best_chunk_preview, + "created_at": bundle.created_at, + } + + +class ProceduralMemoryStore: + def __init__(self): + self._policies: Dict[str, str] = { + "chat": "Use semantic retrieval first, then compose a grounded response.", + "ingest": "Normalize source text, add it to semantic memory, and persist the new state.", + "fallback": "If no evidence is strong enough, return a grounded insufficiency message instead of hallucinating.", + } + + def get(self, key: str) -> str: + return self._policies.get(key, "") + + def all(self) -> Dict[str, str]: + return dict(self._policies) diff --git a/services_and_experiments/uet_agents/orchestrator.py b/services_and_experiments/uet_agents/orchestrator.py new file mode 100644 index 000000000..0a72eda0e --- /dev/null +++ b/services_and_experiments/uet_agents/orchestrator.py @@ -0,0 +1,69 @@ +from typing import List, Dict, Type +import json +from .base_agent import BaseAgent + + +class OrchestratorAgent(BaseAgent): + """ + Central router for the UET Multi-Agent System. + Analyzes user intent and routes to: + - ResearchAgent (for knowledge based queries) + - EquationExpert (for math/equation analysis) + - Direct response (for simple greetings/meta questions) + """ + + def __init__( + self, + name: str, + client, + research_agent: BaseAgent, + equation_agent: BaseAgent, + marketing_agent: BaseAgent, + ): + system_prompt = ( + "You are the UET Orchestrator. Your role is to route user queries to the best specialist.\n" + "Your available agents are:\n" + "1. ResearchAgent: Can search the UET Knowledge Base (papers, code, theory). Use for 'Find', 'What is', 'Explain', 'Search'.\n" + "2. EquationExpert: Can analyze raw UET equations and math. Use for 'Solve', 'Calculate', 'Check equation'.\n" + "3. MarketingAgent: Can draft and post to social media. Use for 'Tweet', 'Post', 'Draft', 'Promote', 'Moltbook'.\n" + "4. Self: Answer simple greetings, meta-questions about the system directly.\n\n" + "If the query requires knowledge from the database, choose ResearchAgent.\n" + "If the query is purely mathematical verification, choose EquationExpert.\n" + "If the query is about social media or posting, choose MarketingAgent.\n" + "Output ONLY the name of the agent to route to: 'ResearchAgent', 'EquationExpert', 'MarketingAgent', or 'Self'." + ) + super().__init__(name, client, system_prompt=system_prompt) + self.research_agent = research_agent + self.equation_agent = equation_agent + self.marketing_agent = marketing_agent + + def run(self, user_query: str) -> str: + # 1. Decide intent + # Use a cheap/fast model call to classify, or just use the main Orchestrator model. + print(f" 🧠 Orchestrator analyzing: '{user_query}'...") + + # We need a robust classification. Even a simple prompt works. + decision = self.chat( + [{"role": "user", "content": f"Query: {user_query}\nTarget Agent:"}], temperature=0.1 + ).strip() + + print(f" 👉 Routing to: {decision}") + + if "ResearchAgent" in decision: + return self.research_agent.run(user_query) + elif "EquationExpert" in decision: + return self.equation_agent.run(user_query) + elif "MarketingAgent" in decision: + return self.marketing_agent.run(user_query) + + else: + # Handle directly + return self.chat( + [ + { + "role": "system", + "content": "You are a helpful assistant for the UET project.", + }, + {"role": "user", "content": user_query}, + ] + ) diff --git a/services_and_experiments/uet_agents/requirements.txt b/services_and_experiments/uet_agents/requirements.txt new file mode 100644 index 000000000..39dd6e547 --- /dev/null +++ b/services_and_experiments/uet_agents/requirements.txt @@ -0,0 +1,5 @@ +fastapi>=0.104.0 +uvicorn>=0.24.0 +pydantic>=2.0.0 +requests>=2.31.0 +python-dotenv>=1.0.0 diff --git a/services_and_experiments/uet_agents/research_agent.py b/services_and_experiments/uet_agents/research_agent.py new file mode 100644 index 000000000..f8705b2ae --- /dev/null +++ b/services_and_experiments/uet_agents/research_agent.py @@ -0,0 +1,65 @@ +from typing import Optional, List +from .base_agent import BaseAgent +from docs.knowledge_base.omega_search import OmegaSearch +from docs.knowledge_base.api_client import OpenRouterClient + + +class ResearchAgent(BaseAgent): + """ + RAG Agent that uses the UET Knowledge Base to answer questions. + Uses OmegaSearch to find relevant documents before answering. + """ + + def __init__( + self, + name: str, + client: OpenRouterClient, + search_engine: OmegaSearch, + system_prompt: str = "", + ): + super().__init__(name, client, system_prompt=system_prompt) + self.search_engine = search_engine + + def run(self, user_query: str, topic_hint: Optional[str] = None) -> str: + """ + Execute RAG pipeline: + 1. Search Knowledge Base (using query + topic_hint) + 2. Construct prompt with retrieved context + 3. Generative Answer + """ + # 1. Search + print(f" 🔍 {self.name} searching for: '{user_query}'...") + results = self.search_engine.search( + query_text=user_query, topic_hint=topic_hint, top_k=5 # Default to top 5 chunks + ) + + if not results: + print(" âš ī¸ No relevant documents found.") + return "I couldn't find any relevant information in the UET knowledge base to answer your question." + + # 2. Build Context + context_str = "" + for i, res in enumerate(results, 1): + explanation = self.search_engine.explain_match(res) + # Add chunk text + explanation metadata + context_str += f"\n--- DOCUMENT {i} ---\n" + context_str += f"Metadata: {explanation}\n" + context_str += ( + f"Content:\n{res['doc'].text}\n" + if isinstance(res, dict) + else f"Content:\n{res.doc.text}\n" + ) + # Wait, uet_vec stored in DB might not have full text if it's just vector. + # Let's check VectorDocument definition. It has 'file_path'. + # We assume we can load content or it's not stored? + # Uh oh. VectorDocument in 'ingest.py' doesn't seem to store the raw text content in the vector DB *unless* we added a text column. + # SQLite schema: `CREATE TABLE documents (..., text TEXT, ...)`? + # Let's check vector_store.py schema. + pass + + # If text is missing, we must fetch it from file_path. + # Let's refine this logic after checking vector_store.py. + # For now, placeholder. + return super().chat( + [{"role": "user", "content": f"Context:\n{context_str}\n\nQuestion: {user_query}"}] + ) diff --git a/services_and_experiments/uet_agents/response_composer.py b/services_and_experiments/uet_agents/response_composer.py new file mode 100644 index 000000000..e30e51588 --- /dev/null +++ b/services_and_experiments/uet_agents/response_composer.py @@ -0,0 +1,86 @@ +import os +from pathlib import Path +from typing import Any, Dict, List, Optional + +import requests +from dotenv import load_dotenv + +load_dotenv(Path(__file__).parent / ".env") + + +class UETResponseComposer: + def compose( + self, + prompt: str, + equilibrium_data: Dict[str, Any], + task_type: str = "chat", + recent_episodes: Optional[List[Dict[str, Any]]] = None, + procedure_hint: str = "", + ) -> str: + api_key = os.getenv("OPENROUTER_API_KEY", "") + + if not equilibrium_data["equilibrium_found"]: + return "āš„ā¸Ąāšˆā¸žā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ⏗ā¸ĩāšˆāš€ā¸Ē⏖ā¸ĩā¸ĸā¸Ŗā¸žā¸­āšƒā¸™ā¸Ŗā¸°ā¸šā¸š (Entropy ā¸Ēā¸šā¸‡āš€ā¸ā¸´ā¸™āš„ā¸›) ā¸ā¸Ŗā¸¸ā¸“ā¸˛āš€ā¸žā¸´āšˆā¸Ąā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ Source āš€ā¸žā¸ˇāšˆā¸­ 'āš€ā¸—ā¸Ŗā¸™' AI ā¸āšˆā¸­ā¸™ā¸—ā¸ŗā¸ā¸˛ā¸Ŗā¸§ā¸´āš€ā¸„ā¸Ŗā¸˛ā¸°ā¸ĢāšŒ" + + top_chunks = equilibrium_data.get("top_chunks", []) + if not top_chunks: + chunk = equilibrium_data["best_chunk"] + top_chunks = [chunk] if chunk else [] + source_text = "\n\n---\n\n".join(c["text"] for c in top_chunks) + work = equilibrium_data["work_computed"] + score = equilibrium_data["resonance_score"] + + if not api_key: + prefix = "ā¸Ŗā¸°ā¸šā¸šā¸„āš‰ā¸™ā¸žā¸šā¸ˆā¸¸ā¸”ā¸Ēā¸Ąā¸”ā¸¸ā¸Ĩ" + if task_type == "calculation": + prefix = "ā¸Ŗā¸°ā¸šā¸šā¸›ā¸Ŗā¸°āš€ā¸Ąā¸´ā¸™āš€ā¸Ēāš‰ā¸™ā¸—ā¸˛ā¸‡ā¸ā¸˛ā¸Ŗā¸„ā¸ŗā¸™ā¸§ā¸“āšā¸Ĩā¸°ā¸žā¸šā¸ˆā¸¸ā¸”ā¸Ēā¸Ąā¸”ā¸¸ā¸Ĩ" + return f"{prefix} (Resonance: {score:.4f}) ā¸ˆā¸˛ā¸ā¸ā¸˛ā¸™ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ:\n\n\"{source_text}\"\n\n*āšƒā¸Šāš‰ Work āš„ā¸› {work:.4f} Ί āšƒā¸™ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞ (ā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆāš„ā¸”āš‰ā¸•āšˆā¸­ API LLM)*" + + try: + episode_context = "" + if recent_episodes: + episode_lines = [] + for episode in recent_episodes[-3:]: + episode_lines.append( + f"- [{episode.get('task_type', 'chat')}] Q: {episode.get('prompt', '')} | A: {episode.get('response', '')[:120]}" + ) + episode_context = "\n".join(episode_lines) + + system_content = "⏄⏏⏓⏄⏎⏭ UET Communicator ā¸Ģā¸™āš‰ā¸˛ā¸—ā¸ĩāšˆā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ā¸„ā¸ˇā¸­ā¸Ŗā¸ąā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸”ā¸´ā¸šā¸ˆā¸˛ā¸ā¸Ēā¸Ąā¸ā¸˛ā¸Ŗ UET āšā¸Ĩāš‰ā¸§ā¸™ā¸ŗā¸Ąā¸˛āš€ā¸Ŗā¸ĩā¸ĸā¸šāš€ā¸Ŗā¸ĩā¸ĸā¸‡ā¸•ā¸­ā¸šā¸„ā¸ŗā¸–ā¸˛ā¸Ąā¸œā¸šāš‰āšƒā¸Šāš‰āšƒā¸Ģāš‰āš€ā¸›āš‡ā¸™ā¸ ā¸˛ā¸Šā¸˛āš„ā¸—ā¸ĸ⏗ā¸ĩāšˆā¸­āšˆā¸˛ā¸™ā¸‡āšˆā¸˛ā¸ĸ āš€ā¸›āš‡ā¸™ā¸˜ā¸Ŗā¸Ŗā¸Ąā¸Šā¸˛ā¸•ā¸´ āšā¸Ĩā¸°ā¸•ā¸­ā¸šā¸•ā¸Ŗā¸‡ā¸›ā¸Ŗā¸°āš€ā¸”āš‡ā¸™ āš‚ā¸”ā¸ĸā¸•āš‰ā¸­ā¸‡ā¸­āš‰ā¸˛ā¸‡ā¸­ā¸´ā¸‡ā¸ˆā¸˛ā¸ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸”ā¸´ā¸šā¸—ā¸ĩāšˆāš„ā¸”āš‰ā¸Ŗā¸ąā¸šāš€ā¸—āšˆā¸˛ā¸™ā¸ąāš‰ā¸™ ā¸Ģāš‰ā¸˛ā¸Ąā¸„ā¸´ā¸”ā¸„ā¸ŗā¸•ā¸­ā¸šāš€ā¸­ā¸‡āš€ā¸”āš‡ā¸”ā¸‚ā¸˛ā¸” ā¸–āš‰ā¸˛ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš„ā¸Ąāšˆāš€ā¸ā¸ĩāšˆā¸ĸā¸§āšƒā¸Ģāš‰ā¸šā¸­ā¸āš„ā¸›ā¸•ā¸Ŗā¸‡āš†" + if procedure_hint: + system_content = f"{system_content}\n\nāšā¸™ā¸§ā¸›ā¸ā¸´ā¸šā¸ąā¸•ā¸´ā¸‚ā¸­ā¸‡ā¸‡ā¸˛ā¸™ā¸™ā¸ĩāš‰: {procedure_hint}" + + user_content = f"ā¸›ā¸Ŗā¸°āš€ā¸ ā¸—ā¸‡ā¸˛ā¸™: {task_type}\nā¸„ā¸ŗā¸–ā¸˛ā¸Ąā¸ˆā¸˛ā¸ā¸œā¸šāš‰āšƒā¸Šāš‰: {prompt}\n\nā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ⏗ā¸ĩāšˆāš„ā¸”āš‰ā¸ˆā¸˛ā¸ UET Engine: {source_text}" + if episode_context: + user_content = f"{user_content}\n\nā¸šā¸Ŗā¸´ā¸šā¸—ā¸•ā¸­ā¸™ā¸āšˆā¸­ā¸™ā¸Ģā¸™āš‰ā¸˛:\n{episode_context}" + + response = requests.post( + url="https://openrouter.ai/api/v1/chat/completions", + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + }, + json={ + "model": "z-ai/glm-4.7-flash", + "max_tokens": 2000, + "messages": [ + { + "role": "system", + "content": system_content + }, + { + "role": "user", + "content": user_content + } + ] + } + ) + + res_json = response.json() + msg = res_json["choices"][0]["message"] + llm_text = msg.get("content") or msg.get("reasoning", "") + + return f"{llm_text}\n\n---\n*⚡ ā¸„ā¸ŗā¸™ā¸§ā¸“ā¸œāšˆā¸˛ā¸™ UET Engine (Resonance: {score:.4f} | Work: {work:.4f} Ί)*" + except Exception as e: + print(f"LLM API Error: {e}") + return f"ā¸Ŗā¸°ā¸šā¸šā¸„āš‰ā¸™ā¸žā¸šā¸ˆā¸¸ā¸”ā¸Ēā¸Ąā¸”ā¸¸ā¸Ĩ (Resonance: {score:.4f}) ā¸ˆā¸˛ā¸ā¸ā¸˛ā¸™ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ:\n\n\"{source_text}\"\n\n*āšƒā¸Šāš‰ Work āš„ā¸› {work:.4f} Ί āšƒā¸™ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞ (API āš€ā¸Ŗā¸ĩā¸ĸā¸šāš€ā¸Ŗā¸ĩā¸ĸā¸‡ā¸ ā¸˛ā¸Šā¸˛ā¸‚ā¸ąā¸”ā¸‚āš‰ā¸­ā¸‡)*" diff --git a/services_and_experiments/uet_agents/semantic_engine.py b/services_and_experiments/uet_agents/semantic_engine.py new file mode 100644 index 000000000..0512eebc3 --- /dev/null +++ b/services_and_experiments/uet_agents/semantic_engine.py @@ -0,0 +1,212 @@ +import re +import math +import json +import os +from datetime import datetime +from collections import defaultdict +from typing import List, Dict, Any, Optional +from .response_composer import UETResponseComposer + +class UETSemanticEngine: + def __init__(self, order: int = 3, state_file: str = "uet_knowledge_state.json"): + self.order = order + self.state_file = state_file + # The Semantic Manifold (Graph of connections) + self.field = defaultdict(lambda: defaultdict(float)) + self.vocab = set() + self.knowledge_chunks = [] + self.response_composer = UETResponseComposer() + self.load_state() + + def load_state(self): + """Load persistent knowledge state if it exists""" + if os.path.exists(self.state_file): + try: + with open(self.state_file, 'r', encoding='utf-8') as f: + data = json.load(f) + self.knowledge_chunks = data.get("chunks", []) + for chunk in self.knowledge_chunks: + chunk.setdefault("metadata", {}) + self.vocab = set(data.get("vocab", [])) + + # Reconstruct field (N-grams) + field_data = data.get("field", {}) + for ctx_str, targets in field_data.items(): + ctx = tuple(ctx_str.split("|||")) if ctx_str else () + for target, val in targets.items(): + self.field[ctx][target] = val + + print(f"Loaded {len(self.knowledge_chunks)} chunks from {self.state_file}") + except Exception as e: + print(f"Error loading state: {e}") + + def save_state(self): + """Save knowledge state to disk""" + # Convert tuple keys to strings for JSON + field_serializable = {} + for ctx, targets in self.field.items(): + ctx_str = "|||".join(ctx) if ctx else "" + field_serializable[ctx_str] = dict(targets) + + data = { + "chunks": [ + { + "doc_id": c["doc_id"], + "chunk_id": c["chunk_id"], + "text": c["text"], + "tokens": list(c["tokens"]), # Set to List + "vector_magnitude": c["vector_magnitude"], + "metadata": c.get("metadata", {}) + } + for c in self.knowledge_chunks + ], + "vocab": list(self.vocab), + "field": field_serializable + } + with open(self.state_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + def tokenize(self, text: str) -> List[str]: + """Normalize and tokenize text, preserving Thai/English structure.""" + text = re.sub(r"\s+", " ", text.lower()) + tokens = text.split(" ") + return [t for t in tokens if t.strip()] + + def build_temp_chunks(self, doc_context: Optional[str]) -> List[Dict[str, Any]]: + temp_chunks = [] + if not doc_context: + return temp_chunks + + raw_paragraphs = doc_context.split("\n") + for i, p in enumerate(raw_paragraphs): + p = p.strip() + if len(p) > 30: + p_tokens = set(self.tokenize(p)) + temp_chunks.append({ + "doc_id": "temp", + "chunk_id": f"temp_p{i}", + "text": p, + "tokens": p_tokens, + "vector_magnitude": len(p_tokens), + "metadata": { + "source_type": "runtime_context", + "project_scope": "runtime_session", + "doc_version": None, + "tags": ["runtime", "context"], + "ingest_mode": "ephemeral", + "paragraph_index": i, + "char_count": len(p), + "created_at": datetime.utcnow().isoformat(), + } + }) + return temp_chunks + + def build_search_space(self, doc_context: Optional[str] = None) -> List[Dict[str, Any]]: + return self.knowledge_chunks + self.build_temp_chunks(doc_context) + + def calculate_equilibrium_path_from_search_space( + self, + prompt: str, + search_space: List[Dict[str, Any]], + top_k: int = 3, + ) -> Dict[str, Any]: + prompt_tokens = set(self.tokenize(prompt.lower())) + + scored = [] + for chunk in search_space: + chunk_tokens = set(chunk["tokens"]) if isinstance(chunk["tokens"], list) else chunk["tokens"] + + overlap = len(prompt_tokens & chunk_tokens) + if overlap > 0: + score = overlap / (math.log(chunk["vector_magnitude"] + 1) + 1) + scored.append((score, chunk)) + + scored.sort(key=lambda x: x[0], reverse=True) + top_chunks = scored[:top_k] + + best_score = top_chunks[0][0] if top_chunks else 0.0 + best_chunk = top_chunks[0][1] if top_chunks else None + + work_done = len(search_space) * len(prompt_tokens) * 0.001 + + return { + "best_chunk": best_chunk, + "top_chunks": [c for _, c in top_chunks], + "resonance_score": best_score, + "equilibrium_found": best_score > 0.05, + "work_computed": work_done + } + + def ingest_document( + self, + doc_id: str, + text: str, + source_type: str = "text", + project_scope: Optional[str] = None, + doc_version: Optional[str] = None, + tags: Optional[List[str]] = None, + ingest_mode: str = "manual", + ): + """Train the manifold on a new document.""" + text = re.sub(r"[\r\n]+", "\n", text) + tokens = self.tokenize(text) + self.vocab.update(tokens) + + # 1. Build N-Gram Lattice (The C parameter - Connection) + for i in range(len(tokens) - self.order): + context = tuple(tokens[i : i + self.order]) + target = tokens[i + self.order] + self.field[context][target] += 1.0 + + # 2. Build Resonance Index (The I parameter - Information chunks) + raw_paragraphs = text.split("\n") + new_chunks = 0 + for i, p in enumerate(raw_paragraphs): + p = p.strip() + if len(p) > 30: # Ignore noise + p_tokens = set(self.tokenize(p)) + self.knowledge_chunks.append({ + "doc_id": doc_id, + "chunk_id": f"{doc_id}_p{i}_{len(self.knowledge_chunks)}", + "text": p, + "tokens": p_tokens, + "vector_magnitude": len(p_tokens), + "metadata": { + "source_type": source_type, + "project_scope": project_scope, + "doc_version": doc_version, + "tags": tags or [], + "ingest_mode": ingest_mode, + "paragraph_index": i, + "char_count": len(p), + "created_at": datetime.utcnow().isoformat(), + } + }) + new_chunks += 1 + + self.save_state() + return new_chunks + + def calculate_equilibrium_path(self, prompt: str, doc_context: Optional[str] = None) -> Dict[str, Any]: + """ + Instead of just generation, this calculates the "Path of Least Resistance" + This mimics solving the UET equation. + """ + search_space = self.build_search_space(doc_context) + return self.calculate_equilibrium_path_from_search_space(prompt, search_space) + + def generate_response( + self, + prompt: str, + equilibrium_data: Dict[str, Any], + task_type: str = "chat", + recent_episodes: Optional[List[Dict[str, Any]]] = None, + procedure_hint: str = "", + ) -> str: + return self.response_composer.compose( + prompt=prompt, + equilibrium_data=equilibrium_data, + task_type=task_type, + recent_episodes=recent_episodes, + procedure_hint=procedure_hint, + ) diff --git a/services_and_experiments/uet_agents/test_marketing.py b/services_and_experiments/uet_agents/test_marketing.py new file mode 100644 index 000000000..c634c8385 --- /dev/null +++ b/services_and_experiments/uet_agents/test_marketing.py @@ -0,0 +1,52 @@ +import sys +from pathlib import Path +from unittest.mock import MagicMock + +# Add project root to path +# Add project root to path +PROJECT_ROOT = Path(__file__).resolve().parent.parent +sys.path.append(str(PROJECT_ROOT)) + +from uet_agents.marketing_agent import MarketingAgent + + +def test_marketing_agent(): + print("đŸ§Ē Testing MarketingAgent...") + + # Mock Client + mock_client = MagicMock() + # Mock chat response to simulate tool call + mock_client.chat.side_effect = [ + # Turn 1: Thoughts -> Tool Call + 'I should post this to Twitter.\n```json\n{\n "tool": "post_tweet",\n "arguments": {\n "content": "Exciting news! UET v0.9.0 is here. #AIPhysics"\n }\n}\n```', + # Turn 2: Final response + "I have posted the update to Twitter.", + ] + + agent = MarketingAgent("marketing", mock_client) + + print(" 👉 Sending query: 'Tweet about update'") + response = agent.run("Tweet about update") + + print(f" 📝 Agent Response: {response}") + + # Verify tool call was attempted (by checking if _post_tweet printed) + # Since we can't easily capture stdout of the method without more mocking, + # we relies on the fact that if it parsed correctly, it calls the method. + # We can inspect the history. + + print(" 🔍 Inspecting history for tool execution...") + tool_output_found = False + for msg in agent.history: + if "TOOL_OUTPUT" in msg.get("content", ""): + print(f" ✅ Tool Output found: {msg['content']}") + tool_output_found = True + + if tool_output_found: + print("✅ TEST PASSED: MarketingAgent executed tool loop.") + else: + print("❌ TEST FAILED: No tool output in history.") + + +if __name__ == "__main__": + test_marketing_agent() diff --git a/services_and_experiments/uet_api/Cargo.toml b/services_and_experiments/uet_api/Cargo.toml new file mode 100644 index 000000000..83c81613b --- /dev/null +++ b/services_and_experiments/uet_api/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "uet_api" +version = "0.1.0" +edition = "2021" + +[dependencies] +# Async runtime +tokio = { version = "1.0", features = ["full"] } + +# Web framework +axum = { version = "0.7", features = ["macros"] } +axum-extra = { version = "0.9", features = ["typed-header"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["cors", "trace"] } + +# Database +sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] } + +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# Auth +argon2 = "0.5" +rand = "0.8" +jsonwebtoken = "9.0" +sha2 = "0.10" + +# OAuth +oauth2 = { version = "4.0", features = ["reqwest"] } +reqwest = { version = "0.11", features = ["json", "rustls-tls"] } + +# Types +uuid = { version = "1.8", features = ["v4", "serde"] } +chrono = { version = "0.4", features = ["serde"] } + +# Error handling +anyhow = "1.0" +thiserror = "1.0" + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Config +dotenvy = "0.15" +once_cell = "1.0" +fastembed = { version = "5.12.0", features = ["hf-hub", "image-models", "ort-download-binaries"] } +ndarray = { version = "0.17.2", features = ["serde"] } +ureq = { version = "3.2.0", features = ["json"] } +lazy_static = "1.5.0" diff --git a/services_and_experiments/uet_api/migrations/20260310000000_init.sql b/services_and_experiments/uet_api/migrations/20260310000000_init.sql new file mode 100644 index 000000000..b05b920bc --- /dev/null +++ b/services_and_experiments/uet_api/migrations/20260310000000_init.sql @@ -0,0 +1,35 @@ +-- Create users table +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT UNIQUE NOT NULL, + password_hash TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create oauth_identities table +CREATE TABLE oauth_identities ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + provider TEXT NOT NULL, + provider_id TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(provider, provider_id) +); + +-- Create user_quotas table +CREATE TABLE user_quotas ( + user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + tokens_used BIGINT NOT NULL DEFAULT 0, + requests_used INT NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create usage_events table (Audit/Log) +CREATE TABLE usage_events ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + event_type TEXT NOT NULL, + tokens_consumed INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); diff --git a/services_and_experiments/uet_api/migrations/20260310000001_full_schema.sql b/services_and_experiments/uet_api/migrations/20260310000001_full_schema.sql new file mode 100644 index 000000000..f8580c494 --- /dev/null +++ b/services_and_experiments/uet_api/migrations/20260310000001_full_schema.sql @@ -0,0 +1,185 @@ +-- Enable pgvector extension +CREATE EXTENSION IF NOT EXISTS vector; + +-- Create users table (if not exists from init) +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT UNIQUE NOT NULL, + password_hash TEXT, + display_name TEXT, + avatar_url TEXT, + is_admin BOOLEAN NOT NULL DEFAULT FALSE, + is_verified BOOLEAN NOT NULL DEFAULT FALSE, + verification_token TEXT, + verification_token_expires_at TIMESTAMPTZ, + password_reset_token TEXT, + password_reset_expires_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create oauth_identities table +CREATE TABLE IF NOT EXISTS oauth_identities ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + provider TEXT NOT NULL, -- 'google' or 'github' + provider_id TEXT NOT NULL, + provider_email TEXT, + provider_name TEXT, + provider_avatar TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(provider, provider_id) +); + +-- Create sessions table +CREATE TABLE IF NOT EXISTS sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL UNIQUE, + user_agent TEXT, + ip_address TEXT, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create refresh_tokens table +CREATE TABLE IF NOT EXISTS refresh_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL UNIQUE, + session_id UUID REFERENCES sessions(id) ON DELETE CASCADE, + expires_at TIMESTAMPTZ NOT NULL, + revoked BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create api_keys table +CREATE TABLE IF NOT EXISTS api_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + key_hash TEXT NOT NULL UNIQUE, + name TEXT, + prefix TEXT NOT NULL, -- First 8 chars for display + last_used_at TIMESTAMPTZ, + expires_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create plans table +CREATE TABLE IF NOT EXISTS plans ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL UNIQUE, + token_limit BIGINT NOT NULL DEFAULT 10000, + request_limit INT NOT NULL DEFAULT 100, + price_monthly_cents INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Insert default plans +INSERT INTO plans (name, token_limit, request_limit, price_monthly_cents) VALUES + ('free', 10000, 100, 0), + ('pro', 100000, 1000, 999), + ('team', 500000, 5000, 2999) +ON CONFLICT (name) DO NOTHING; + +-- Create user_quotas table +CREATE TABLE IF NOT EXISTS user_quotas ( + user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + plan_id UUID NOT NULL REFERENCES plans(id) DEFAULT (SELECT id FROM plans WHERE name = 'free'), + tokens_used BIGINT NOT NULL DEFAULT 0, + requests_used INT NOT NULL DEFAULT 0, + period_start TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create usage_events table +CREATE TABLE IF NOT EXISTS usage_events ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + event_type TEXT NOT NULL, -- 'chat', 'api', 'mcp' + tokens_consumed BIGINT NOT NULL DEFAULT 0, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create documents table (for docs/knowledge base) +CREATE TABLE IF NOT EXISTS documents ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + slug TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + collection TEXT NOT NULL, -- 'install', 'theory', 'topics', 'equations' + source_path TEXT NOT NULL, + content TEXT, + metadata JSONB DEFAULT '{}'::jsonb, + visibility TEXT NOT NULL DEFAULT 'public', -- 'public', 'private', 'draft' + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create document_versions table +CREATE TABLE IF NOT EXISTS document_versions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + doc_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE, + version INT NOT NULL, + content TEXT NOT NULL, + created_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(doc_id, version) +); + +-- Create document_chunks table (for vector search) +CREATE TABLE IF NOT EXISTS document_chunks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + doc_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE, + text TEXT NOT NULL, + embedding vector(1024), + chunk_index INT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create chat_threads table +CREATE TABLE IF NOT EXISTS chat_threads ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create chat_messages table +CREATE TABLE IF NOT EXISTS chat_messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + thread_id UUID NOT NULL REFERENCES chat_threads(id) ON DELETE CASCADE, + role TEXT NOT NULL, -- 'user', 'assistant', 'system' + content TEXT NOT NULL, + tokens_used INT NOT NULL DEFAULT 0, + retrieval_context JSONB, -- chunks used for this response + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create audit_logs table +CREATE TABLE IF NOT EXISTS audit_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + action TEXT NOT NULL, -- 'login', 'logout', 'api_key_created', 'quota_exceeded' + ip_address TEXT, + user_agent TEXT, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create indexes for performance +CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id); +CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token_hash); +CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id); +CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id); +CREATE INDEX IF NOT EXISTS idx_usage_events_user ON usage_events(user_id); +CREATE INDEX IF NOT EXISTS idx_usage_events_created ON usage_events(created_at); +CREATE INDEX IF NOT EXISTS idx_documents_collection ON documents(collection); +CREATE INDEX IF NOT EXISTS idx_documents_slug ON documents(slug); +CREATE INDEX IF NOT EXISTS idx_document_chunks_doc ON document_chunks(doc_id); +CREATE INDEX IF NOT EXISTS idx_document_chunks_embedding ON document_chunks USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); +CREATE INDEX IF NOT EXISTS idx_chat_threads_user ON chat_threads(user_id); +CREATE INDEX IF NOT EXISTS idx_chat_messages_thread ON chat_messages(thread_id); +CREATE INDEX IF NOT EXISTS idx_audit_logs_user ON audit_logs(user_id); +CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at); diff --git a/services_and_experiments/uet_api/migrations/20260310000002_alter_users.sql b/services_and_experiments/uet_api/migrations/20260310000002_alter_users.sql new file mode 100644 index 000000000..3eeab4dad --- /dev/null +++ b/services_and_experiments/uet_api/migrations/20260310000002_alter_users.sql @@ -0,0 +1,22 @@ +-- Add missing columns to users table +ALTER TABLE users ADD COLUMN IF NOT EXISTS display_name TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE users ADD COLUMN IF NOT EXISTS is_verified BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE users ADD COLUMN IF NOT EXISTS verification_token TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS verification_token_expires_at TIMESTAMPTZ; +ALTER TABLE users ADD COLUMN IF NOT EXISTS password_reset_token TEXT; +ALTER TABLE users ADD COLUMN IF NOT EXISTS password_reset_expires_at TIMESTAMPTZ; + +-- Add missing columns to oauth_identities +ALTER TABLE oauth_identities ADD COLUMN IF NOT EXISTS provider_email TEXT; +ALTER TABLE oauth_identities ADD COLUMN IF NOT EXISTS provider_name TEXT; +ALTER TABLE oauth_identities ADD COLUMN IF NOT EXISTS provider_avatar TEXT; + +-- Add missing columns to user_quotas +ALTER TABLE user_quotas ADD COLUMN IF NOT EXISTS plan_id UUID REFERENCES plans(id); +ALTER TABLE user_quotas ADD COLUMN IF NOT EXISTS period_start TIMESTAMPTZ NOT NULL DEFAULT NOW(); +ALTER TABLE user_quotas ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); + +-- Set default plan for existing quotas +UPDATE user_quotas SET plan_id = (SELECT id FROM plans WHERE name = 'free' LIMIT 1) WHERE plan_id IS NULL; diff --git a/services_and_experiments/uet_api/src/agent.rs b/services_and_experiments/uet_api/src/agent.rs new file mode 100644 index 000000000..bafe82323 --- /dev/null +++ b/services_and_experiments/uet_api/src/agent.rs @@ -0,0 +1,117 @@ +use axum::{ + extract::{Json, State}, + http::StatusCode, +}; +use serde::{Deserialize, Serialize}; + +use crate::handlers::{ApiError, AppState, CurrentUser}; +use crate::db; + +#[derive(Deserialize)] +pub struct WorkchatRequest { + pub prompt: String, + pub source_data: Option, // Document or data to ingest + pub is_numerical: bool, // False for semantic/chat, True for calculation +} + +#[derive(Serialize)] +pub struct WorkchatResponse { + pub response: String, + pub status: String, + pub work_computed: f64, +} + +// Request to Python Semantic Engine +#[derive(Serialize)] +struct PythonChatRequest { + prompt: String, + doc_context: Option, +} + +#[derive(Deserialize)] +struct PythonChatResponse { + response: String, + // equilibrium_data omitted for brevity, or we can parse it + work_computed: f64, +} + +#[derive(Serialize, Deserialize)] +pub struct IngestRequest { + pub doc_id: String, + pub text: String, +} + +#[derive(Serialize, Deserialize)] +pub struct IngestResponse { + pub status: String, + pub message: String, +} + +pub async fn ingest_handler( + State(_state): State, + Json(req): Json, +) -> Result, ApiError> { + let client = reqwest::Client::new(); + let python_url = "http://localhost:8001/ingest"; + + let res = client.post(python_url) + .json(&req) + .send() + .await + .map_err(|e| ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: format!("Failed to contact Semantic Engine: {}", e), + })?; + + let py_res: IngestResponse = res.json() + .await + .map_err(|e| ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: format!("Invalid response from Semantic Engine: {}", e), + })?; + + Ok(Json(py_res)) +} + +pub async fn workchat_handler( + State(_state): State, + Json(req): Json, +) -> Result, ApiError> { + + // In a real mining scenario, this would create a task in the DB, + // wait for miners to pick it up, and return the result. + // For this bridge, we'll directly call the Python Agent API (Port 8001) as a "mock" miner. + + let client = reqwest::Client::new(); + + let python_url = "http://localhost:8001/chat"; + let py_req = PythonChatRequest { + prompt: req.prompt.clone(), + doc_context: req.source_data.clone(), + }; + + let res = client.post(python_url) + .json(&py_req) + .send() + .await + .map_err(|e| ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: format!("Failed to contact Semantic Engine: {}", e), + })?; + + let py_res: PythonChatResponse = res.json() + .await + .map_err(|e| ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: format!("Invalid response from Semantic Engine: {}", e), + })?; + + // Removed DB record_usage temporarily to fix connection pool crash + // when PostgreSQL is not running in Docker. + + Ok(Json(WorkchatResponse { + response: py_res.response, + status: "Equilibrium Found".to_string(), + work_computed: py_res.work_computed, + })) +} diff --git a/services_and_experiments/uet_api/src/auth.rs b/services_and_experiments/uet_api/src/auth.rs new file mode 100644 index 000000000..8370b58a7 --- /dev/null +++ b/services_and_experiments/uet_api/src/auth.rs @@ -0,0 +1,138 @@ +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, +}; +use chrono::{Duration, Utc}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use rand::Rng; +use sha2::{Digest, Sha256}; + +use crate::config::CONFIG; +use crate::models::User; + +pub type AuthResult = Result; + +#[derive(Debug)] +pub enum AuthError { + InvalidCredentials, + UserExists, + InvalidToken, + TokenExpired, + PasswordHashError(String), + JwtError(String), +} + +impl std::fmt::Display for AuthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AuthError::InvalidCredentials => write!(f, "Invalid credentials"), + AuthError::UserExists => write!(f, "User already exists"), + AuthError::InvalidToken => write!(f, "Invalid token"), + AuthError::TokenExpired => write!(f, "Token expired"), + AuthError::PasswordHashError(e) => write!(f, "Password hashing error: {}", e), + AuthError::JwtError(e) => write!(f, "JWT error: {}", e), + } + } +} + +impl std::error::Error for AuthError {} + +impl From for AuthError { + fn from(e: argon2::password_hash::Error) -> Self { + AuthError::PasswordHashError(e.to_string()) + } +} + +impl From for AuthError { + fn from(e: jsonwebtoken::errors::Error) -> Self { + AuthError::JwtError(e.to_string()) + } +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct Claims { + pub sub: String, // User ID + pub email: String, + pub exp: i64, + pub iat: i64, + pub type_: String, // "access" or "refresh" +} + +/// Hash a password using Argon2 +pub fn hash_password(password: &str) -> AuthResult { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + Ok(argon2.hash_password(password.as_bytes(), &salt)?.to_string()) +} + +/// Verify a password against a hash +pub fn verify_password(password: &str, hash: &str) -> AuthResult { + let parsed_hash = PasswordHash::new(hash)?; + let argon2 = Argon2::default(); + Ok(argon2.verify_password(password.as_bytes(), &parsed_hash).is_ok()) +} + +/// Generate an access token (JWT) +pub fn generate_access_token(user: &User) -> AuthResult { + let now = Utc::now(); + let exp = now + Duration::hours(CONFIG.jwt_expiry_hours); + + let claims = Claims { + sub: user.id.to_string(), + email: user.email.clone(), + exp: exp.timestamp(), + iat: now.timestamp(), + type_: "access".to_string(), + }; + + encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(CONFIG.jwt_secret.as_bytes()), + ) + .map_err(AuthError::from) +} + +/// Generate a refresh token (random string) +pub fn generate_refresh_token() -> String { + let mut rng = rand::thread_rng(); + (0..64) + .map(|_| rng.sample(rand::distributions::Alphanumeric) as char) + .collect() +} + +/// Hash a token for storage (SHA256) +pub fn hash_token(token: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(token.as_bytes()); + format!("{:x}", hasher.finalize()) +} + +/// Validate an access token +pub fn validate_access_token(token: &str) -> AuthResult { + let token_data = decode::( + token, + &DecodingKey::from_secret(CONFIG.jwt_secret.as_bytes()), + &Validation::default(), + ) + .map_err(AuthError::from)?; + + if token_data.claims.type_ != "access" { + return Err(AuthError::InvalidToken); + } + + Ok(token_data.claims) +} + +/// Generate a secure API key +pub fn generate_api_key() -> (String, String, String) { + let mut rng = rand::thread_rng(); + let key: String = (0..32) + .map(|_| rng.sample(rand::distributions::Alphanumeric) as char) + .collect(); + + let prefix = format!("uet_{}", &key[..8]); + let key_hash = hash_token(&key); + + (key, prefix, key_hash) +} diff --git a/services_and_experiments/uet_api/src/config.rs b/services_and_experiments/uet_api/src/config.rs new file mode 100644 index 000000000..68b65a4d7 --- /dev/null +++ b/services_and_experiments/uet_api/src/config.rs @@ -0,0 +1,46 @@ +use once_cell::sync::Lazy; +use std::env; + +pub struct Config { + pub database_url: String, + pub jwt_secret: String, + pub jwt_expiry_hours: i64, + pub refresh_expiry_days: i64, + pub google_client_id: String, + pub google_client_secret: String, + pub github_client_id: String, + pub github_client_secret: String, + pub oauth_redirect_url: String, + pub frontend_url: String, +} + +pub static CONFIG: Lazy = Lazy::new(|| { + dotenvy::dotenv().ok(); + + Config { + database_url: env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5433/uet_kb".to_string()), + jwt_secret: env::var("JWT_SECRET") + .unwrap_or_else(|_| "dev-secret-change-in-production".to_string()), + jwt_expiry_hours: env::var("JWT_EXPIRY_HOURS") + .unwrap_or_else(|_| "24".to_string()) + .parse() + .unwrap_or(24), + refresh_expiry_days: env::var("REFRESH_EXPIRY_DAYS") + .unwrap_or_else(|_| "30".to_string()) + .parse() + .unwrap_or(30), + google_client_id: env::var("GOOGLE_CLIENT_ID") + .unwrap_or_default(), + google_client_secret: env::var("GOOGLE_CLIENT_SECRET") + .unwrap_or_default(), + github_client_id: env::var("GITHUB_CLIENT_ID") + .unwrap_or_default(), + github_client_secret: env::var("GITHUB_CLIENT_SECRET") + .unwrap_or_default(), + oauth_redirect_url: env::var("OAUTH_REDIRECT_URL") + .unwrap_or_else(|_| "http://localhost:3000/auth/callback".to_string()), + frontend_url: env::var("FRONTEND_URL") + .unwrap_or_else(|_| "http://localhost:3000".to_string()), + } +}); diff --git a/services_and_experiments/uet_api/src/db.rs b/services_and_experiments/uet_api/src/db.rs new file mode 100644 index 000000000..ea86c70e9 --- /dev/null +++ b/services_and_experiments/uet_api/src/db.rs @@ -0,0 +1,508 @@ +use anyhow::Result; +use chrono::{Duration, Utc}; +use sqlx::{PgPool, Postgres, QueryBuilder}; +use uuid::Uuid; + +use crate::auth::{hash_password, hash_token, verify_password, generate_access_token, generate_refresh_token}; +use crate::config::CONFIG; +use crate::models::*; + +// ==================== User Operations ==================== + +pub async fn create_user( + pool: &PgPool, + email: &str, + password: &str, + display_name: Option<&str>, +) -> Result { + let password_hash = hash_password(password)?; + let now = Utc::now(); + let id = Uuid::new_v4(); + + sqlx::query_as::<_, User>( + r#" + INSERT INTO users (id, email, password_hash, display_name, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $5) + RETURNING * + "#, + ) + .bind(id) + .bind(email) + .bind(&password_hash) + .bind(display_name) + .bind(now) + .fetch_one(pool) + .await + .map_err(Into::into) +} + +pub async fn get_user_by_email(pool: &PgPool, email: &str) -> Result> { + let user = sqlx::query_as::<_, User>( + "SELECT * FROM users WHERE email = $1" + ) + .bind(email) + .fetch_optional(pool) + .await?; + + Ok(user) +} + +pub async fn get_user_by_id(pool: &PgPool, id: Uuid) -> Result> { + let user = sqlx::query_as::<_, User>( + "SELECT * FROM users WHERE id = $1" + ) + .bind(id) + .fetch_optional(pool) + .await?; + + Ok(user) +} + +// ==================== Auth Operations ==================== + +pub async fn authenticate_user( + pool: &PgPool, + email: &str, + password: &str, +) -> Result> { + let user = match get_user_by_email(pool, email).await? { + Some(u) => u, + None => return Ok(None), + }; + + let hash = match &user.password_hash { + Some(h) => h, + None => return Ok(None), + }; + + if !verify_password(password, hash)? { + return Ok(None); + } + + let access_token = generate_access_token(&user)?; + let refresh_token = generate_refresh_token(); + let refresh_hash = hash_token(&refresh_token); + + // Store refresh token + let expires_at = Utc::now() + Duration::days(CONFIG.refresh_expiry_days); + sqlx::query( + r#" + INSERT INTO refresh_tokens (user_id, token_hash, expires_at) + VALUES ($1, $2, $3) + "#, + ) + .bind(user.id) + .bind(&refresh_hash) + .bind(expires_at) + .execute(pool) + .await?; + + Ok(Some((user, access_token, refresh_token))) +} + +pub async fn validate_refresh_token(pool: &PgPool, token: &str) -> Result> { + let hash = hash_token(token); + + let rt = sqlx::query_as::<_, RefreshToken>( + "SELECT * FROM refresh_tokens WHERE token_hash = $1 AND revoked = false AND expires_at > NOW()" + ) + .bind(&hash) + .fetch_optional(pool) + .await?; + + match rt { + Some(rt) => get_user_by_id(pool, rt.user_id).await, + None => Ok(None), + } +} + +pub async fn revoke_refresh_token(pool: &PgPool, token: &str) -> Result<()> { + let hash = hash_token(token); + sqlx::query( + "UPDATE refresh_tokens SET revoked = true WHERE token_hash = $1" + ) + .bind(&hash) + .execute(pool) + .await?; + Ok(()) +} + +// ==================== OAuth Operations ==================== + +pub async fn find_or_create_oauth_user( + pool: &PgPool, + provider: &str, + provider_id: &str, + email: &str, + name: Option<&str>, + avatar: Option<&str>, +) -> Result { + // Check if OAuth identity exists + let existing = sqlx::query_as::<_, OAuthIdentity>( + "SELECT * FROM oauth_identities WHERE provider = $1 AND provider_id = $2" + ) + .bind(provider) + .bind(provider_id) + .fetch_optional(pool) + .await?; + + if let Some(identity) = existing { + return get_user_by_id(pool, identity.user_id) + .await? + .ok_or_else(|| anyhow::anyhow!("User not found")); + } + + // Check if user with this email exists + let existing_user = get_user_by_email(pool, email).await?; + + let user = if let Some(user) = existing_user { + // Link OAuth to existing user + sqlx::query( + r#" + INSERT INTO oauth_identities (user_id, provider, provider_id, provider_email, provider_name, provider_avatar) + VALUES ($1, $2, $3, $4, $5, $6) + "#, + ) + .bind(user.id) + .bind(provider) + .bind(provider_id) + .bind(email) + .bind(name) + .bind(avatar) + .execute(pool) + .await?; + + user + } else { + // Create new user + let now = Utc::now(); + let id = Uuid::new_v4(); + + let user = sqlx::query_as::<_, User>( + r#" + INSERT INTO users (id, email, display_name, avatar_url, is_verified, created_at, updated_at) + VALUES ($1, $2, $3, $4, true, $5, $5) + RETURNING * + "#, + ) + .bind(id) + .bind(email) + .bind(name) + .bind(avatar) + .bind(now) + .fetch_one(pool) + .await?; + + // Create OAuth identity + sqlx::query( + r#" + INSERT INTO oauth_identities (user_id, provider, provider_id, provider_email, provider_name, provider_avatar) + VALUES ($1, $2, $3, $4, $5, $6) + "#, + ) + .bind(user.id) + .bind(provider) + .bind(provider_id) + .bind(email) + .bind(name) + .bind(avatar) + .execute(pool) + .await?; + + user + }; + + Ok(user) +} + +// ==================== API Key Operations ==================== + +pub async fn create_api_key(pool: &PgPool, user_id: Uuid, name: Option<&str>) -> Result<(String, ApiKey)> { + let (key, prefix, key_hash) = crate::auth::generate_api_key(); + + let api_key = sqlx::query_as::<_, ApiKey>( + r#" + INSERT INTO api_keys (user_id, key_hash, name, prefix) + VALUES ($1, $2, $3, $4) + RETURNING * + "#, + ) + .bind(user_id) + .bind(&key_hash) + .bind(name) + .bind(&prefix) + .fetch_one(pool) + .await?; + + Ok((key, api_key)) +} + +pub async fn list_api_keys(pool: &PgPool, user_id: Uuid) -> Result> { + let keys = sqlx::query_as::<_, ApiKey>( + "SELECT * FROM api_keys WHERE user_id = $1 ORDER BY created_at DESC" + ) + .bind(user_id) + .fetch_all(pool) + .await?; + + Ok(keys) +} + +pub async fn delete_api_key(pool: &PgPool, user_id: Uuid, key_id: Uuid) -> Result { + let result = sqlx::query( + "DELETE FROM api_keys WHERE id = $1 AND user_id = $2" + ) + .bind(key_id) + .bind(user_id) + .execute(pool) + .await?; + + Ok(result.rows_affected() > 0) +} + +// ==================== Quota Operations ==================== + +#[derive(sqlx::FromRow)] +pub struct QuotaWithPlan { + pub user_id: Uuid, + pub plan_id: Uuid, + pub tokens_used: i64, + pub requests_used: i32, + pub period_start: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub p_id: Uuid, + pub plan_name: String, + pub token_limit: i64, + pub request_limit: i32, + pub price_monthly_cents: i32, + pub plan_created_at: chrono::DateTime, +} + +pub async fn get_user_quota(pool: &PgPool, user_id: Uuid) -> Result> { + let row = sqlx::query_as::<_, QuotaWithPlan>( + r#" + SELECT + uq.user_id, uq.plan_id, uq.tokens_used, uq.requests_used, uq.period_start, uq.updated_at, + p.id as p_id, p.name as plan_name, p.token_limit, p.request_limit, p.price_monthly_cents, p.created_at as plan_created_at + FROM user_quotas uq + JOIN plans p ON uq.plan_id = p.id + WHERE uq.user_id = $1 + "#, + ) + .bind(user_id) + .fetch_optional(pool) + .await?; + + Ok(row.map(|r| { + let quota = UserQuota { + user_id: r.user_id, + plan_id: r.plan_id, + tokens_used: r.tokens_used, + requests_used: r.requests_used, + period_start: r.period_start, + updated_at: r.updated_at, + }; + let plan = Plan { + id: r.p_id, + name: r.plan_name, + token_limit: r.token_limit, + request_limit: r.request_limit, + price_monthly_cents: r.price_monthly_cents, + created_at: r.plan_created_at, + }; + (quota, plan) + })) +} + +pub async fn init_user_quota(pool: &PgPool, user_id: Uuid) -> Result<()> { + sqlx::query( + r#" + INSERT INTO user_quotas (user_id, plan_id) + SELECT $1, id FROM plans WHERE name = 'free' + ON CONFLICT (user_id) DO NOTHING + "# + ) + .bind(user_id) + .execute(pool) + .await?; + + Ok(()) +} + +pub async fn record_usage( + pool: &PgPool, + user_id: Uuid, + event_type: &str, + tokens: i64, + metadata: Option, +) -> Result<()> { + let mut tx = pool.begin().await?; + + // Insert usage event + sqlx::query( + r#" + INSERT INTO usage_events (user_id, event_type, tokens_consumed, metadata) + VALUES ($1, $2, $3, $4) + "#, + ) + .bind(user_id) + .bind(event_type) + .bind(tokens) + .bind(&metadata) + .execute(&mut *tx) + .await?; + + // Update quota + sqlx::query( + r#" + UPDATE user_quotas + SET tokens_used = tokens_used + $2, + requests_used = requests_used + 1, + updated_at = NOW() + WHERE user_id = $1 + "# + ) + .bind(user_id) + .bind(tokens) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + Ok(()) +} + +// ==================== Audit Log ==================== + +pub async fn log_audit( + pool: &PgPool, + user_id: Option, + action: &str, + ip: Option<&str>, + user_agent: Option<&str>, + metadata: Option, +) -> Result<()> { + sqlx::query( + r#" + INSERT INTO audit_logs (user_id, action, ip_address, user_agent, metadata) + VALUES ($1, $2, $3, $4, $5) + "#, + ) + .bind(user_id) + .bind(action) + .bind(ip) + .bind(user_agent) + .bind(&metadata) + .execute(pool) + .await?; + + Ok(()) +} + +// ==================== Email Verification ==================== + +/// Set verification token for a user +pub async fn set_verification_token(pool: &PgPool, user_id: Uuid, token: &str) -> Result<()> { + let expires_at = Utc::now() + Duration::hours(24); + + sqlx::query( + r#" + UPDATE users + SET verification_token = $2, + verification_token_expires_at = $3, + updated_at = NOW() + WHERE id = $1 + "# + ) + .bind(user_id) + .bind(token) + .bind(expires_at) + .execute(pool) + .await?; + + Ok(()) +} + +/// Verify email with token +pub async fn verify_email(pool: &PgPool, token: &str) -> Result> { + let user = sqlx::query_as::<_, User>( + r#" + UPDATE users + SET is_verified = true, + verification_token = null, + verification_token_expires_at = null, + updated_at = NOW() + WHERE verification_token = $1 + AND verification_token_expires_at > NOW() + RETURNING * + "# + ) + .bind(token) + .fetch_optional(pool) + .await?; + + Ok(user) +} + +/// Get user by verification token +pub async fn get_user_by_verification_token(pool: &PgPool, token: &str) -> Result> { + let user = sqlx::query_as::<_, User>( + r#" + SELECT * FROM users + WHERE verification_token = $1 + AND verification_token_expires_at > NOW() + "# + ) + .bind(token) + .fetch_optional(pool) + .await?; + + Ok(user) +} + +// ==================== Password Reset ==================== + +/// Set password reset token for a user +pub async fn set_password_reset_token(pool: &PgPool, email: &str, token: &str) -> Result> { + let expires_at = Utc::now() + Duration::hours(1); + + let user = sqlx::query_as::<_, User>( + r#" + UPDATE users + SET password_reset_token = $2, + password_reset_expires_at = $3, + updated_at = NOW() + WHERE email = $1 + RETURNING * + "# + ) + .bind(email) + .bind(token) + .bind(expires_at) + .fetch_optional(pool) + .await?; + + Ok(user) +} + +/// Reset password with token +pub async fn reset_password(pool: &PgPool, token: &str, new_password_hash: &str) -> Result> { + let user = sqlx::query_as::<_, User>( + r#" + UPDATE users + SET password_hash = $2, + password_reset_token = null, + password_reset_expires_at = null, + is_verified = true, + updated_at = NOW() + WHERE password_reset_token = $1 + AND password_reset_expires_at > NOW() + RETURNING * + "# + ) + .bind(token) + .bind(new_password_hash) + .fetch_optional(pool) + .await?; + + Ok(user) +} diff --git a/services_and_experiments/uet_api/src/email.rs b/services_and_experiments/uet_api/src/email.rs new file mode 100644 index 000000000..9faa3ce77 --- /dev/null +++ b/services_and_experiments/uet_api/src/email.rs @@ -0,0 +1,254 @@ +use anyhow::Result; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use tracing::info; + +use crate::config::CONFIG; + +#[derive(Debug, Serialize)] +struct ResendEmailRequest { + from: String, + to: Vec, + subject: String, + html: String, +} + +#[derive(Debug, Deserialize)] +struct ResendResponse { + id: String, +} + +pub struct EmailService { + client: Client, + api_key: String, + from_email: String, +} + +impl Clone for EmailService { + fn clone(&self) -> Self { + Self { + client: Client::new(), + api_key: self.api_key.clone(), + from_email: self.from_email.clone(), + } + } +} + +impl EmailService { + pub fn new() -> Self { + Self { + client: Client::new(), + api_key: std::env::var("RESEND_API_KEY").unwrap_or_default(), + from_email: std::env::var("EMAIL_FROM") + .unwrap_or_else(|_| "noreply@uet.ai".to_string()), + } + } + + /// Send verification email + pub async fn send_verification_email( + &self, + to: &str, + username: &str, + verify_url: &str, + ) -> Result<()> { + let html = format!( + r#" + + + + + + + + + + + +
+ + + + +
+ +
+ ⚛ UET +
+

Unity Equilibrium Theory

+ + +

+ Welcome, {username}! +

+

+ Verify your email to start exploring the equations of the universe. +

+ + + + Verify Email + + + +

+ This link expires in 24 hours.
+ If you didn't create an account, ignore this email. +

+
+ +

+ Š 2026 UET Platform ¡ Unity Equilibrium Theory +

+
+ + +"#, + username = username, + verify_url = verify_url + ); + + self.send_email(to, "Verify your UET account", &html).await + } + + /// Send password reset email + pub async fn send_password_reset_email( + &self, + to: &str, + username: &str, + reset_url: &str, + ) -> Result<()> { + let html = format!( + r#" + + + + + + + + + + + +
+ + + + +
+
+ ⚛ UET +
+ +

+ Reset Password +

+

+ Hi {username}, click below to reset your password. +

+ + + Reset Password + + +

+ This link expires in 1 hour.
+ If you didn't request this, ignore this email. +

+
+
+ + +"#, + username = username, + reset_url = reset_url + ); + + self.send_email(to, "Reset your UET password", &html).await + } + + /// Send welcome email after verification + pub async fn send_welcome_email(&self, to: &str, username: &str) -> Result<()> { + let html = format!( + r#" + + + + + + + + + + + +
+ + + + +
+
+ ✓ Verified +
+ +

+ Welcome to UET, {username}! +

+

+ Your account is ready. Start exploring the equations that describe our universe. +

+ +
+

Quick Start

+ pip install uet +
+ + + Read the Docs + +
+
+ + +"#, + username = username + ); + + self.send_email(to, "Welcome to UET!", &html).await + } + + /// Core send function via Resend API + async fn send_email(&self, to: &str, subject: &str, html: &str) -> Result<()> { + if self.api_key.is_empty() { + info!("Email skipped (no API key): to={}, subject={}", to, subject); + return Ok(()); + } + + let request = ResendEmailRequest { + from: self.from_email.clone(), + to: vec![to.to_string()], + subject: subject.to_string(), + html: html.to_string(), + }; + + let response = self + .client + .post("https://api.resend.com/emails") + .header("Authorization", format!("Bearer {}", self.api_key)) + .header("Content-Type", "application/json") + .json(&request) + .send() + .await?; + + if response.status().is_success() { + let result: ResendResponse = response.json().await?; + info!("Email sent: id={}, to={}", result.id, to); + } else { + let error = response.text().await?; + anyhow::bail!("Resend API error: {}", error); + } + + Ok(()) + } +} diff --git a/services_and_experiments/uet_api/src/handlers.rs b/services_and_experiments/uet_api/src/handlers.rs new file mode 100644 index 000000000..c3d8b1a99 --- /dev/null +++ b/services_and_experiments/uet_api/src/handlers.rs @@ -0,0 +1,685 @@ +use axum::{ + async_trait, + extract::{FromRequestParts, Json, Path, Query, State}, + http::{header, request::Parts, StatusCode}, + middleware::Next, + response::{IntoResponse, Response}, + Router, +}; +use axum::http::Request; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use uuid::Uuid; + +use crate::auth::{validate_access_token, AuthError}; +use crate::config::CONFIG; +use crate::db; +use crate::email::EmailService; +use crate::mcp; +use crate::models::*; +use crate::oauth::{self, OAuthClients}; + +pub fn auth_routes() -> Router { + Router::new() + .route("/register", axum::routing::post(register)) + .route("/login", axum::routing::post(login)) + .route("/refresh", axum::routing::post(refresh_token)) + .route("/logout", axum::routing::post(logout)) + .route("/verify-email", axum::routing::post(verify_email)) + .route("/request-reset", axum::routing::post(request_password_reset)) + .route("/reset-password", axum::routing::post(reset_password)) + .route("/oauth/google", axum::routing::get(google_auth)) + .route("/oauth/google/callback", axum::routing::get(google_callback)) + .route("/oauth/github", axum::routing::get(github_auth)) + .route("/oauth/github/callback", axum::routing::get(github_callback)) + .route("/me", axum::routing::get(get_me)) + .route("/quota", axum::routing::get(get_quota)) + .route("/api-keys", axum::routing::get(list_keys)) + .route("/api-keys", axum::routing::post(create_key)) + .route("/api-keys/:id", axum::routing::delete(delete_key)) +} + +#[derive(Clone)] +pub struct AppState { + pub pool: PgPool, + pub oauth: OAuthClients, + pub email: EmailService, +} + +// ==================== Error Handling ==================== + +#[derive(Debug)] +pub struct ApiError { + pub status: StatusCode, + pub message: String, +} + +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + let body = Json(serde_json::json!({ + "error": self.message + })); + (self.status, body).into_response() + } +} + +impl From for ApiError { + fn from(err: AuthError) -> Self { + match err { + AuthError::InvalidCredentials => ApiError { + status: StatusCode::UNAUTHORIZED, + message: "Invalid credentials".to_string(), + }, + AuthError::UserExists => ApiError { + status: StatusCode::CONFLICT, + message: "User already exists".to_string(), + }, + AuthError::InvalidToken | AuthError::TokenExpired => ApiError { + status: StatusCode::UNAUTHORIZED, + message: "Invalid or expired token".to_string(), + }, + _ => ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: err.to_string(), + }, + } + } +} + +impl From for ApiError { + fn from(err: anyhow::Error) -> Self { + ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: err.to_string(), + } + } +} + +impl From for ApiError { + fn from(err: sqlx::Error) -> Self { + if let sqlx::Error::Database(db_err) = &err { + if db_err.constraint().map(|c| c.contains("email")).unwrap_or(false) { + return ApiError { + status: StatusCode::CONFLICT, + message: "Email already registered".to_string(), + }; + } + } + ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: "Database error".to_string(), + } + } +} + +// ==================== Auth Handlers ==================== + +#[derive(Deserialize)] +pub struct RefreshRequest { + refresh_token: String, +} + +pub async fn register( + State(state): State, + Json(req): Json, +) -> Result, ApiError> { + // Check if user exists + if db::get_user_by_email(&state.pool, &req.email).await?.is_some() { + return Err(ApiError { + status: StatusCode::CONFLICT, + message: "Email already registered".to_string(), + }); + } + + let user = db::create_user(&state.pool, &req.email, &req.password, req.display_name.as_deref()).await?; + db::init_user_quota(&state.pool, user.id).await?; + + // Generate verification token + let verification_token = crate::auth::generate_refresh_token(); // Reuse as random token + db::set_verification_token(&state.pool, user.id, &verification_token).await?; + + // Send verification email (non-fatal) + let verify_url = format!("{}/auth/verify?token={}", CONFIG.frontend_url, verification_token); + let username = user.display_name.as_deref().unwrap_or("there"); + if let Err(e) = state.email.send_verification_email(&user.email, username, &verify_url).await { + tracing::warn!("Failed to send verification email: {}", e); + } + + db::log_audit(&state.pool, Some(user.id), "register", None, None, None).await?; + + Ok(Json(serde_json::json!({ + "message": "Registration successful. Please check your email to verify your account.", + "email": user.email + }))) +} + +pub async fn login( + State(state): State, + Json(req): Json, +) -> Result, ApiError> { + let (user, access_token, refresh_token) = db::authenticate_user(&state.pool, &req.email, &req.password) + .await? + .ok_or_else(|| ApiError { + status: StatusCode::UNAUTHORIZED, + message: "Invalid credentials".to_string(), + })?; + + // Check if email is verified (skip for OAuth users without password) + if user.password_hash.is_some() && !user.is_verified { + return Err(ApiError { + status: StatusCode::FORBIDDEN, + message: "Please verify your email before logging in".to_string(), + }); + } + + db::log_audit(&state.pool, Some(user.id), "login", None, None, None).await?; + + Ok(Json(AuthResponse { + access_token, + refresh_token, + expires_in: CONFIG.jwt_expiry_hours * 3600, + user: user.into(), + })) +} + +pub async fn refresh_token( + State(state): State, + Json(req): Json, +) -> Result, ApiError> { + let user = db::validate_refresh_token(&state.pool, &req.refresh_token) + .await? + .ok_or_else(|| ApiError { + status: StatusCode::UNAUTHORIZED, + message: "Invalid refresh token".to_string(), + })?; + + db::revoke_refresh_token(&state.pool, &req.refresh_token).await?; + + let access_token = crate::auth::generate_access_token(&user)?; + let refresh_token = crate::auth::generate_refresh_token(); + let refresh_hash = crate::auth::hash_token(&refresh_token); + + let expires_at = chrono::Utc::now() + chrono::Duration::days(CONFIG.refresh_expiry_days); + sqlx::query( + "INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)", + ) + .bind(user.id) + .bind(&refresh_hash) + .bind(expires_at) + .execute(&state.pool) + .await?; + + Ok(Json(AuthResponse { + access_token, + refresh_token, + expires_in: CONFIG.jwt_expiry_hours * 3600, + user: user.into(), + })) +} + +pub async fn logout( + State(state): State, + user: CurrentUser, + Json(req): Json, +) -> Result { + db::revoke_refresh_token(&state.pool, &req.refresh_token).await?; + db::log_audit(&state.pool, Some(user.0), "logout", None, None, None).await?; + Ok(StatusCode::NO_CONTENT) +} + +// ==================== Email Verification ==================== + +pub async fn verify_email( + State(state): State, + Json(req): Json, +) -> Result, ApiError> { + let user = db::verify_email(&state.pool, &req.token) + .await? + .ok_or_else(|| ApiError { + status: StatusCode::BAD_REQUEST, + message: "Invalid or expired verification token".to_string(), + })?; + + // Send welcome email + let username = user.display_name.as_deref().unwrap_or("there"); + state.email.send_welcome_email(&user.email, username).await?; + + // Generate tokens for auto-login + let access_token = crate::auth::generate_access_token(&user)?; + let refresh_token = crate::auth::generate_refresh_token(); + let refresh_hash = crate::auth::hash_token(&refresh_token); + + let expires_at = chrono::Utc::now() + chrono::Duration::days(CONFIG.refresh_expiry_days); + sqlx::query( + "INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)", + ) + .bind(user.id) + .bind(&refresh_hash) + .bind(expires_at) + .execute(&state.pool) + .await?; + + db::log_audit(&state.pool, Some(user.id), "email_verified", None, None, None).await?; + + Ok(Json(AuthResponse { + access_token, + refresh_token, + expires_in: CONFIG.jwt_expiry_hours * 3600, + user: user.into(), + })) +} + +pub async fn request_password_reset( + State(state): State, + Json(req): Json, +) -> Result, ApiError> { + // Generate reset token + let reset_token = crate::auth::generate_refresh_token(); + + // Try to set reset token (returns None if email not found) + let user = db::set_password_reset_token(&state.pool, &req.email, &reset_token).await?; + + if let Some(user) = user { + // Send reset email + let reset_url = format!("{}/auth/reset?token={}", CONFIG.frontend_url, reset_token); + let username = user.display_name.as_deref().unwrap_or("there"); + if let Err(e) = state.email.send_password_reset_email(&user.email, username, &reset_url).await { + tracing::warn!("Failed to send password reset email: {}", e); + } + + db::log_audit(&state.pool, Some(user.id), "password_reset_requested", None, None, None).await?; + } + + // Always return success to prevent email enumeration + Ok(Json(serde_json::json!({ + "message": "If an account exists with this email, a reset link has been sent." + }))) +} + +pub async fn reset_password( + State(state): State, + Json(req): Json, +) -> Result, ApiError> { + // Validate password strength + if req.new_password.len() < 8 { + return Err(ApiError { + status: StatusCode::BAD_REQUEST, + message: "Password must be at least 8 characters".to_string(), + }); + } + + let password_hash = crate::auth::hash_password(&req.new_password)?; + + let user = db::reset_password(&state.pool, &req.token, &password_hash) + .await? + .ok_or_else(|| ApiError { + status: StatusCode::BAD_REQUEST, + message: "Invalid or expired reset token".to_string(), + })?; + + // Generate tokens for auto-login + let access_token = crate::auth::generate_access_token(&user)?; + let refresh_token = crate::auth::generate_refresh_token(); + let refresh_hash = crate::auth::hash_token(&refresh_token); + + let expires_at = chrono::Utc::now() + chrono::Duration::days(CONFIG.refresh_expiry_days); + sqlx::query( + "INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)", + ) + .bind(user.id) + .bind(&refresh_hash) + .bind(expires_at) + .execute(&state.pool) + .await?; + + db::log_audit(&state.pool, Some(user.id), "password_reset", None, None, None).await?; + + Ok(Json(AuthResponse { + access_token, + refresh_token, + expires_in: CONFIG.jwt_expiry_hours * 3600, + user: user.into(), + })) +} + +// ==================== OAuth Handlers ==================== + +#[derive(Deserialize)] +pub struct CallbackQuery { + code: String, + state: String, +} + +pub async fn google_auth( + State(state): State, +) -> Result, ApiError> { + let client = state.oauth.google.as_ref().ok_or_else(|| ApiError { + status: StatusCode::SERVICE_UNAVAILABLE, + message: "Google OAuth not configured".to_string(), + })?; + + Ok(Json(oauth::get_google_auth_url(client))) +} + +pub async fn google_callback( + State(state): State, + Query(query): Query, +) -> Result { + let client = state.oauth.google.as_ref().ok_or_else(|| ApiError { + status: StatusCode::SERVICE_UNAVAILABLE, + message: "Google OAuth not configured".to_string(), + })?; + + let user_info = oauth::exchange_google_code(client, &query.code).await?; + + let user = db::find_or_create_oauth_user( + &state.pool, + "google", + &user_info.id, + &user_info.email, + user_info.name.as_deref(), + user_info.picture.as_deref(), + ).await?; + + db::init_user_quota(&state.pool, user.id).await?; + + let access_token = crate::auth::generate_access_token(&user)?; + let refresh_token = crate::auth::generate_refresh_token(); + let refresh_hash = crate::auth::hash_token(&refresh_token); + + let expires_at = chrono::Utc::now() + chrono::Duration::days(CONFIG.refresh_expiry_days); + sqlx::query( + "INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)", + ) + .bind(user.id) + .bind(&refresh_hash) + .bind(expires_at) + .execute(&state.pool) + .await?; + + // Redirect to frontend with tokens + let redirect_url = format!( + "{}/auth/callback?access_token={}&refresh_token={}", + CONFIG.frontend_url, access_token, refresh_token + ); + + let response = Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, redirect_url) + .body(axum::body::Body::empty()) + .unwrap(); + Ok(response) +} + +pub async fn github_auth( + State(state): State, +) -> Result, ApiError> { + let client = state.oauth.github.as_ref().ok_or_else(|| ApiError { + status: StatusCode::SERVICE_UNAVAILABLE, + message: "GitHub OAuth not configured".to_string(), + })?; + + Ok(Json(oauth::get_github_auth_url(client))) +} + +pub async fn github_callback( + State(state): State, + Query(query): Query, +) -> Result { + let client = state.oauth.github.as_ref().ok_or_else(|| ApiError { + status: StatusCode::SERVICE_UNAVAILABLE, + message: "GitHub OAuth not configured".to_string(), + })?; + + let user_info = oauth::exchange_github_code(client, &query.code).await?; + + // GitHub may not return email directly, use login as fallback + let email = user_info.email.clone().unwrap_or_else(|| format!("{}@github", user_info.login)); + + let user = db::find_or_create_oauth_user( + &state.pool, + "github", + &user_info.id.to_string(), + &email, + Some(&user_info.login), + user_info.avatar_url.as_deref(), + ).await?; + + db::init_user_quota(&state.pool, user.id).await?; + + let access_token = crate::auth::generate_access_token(&user)?; + let refresh_token = crate::auth::generate_refresh_token(); + let refresh_hash = crate::auth::hash_token(&refresh_token); + + let expires_at = chrono::Utc::now() + chrono::Duration::days(CONFIG.refresh_expiry_days); + sqlx::query( + "INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)", + ) + .bind(user.id) + .bind(&refresh_hash) + .bind(expires_at) + .execute(&state.pool) + .await?; + + let redirect_url = format!( + "{}/auth/callback?access_token={}&refresh_token={}", + CONFIG.frontend_url, access_token, refresh_token + ); + + let response = Response::builder() + .status(StatusCode::FOUND) + .header(header::LOCATION, redirect_url) + .body(axum::body::Body::empty()) + .unwrap(); + Ok(response) +} + +// ==================== User Handlers ==================== + +#[derive(Clone)] +pub struct CurrentUser(pub Uuid); + +#[async_trait] +impl FromRequestParts for CurrentUser +where + S: Send + Sync, +{ + type Rejection = ApiError; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + use axum::RequestPartsExt; + let TypedHeader(bearer) = parts + .extract::>>() + .await + .map_err(|_| ApiError { + status: StatusCode::UNAUTHORIZED, + message: "Missing or invalid Authorization header".to_string(), + })?; + + let claims = validate_access_token(bearer.token()).map_err(|_| ApiError { + status: StatusCode::UNAUTHORIZED, + message: "Invalid or expired token".to_string(), + })?; + + let user_id = Uuid::parse_str(&claims.sub).map_err(|_| ApiError { + status: StatusCode::UNAUTHORIZED, + message: "Invalid token subject".to_string(), + })?; + + Ok(CurrentUser(user_id)) + } +} + +pub async fn auth_middleware( + TypedHeader(bearer): TypedHeader>, + request: Request, + next: Next, +) -> Result { + let _claims = validate_access_token(bearer.token()).map_err(|_| ApiError { + status: StatusCode::UNAUTHORIZED, + message: "Invalid or expired token".to_string(), + })?; + Ok(next.run(request).await) +} + +pub async fn get_me( + user: CurrentUser, + State(state): State, +) -> Result, ApiError> { + let user = db::get_user_by_id(&state.pool, user.0) + .await? + .ok_or_else(|| ApiError { + status: StatusCode::NOT_FOUND, + message: "User not found".to_string(), + })?; + + Ok(Json(user.into())) +} + +pub async fn get_quota( + user: CurrentUser, + State(state): State, +) -> Result, ApiError> { + let (quota, plan) = db::get_user_quota(&state.pool, user.0) + .await? + .ok_or_else(|| ApiError { + status: StatusCode::NOT_FOUND, + message: "Quota not found".to_string(), + })?; + + Ok(Json(QuotaResponse { + tokens_used: quota.tokens_used, + tokens_limit: plan.token_limit, + requests_used: quota.requests_used, + requests_limit: plan.request_limit, + period_start: quota.period_start, + plan_name: plan.name, + })) +} + +// ==================== API Key Handlers ==================== + +#[derive(Deserialize)] +pub struct CreateKeyRequest { + name: Option, +} + +#[derive(Serialize)] +pub struct CreateKeyResponse { + key: String, + id: Uuid, + prefix: String, + name: Option, + created_at: chrono::DateTime, +} + +pub async fn list_keys( + user: CurrentUser, + State(state): State, +) -> Result>, ApiError> { + let keys = db::list_api_keys(&state.pool, user.0).await?; + Ok(Json(keys)) +} + +pub async fn create_key( + user: CurrentUser, + State(state): State, + Json(req): Json, +) -> Result, ApiError> { + let (key, api_key) = db::create_api_key(&state.pool, user.0, req.name.as_deref()).await?; + + db::log_audit(&state.pool, Some(user.0), "api_key_created", None, None, None).await?; + + Ok(Json(CreateKeyResponse { + key, + id: api_key.id, + prefix: api_key.prefix, + name: api_key.name, + created_at: api_key.created_at, + })) +} + +pub async fn delete_key( + user: CurrentUser, + State(state): State, + Path(key_id): Path, +) -> Result { + let deleted = db::delete_api_key(&state.pool, user.0, key_id).await?; + + if deleted { + db::log_audit(&state.pool, Some(user.0), "api_key_deleted", None, None, None).await?; + Ok(StatusCode::NO_CONTENT) + } else { + Err(ApiError { + status: StatusCode::NOT_FOUND, + message: "API key not found".to_string(), + }) + } +} + +// ==================== MCP Query Handlers ==================== + +pub async fn mcp_query( + user: CurrentUser, + State(state): State, + Json(req): Json, +) -> Result, ApiError> { + // Check quota + let (quota, plan) = db::get_user_quota(&state.pool, user.0) + .await? + .ok_or_else(|| ApiError { + status: StatusCode::FORBIDDEN, + message: "No quota found".to_string(), + })?; + + if quota.requests_used >= plan.request_limit { + return Err(ApiError { + status: StatusCode::TOO_MANY_REQUESTS, + message: "Quota exceeded. Please upgrade your plan.".to_string(), + }); + } + + // Execute query + let response = mcp::mcp_query(&state.pool, req).await?; + + // Record usage + db::record_usage( + &state.pool, + user.0, + "mcp_query", + response.total as i64, + Some(serde_json::json!({ "query_type": response.query_type })), + ).await?; + + Ok(Json(response)) +} + +pub async fn mcp_get_equation( + user: CurrentUser, + State(state): State, + Path(name): Path, +) -> Result, ApiError> { + let result = mcp::get_equation(&state.pool, &name) + .await? + .ok_or_else(|| ApiError { + status: StatusCode::NOT_FOUND, + message: format!("Equation '{}' not found", name), + })?; + + // Record usage + db::record_usage(&state.pool, user.0, "mcp_get_equation", 1, None).await?; + + Ok(Json(result)) +} + +pub async fn mcp_list_topics( + user: CurrentUser, + State(state): State, +) -> Result>, ApiError> { + let topics = mcp::list_topics(&state.pool).await?; + Ok(Json(topics)) +} diff --git a/services_and_experiments/uet_api/src/main.rs b/services_and_experiments/uet_api/src/main.rs new file mode 100644 index 000000000..1203c22d6 --- /dev/null +++ b/services_and_experiments/uet_api/src/main.rs @@ -0,0 +1,101 @@ +mod agent; +mod auth; +mod config; +mod db; +mod email; +mod handlers; +mod mcp; +mod models; +mod oauth; + +use axum::{ + routing::{get, post}, + Router, +}; +use sqlx::postgres::PgPoolOptions; +use std::net::SocketAddr; +use tower_http::cors::{Any, CorsLayer}; +use tracing::info; + +use crate::config::CONFIG; +use crate::handlers::AppState; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + dotenvy::dotenv().ok(); + + info!("Connecting to database: {}", CONFIG.database_url); + + let pool = PgPoolOptions::new() + .max_connections(10) + .connect(&CONFIG.database_url) + .await?; + + info!("Running migrations..."); + sqlx::migrate!("./migrations") + .run(&pool) + .await?; + + let oauth_clients = oauth::OAuthClients::new(); + let email_service = email::EmailService::new(); + + let state = AppState { + pool, + oauth: oauth_clients, + email: email_service, + }; + + // CORS for frontend + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); + + // Public routes (no auth required) + let public_routes = Router::new() + .route("/api/health", get(health_check)) + .route("/api/auth/register", post(handlers::register)) + .route("/api/auth/login", post(handlers::login)) + .route("/api/auth/refresh", post(handlers::refresh_token)) + .route("/api/auth/verify-email", post(handlers::verify_email)) + .route("/api/auth/request-reset", post(handlers::request_password_reset)) + .route("/api/auth/reset-password", post(handlers::reset_password)) + .route("/api/auth/oauth/google", get(handlers::google_auth)) + .route("/api/auth/oauth/google/callback", get(handlers::google_callback)) + .route("/api/auth/oauth/github", get(handlers::github_auth)) + .route("/api/auth/oauth/github/callback", get(handlers::github_callback)) + .route("/api/workchat", post(agent::workchat_handler)) + .route("/api/workchat/ingest", post(agent::ingest_handler)); + + // Protected routes (auth middleware applied) + let protected_routes = Router::new() + .route("/api/auth/logout", post(handlers::logout)) + .route("/api/auth/me", get(handlers::get_me)) + .route("/api/auth/quota", get(handlers::get_quota)) + .route("/api/auth/api-keys", get(handlers::list_keys).post(handlers::create_key)) + .route("/api/auth/api-keys/:id", axum::routing::delete(handlers::delete_key)) + .route("/api/mcp/query", post(handlers::mcp_query)) + .route("/api/mcp/equation/:name", get(handlers::mcp_get_equation)) + .route("/api/mcp/topics", get(handlers::mcp_list_topics)); + + let app = public_routes + .merge(protected_routes) + .layer(cors) + .with_state(state); + + let addr = SocketAddr::from(([0, 0, 0, 0], 3001)); + info!("UET API listening on {}", addr); + + let listener = tokio::net::TcpListener::bind(&addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} + +async fn health_check() -> &'static str { + "OK" +} diff --git a/services_and_experiments/uet_api/src/mcp.rs b/services_and_experiments/uet_api/src/mcp.rs new file mode 100644 index 000000000..b3c388817 --- /dev/null +++ b/services_and_experiments/uet_api/src/mcp.rs @@ -0,0 +1,205 @@ +use anyhow::Result; +use fastembed::{TextEmbedding, InitOptions, EmbeddingModel}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use std::sync::Arc; +use tokio::sync::OnceCell; + +// Global lazy-loaded embedding model to avoid reloading for every request +static EMBEDDING_MODEL: OnceCell>> = OnceCell::const_new(); + +async fn get_embedding_model() -> Result>> { + EMBEDDING_MODEL.get_or_try_init(|| async { + // Initialize the BAAI/bge-m3 model (same as Python ingest) + let model = TextEmbedding::try_new( + InitOptions::new(EmbeddingModel::BGEM3) + .with_show_download_progress(true) + )?; + Ok(Arc::new(tokio::sync::Mutex::new(model))) + }).await.cloned() +} + +#[derive(Debug, Deserialize)] +pub struct McpQueryRequest { + /// Text query (will do text search if no embedding provided) + pub query: String, + /// Pre-computed embedding vector (optional, for semantic search) + pub embedding: Option>, + /// Number of results to return + pub top_k: Option, +} + +#[derive(Debug, Serialize)] +pub struct McpQueryResponse { + pub results: Vec, + pub query_type: String, + pub total: usize, +} + +#[derive(Debug, Serialize, sqlx::FromRow)] +pub struct McpSearchResult { + pub chunk_id: String, + pub doc_id: String, + pub text: String, + pub path: String, + pub score: f64, + pub metadata: serde_json::Value, +} + +/// MCP Query endpoint - searches UET knowledge base +pub async fn mcp_query(pool: &PgPool, req: McpQueryRequest) -> Result { + let top_k = req.top_k.unwrap_or(5); + + // If embedding provided, do semantic search + if let Some(embedding) = &req.embedding { + return semantic_search(pool, embedding, top_k).await; + } + + // Try to generate embedding from text query + match generate_embedding(&req.query).await { + Ok(embedding) => { + // Semantic search with generated embedding + semantic_search(pool, &embedding, top_k).await + } + Err(e) => { + tracing::warn!("Failed to generate embedding: {}. Falling back to text search.", e); + // Fallback to text search if model fails + text_search(pool, &req.query, top_k).await + } + } +} + +/// Generate embedding using fastembed +async fn generate_embedding(text: &str) -> Result> { + let model = get_embedding_model().await?; + let mut model_lock = model.lock().await; + let embeddings = model_lock.embed(vec![text], None)?; + + // Convert f32 vector to f64 for our DB schema + let first_embedding = embeddings.into_iter().next() + .ok_or_else(|| anyhow::anyhow!("No embedding generated"))?; + + Ok(first_embedding.into_iter().map(|v| v as f64).collect()) +} + +/// Semantic search using pgvector +async fn semantic_search(pool: &PgPool, embedding: &[f64], top_k: i64) -> Result { + let vector_string = format!( + "[{}]", + embedding.iter().map(|f| f.to_string()).collect::>().join(",") + ); + + let rows = sqlx::query_as::<_, McpSearchResult>( + r#" + SELECT + c.id::text as chunk_id, + c.doc_id::text as doc_id, + c.text, + d.source_path as path, + (1 - (c.embedding <=> $1::vector))::float8 as score, + d.metadata + FROM document_chunks c + JOIN documents d ON c.doc_id = d.id + ORDER BY c.embedding <=> $1::vector + LIMIT $2 + "# + ) + .bind(&vector_string) + .bind(top_k) + .fetch_all(pool) + .await?; + + let total = rows.len(); + Ok(McpQueryResponse { + results: rows, + query_type: "semantic".to_string(), + total, + }) +} + +/// Full-text search using ILIKE +async fn text_search(pool: &PgPool, query: &str, top_k: i64) -> Result { + // Create search pattern with wildcards + let pattern = format!("%{}%", query.to_lowercase()); + + let rows = sqlx::query_as::<_, McpSearchResult>( + r#" + SELECT + c.id::text as chunk_id, + c.doc_id::text as doc_id, + c.text, + d.source_path as path, + (CASE WHEN c.text ILIKE $1 THEN 1.0 ELSE 0.5 END)::float8 as score, + d.metadata + FROM document_chunks c + JOIN documents d ON c.doc_id = d.id + WHERE c.text ILIKE $1 + ORDER BY score DESC + LIMIT $2 + "# + ) + .bind(&pattern) + .bind(top_k) + .fetch_all(pool) + .await?; + + let total = rows.len(); + Ok(McpQueryResponse { + results: rows, + query_type: "text".to_string(), + total, + }) +} + +/// Get equation by name/topic (structured query) +pub async fn get_equation(pool: &PgPool, name: &str) -> Result> { + let pattern = format!("%{}%", name.to_lowercase()); + + let row = sqlx::query_as::<_, McpSearchResult>( + r#" + SELECT + c.id::text as chunk_id, + c.doc_id::text as doc_id, + c.text, + d.path, + 1.0 as score, + d.metadata + FROM chunks c + JOIN documents d ON c.doc_id = d.id + WHERE c.text ILIKE $1 + OR d.metadata->>'title' ILIKE $1 + OR d.metadata->>'equation' ILIKE $1 + LIMIT 1 + "# + ) + .bind(&pattern) + .fetch_optional(pool) + .await?; + + Ok(row) +} + +/// List all available topics/documents +pub async fn list_topics(pool: &PgPool) -> Result> { + let rows = sqlx::query_as::<_, TopicInfo>( + r#" + SELECT DISTINCT + d.path, + d.metadata->>'title' as title, + d.metadata->>'type' as doc_type + FROM documents d + ORDER BY d.path + "# + ) + .fetch_all(pool) + .await?; + + Ok(rows) +} + +#[derive(Debug, Serialize, sqlx::FromRow)] +pub struct TopicInfo { + pub path: String, + pub title: Option, + pub doc_type: Option, +} diff --git a/services_and_experiments/uet_api/src/models.rs b/services_and_experiments/uet_api/src/models.rs new file mode 100644 index 000000000..fdc3f9b09 --- /dev/null +++ b/services_and_experiments/uet_api/src/models.rs @@ -0,0 +1,167 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct User { + pub id: Uuid, + pub email: String, + pub password_hash: Option, + pub display_name: Option, + pub avatar_url: Option, + pub is_admin: bool, + pub is_verified: bool, + pub verification_token: Option, + pub verification_token_expires_at: Option>, + pub password_reset_token: Option, + pub password_reset_expires_at: Option>, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct OAuthIdentity { + pub id: Uuid, + pub user_id: Uuid, + pub provider: String, + pub provider_id: String, + pub provider_email: Option, + pub provider_name: Option, + pub provider_avatar: Option, + pub created_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Session { + pub id: Uuid, + pub user_id: Uuid, + pub token_hash: String, + pub user_agent: Option, + pub ip_address: Option, + pub expires_at: DateTime, + pub created_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct RefreshToken { + pub id: Uuid, + pub user_id: Uuid, + pub token_hash: String, + pub session_id: Option, + pub expires_at: DateTime, + pub revoked: bool, + pub created_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct ApiKey { + pub id: Uuid, + pub user_id: Uuid, + pub key_hash: String, + pub name: Option, + pub prefix: String, + pub last_used_at: Option>, + pub expires_at: Option>, + pub created_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Plan { + pub id: Uuid, + pub name: String, + pub token_limit: i64, + pub request_limit: i32, + pub price_monthly_cents: i32, + pub created_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct UserQuota { + pub user_id: Uuid, + pub plan_id: Uuid, + pub tokens_used: i64, + pub requests_used: i32, + pub period_start: DateTime, + pub updated_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct UsageEvent { + pub id: Uuid, + pub user_id: Uuid, + pub event_type: String, + pub tokens_consumed: i64, + pub metadata: serde_json::Value, + pub created_at: DateTime, +} + +// Request/Response DTOs + +#[derive(Debug, Deserialize)] +pub struct RegisterRequest { + pub email: String, + pub password: String, + pub display_name: Option, +} + +#[derive(Debug, Deserialize)] +pub struct LoginRequest { + pub email: String, + pub password: String, +} + +#[derive(Debug, Deserialize)] +pub struct VerifyEmailRequest { + pub token: String, +} + +#[derive(Debug, Deserialize)] +pub struct RequestResetRequest { + pub email: String, +} + +#[derive(Debug, Deserialize)] +pub struct ResetPasswordRequest { + pub token: String, + pub new_password: String, +} + +#[derive(Debug, Serialize)] +pub struct AuthResponse { + pub access_token: String, + pub refresh_token: String, + pub expires_in: i64, + pub user: UserPublic, +} + +#[derive(Debug, Serialize)] +pub struct UserPublic { + pub id: Uuid, + pub email: String, + pub display_name: Option, + pub avatar_url: Option, + pub is_admin: bool, +} + +impl From for UserPublic { + fn from(user: User) -> Self { + Self { + id: user.id, + email: user.email, + display_name: user.display_name, + avatar_url: user.avatar_url, + is_admin: user.is_admin, + } + } +} + +#[derive(Debug, Serialize)] +pub struct QuotaResponse { + pub tokens_used: i64, + pub tokens_limit: i64, + pub requests_used: i32, + pub requests_limit: i32, + pub period_start: DateTime, + pub plan_name: String, +} diff --git a/services_and_experiments/uet_api/src/oauth.rs b/services_and_experiments/uet_api/src/oauth.rs new file mode 100644 index 000000000..a211f52f5 --- /dev/null +++ b/services_and_experiments/uet_api/src/oauth.rs @@ -0,0 +1,151 @@ +use anyhow::Result; +use oauth2::{AuthUrl, ClientId, ClientSecret, CsrfToken, RedirectUrl, Scope, TokenUrl, TokenResponse}; +use oauth2::basic::BasicClient; +use oauth2::reqwest::async_http_client; +use serde::{Deserialize, Serialize}; + +use crate::config::CONFIG; + +#[derive(Clone)] +pub struct OAuthClients { + pub google: Option, + pub github: Option, +} + +impl OAuthClients { + pub fn new() -> Self { + let google = if !CONFIG.google_client_id.is_empty() { + let auth_url = AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string()) + .expect("Invalid Google auth URL"); + let token_url = TokenUrl::new("https://oauth2.googleapis.com/token".to_string()) + .expect("Invalid Google token URL"); + let redirect_url = RedirectUrl::new(format!("{}/google", CONFIG.oauth_redirect_url)) + .expect("Invalid redirect URL"); + + Some( + BasicClient::new( + ClientId::new(CONFIG.google_client_id.clone()), + Some(ClientSecret::new(CONFIG.google_client_secret.clone())), + auth_url, + Some(token_url), + ) + .set_redirect_uri(redirect_url) + ) + } else { + None + }; + + let github = if !CONFIG.github_client_id.is_empty() { + let auth_url = AuthUrl::new("https://github.com/login/oauth/authorize".to_string()) + .expect("Invalid GitHub auth URL"); + let token_url = TokenUrl::new("https://github.com/login/oauth/access_token".to_string()) + .expect("Invalid GitHub token URL"); + let redirect_url = RedirectUrl::new(format!("{}/github", CONFIG.oauth_redirect_url)) + .expect("Invalid redirect URL"); + + Some( + BasicClient::new( + ClientId::new(CONFIG.github_client_id.clone()), + Some(ClientSecret::new(CONFIG.github_client_secret.clone())), + auth_url, + Some(token_url), + ) + .set_redirect_uri(redirect_url) + ) + } else { + None + }; + + Self { google, github } + } +} + +#[derive(Debug, Serialize)] +pub struct OAuthUrl { + pub url: String, + pub state: String, +} + +#[derive(Debug, Deserialize)] +pub struct GoogleUserInfo { + pub id: String, + pub email: String, + pub name: Option, + pub picture: Option, +} + +#[derive(Debug, Deserialize)] +pub struct GitHubUserInfo { + pub id: i64, + pub email: Option, + pub login: String, + pub avatar_url: Option, +} + +/// Generate Google OAuth authorization URL +pub fn get_google_auth_url(client: &BasicClient) -> OAuthUrl { + let (url, csrf) = client + .authorize_url(CsrfToken::new_random) + .add_scope(Scope::new("email".to_string())) + .add_scope(Scope::new("profile".to_string())) + .url(); + + OAuthUrl { + url: url.to_string(), + state: csrf.secret().clone(), + } +} + +/// Exchange Google auth code for user info +pub async fn exchange_google_code(client: &BasicClient, code: &str) -> Result { + let token = client + .exchange_code(oauth2::AuthorizationCode::new(code.to_string())) + .request_async(async_http_client) + .await?; + + let access_token = token.access_token().secret(); + + let user_info = reqwest::Client::new() + .get("https://www.googleapis.com/oauth2/v3/userinfo") + .bearer_auth(access_token) + .send() + .await? + .json::() + .await?; + + Ok(user_info) +} + +/// Generate GitHub OAuth authorization URL +pub fn get_github_auth_url(client: &BasicClient) -> OAuthUrl { + let (url, csrf) = client + .authorize_url(CsrfToken::new_random) + .add_scope(Scope::new("user:email".to_string())) + .url(); + + OAuthUrl { + url: url.to_string(), + state: csrf.secret().clone(), + } +} + +/// Exchange GitHub auth code for user info +pub async fn exchange_github_code(client: &BasicClient, code: &str) -> Result { + let token = client + .exchange_code(oauth2::AuthorizationCode::new(code.to_string())) + .request_async(async_http_client) + .await?; + + let access_token = token.access_token().secret(); + + let user_info = reqwest::Client::new() + .get("https://api.github.com/user") + .bearer_auth(access_token) + .header("User-Agent", "UET-Platform") + .send() + .await? + .json::() + .await?; + + Ok(user_info) +} diff --git a/services_and_experiments/uet_build/README.md b/services_and_experiments/uet_build/README.md new file mode 100644 index 000000000..b141170bb --- /dev/null +++ b/services_and_experiments/uet_build/README.md @@ -0,0 +1,24 @@ +# đŸ—ī¸ UET Build Artifacts (`uet_build`) + +![Status](https://img.shields.io/badge/Status-GENERATED-yellow) +![Safe_to_Delete](https://img.shields.io/badge/Action-Safe_To_Delete-green) + +> **"The Construction Site"** - āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šāš€ā¸āš‡ā¸šāš„ā¸Ÿā¸ĨāšŒā¸—ā¸ĩāšˆāš€ā¸ā¸´ā¸”ā¸ˆā¸˛ā¸ā¸ā¸˛ā¸Ŗ Build/Compile āš‚ā¸›ā¸Ŗāšā¸ā¸Ŗā¸Ą (Transient Files). + +--- + +## âš ī¸ Important Note + +- **ā¸Ĩā¸šāš„ā¸”āš‰āš„ā¸Ģā¸Ą?:** ✅ **ā¸Ĩā¸šāš„ā¸”āš‰ 100%** (Safe to Delete) +- **ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āšƒā¸Ģā¸Ąāšˆāš„ā¸”āš‰āš„ā¸Ģā¸Ą?:** ✅ ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āšƒā¸Ģā¸Ąāšˆāš„ā¸”āš‰ā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´āš€ā¸Ąā¸ˇāšˆā¸­ā¸Ēā¸ąāšˆā¸‡ Build ā¸„ā¸Ŗā¸ąāš‰ā¸‡ā¸Ģā¸™āš‰ā¸˛ +- **ā¸„ā¸§ā¸Ŗāšā¸āš‰āš„ā¸Ÿā¸ĨāšŒāšƒā¸™ā¸™ā¸ĩāš‰āš„ā¸Ģā¸Ą?:** ❌ **ā¸Ģāš‰ā¸˛ā¸Ąāšā¸āš‰āš„ā¸‚** (Do Not Edit) āš€ā¸žā¸Ŗā¸˛ā¸°ā¸Ąā¸ąā¸™ā¸ˆā¸°ā¸–ā¸šā¸āš€ā¸‚ā¸ĩā¸ĸā¸™ā¸—ā¸ąā¸šā¸•ā¸­ā¸™ Build āšƒā¸Ģā¸Ąāšˆ + +--- + +## 📂 Contents + +| File Type | Description | +| :--- | :--- | +| **`.exe` / Binary** | āš‚ā¸›ā¸Ŗāšā¸ā¸Ŗā¸Ąā¸—ā¸ĩāšˆ Compile āš€ā¸Ēā¸Ŗāš‡ā¸ˆāšā¸Ĩāš‰ā¸§. | +| **`.obj` / `.lib`** | āš„ā¸Ÿā¸ĨāšŒ Object ⏁ā¸Ĩ⏞⏇⏗⏞⏇⏪⏰ā¸Ģā¸§āšˆā¸˛ā¸‡ Compile. | +| **Logs** | ā¸šā¸ąā¸™ā¸—ā¸ļ⏁⏁⏞⏪ Build. | diff --git a/services_and_experiments/uet_chain/Cargo.toml b/services_and_experiments/uet_chain/Cargo.toml new file mode 100644 index 000000000..42ca6e784 --- /dev/null +++ b/services_and_experiments/uet_chain/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "uet_chain" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +chrono = { version = "0.4", features = ["serde"] } +thiserror = "1.0" +uet_security = { path = "../uet_security" } +hex = "0.4.3" +log = "0.4.29" +bincode = "1.3.3" +sled = "0.34.7" +libp2p = { version = "0.53.2", features = ["tokio", "tcp", "noise", "yamux", "gossipsub", "macros"] } +tokio = { version = "1.0", features = ["sync"] } +tracing = "0.1.44" + +[dev-dependencies] +tempfile = "3.27.0" diff --git a/services_and_experiments/uet_chain/src/canonical.rs b/services_and_experiments/uet_chain/src/canonical.rs new file mode 100644 index 000000000..2a5f79185 --- /dev/null +++ b/services_and_experiments/uet_chain/src/canonical.rs @@ -0,0 +1,44 @@ +use serde::Serialize; +use thiserror::Error; +use uet_security::{hashing::digest_hex, HashAlgorithm}; + +#[derive(Debug, Error)] +pub enum CanonicalError { + #[error("serialization error: {0}")] + Serialization(String), +} + +pub fn canonical_json(value: &T) -> Result { + serde_json::to_string(value).map_err(|e| CanonicalError::Serialization(e.to_string())) +} + +pub fn canonical_hash_hex( + alg: HashAlgorithm, + value: &T, +) -> Result { + let json = canonical_json(value)?; + Ok(digest_hex(alg, json.as_bytes())) +} + +pub fn merkle_root_hex(hashes_hex: &[String], alg: HashAlgorithm) -> String { + if hashes_hex.is_empty() { + return digest_hex(alg, b""); + } + + let mut layer = hashes_hex.to_vec(); + + while layer.len() > 1 { + let mut next = Vec::with_capacity((layer.len() + 1) / 2); + for pair in layer.chunks(2) { + let left = &pair[0]; + let right = pair.get(1).unwrap_or(left); + let mut cat = String::with_capacity(left.len() + right.len()); + cat.push_str(left); + cat.push_str(right); + next.push(digest_hex(alg, cat.as_bytes())); + } + layer = next; + } + + layer[0].clone() +} diff --git a/services_and_experiments/uet_chain/src/consensus/mod.rs b/services_and_experiments/uet_chain/src/consensus/mod.rs new file mode 100644 index 000000000..304846400 --- /dev/null +++ b/services_and_experiments/uet_chain/src/consensus/mod.rs @@ -0,0 +1,98 @@ +use std::sync::Arc; +use tokio::sync::RwLock; +use thiserror::Error; + +use crate::types::Block; +use crate::state::{StateMachine, StateError}; +use crate::storage::{ChainStorage, StorageError}; + +#[derive(Error, Debug)] +pub enum ConsensusError { + #[error("State error: {0}")] + StateError(#[from] StateError), + #[error("Storage error: {0}")] + StorageError(#[from] StorageError), + #[error("Invalid block: {0}")] + InvalidBlock(String), +} + +pub type Result = std::result::Result; + +/// Handles rules for accepting blocks and processing transactions +pub struct ConsensusEngine { + storage: Arc, + state: Arc>, +} + +impl ConsensusEngine { + pub fn new(storage: Arc, state: Arc>) -> Self { + Self { storage, state } + } + + /// Validates a new block before accepting it + pub async fn validate_block(&self, block: &Block) -> Result<()> { + let latest_height = self.storage.get_latest_height()?.unwrap_or(0); + + // 1. Check height sequence + if block.header.height != latest_height + 1 && latest_height != 0 { + return Err(ConsensusError::InvalidBlock(format!( + "Expected height {}, got {}", + latest_height + 1, + block.header.height + ))); + } + + // 2. Check previous hash + if latest_height > 0 { + if let Some(prev_block) = self.storage.get_block(latest_height)? { + let hash_alg = prev_block.header.suite.hash_alg.clone(); + let expected_prev_hash = crate::canonical_hash_hex(hash_alg, &prev_block.header) + .unwrap_or_default(); + + if block.header.previous_block_hash_hex != expected_prev_hash { + return Err(ConsensusError::InvalidBlock("Previous block hash mismatch".to_string())); + } + } + } + + // 3. Verify Merkle Roots + let hash_alg = block.header.suite.hash_alg.clone(); + + let computed_tx_root = crate::tx_hashes_hex(&block.transactions, hash_alg.clone()) + .map(|hashes| crate::merkle_root_hex(&hashes, hash_alg.clone())) + .unwrap_or_default(); + + if computed_tx_root != block.header.tx_merkle_root_hex { + return Err(ConsensusError::InvalidBlock("Transaction Merkle root mismatch".to_string())); + } + + let computed_proof_root = crate::proof_hashes_hex(&block.work_proofs, hash_alg.clone()) + .map(|hashes| crate::merkle_root_hex(&hashes, hash_alg.clone())) + .unwrap_or_default(); + + if computed_proof_root != block.header.proof_root_hex { + return Err(ConsensusError::InvalidBlock("Proof Merkle root mismatch".to_string())); + } + + // 4. Verify Signatures (In a real system, verify the proposer's Dilithium signature here) + // This relies on uet_security verification logic + + Ok(()) + } + + /// Accepts a valid block, applying it to state and saving it to storage + pub async fn process_block(&self, block: Block) -> Result<()> { + self.validate_block(&block).await?; + + // Lock state for writing + let state = self.state.write().await; + + // Apply to state machine (this will revert if invalid transaction is found) + state.apply_block(&block)?; + + // Save to persistent storage + self.storage.save_block(&block)?; + + Ok(()) + } +} \ No newline at end of file diff --git a/services_and_experiments/uet_chain/src/ledger.rs b/services_and_experiments/uet_chain/src/ledger.rs new file mode 100644 index 000000000..f9f070dd1 --- /dev/null +++ b/services_and_experiments/uet_chain/src/ledger.rs @@ -0,0 +1,87 @@ +use chrono::Utc; +use serde::Serialize; +use uet_security::{hashing::digest_hex, HashAlgorithm, Signer}; + +use crate::{ + canonical::{canonical_hash_hex, merkle_root_hex, CanonicalError}, + types::{Block, BlockHeader, Transaction, WorkProof}, +}; + +pub fn tx_hashes_hex( + txs: &[Transaction], + alg: HashAlgorithm, +) -> Result, CanonicalError> { + txs.iter().map(|tx| canonical_hash_hex(alg, tx)).collect() +} + +pub fn proof_hashes_hex( + proofs: &[WorkProof], + alg: HashAlgorithm, +) -> Result, CanonicalError> { + proofs + .iter() + .map(|proof| canonical_hash_hex(alg, proof)) + .collect() +} + +pub fn build_unsigned_header( + height: u64, + previous_block_hash_hex: impl Into, + proposer_node_id: impl Into, + txs: &[Transaction], + proofs: &[WorkProof], + alg: HashAlgorithm, +) -> Result { + let tx_hashes = tx_hashes_hex(txs, alg)?; + let proof_hashes = proof_hashes_hex(proofs, alg)?; + + let tx_merkle_root_hex = merkle_root_hex(&tx_hashes, alg); + let proof_root_hex = merkle_root_hex(&proof_hashes, alg); + + let state_root_hex = digest_hex( + alg, + format!("{}:{}", tx_merkle_root_hex, proof_root_hex).as_bytes(), + ); + + Ok(BlockHeader { + height, + previous_block_hash_hex: previous_block_hash_hex.into(), + tx_merkle_root_hex, + proof_root_hex, + state_root_hex, + proposer_node_id: proposer_node_id.into(), + timestamp: Utc::now(), + suite: uet_security::CryptoSuite::default(), + signature_hex: String::new(), + }) +} + +pub fn sign_header( + header: &mut BlockHeader, + signer: &dyn Signer, + alg: HashAlgorithm, + payload: &T, +) -> Result<(), CanonicalError> { + header.suite.key_id = signer.key_id().to_string(); + header.suite.sig_alg = signer.algorithm(); + header.suite.hash_alg = alg; + + let payload_hash = canonical_hash_hex(alg, payload)?; + let mut sign_bytes = payload_hash.as_bytes().to_vec(); + sign_bytes.extend_from_slice(header.state_root_hex.as_bytes()); + + let sig = signer + .sign(&sign_bytes) + .map_err(|e| CanonicalError::Serialization(e.to_string()))?; + + header.signature_hex = sig.iter().map(|b| format!("{b:02x}")).collect(); + Ok(()) +} + +pub fn assemble_block(header: BlockHeader, transactions: Vec, work_proofs: Vec) -> Block { + Block { + header, + transactions, + work_proofs, + } +} diff --git a/services_and_experiments/uet_chain/src/lib.rs b/services_and_experiments/uet_chain/src/lib.rs new file mode 100644 index 000000000..e225b031a --- /dev/null +++ b/services_and_experiments/uet_chain/src/lib.rs @@ -0,0 +1,77 @@ +pub mod canonical; +pub mod ledger; +pub mod types; +pub mod storage; +pub mod state; +pub mod p2p; +pub mod consensus; + +pub use canonical::{canonical_hash_hex, canonical_json, merkle_root_hex, CanonicalError}; +pub use ledger::{assemble_block, build_unsigned_header, proof_hashes_hex, sign_header, tx_hashes_hex}; +pub use types::{ + Block, BlockHeader, TaskFamily, Transaction, TransactionType, VerificationArtifact, WorkProof, + WorkTask, +}; + +#[cfg(test)] +mod tests { + use chrono::Utc; + use uet_security::{CryptoSuite, HashAlgorithm, MockSigner, SignatureAlgorithm}; + + use crate::{ + assemble_block, build_unsigned_header, + types::{TaskFamily, Transaction, TransactionType, VerificationArtifact, WorkProof}, + }; + + #[test] + fn build_block_with_pouw_proof() { + let tx = Transaction { + tx_id: "tx-1".to_string(), + tx_type: TransactionType::ComputeReward, + payload_json: "{\"reward\":1}".to_string(), + suite: CryptoSuite::default(), + signature_hex: "aa".to_string(), + created_at: Utc::now(), + }; + + let proof = WorkProof { + task_id: "task-1".to_string(), + node_id: "node-a".to_string(), + result_hash_hex: "deadbeef".to_string(), + verification_artifact: VerificationArtifact { + artifact_kind: "equilibrium_certificate".to_string(), + artifact_hash_hex: "cafe".to_string(), + verifier_hint: "deterministic-check-v1".to_string(), + }, + runtime_ms: 120, + nonce: 7, + suite: CryptoSuite { + schema_version: 1, + sig_alg: SignatureAlgorithm::Dilithium3, + hash_alg: HashAlgorithm::Sha3256, + key_id: "node-a#k1".to_string(), + }, + signature_hex: "bb".to_string(), + }; + + let mut header = build_unsigned_header( + 1, + "genesis", + "node-a", + &[tx.clone()], + &[proof.clone()], + HashAlgorithm::Sha3256, + ) + .expect("header build"); + + let signer = MockSigner::new("node-a#k1", SignatureAlgorithm::Dilithium3); + crate::sign_header(&mut header, &signer, HashAlgorithm::Sha3256, &TaskFamily::EquilibriumCertificate) + .expect("sign header"); + + let block = assemble_block(header, vec![tx], vec![proof]); + assert_eq!(block.header.height, 1); + assert!(!block.header.tx_merkle_root_hex.is_empty()); + assert!(!block.header.proof_root_hex.is_empty()); + assert!(!block.header.signature_hex.is_empty()); + } +} diff --git a/services_and_experiments/uet_chain/src/p2p/mod.rs b/services_and_experiments/uet_chain/src/p2p/mod.rs new file mode 100644 index 000000000..88c4f0136 --- /dev/null +++ b/services_and_experiments/uet_chain/src/p2p/mod.rs @@ -0,0 +1,102 @@ +use libp2p::{ + gossipsub, noise, swarm::NetworkBehaviour, tcp, yamux, identity, + PeerId, Swarm, SwarmBuilder, Multiaddr, +}; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::time::Duration; +use std::str::FromStr; +use thiserror::Error; +use tracing::info; + +#[derive(Error, Debug)] +pub enum P2pError { + #[error("Swarm build error")] + BuildError, + #[error("Network error: {0}")] + NetworkError(String), +} + +// Custom behaviour combining Gossipsub (for blocks/txs) +#[derive(NetworkBehaviour)] +pub struct UetBehaviour { + pub gossipsub: gossipsub::Behaviour, +} + +pub struct UetNode { + swarm: Swarm, +} + +impl UetNode { + pub fn new() -> Result { + // Generate a random PeerId + let id_keys = identity::Keypair::generate_ed25519(); + let peer_id = PeerId::from(id_keys.public()); + info!("Local peer id: {peer_id}"); + + // Setup Gossipsub config + let message_id_fn = |message: &gossipsub::Message| { + let mut s = DefaultHasher::new(); + message.data.hash(&mut s); + gossipsub::MessageId::from(s.finish().to_string()) + }; + + let gossipsub_config = gossipsub::ConfigBuilder::default() + .heartbeat_interval(Duration::from_secs(10)) + .validation_mode(gossipsub::ValidationMode::Strict) + .message_id_fn(message_id_fn) + .build() + .map_err(|e| P2pError::NetworkError(e.to_string()))?; + + let gossipsub = gossipsub::Behaviour::new( + gossipsub::MessageAuthenticity::Signed(id_keys.clone()), + gossipsub_config, + ).map_err(|e| P2pError::NetworkError(e.to_string()))?; + + let behaviour = UetBehaviour { gossipsub }; + + // Build the Swarm + let swarm = SwarmBuilder::with_existing_identity(id_keys) + .with_tokio() + .with_tcp( + tcp::Config::default(), + noise::Config::new, + yamux::Config::default, + ) + .map_err(|_| P2pError::BuildError)? + .with_behaviour(|_| behaviour) + .map_err(|_| P2pError::BuildError)? + .with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(60))) + .build(); + + Ok(Self { swarm }) + } + + pub fn listen(&mut self, addr: &str) -> Result<(), P2pError> { + let multiaddr = Multiaddr::from_str(addr) + .map_err(|e| P2pError::NetworkError(format!("Invalid address: {}", e)))?; + self.swarm.listen_on(multiaddr).map_err(|e| P2pError::NetworkError(e.to_string()))?; + Ok(()) + } + + pub fn dial(&mut self, addr: &str) -> Result<(), P2pError> { + let multiaddr = Multiaddr::from_str(addr) + .map_err(|e| P2pError::NetworkError(format!("Invalid address: {}", e)))?; + self.swarm.dial(multiaddr).map_err(|e| P2pError::NetworkError(e.to_string()))?; + Ok(()) + } + + pub fn subscribe(&mut self, topic_name: &str) -> Result<(), P2pError> { + let topic = gossipsub::IdentTopic::new(topic_name); + self.swarm.behaviour_mut().gossipsub.subscribe(&topic) + .map_err(|e| P2pError::NetworkError(e.to_string()))?; + Ok(()) + } + + pub fn publish(&mut self, topic_name: &str, data: Vec) -> Result<(), P2pError> { + let topic = gossipsub::IdentTopic::new(topic_name); + self.swarm.behaviour_mut().gossipsub.publish(topic, data) + .map_err(|e| P2pError::NetworkError(e.to_string()))?; + Ok(()) + } +} \ No newline at end of file diff --git a/services_and_experiments/uet_chain/src/state/mod.rs b/services_and_experiments/uet_chain/src/state/mod.rs new file mode 100644 index 000000000..472ecb864 --- /dev/null +++ b/services_and_experiments/uet_chain/src/state/mod.rs @@ -0,0 +1,171 @@ +use std::collections::HashMap; +use sled::Db; +use thiserror::Error; + +use crate::types::{Block, Transaction, TransactionType}; + +#[derive(Error, Debug)] +pub enum StateError { + #[error("Database error: {0}")] + DbError(#[from] sled::Error), + #[error("Serialization error: {0}")] + SerializationError(#[from] bincode::Error), + #[error("Insufficient funds for account {0}")] + InsufficientFunds(String), + #[error("Invalid transaction signature")] + InvalidSignature, +} + +pub type Result = std::result::Result; + +/// Represents the global state of user balances +#[derive(Clone)] +pub struct StateMachine { + db: Db, +} + +impl StateMachine { + pub fn new(db: Db) -> Self { + Self { db } + } + + /// Retrieve the balance for a specific address + pub fn get_balance(&self, address: &str) -> Result { + let key = format!("balance:{}", address); + match self.db.get(key.as_bytes())? { + Some(bytes) => { + let mut amount_bytes = [0u8; 8]; + amount_bytes.copy_from_slice(&bytes); + Ok(u64::from_be_bytes(amount_bytes)) + } + None => Ok(0), // Default balance is 0 + } + } + + /// Set the balance for a specific address + fn set_balance(&self, address: &str, amount: u64) -> Result<()> { + let key = format!("balance:{}", address); + self.db.insert(key.as_bytes(), amount.to_be_bytes().to_vec())?; + Ok(()) + } + + /// Apply a single transaction to the state + pub fn apply_transaction(&self, tx: &Transaction) -> Result<()> { + // Parse the payload (assuming a simple format for now) + // In reality, this would use a proper serialization format like JSON/bincode + let parsed_payload: HashMap = serde_json::from_str(&tx.payload_json) + .unwrap_or_default(); + + match tx.tx_type { + TransactionType::ComputeReward => { + // Miner earns reward + if let Some(to_address) = parsed_payload.get("miner_address") { + let reward: u64 = parsed_payload.get("reward").and_then(|v| v.parse().ok()).unwrap_or(0); + let current = self.get_balance(to_address)?; + self.set_balance(to_address, current + reward)?; + } + } + TransactionType::Transfer => { + if let (Some(from_address), Some(to_address), Some(amount_str)) = ( + parsed_payload.get("from"), + parsed_payload.get("to"), + parsed_payload.get("amount"), + ) { + let amount: u64 = amount_str.parse().unwrap_or(0); + let from_balance = self.get_balance(from_address)?; + + if from_balance < amount { + return Err(StateError::InsufficientFunds(from_address.clone())); + } + + // Deduct from sender + self.set_balance(from_address, from_balance - amount)?; + + // Add to receiver + let to_balance = self.get_balance(to_address)?; + self.set_balance(to_address, to_balance + amount)?; + } + } + TransactionType::Governance => { + // Not implemented yet + } + } + + Ok(()) + } + + /// Apply an entire block to the state + pub fn apply_block(&self, block: &Block) -> Result<()> { + // In a real system, you would create an atomic batch here + for tx in &block.transactions { + self.apply_transaction(tx)?; + } + + self.db.flush()?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Utc; + use uet_security::{CryptoSuite, HashAlgorithm, SignatureAlgorithm}; + + #[test] + fn test_state_machine_transfer() { + let db = sled::Config::new().temporary(true).open().unwrap(); + let state = StateMachine::new(db); + + // Give initial reward to Alice + let reward_tx = Transaction { + tx_id: "tx1".to_string(), + tx_type: TransactionType::ComputeReward, + payload_json: r#"{"miner_address":"alice","reward":"100"}"#.to_string(), + suite: CryptoSuite { + schema_version: 1, + sig_alg: SignatureAlgorithm::Dilithium3, + hash_alg: HashAlgorithm::Sha3256, + key_id: "sys".to_string(), + }, + signature_hex: "sig".to_string(), + created_at: Utc::now(), + }; + + state.apply_transaction(&reward_tx).unwrap(); + assert_eq!(state.get_balance("alice").unwrap(), 100); + assert_eq!(state.get_balance("bob").unwrap(), 0); + + // Transfer from Alice to Bob + let transfer_tx = Transaction { + tx_id: "tx2".to_string(), + tx_type: TransactionType::Transfer, + payload_json: r#"{"from":"alice","to":"bob","amount":"30"}"#.to_string(), + suite: CryptoSuite { + schema_version: 1, + sig_alg: SignatureAlgorithm::Dilithium3, + hash_alg: HashAlgorithm::Sha3256, + key_id: "alice#k1".to_string(), + }, + signature_hex: "sig".to_string(), + created_at: Utc::now(), + }; + + state.apply_transaction(&transfer_tx).unwrap(); + assert_eq!(state.get_balance("alice").unwrap(), 70); + assert_eq!(state.get_balance("bob").unwrap(), 30); + + // Insufficient funds test + let fail_tx = Transaction { + tx_id: "tx3".to_string(), + tx_type: TransactionType::Transfer, + payload_json: r#"{"from":"bob","to":"alice","amount":"50"}"#.to_string(), + suite: CryptoSuite::default(), + signature_hex: "sig".to_string(), + created_at: Utc::now(), + }; + + let res = state.apply_transaction(&fail_tx); + assert!(matches!(res, Err(StateError::InsufficientFunds(_)))); + } +} \ No newline at end of file diff --git a/services_and_experiments/uet_chain/src/storage/mod.rs b/services_and_experiments/uet_chain/src/storage/mod.rs new file mode 100644 index 000000000..804460ff5 --- /dev/null +++ b/services_and_experiments/uet_chain/src/storage/mod.rs @@ -0,0 +1,140 @@ +use std::path::Path; +use sled::Db; +use thiserror::Error; + +use crate::types::Block; + +#[derive(Error, Debug)] +pub enum StorageError { + #[error("Database error: {0}")] + DbError(#[from] sled::Error), + #[error("Serialization error: {0}")] + SerializationError(#[from] bincode::Error), + #[error("Block not found at height {0}")] + BlockNotFound(u64), + #[error("Invalid data format")] + InvalidData, +} + +pub type Result = std::result::Result; + +/// Handles persistent storage of blocks and chain metadata +#[derive(Clone)] +pub struct ChainStorage { + db: Db, +} + +impl ChainStorage { + /// Open or create the database at the given path + pub fn open>(path: P) -> Result { + let db = sled::open(path)?; + Ok(Self { db }) + } + + /// Save a block to storage + pub fn save_block(&self, block: &Block) -> Result<()> { + let height_bytes = block.header.height.to_be_bytes(); + let block_bytes = bincode::serialize(block)?; + + // Key is prefix "b:" + height + let mut key = b"b:".to_vec(); + key.extend_from_slice(&height_bytes); + + self.db.insert(key, block_bytes)?; + + // Also update the latest height + self.save_latest_height(block.header.height)?; + + // Ensure written to disk + self.db.flush()?; + + Ok(()) + } + + /// Retrieve a block by its height + pub fn get_block(&self, height: u64) -> Result> { + let height_bytes = height.to_be_bytes(); + let mut key = b"b:".to_vec(); + key.extend_from_slice(&height_bytes); + + match self.db.get(&key)? { + Some(bytes) => { + let block: Block = bincode::deserialize(&bytes)?; + Ok(Some(block)) + } + None => Ok(None), + } + } + + /// Save the latest block height + fn save_latest_height(&self, height: u64) -> Result<()> { + let height_bytes = height.to_be_bytes(); + self.db.insert(b"meta:latest_height", height_bytes.to_vec())?; + Ok(()) + } + + /// Get the latest block height + pub fn get_latest_height(&self) -> Result> { + match self.db.get(b"meta:latest_height")? { + Some(bytes) => { + if bytes.len() != 8 { + return Err(StorageError::InvalidData); + } + let mut height_bytes = [0u8; 8]; + height_bytes.copy_from_slice(&bytes); + Ok(Some(u64::from_be_bytes(height_bytes))) + } + None => Ok(None), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{BlockHeader, TaskFamily}; + use chrono::Utc; + use tempfile::tempdir; + use uet_security::{CryptoSuite, HashAlgorithm, SignatureAlgorithm}; + + #[test] + fn test_save_and_get_block() { + let dir = tempdir().unwrap(); + let storage = ChainStorage::open(dir.path()).unwrap(); + + let header = BlockHeader { + height: 42, + previous_block_hash_hex: "0000000".to_string(), + proposer_node_id: "node-1".to_string(), + tx_merkle_root_hex: "tx-root".to_string(), + proof_root_hex: "proof-root".to_string(), + state_root_hex: "state-root".to_string(), + timestamp: Utc::now(), + suite: CryptoSuite { + schema_version: 1, + sig_alg: SignatureAlgorithm::Dilithium3, + hash_alg: HashAlgorithm::Sha3256, + key_id: "node-1#k1".to_string(), + }, + signature_hex: "signature".to_string(), + }; + + let block = Block { + header, + transactions: vec![], + work_proofs: vec![], + }; + + // Save block + storage.save_block(&block).unwrap(); + + // Get block back + let retrieved = storage.get_block(42).unwrap().unwrap(); + assert_eq!(retrieved.header.height, 42); + assert_eq!(retrieved.header.proposer_node_id, "node-1"); + + // Check latest height + let latest = storage.get_latest_height().unwrap().unwrap(); + assert_eq!(latest, 42); + } +} diff --git a/services_and_experiments/uet_chain/src/types.rs b/services_and_experiments/uet_chain/src/types.rs new file mode 100644 index 000000000..f9bf9461d --- /dev/null +++ b/services_and_experiments/uet_chain/src/types.rs @@ -0,0 +1,77 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uet_security::CryptoSuite; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TaskFamily { + DeterministicSimulation, + OptimizationBounded, + EquilibriumCertificate, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WorkTask { + pub task_id: String, + pub family: TaskFamily, + pub input_seed: String, + pub difficulty: u32, + pub created_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VerificationArtifact { + pub artifact_kind: String, + pub artifact_hash_hex: String, + pub verifier_hint: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WorkProof { + pub task_id: String, + pub node_id: String, + pub result_hash_hex: String, + pub verification_artifact: VerificationArtifact, + pub runtime_ms: u64, + pub nonce: u64, + pub suite: CryptoSuite, + pub signature_hex: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum TransactionType { + ComputeReward, + Transfer, + Governance, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Transaction { + pub tx_id: String, + pub tx_type: TransactionType, + pub payload_json: String, + pub suite: CryptoSuite, + pub signature_hex: String, + pub created_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BlockHeader { + pub height: u64, + pub previous_block_hash_hex: String, + pub tx_merkle_root_hex: String, + pub proof_root_hex: String, + pub state_root_hex: String, + pub proposer_node_id: String, + pub timestamp: DateTime, + pub suite: CryptoSuite, + pub signature_hex: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Block { + pub header: BlockHeader, + pub transactions: Vec, + pub work_proofs: Vec, +} diff --git a/services_and_experiments/uet_core/Cargo.toml b/services_and_experiments/uet_core/Cargo.toml new file mode 100644 index 000000000..0c3429dad --- /dev/null +++ b/services_and_experiments/uet_core/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "uet_core" +version = "0.1.0" +edition = "2021" + +[dependencies] +ndarray = "0.15" +serde = { version = "1.0", features = ["derive"] } +thiserror = "1.0" +schemars = "0.8" # For JSON Schema generation (MCP compatible) diff --git a/services_and_experiments/uet_core/README.md b/services_and_experiments/uet_core/README.md new file mode 100644 index 000000000..7499a966e --- /dev/null +++ b/services_and_experiments/uet_core/README.md @@ -0,0 +1,49 @@ +# âš›ī¸ UET Core Engine (`uet_core`) + +![Status](https://img.shields.io/badge/Status-ACTIVE-brightgreen) +![Language](https://img.shields.io/badge/Language-Rust-orange) +![Performance](https://img.shields.io/badge/Performance-High_Precision-blue) + +> **"The Physics Engine of UET"** - ⏄⏺⏙⏧⏓ā¸Ēā¸Ąā¸ā¸˛ā¸Ŗāšā¸Ąāšˆā¸šā¸— (Master Equation) ā¸”āš‰ā¸§ā¸ĸā¸„ā¸§ā¸˛ā¸Ąāš€ā¸Ŗāš‡ā¸§āšā¸Ĩ⏰⏕⏪⏪⏁⏰⏗ā¸ĩāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡āšā¸Ąāšˆā¸™ā¸ĸ⏺⏗ā¸ĩāšˆā¸Ē⏏⏔ (High-Performance Computing). + +--- + +## đŸ›ī¸ Architecture Pillars + +| Component | Description | +| :--- | :--- | +| **Dynamics** | ā¸„ā¸ŗā¸™ā¸§ā¸“ā¸„āšˆā¸˛ $\Omega$ (Omega), $\kappa$ (Kappa), $\beta$ (Beta) ā¸•ā¸˛ā¸Ąā¸—ā¸¤ā¸Šā¸Žā¸ĩ. | +| **Fields** | ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗāš‚ā¸„ā¸Ŗā¸‡ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ Tensor Field (C, I) ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗā¸„ā¸ŗā¸™ā¸§ā¸“. | +| **Safety** | āšƒā¸Šāš‰ā¸Ŗā¸°ā¸šā¸š Type System ⏂⏭⏇ Rust ā¸›āš‰ā¸­ā¸‡ā¸ā¸ąā¸™ Logical Error 100%. | + +--- + +## 🔗 Theory Connection + +```mermaid +graph LR + Input[("Raw Input (C, I)")] --> Engine{"đŸĻ€ uet_core"} + Engine -->|Compute| Omega["Ί (Optimization)"] + Engine -->|Compute| Kappa["Îē (Boundary)"] + Engine -->|Compute| Beta["β (Coupling)"] + + style Engine fill:#ffab91,stroke:#e64a19 +``` + +--- + +## 🚀 Key Functions + +- **`compute_omega(c, i)`**: ā¸Ģā¸ąā¸§āšƒā¸ˆā¸Ģā¸Ĩā¸ąā¸ā¸‚ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ģā¸˛ā¸„āšˆā¸˛ā¸„ā¸§ā¸˛ā¸Ąā¸Ēā¸Ąā¸”ā¸¸ā¸Ĩ (Balance). +- **`Field::new(data)`**: ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸Ēā¸™ā¸˛ā¸Ąā¸žā¸Ĩā¸ąā¸‡ā¸‡ā¸˛ā¸™ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗā¸—ā¸”ā¸Ē⏭⏚. + +--- + +## đŸ› ī¸ Usage + +āšƒā¸Šāš‰āš€ā¸›āš‡ā¸™ Library ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸š `uet_kb` ā¸Ģ⏪⏎⏭ Python Binding: + +```rust +use uet_core::dynamics; +let omega = dynamics::compute_omega(&config, &field_c, &field_i); +``` diff --git a/services_and_experiments/uet_core/src/calculator.rs b/services_and_experiments/uet_core/src/calculator.rs new file mode 100644 index 000000000..f308bf887 --- /dev/null +++ b/services_and_experiments/uet_core/src/calculator.rs @@ -0,0 +1,94 @@ +use crate::parameters::{UETParameters, K_B, ParameterDeriver}; +use serde::{Deserialize, Serialize}; + +/// Core engine for the Unity Equilibrium Theory. +/// Derives physical parameters (Kappa, Beta) from first principles: +/// 1. Beta = k_B * T * ln(2) (Landauer Principle) +/// 2. Kappa = Beta / InfoDensity +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UnityCalculator { + pub scale: f64, // Characteristic length scale (m) + pub temperature: f64, // System temperature (K) + pub info_density: f64, // Information density (bits/m^3) +} + +impl ParameterDeriver for UnityCalculator { + fn derive(&self) -> UETParameters { + self.derive_parameters() + } +} + +impl UnityCalculator { + pub fn new(scale: f64, temperature: f64, info_density: f64) -> Self { + Self { + scale, + temperature, + info_density, + } + } + + /// Derives UETParameters from the current state. + /// Implements the universal scaling laws and Landauer coupling. + pub fn derive_parameters(&self) -> UETParameters { + let ln2 = 2.0f64.ln(); + + // 1. Beta derivation (Energy per bit) + let beta = K_B * self.temperature * ln2; + + // 2. Kappa derivation (Information Inertia) + // If info_density is 0, we fallback to a safe small value to avoid NaN + let safe_density = if self.info_density > 0.0 { self.info_density } else { 1e-10 }; + let mut kappa = beta / safe_density; + + // Apply Planck Boundary (The Universe's Floor) + // From 0.23_Unity_Scale_Link: The minimum valid information tension in the universe is 0.5 + if kappa < 0.5 { + kappa = 0.5; + } + + // 3. Construct Parameters with Context + let mut params = UETParameters::default(); + params.kappa = kappa; + params.beta = beta; + params.temperature = self.temperature; + params.scale = format!("{:.2e}m", self.scale); + params.origin = "Unity First-Principles (Landauer)".to_string(); + + params + } + + /// Calculate the Unity-Scaling factor for a transition between two scales. + pub fn get_scaling_ratio(scale_from: f64, scale_to: f64) -> f64 { + // Based on T ∝ scale^(-2/3) and Rho ∝ scale^(-3) + let temp_ratio = (scale_to / scale_from).powf(-2.0 / 3.0); + let density_ratio = (scale_from / scale_to).powf(3.0); + temp_ratio * density_ratio + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_landauer_at_room_temp() { + // At 300K, Beta should be ~0.0179 eV + let calc = UnityCalculator::new(0.001, 300.0, 1e18); + let params = calc.derive_parameters(); + + let beta_ev = params.beta / 1.602176634e-19; + assert!((beta_ev - 0.0179).abs() < 0.001); + } + + #[test] + fn test_planck_boundary() { + // Scenario with extreme information density near a black hole singularity + // Density is massive, which would naturally drive kappa towards 0. + let extrem_density = 1e40; + let calc = UnityCalculator::new(1e-35, 300.0, extrem_density); + let params = calc.derive_parameters(); + + // The Planck boundary safety must arrest the fall precisely at 0.5 + assert_eq!(params.kappa, 0.5, "Planck boundary minimum of 0.5 not enforced!"); + } +} diff --git a/services_and_experiments/uet_core/src/dynamics.rs b/services_and_experiments/uet_core/src/dynamics.rs new file mode 100644 index 000000000..79ff8c703 --- /dev/null +++ b/services_and_experiments/uet_core/src/dynamics.rs @@ -0,0 +1,76 @@ +use crate::fields::Field; +use crate::parameters::{UETParameters, ParameterDeriver}; +use crate::calculator::UnityCalculator; +use ndarray::ArrayD; + +// ============================================================================= +// AXIOM 1: ENERGY CONSERVATION - POTENTIAL V(C) +// ============================================================================= + +pub fn potential_v(c: &Field, params: &UETParameters) -> Field { + // V(C) = (Îą/2)(C-C0)² + (Îŗ/4)(C-C0)⁴ + let diff = c.clone() - Field::new(ArrayD::from_elem(c.0.raw_dim(), params.c0)); + + // (Îą/2) * diff^2 + let term1 = diff.0.mapv(|x| params.alpha / 2.0 * x.powi(2)); + // (Îŗ/4) * diff^4 + let term2 = diff.0.mapv(|x| params.gamma / 4.0 * x.powi(4)); + + Field::new(term1 + term2) +} + +// ============================================================================= +// AXIOM 3: GRADIENT TERM +// ============================================================================= + +pub fn gradient_term(c: &Field, dx: f64, params: &UETParameters) -> f64 { + // (Îē/2)|∇C|² + let grad_sq = c.gradient_magnitude_squared(dx); + let integral = grad_sq.integral(dx); // âˆĢ |∇C|² dx + + (params.kappa / 2.0) * integral +} + +// ============================================================================= +// MASTER EQUATION: OMEGA FUNCTIONAL +// ============================================================================= + +pub fn compute_omega( + c: &Field, + i: Option<&Field>, + dx: f64, + params: &UETParameters, +) -> f64 { + // 1. Potential Energy V(C) + let v_field = potential_v(c, params); + let potential_integral = v_field.integral(dx); + + // 2. Gradient Term (A3) + let grad_integral = gradient_term(c, dx, params); + + // 3. Information Coupling (A2) + let info_integral = if let Some(info_field) = i { + // β âˆĢ C¡I dx + // Element-wise product + let product = c.0.clone() * info_field.0.clone(); // Array * Array + let integral = Field::new(product).integral(dx); + params.beta * integral + } else { + 0.0 + }; + + // Sum them up (Basic version with A1, A2, A3) + // Add more axioms as needed (A4, A5, etc.) + potential_integral + grad_integral + info_integral +} + +/// Dynamic Master Equation calculation using Unity Scaling. +pub fn compute_omega_unity( + c: &Field, + i: Option<&Field>, + dx: f64, + calculator: &UnityCalculator, +) -> f64 { + let params = calculator.derive(); + compute_omega(c, i, dx, ¶ms) +} diff --git a/services_and_experiments/uet_core/src/fields.rs b/services_and_experiments/uet_core/src/fields.rs new file mode 100644 index 000000000..c1eadc9cd --- /dev/null +++ b/services_and_experiments/uet_core/src/fields.rs @@ -0,0 +1,173 @@ +use ndarray::{ArrayD, Axis, IxDyn}; +use std::ops::{Add, Mul, Sub}; + +#[derive(Debug, Clone)] +pub struct Field(pub ArrayD); + +impl Field { + /// Create a new field from an array + pub fn new(data: ArrayD) -> Self { + Self(data) + } + + /// Compute the integral of the field + pub fn integral(&self, dx: f64) -> f64 { + let sum: f64 = self.0.sum(); + let ndim = self.0.ndim() as i32; + // dx^ndim + sum * dx.powi(ndim) + } + + /// Compute Gradient (Finite Difference) + /// Returns vector of fields [dF/dx, dF/dy, ...] + pub fn gradient(&self, dx: f64) -> Vec { + let ndim = self.0.ndim(); + let mut grads = Vec::with_capacity(ndim); + + for axis in 0..ndim { + let grad_data = compute_gradient_axis(&self.0, axis, dx); + grads.push(Field(grad_data)); + } + grads + } + + /// Compute Laplacian (Finite Difference) + /// ∇²F = d²F/dx² + d²F/dy² + ... + pub fn laplacian(&self, dx: f64) -> Field { + let ndim = self.0.ndim(); + let mut laplacian = ArrayD::zeros(self.0.raw_dim()); + + for axis in 0..ndim { + let d2 = compute_second_derivative(&self.0, axis, dx); + laplacian = laplacian + d2; + } + Field(laplacian) + } + + /// Compute squared magnitude of gradient |∇C|² + pub fn gradient_magnitude_squared(&self, dx: f64) -> Field { + let grads = self.gradient(dx); + let mut sum_sq = ArrayD::zeros(self.0.raw_dim()); + + for g in grads { + sum_sq = sum_sq + (g.0.mapv(|x| x.powi(2))); + } + Field(sum_sq) + } +} + +// Helper: Finite difference gradient along axis +fn compute_gradient_axis(data: &ArrayD, axis: usize, dx: f64) -> ArrayD { + let mut grad = ArrayD::zeros(data.raw_dim()); + let shape = data.shape(); + let axis_len = shape[axis]; + + if axis_len < 2 { + return grad; + } + + // Interior points: (f(x+h) - f(x-h)) / 2h + // Boundary points: Forward/Backward difference + + // Note: This is an unoptimized implementation. + // For production, we should slice operations. + // Given the deadline, we iterate. + + // Create Zip of indices? slice? + // ndarray slicing is tricky for dynamic dims. + // Simplified Loop: + for (idx, val) in data.indexed_iter() { + // Need to construct "prev" and "next" indices + // ndarray::Index is usually [usize, usize...] + + // Skip for now, implement 1D/2D specific optimizations later if needed. + // Or strictly use slicing. + } + + // Better Approach: Use slicing with ndarray + // grad[1..-1] = (data[2..] - data[0..-2]) / (2*dx) + + let ax = Axis(axis); + let n = data.len_of(ax); + + if n < 2 { return grad; } + + // Central difference + let s_next = data.slice_axis(ax, ndarray::Slice::from(2..n)); + let s_prev = data.slice_axis(ax, ndarray::Slice::from(0..n-2)); + let mut s_mid = grad.slice_axis_mut(ax, ndarray::Slice::from(1..n-1)); + + // Assign: (next - prev) / 2dx + // ndarray supports arithmetic on views + // We need to match shapes. + // s_next and s_prev have shape (n-2) on axis. s_mid has shape (n-2). + + // Use `azip!` or `assign` + // s_mid.assign(&((&s_next - &s_prev) / (2.0 * dx))); -> This consumes views? No. + let diff = &s_next - &s_prev; + s_mid.assign(&(diff / (2.0 * dx))); + + // Boundaries + // forward at 0: (f(1) - f(0)) / dx + { + let mut g0 = grad.slice_axis_mut(ax, ndarray::Slice::from(0..1)); + let d1 = data.slice_axis(ax, ndarray::Slice::from(1..2)); + let d0 = data.slice_axis(ax, ndarray::Slice::from(0..1)); + g0.assign(&((&d1 - &d0) / dx)); + } + + // backward at -1: (f(n-1) - f(n-2)) / dx + { + let mut g_end = grad.slice_axis_mut(ax, ndarray::Slice::from(n-1..n)); + let d_last = data.slice_axis(ax, ndarray::Slice::from(n-1..n)); + let d_prev = data.slice_axis(ax, ndarray::Slice::from(n-2..n-1)); + g_end.assign(&((&d_last - &d_prev) / dx)); + } + + grad +} + +fn compute_second_derivative(data: &ArrayD, axis: usize, dx: f64) -> ArrayD { + let mut d2 = ArrayD::zeros(data.raw_dim()); + let ax = Axis(axis); + let n = data.len_of(ax); + + if n < 3 { return d2; } + + // Central: (f(x+h) - 2f(x) + f(x-h)) / dx^2 + let s_next = data.slice_axis(ax, ndarray::Slice::from(2..n)); + let s_curr = data.slice_axis(ax, ndarray::Slice::from(1..n-1)); + let s_prev = data.slice_axis(ax, ndarray::Slice::from(0..n-2)); + let mut s_mid = d2.slice_axis_mut(ax, ndarray::Slice::from(1..n-1)); + + let term = &s_next - &(2.0 * &s_curr) + &s_prev; + s_mid.assign(&(term / dx.powi(2))); + + // Boundaries: Copy nearest neighbor (Neumann) or zero? + // Python code uses: laplacian[0] = laplacian[1] + { + let l1 = d2.slice_axis(ax, ndarray::Slice::from(1..2)).to_owned(); + let mut l0 = d2.slice_axis_mut(ax, ndarray::Slice::from(0..1)); + l0.assign(&l1); + + let l_n2 = d2.slice_axis(ax, ndarray::Slice::from(n-2..n-1)).to_owned(); + let mut l_end = d2.slice_axis_mut(ax, ndarray::Slice::from(n-1..n)); + l_end.assign(&l_n2); + } + + d2 +} + +// arithmetic ops for Field +impl Add for Field { + type Output = Self; + fn add(self, other: Self) -> Self { Self(self.0 + other.0) } +} +impl Sub for Field { + type Output = Self; + fn sub(self, other: Self) -> Self { Self(self.0 - other.0) } +} +impl Mul for Field { + type Output = Self; + fn mul(self, scalar: f64) -> Self { Self(self.0 * scalar) } +} diff --git a/services_and_experiments/uet_core/src/lib.rs b/services_and_experiments/uet_core/src/lib.rs new file mode 100644 index 000000000..c1f5ae39d --- /dev/null +++ b/services_and_experiments/uet_core/src/lib.rs @@ -0,0 +1,9 @@ +pub mod parameters; +pub mod fields; +pub mod dynamics; +pub mod calculator; +pub mod master_equation; + +pub use parameters::UETParameters; +pub use fields::Field; +pub use master_equation::UETMasterEquation; diff --git a/services_and_experiments/uet_core/src/master_equation.rs b/services_and_experiments/uet_core/src/master_equation.rs new file mode 100644 index 000000000..aeb7c5b20 --- /dev/null +++ b/services_and_experiments/uet_core/src/master_equation.rs @@ -0,0 +1,221 @@ +use crate::parameters::UETParameters; +use ndarray::{Array1, Array2, ArrayView1}; +use serde::{Deserialize, Serialize}; + +/// UET Master Equation - Complete 7-term functional +/// Ί[C,I,J] = âˆĢ dÂŗx [ +/// V(C) # A1: Energy Conservation +/// + (Îē/2)|∇C|² # A3: Space-Memory Gradient +/// + β C¡I # A2: Information-Energy Coupling +/// + Îŗ_J (J_in - J_out)¡C # A4: Semi-open Exchange (In-Ex) +/// + W_N |∇Ω_local| # A5: Natural Will +/// + β_U(ÎŖ,R) ¡ V_game(C) # A8: Dynamic Game +/// + Îģ ÎŖ_layers(C_i-C_j)² # A10: Multi-layer Coherence +/// ] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UETMasterEquation { + pub params: UETParameters, +} + +impl UETMasterEquation { + pub fn new(params: UETParameters) -> Self { + Self { params } + } + + /// A1: Potential V(C) = (Îą/2)(C-C0)² + (Îŗ/4)(C-C0)⁴ + pub fn potential_V(&self, C: &Array1) -> Array1 { + let diff = C - self.params.c0; + let alpha_term = (self.params.alpha / 2.0) * diff.mapv(|x| x * x); + let gamma_term = (self.params.gamma / 4.0) * diff.mapv(|x| x * x * x * x); + alpha_term + gamma_term + } + + /// A3: Gradient term (Îē/2)|∇C|² + pub fn gradient_term(&self, C: &Array1, dx: f64) -> f64 { + if C.len() < 2 { + return 0.0; + } + let grad = self.gradient(C, dx); + (self.params.kappa / 2.0) * grad.mapv(|x| x * x).sum() + } + + /// A2: Information coupling βCI + pub fn information_coupling(&self, C: &Array1, I: &Array1, dx: f64) -> f64 { + self.params.beta * (C * I).sum() * dx + } + + /// A4: Semi-open exchange Îŗ_J (J_in - J_out)¡C + pub fn semi_open_exchange( + &self, + C: &Array1, + J_in: &Array1, + J_out: &Array1, + dx: f64, + ) -> f64 { + let net_flux = J_in - J_out; + self.params.gamma_j * (net_flux * C).sum() * dx + } + + /// A5: Natural Will W_N |∇Ω_local| + pub fn natural_will(&self, C: &Array1, dx: f64) -> f64 { + if C.len() < 2 { + return 0.0; + } + let grad = self.gradient(C, dx); + self.params.w_n * grad.mapv(|x| x.abs()).sum() * dx + } + + /// Calculate gradient of 1D array + fn gradient(&self, C: &Array1, dx: f64) -> Array1 { + if C.len() < 2 { + return Array1::zeros(C.len()); + } + + let mut grad = Array1::zeros(C.len()); + for i in 1..C.len() - 1 { + grad[i] = (C[i + 1] - C[i - 1]) / (2.0 * dx); + } + grad[0] = (C[1] - C[0]) / dx; + grad[C.len() - 1] = (C[C.len() - 1] - C[C.len() - 2]) / dx; + grad + } + + /// A8: Dynamic Game potential V_game = β_U × C² + pub fn game_theory_potential(&self, C: &Array1, density: f64, scale: f64) -> Array1 { + let beta_U = self.strategic_boost(density, scale); + beta_U * C.mapv(|x| x * x) + } + + /// Strategic boost β_U for energy-competitive systems + pub fn strategic_boost(&self, density: f64, scale: f64) -> f64 { + const SIGMA_CRIT: f64 = 1.37e9; // M_sun/kpc² + let density_ratio = density / SIGMA_CRIT; + + let beta_base = 1.5 * density_ratio; + + let payoff_gradient = if density_ratio > 1.0 { + 2.0 * (1.0 + density_ratio).log10() + } else if density_ratio < 0.1 && density_ratio > 0.0 { + 1.5 * (0.1 / (density_ratio + 1e-9)).powf(0.25) + } else { + 0.0 + }; + + let mut beta_U = beta_base + payoff_gradient; + + if scale < 2.0 && scale > 0.0 { + beta_U *= (2.0 / scale).powf(0.3); + } + + beta_U.clamp(1.5, 15.0) + } + + /// A10: Multi-layer coherence Îģ ÎŖ_ij (C_i - C_j)² + pub fn layer_coherence(&self, C_layers: &[Array1], dx: f64) -> f64 { + if C_layers.len() < 2 { + return 0.0; + } + + let mut coherence = 0.0; + for i in 0..C_layers.len() { + for j in (i + 1)..C_layers.len() { + let diff = &C_layers[i] - &C_layers[j]; + coherence += diff.mapv(|x| x * x).sum(); + } + } + + self.params.lambda_coherence * coherence * dx + } + + /// Complete Omega functional Ί[C,I,J] + pub fn omega_functional( + &self, + C: &Array1, + I: Option<&Array1>, + J_in: Option<&Array1>, + J_out: Option<&Array1>, + C_layers: Option<&[Array1]>, + density: f64, + scale: f64, + dx: f64, + ) -> f64 { + // A1: Potential + let V = self.potential_V(C); + let potential_integral = V.sum() * dx; + + // A3: Gradient + let gradient_integral = self.gradient_term(C, dx); + + // A2: Information coupling + let info_integral = if let Some(I_arr) = I { + self.information_coupling(C, I_arr, dx) + } else { + 0.0 + }; + + // A4: Semi-open exchange + let exchange_integral = if let (Some(J_in_arr), Some(J_out_arr)) = (J_in, J_out) { + self.semi_open_exchange(C, J_in_arr, J_out_arr, dx) + } else { + 0.0 + }; + + // A5: Natural Will + let will_integral = self.natural_will(C, dx); + + // A8: Dynamic Game + let game_integral = if density > 0.0 { + let V_game = self.game_theory_potential(C, density, scale); + V_game.sum() * dx + } else { + 0.0 + }; + + // A10: Multi-layer coherence + let coherence_integral = if let Some(layers) = C_layers { + self.layer_coherence(layers, dx) + } else { + 0.0 + }; + + potential_integral + + gradient_integral + + info_integral + + exchange_integral + + will_integral + + game_integral + + coherence_integral + } + + /// Value equation: 𝒱 = -ΔΩ + pub fn calculate_value(&self, omega_prev: f64, omega_curr: f64) -> f64 { + -(omega_curr - omega_prev) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_potential_V() { + let params = UETParameters::default(); + let eq = UETMasterEquation::new(params); + let C = Array1::linspace(0.0, 1.0, 10); + let V = eq.potential_V(&C); + assert!(V.len() == 10); + } + + #[test] + fn test_omega_functional() { + let params = UETParameters::default(); + let eq = UETMasterEquation::new(params); + let C = Array1::linspace(0.0, 1.0, 10); + let I = Array1::ones(10) * 0.1; + let J_in = Array1::ones(10) * 0.05; + let J_out = Array1::ones(10) * 0.03; + + let omega = eq.omega_functional(&C, Some(&I), Some(&J_in), Some(&J_out), None, 0.0, 1.0, 0.1); + assert!(omega >= 0.0); + } +} diff --git a/services_and_experiments/uet_core/src/parameters.rs b/services_and_experiments/uet_core/src/parameters.rs new file mode 100644 index 000000000..d435bc616 --- /dev/null +++ b/services_and_experiments/uet_core/src/parameters.rs @@ -0,0 +1,102 @@ +use serde::{Deserialize, Serialize}; + +// ============================================================================= +// FUNDAMENTAL CONSTANTS (CODATA 2018 / SI Exact) +// ============================================================================= + +pub const HBAR: f64 = 1.054571817e-34; // Planck constant [J¡s] +pub const C: f64 = 299792458.0; // Speed of light [m/s] +pub const G: f64 = 6.67430e-11; // Gravitational constant [mÂŗ/kg/s²] +pub const K_B: f64 = 1.380649e-23; // Boltzmann constant [J/K] +pub const ALPHA_EM: f64 = 1.0 / 137.035999; // Fine structure constant +pub const M_SUN: f64 = 1.98847e30; // Solar Mass (kg) [IAU 2015] + +// Derived +pub const L_PLANCK: f64 = 1.616255e-35; // approx sqrt(hbar*G/c^3) + +// ============================================================================= +// UET PARAMETERS STRUCT +// ============================================================================= + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UETParameters { + pub kappa: f64, // Gradient penalty (A3) + pub beta: f64, // Coupling constant (A2) + pub alpha: f64, // Equilibrium stiffness (A1) + pub gamma: f64, // Nonlinear stability (A1) + pub c0: f64, // Vacuum Expectation Value (A1) + pub gamma_j: f64, // Exchange rate (A4) + pub w_n: f64, // Natural Will (A5) + pub lambda_coherence: f64, // Layer coherence (A10) + + // Astrophysical (A7) + pub rho_unity: f64, // Pivot density + pub ratio_0: f64, // Halo ratio pivot + pub gamma_uet: f64, // Thermodynamic scaling index + + // Context + pub temperature: f64, + pub scale: String, // e.g. "1e-15 m" + pub origin: String, +} + +impl Default for UETParameters { + fn default() -> Self { + Self { + kappa: 0.1, + beta: 0.1, + alpha: 1.0, + gamma: 0.025, + c0: 1.0, + gamma_j: 0.1, + w_n: 0.05, + lambda_coherence: 0.01, + rho_unity: 5e7, + ratio_0: 8.5, + gamma_uet: 0.48, + temperature: 293.15, + scale: "general".to_string(), + origin: "Fallback/Default".to_string(), + } + } +} + +impl UETParameters { + pub fn new(kappa: f64, beta: f64, scale: &str, origin: &str) -> Self { + let mut p = Self::default(); + p.kappa = kappa; + p.beta = beta; + p.scale = scale.to_string(); + p.origin = origin.to_string(); + p + } +} + +/// Trait for entities that can derive UET parameters from their state. +pub trait ParameterDeriver { + fn derive(&self) -> UETParameters; +} + +// ============================================================================= +// LEGACY PARAMETER REGISTRY (For Backward Compatibility) +// ============================================================================= + +pub fn get_params_legacy(scale_name: &str) -> Result { + let scale = match scale_name { + "0.1" | "0.15" | "0.26" | "0.31" => "astrophysical", + "0.2" | "0.9" | "0.12" | "0.19" => "planck", + "0.3" | "0.4" | "0.6" | "0.7" | "0.8" | "0.11" | "0.13" | "0.14" | "0.17" | "0.18" | "0.20" => "electroweak", + "0.5" | "0.16" => "nuclear", + "0.10" | "0.21" | "0.22" | "0.24" | "0.25" | "0.27" | "0.28" | "0.29" | "0.30" => "macroscopic", + s => s, + }; + + match scale { + "planck" => Ok(UETParameters::new(0.5, 1.0, "planck", "Legacy Bekenstein Bound")), + "electroweak" => Ok(UETParameters::new(0.5, 1.0, "electroweak", "Legacy Natural O(1)")), + "nuclear" => Ok(UETParameters::new(0.57, 1.0, "nuclear", "Legacy QCD Calibration")), + "astrophysical" => Ok(UETParameters::new(0.1, 0.05, "astrophysical", "Legacy SPARC Calibration")), + "macroscopic" => Ok(UETParameters::new(0.1, 0.5, "macroscopic", "Legacy Fluid Calibration")), + _ => Err(format!("Unknown scale: {}", scale)), + } +} diff --git a/services_and_experiments/uet_core/tests/test_physics.rs b/services_and_experiments/uet_core/tests/test_physics.rs new file mode 100644 index 000000000..6740ada71 --- /dev/null +++ b/services_and_experiments/uet_core/tests/test_physics.rs @@ -0,0 +1,48 @@ +use uet_core::parameters::UETParameters; +use uet_core::fields::Field; +use uet_core::dynamics::compute_omega; +use ndarray::ArrayD; + +#[test] +fn test_omega_vacuum_state() { + let params = UETParameters::default(); + // C = C0 everywhere (Vacuum) + let shape = vec![10]; + let data = ArrayD::from_elem(shape, params.c0); + let c = Field::new(data); + + // Ί should be 0 because V(C0) = 0 and ∇C0 = 0 + let omega = compute_omega(&c, None, 0.1, ¶ms); + assert!(omega.abs() < 1e-9, "Vacuum energy should be zero, got {}", omega); +} + +#[test] +fn test_omega_perturbed_state() { + let params = UETParameters::default(); + // C = C0 + 1.0 + let shape = vec![10]; + let data = ArrayD::from_elem(shape, params.c0 + 1.0); + let c = Field::new(data); + + // V(C) > 0, Gradient = 0 + let omega = compute_omega(&c, None, 0.1, ¶ms); + assert!(omega > 0.0, "Perturbed state should have positive energy"); +} + +#[test] +fn test_gradient_energy() { + let params = UETParameters::default(); + // Linear gradient: 0, 1, 2... + let points = 10; + let dx = 1.0; + let data = ArrayD::from_shape_vec(vec![points], (0..points).map(|x| x as f64).collect()).unwrap(); + let c = Field::new(data); + + // Gradient is constant = 1.0 + // (Îē/2) * |1|^2 * Volume + // But calculate it via function + let omega = compute_omega(&c, None, dx, ¶ms); + + // We expect some positive value from Potential + Gradient + assert!(omega > 0.0); +} diff --git a/services_and_experiments/uet_kb/Cargo.toml b/services_and_experiments/uet_kb/Cargo.toml new file mode 100644 index 000000000..7138365a9 --- /dev/null +++ b/services_and_experiments/uet_kb/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "uet_kb" +version = "0.1.0" +edition = "2021" + +[dependencies] +uet_core = { path = "../uet_core" } +tokio = { version = "1.0", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +anyhow = "1.0" +clap = { version = "4.0", features = ["derive"] } +toml = "0.8" +ndarray = "0.15" + +# For LanceDB (Vector Database) +# lancedb = "0.4" +# arrow-array = "50" + + + +# Database (Postgres) +sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono"] } +uuid = { version = "1.8", features = ["v4", "serde"] } + +# For MCP (JSON-RPC) +# We will implement basic stdio JSON-RPC loop manually or use a crate if available. +# Simple text-io for now. +crossbeam-channel = "0.5" +chrono = { version = "=0.4.38", features = ["serde"] } # Pin to avoid conflict with arrow-arith 51.0.0 +sha3 = "0.10.8" +axum = { version = "0.7", features = ["macros"] } +tower-http = { version = "0.5", features = ["cors"] } diff --git a/services_and_experiments/uet_kb/README.md b/services_and_experiments/uet_kb/README.md new file mode 100644 index 000000000..9d9f2d5b4 --- /dev/null +++ b/services_and_experiments/uet_kb/README.md @@ -0,0 +1,51 @@ +# đŸ—„ī¸ UET Knowledge Base (`uet_kb`) + +![Status](https://img.shields.io/badge/Status-ACTIVE-brightgreen) +![Protocol](https://img.shields.io/badge/Protocol-MCP-orange) +![Database](https://img.shields.io/badge/DB-Postgres_Vector-blue) + +> **"The Brain of UET"** - Server ⏗ā¸ĩāšˆā¸—ā¸ŗā¸Ģā¸™āš‰ā¸˛ā¸—ā¸ĩāšˆāš€ā¸āš‡ā¸šā¸„ā¸§ā¸˛ā¸Ąā¸Ŗā¸šāš‰ (Memory) āšā¸Ĩā¸°āšƒā¸Ģāš‰ā¸šā¸Ŗā¸´ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞ (Search) āšā¸āšˆ Agent ā¸­ā¸ˇāšˆā¸™āš† ā¸œāšˆā¸˛ā¸™ MCP Protocol. + +--- + +## đŸ›ī¸ Architecture Pillars + +| Component | Description | +| :--- | :--- | +| **MCP Server** | Implement JSON-RPC 2.0 āšƒā¸Ģāš‰ Agent (Python) āš€ā¸Ŗā¸ĩā¸ĸā¸āšƒā¸Šāš‰ Tools āš„ā¸”āš‰. | +| **Vector DB** | ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗāš€ā¸āš‡ā¸šāšā¸Ĩā¸°ā¸„āš‰ā¸™ā¸Ģ⏞ Vector (Semantic + Physics Embedding). | +| **API** | āšƒā¸Ģāš‰ā¸šā¸Ŗā¸´ā¸ā¸˛ā¸Ŗā¸„ā¸ŗā¸Ēā¸ąāšˆā¸‡ `ingest`, `search`, `delete` āšā¸šā¸š Real-time. | + +--- + +## 🔗 System Connection + +```mermaid +graph TD + Agent["🤖 Python Agent"] <-->|JSON-RPC| KB["đŸ—„ī¸ uet_kb (This)"] + KB <-->|SQLx| DB[("🐘 Postgres + pgvector")] + KB -.->|Link| Core["âš›ī¸ uet_core"] + + style KB fill:#fff9c4,stroke:#fbc02d +``` + +--- + +## đŸ› ī¸ Available MCP Tools + +| Tool Name | Description | +| :--- | :--- | +| `search_knowledge_base` | ā¸„āš‰ā¸™ā¸Ģā¸˛ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸”āš‰ā¸§ā¸ĸ Vector Search (Semantic). | +| `search_physics` | ā¸„āš‰ā¸™ā¸Ģā¸˛ā¸”āš‰ā¸§ā¸ĸā¸„āšˆā¸˛ā¸Ÿā¸´ā¸Ē⏴⏁ā¸ĒāšŒ (UET Physics Vector). | +| `ingest_document` | āš€ā¸žā¸´āšˆā¸Ą/āšā¸āš‰āš„ā¸‚ āš€ā¸­ā¸ā¸Ēā¸˛ā¸Ŗāš€ā¸‚āš‰ā¸˛ā¸Ēā¸šāšˆā¸Ŗā¸°ā¸šā¸š. | +| `delete_document` | ā¸Ĩā¸šāš€ā¸­ā¸ā¸Ē⏞⏪⏭⏭⏁⏈⏞⏁⏪⏰⏚⏚. | +| `list_topics` | ā¸”ā¸šā¸Ŗā¸˛ā¸ĸā¸Šā¸ˇāšˆā¸­ā¸Ģā¸ąā¸§ā¸‚āš‰ā¸­ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”āšƒā¸™ā¸Ŗā¸°ā¸šā¸š. | + +--- + +## 🚀 Quick Start (Dev Mode) + +```bash +# ā¸Ŗā¸ąā¸™ server (ā¸Ÿā¸ąā¸‡ā¸—ā¸˛ā¸‡ Stdin) +cargo run --bin uet_kb +``` diff --git a/services_and_experiments/uet_kb/src/db.rs b/services_and_experiments/uet_kb/src/db.rs new file mode 100644 index 000000000..ff1080cea --- /dev/null +++ b/services_and_experiments/uet_kb/src/db.rs @@ -0,0 +1,262 @@ +use sqlx::{postgres::PgPoolOptions, Pool, Postgres, Row}; +use uuid::Uuid; +use chrono::Utc; +use anyhow::Result; +use serde::{Serialize, Deserialize}; +use serde_json::Value; + +#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] +pub struct Document { + pub id: Uuid, + pub path: String, + pub extracted_text: String, + pub created_at: chrono::DateTime, + pub metadata: Value, +} + +#[derive(Debug, sqlx::FromRow)] +pub struct Chunk { + pub id: String, + pub doc_id: String, + pub text: String, + // We don't necessarily need to fetch the embedding back into Rust for basic RAG, + // but if we did, we'd need a mapping. For search, we just return text. +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SearchResult { + pub chunk_id: String, + pub doc_id: String, + pub text: String, + pub score: f64, + pub path: String, + pub metadata: Value, +} + +pub async fn init_db(database_url: &str) -> Result> { + let pool = PgPoolOptions::new() + .max_connections(5) + .connect(database_url) + .await?; + + // Enable pgvector extension + sqlx::query("CREATE EXTENSION IF NOT EXISTS vector") + .execute(&pool) + .await?; + + // Create documents table with JSONB metadata + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS documents ( + id UUID PRIMARY KEY, + path TEXT NOT NULL, + extracted_text TEXT, + created_at TIMESTAMPTZ NOT NULL, + metadata JSONB DEFAULT '{}'::jsonb + ) + "# + ) + .execute(&pool) + .await?; + + // Create chunks table with Vector(1024) for semantic and Vector(20) for physics + sqlx::query( + r#" + CREATE TABLE IF NOT EXISTS chunks ( + id UUID PRIMARY KEY, + doc_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE, + text TEXT NOT NULL, + embedding vector(1024), + physics_vector vector(20) + ) + "# + ) + .execute(&pool) + .await?; + + Ok(pool) +} + +pub async fn insert_document(pool: &Pool, id: Option, path: &str, text: &str, metadata: Option) -> Result { + // Use provided ID or generate new one + let id = id.unwrap_or_else(Uuid::new_v4); + let now = Utc::now(); + let meta = metadata.unwrap_or(serde_json::json!({})); + + sqlx::query( + r#" + INSERT INTO documents (id, path, extracted_text, created_at, metadata) + VALUES ($1, $2, $3, $4, $5) + "# + ) + .bind(id) + .bind(path) + .bind(text) + .bind(now) + .bind(meta) + .execute(pool) + .await?; + + Ok(id.to_string()) +} + +pub async fn insert_chunk( + pool: &Pool, + doc_id: &str, + text: &str, + semantic_vec: &[f64], + physics_vec: &[f64] +) -> Result<()> { + let id = Uuid::new_v4(); + let doc_uuid = Uuid::parse_str(doc_id)?; + + // Format semantic vector + let sem_vector_string = format!( + "[{}]", + semantic_vec.iter().map(|f| f.to_string()).collect::>().join(",") + ); + + // Format physics vector + let phys_vector_string = format!( + "[{}]", + physics_vec.iter().map(|f| f.to_string()).collect::>().join(",") + ); + + sqlx::query( + r#" + INSERT INTO chunks (id, doc_id, text, embedding, physics_vector) + VALUES ($1, $2, $3, $4::vector, $5::vector) + "# + ) + .bind(id) + .bind(doc_uuid) + .bind(text) + .bind(sem_vector_string) + .bind(phys_vector_string) + .execute(pool) + .await?; + + Ok(()) +} + +// Search using Cosine Distance (<=> operator) +pub async fn search_similar(pool: &Pool, query_vec: &[f64], top_k: i64) -> Result> { + let vector_string = format!( + "[{}]", + query_vec.iter().map(|f| f.to_string()).collect::>().join(",") + ); + + let rows = sqlx::query( + r#" + SELECT c.id, c.doc_id, c.text, d.path, d.metadata, 1 - (c.embedding <=> $1::vector) as similarity + FROM chunks c + JOIN documents d ON c.doc_id = d.id + ORDER BY c.embedding <=> $1::vector + LIMIT $2 + "# + ) + .bind(vector_string) + .bind(top_k) + .fetch_all(pool) + .await?; + + let results = rows.into_iter().map(|row| { + SearchResult { + chunk_id: row.get::("id").to_string(), + doc_id: row.get::("doc_id").to_string(), + text: row.get("text"), + path: row.get("path"), + score: row.get("similarity"), + metadata: row.get("metadata"), + } + }).collect(); + + Ok(results) +} + +// Search using Physics Vector (Euclidean Distance <->) +pub async fn search_physics(pool: &Pool, query_vec: &[f64], top_k: i64) -> Result> { + let vector_string = format!( + "[{}]", + query_vec.iter().map(|f| f.to_string()).collect::>().join(",") + ); + + let rows = sqlx::query( + r#" + SELECT c.id, c.doc_id, c.text, d.path, d.metadata, c.physics_vector <-> $1::vector as distance + FROM chunks c + JOIN documents d ON c.doc_id = d.id + ORDER BY c.physics_vector <-> $1::vector + LIMIT $2 + "# + ) + .bind(vector_string) + .bind(top_k) + .fetch_all(pool) + .await?; + + let results = rows.into_iter().map(|row| { + SearchResult { + chunk_id: row.get::("id").to_string(), + doc_id: row.get::("doc_id").to_string(), + text: row.get("text"), + path: row.get("path"), + score: row.get("distance"), + metadata: row.get("metadata"), + } + }).collect(); + + Ok(results) +} + +// --- CRUD Operations --- + +pub async fn get_document(pool: &Pool, doc_id: &str) -> Result> { + let uuid_val = match Uuid::parse_str(doc_id) { + Ok(u) => u, + Err(_) => return Ok(None), + }; + + let doc = sqlx::query_as::<_, Document>( + "SELECT id, path, extracted_text, created_at, metadata FROM documents WHERE id = $1" + ) + .bind(uuid_val) + .fetch_optional(pool) + .await?; + + Ok(doc) +} + +pub async fn delete_document(pool: &Pool, doc_id: &str) -> Result { + let uuid_val = match Uuid::parse_str(doc_id) { + Ok(u) => u, + Err(_) => return Ok(false), + }; + + // Because of ON DELETE CASCADE in chunks table def above, this deletes chunks too. + let result = sqlx::query("DELETE FROM documents WHERE id = $1") + .bind(uuid_val) + .execute(pool) + .await?; + + Ok(result.rows_affected() > 0) +} + +pub async fn count_documents(pool: &Pool) -> Result { + let result: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM documents") + .fetch_one(pool) + .await?; + Ok(result.0) +} + +pub async fn list_topics(pool: &Pool) -> Result> { + // Assuming metadata has a key "topic_id" + let rows = sqlx::query( + "SELECT DISTINCT metadata->>'topic_id' as topic FROM documents WHERE metadata->>'topic_id' IS NOT NULL ORDER BY topic" + ) + .fetch_all(pool) + .await?; + + let topics = rows.into_iter().map(|r| r.get("topic")).collect(); + Ok(topics) +} diff --git a/services_and_experiments/uet_kb/src/embeddings.rs b/services_and_experiments/uet_kb/src/embeddings.rs new file mode 100644 index 000000000..0964e6b96 --- /dev/null +++ b/services_and_experiments/uet_kb/src/embeddings.rs @@ -0,0 +1,119 @@ +use sha3::{Digest, Sha3_256}; + +/// Lightweight deterministic embedding generator. +/// Uses SHA3 hashing to produce a fixed-dimension vector from text. +/// This is NOT a learned semantic embedding — it's a deterministic hash-based +/// projection suitable for exact-match retrieval and deduplication. +/// For real semantic search, integrate with an external embedding API (OpenAI, Cohere, etc.) +pub fn hash_embed(text: &str, dim: usize) -> Vec { + let normalized = text.to_lowercase().trim().to_string(); + let mut result = Vec::with_capacity(dim); + + // Generate enough hash bytes to fill the vector + let chunks_needed = (dim * 8 + 31) / 32; // Each SHA3-256 gives 32 bytes + let mut all_bytes = Vec::new(); + + for i in 0..chunks_needed { + let input = format!("{}:{}", i, normalized); + let hash = Sha3_256::digest(input.as_bytes()); + all_bytes.extend_from_slice(&hash); + } + + // Convert bytes to f64 in [-1, 1] range + for i in 0..dim { + let offset = i * 8; + if offset + 8 <= all_bytes.len() { + let mut bytes = [0u8; 8]; + bytes.copy_from_slice(&all_bytes[offset..offset + 8]); + let raw = u64::from_le_bytes(bytes); + // Map to [-1, 1] + let val = (raw as f64 / u64::MAX as f64) * 2.0 - 1.0; + result.push(val); + } else { + result.push(0.0); + } + } + + // L2-normalize the vector + let norm: f64 = result.iter().map(|x| x * x).sum::().sqrt(); + if norm > 0.0 { + for v in result.iter_mut() { + *v /= norm; + } + } + + result +} + +/// Generate a physics-informed embedding from UET equation parameters. +/// Encodes known physical quantities into a 20-dimensional vector. +pub fn physics_embed(params: &PhysicsParams) -> Vec { + let mut vec = vec![0.0; 20]; + vec[0] = params.energy.unwrap_or(0.0); + vec[1] = params.information.unwrap_or(0.0); + vec[2] = params.gamma.unwrap_or(0.0); + vec[3] = params.temperature.unwrap_or(0.0); + vec[4] = params.entropy.unwrap_or(0.0); + vec[5] = params.mass.unwrap_or(0.0); + vec[6] = params.velocity.unwrap_or(0.0); + vec[7] = params.frequency.unwrap_or(0.0); + vec[8] = params.wavelength.unwrap_or(0.0); + vec[9] = params.coupling_constant.unwrap_or(0.0); + // Slots 10-19 reserved for future UET parameters + vec +} + +#[derive(Debug, Default)] +pub struct PhysicsParams { + pub energy: Option, + pub information: Option, + pub gamma: Option, + pub temperature: Option, + pub entropy: Option, + pub mass: Option, + pub velocity: Option, + pub frequency: Option, + pub wavelength: Option, + pub coupling_constant: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hash_embed_deterministic() { + let v1 = hash_embed("hello world", 1024); + let v2 = hash_embed("hello world", 1024); + assert_eq!(v1, v2); + assert_eq!(v1.len(), 1024); + } + + #[test] + fn test_hash_embed_different_texts() { + let v1 = hash_embed("UET equilibrium equation", 1024); + let v2 = hash_embed("quantum gravity theory", 1024); + assert_ne!(v1, v2); + } + + #[test] + fn test_hash_embed_normalized() { + let v = hash_embed("test normalization", 1024); + let norm: f64 = v.iter().map(|x| x * x).sum::().sqrt(); + assert!((norm - 1.0).abs() < 1e-10); + } + + #[test] + fn test_physics_embed() { + let params = PhysicsParams { + energy: Some(1.5), + gamma: Some(0.8), + ..Default::default() + }; + let v = physics_embed(¶ms); + assert_eq!(v.len(), 20); + assert_eq!(v[0], 1.5); + assert_eq!(v[2], 0.8); + assert_eq!(v[3], 0.0); + } +} diff --git a/services_and_experiments/uet_kb/src/main.rs b/services_and_experiments/uet_kb/src/main.rs new file mode 100644 index 000000000..d3fa3373a --- /dev/null +++ b/services_and_experiments/uet_kb/src/main.rs @@ -0,0 +1,125 @@ +mod db; +mod mcp; +mod mcp_http; +mod embeddings; + +use clap::{Parser, Subcommand}; +use std::path::Path; + +#[derive(Parser)] +#[command(name = "uet_kb")] +#[command(about = "UET Knowledge Base Server (Rust + Postgres)", long_about = None)] +struct Cli { + #[arg(short, long, default_value = "postgres://postgres:postgres@localhost:5433/uet_kb")] + db_url: String, + + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + /// Initialize the database (Install extensions & Tables) + InitDb, + /// Ingest a text file with real hash-based embeddings + Ingest { + #[arg(short, long)] + file: String, + }, + /// Search for concepts + Search { + #[arg(short, long)] + query: String, + }, + /// Start the MCP JSON-RPC Server (stdin/stdout) + StartMcpServer, + /// Start the MCP HTTP Server + StartHttpServer { + #[arg(short, long, default_value = "3002")] + port: u16, + }, +} + +#[tokio::main] +async fn main() { + let cli = Cli::parse(); + + match &cli.command { + Some(Commands::InitDb) => { + println!("Initializing database at: {}", cli.db_url); + match db::init_db(&cli.db_url).await { + Ok(_) => println!("✅ Database initialized (pgvector enabled)."), + Err(e) => eprintln!("❌ Error: {}", e), + } + } + Some(Commands::Ingest { file }) => { + println!("Ingesting file: {}", file); + // Connect to DB + let pool = match db::init_db(&cli.db_url).await { + Ok(p) => p, + Err(e) => { + eprintln!("❌ Failed to connect/init DB: {}", e); + return; + } + }; + + // Read Content + let content = if Path::new(file).exists() { + std::fs::read_to_string(file).expect("Failed to read file") + } else { + file.clone() // Treat as raw text + }; + + // Insert Doc + let doc_id = db::insert_document(&pool, None, file, &content, None).await.expect("Failed to insert doc"); + println!("Created Document ID: {}", doc_id); + + // Chunk & Embed with real hash-based embeddings + let chunks: Vec<&str> = content.split('\n').filter(|s| !s.trim().is_empty()).collect(); + for chunk_text in &chunks { + let s_vec = embeddings::hash_embed(chunk_text, 1024); + let p_vec = vec![0.0; 20]; // Physics vector filled by domain-specific tools + + db::insert_chunk(&pool, &doc_id, chunk_text, &s_vec, &p_vec).await.expect("Failed to insert chunk"); + } + println!("✅ Ingested {} chunks with embeddings.", chunks.len()); + } + Some(Commands::Search { query }) => { + println!("Searching for: '{}'", query); + let pool = match db::init_db(&cli.db_url).await { + Ok(p) => p, + Err(e) => { + eprintln!("❌ Failed to connect/init DB: {}", e); + return; + } + }; + + // Generate query embedding + let vec = embeddings::hash_embed(query, 1024); + + // Top K = 5 + match db::search_similar(&pool, &vec, 5).await { + Ok(results) => { + for res in results { + println!("- [Score: {:.4}] (Doc: {}) {}", res.score, res.path, res.text.trim()); + } + } + Err(e) => eprintln!("❌ Search failed: {}", e), + } + } + Some(Commands::StartMcpServer) => { + if let Err(e) = mcp::run_mcp_server(&cli.db_url).await { + eprintln!("MCP Server Error: {}", e); + } + } + Some(Commands::StartHttpServer { port }) => { + if let Err(e) = mcp_http::run_http_mcp_server(&cli.db_url, *port).await { + eprintln!("MCP HTTP Server Error: {}", e); + } + } + None => { + println!("UET Knowledge Base Server v0.1.0 (Postgres Edition)"); + println!("Use --help for commands."); + } + } +} diff --git a/services_and_experiments/uet_kb/src/mcp.rs b/services_and_experiments/uet_kb/src/mcp.rs new file mode 100644 index 000000000..190bdfb92 --- /dev/null +++ b/services_and_experiments/uet_kb/src/mcp.rs @@ -0,0 +1,283 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader}; +use crate::db; + +#[derive(Serialize, Deserialize, Debug)] +struct JsonRpcRequest { + jsonrpc: String, + method: String, + params: Option, + id: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +struct JsonRpcResponse { + jsonrpc: String, + #[serde(skip_serializing_if = "Option::is_none")] + result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, + id: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +struct JsonRpcError { + code: i32, + message: String, + data: Option, +} + +pub async fn run_mcp_server(db_url: &str) -> anyhow::Result<()> { + eprintln!("MCP Server Running... (Listening on Stdin)"); + + // Connect to DB once + let pool = db::init_db(db_url).await?; + + let stdin = io::stdin(); + let mut reader = BufReader::new(stdin); + let mut stdout = io::stdout(); + + let mut line = String::new(); + loop { + line.clear(); + let bytes_read = reader.read_line(&mut line).await?; + if bytes_read == 0 { + break; // EOF + } + + let trim_line = line.trim(); + if trim_line.is_empty() { + continue; + } + + eprintln!("Received: {}", trim_line); + + match serde_json::from_str::(trim_line) { + Ok(req) => { + // Check if it's a notification (no id) + let is_notification = req.id.is_none(); + + let response = handle_request(req, &pool).await; + + // Only send response if it's NOT a notification + if !is_notification { + let mut resp_str = serde_json::to_string(&response)?; + resp_str.push('\n'); + stdout.write_all(resp_str.as_bytes()).await?; + stdout.flush().await?; + } else { + eprintln!("Processed notification, no response sent."); + } + } + Err(e) => { + eprintln!("Failed to parse JSON: {}", e); + let error_response = json!({ + "jsonrpc": "2.0", + "error": { + "code": -32700, + "message": "Parse error", + "data": e.to_string() + }, + "id": null + }); + let mut resp_str = serde_json::to_string(&error_response)?; + resp_str.push('\n'); + stdout.write_all(resp_str.as_bytes()).await?; + stdout.flush().await?; + } + } + } + Ok(()) +} + +async fn handle_request(req: JsonRpcRequest, pool: &sqlx::Pool) -> JsonRpcResponse { + let result = match req.method.as_str() { + "initialize" => Ok(json!({ + "protocolVersion": "2024-11-05", + "serverInfo": { + "name": "uet_kb_mcp", + "version": "0.1.0" + }, + "capabilities": { + "tools": { + "listChanged": false + } + } + })), + "notifications/initialized" => Ok(json!(true)), // Acknowledgement + "tools/list" => Ok(json!({ + "tools": [ + { + "name": "search_knowledge_base", + "description": "Search for relevant concepts using semantic vector search", + "inputSchema": { + "type": "object", + "properties": { + "query": { "type": "string" }, + "query_vector": { "type": "array", "items": { "type": "number" } } + }, + "required": ["query"] + } + }, + { + "name": "search_physics", + "description": "Search for relevant concepts using UET physics-informed vector search", + "inputSchema": { + "type": "object", + "properties": { + "physics_vector": { "type": "array", "items": { "type": "number" } } + }, + "required": ["physics_vector"] + } + }, + { + "name": "get_document", + "description": "Retrieve a document by ID (no vectors returned)", + "inputSchema": { + "type": "object", + "properties": { + "doc_id": { "type": "string" } + }, + "required": ["doc_id"] + } + }, + { + "name": "count_documents", + "description": "Count total documents in knowledge base", + "inputSchema": { + "type": "object", + "properties": {}, + "required": [] + } + }, + { + "name": "list_topics", + "description": "List all unique topics from metadata", + "inputSchema": { + "type": "object", + "properties": {}, + "required": [] + } + } + ] + })), + "tools/call" => handle_tool_call(req.params, pool).await, + "ping" => Ok(json!({})), + + _ => Err(JsonRpcError { + code: -32601, + message: format!("Method not found: {}", req.method), + data: None, + }), + }; + + match result { + Ok(res) => JsonRpcResponse { + jsonrpc: "2.0".to_string(), + result: Some(res), + error: None, + id: req.id, + }, + Err(err) => JsonRpcResponse { + jsonrpc: "2.0".to_string(), + result: None, + error: Some(err), + id: req.id, + }, + } +} + +async fn handle_tool_call(params: Option, pool: &sqlx::Pool) -> Result { + let params = params.ok_or(JsonRpcError { + code: -32602, + message: "Missing params".to_string(), + data: None, + })?; + + let name = params.get("name").and_then(|v| v.as_str()).ok_or(JsonRpcError { + code: -32602, + message: "Missing tool name".to_string(), + data: None, + })?; + + let args = params.get("arguments").cloned().unwrap_or(json!({})); + + match name { + "search_knowledge_base" => { + let semantic_vec_arg = args.get("query_vector").and_then(|v| v.as_array()); + + let query_vec: Vec = if let Some(v) = semantic_vec_arg { + v.iter().map(|val| val.as_f64().unwrap_or(0.0)).collect() + } else { + vec![0.0; 1024] // Fallback to zero vector + }; + + let results = db::search_similar(pool, &query_vec, 10).await.map_err(|e| JsonRpcError { + code: -32000, + message: e.to_string(), + data: None + })?; + + Ok(json!({ + "results": results + })) + }, + "search_physics" => { + let physics_vec_arg = args.get("physics_vector").and_then(|v| v.as_array()); + + let query_vec: Vec = if let Some(v) = physics_vec_arg { + v.iter().map(|val| val.as_f64().unwrap_or(0.0)).collect() + } else { + return Err(JsonRpcError { + code: -32602, + message: "Missing physics_vector".to_string(), + data: None + }); + }; + + let results = db::search_physics(pool, &query_vec, 10).await.map_err(|e| JsonRpcError { + code: -32000, + message: e.to_string(), + data: None + })?; + + Ok(json!({ + "results": results + })) + }, + "get_document" => { + let doc_id = args.get("doc_id").and_then(|v| v.as_str()).unwrap_or(""); + let doc = db::get_document(pool, doc_id).await.map_err(|e| JsonRpcError { + code: -32000, + message: e.to_string(), + data: None + })?; + + Ok(json!({ "document": doc })) + }, + "count_documents" => { + let count = db::count_documents(pool).await.map_err(|e| JsonRpcError { + code: -32000, + message: e.to_string(), + data: None + })?; + + Ok(json!({ "count": count })) + }, + "list_topics" => { + let topics = db::list_topics(pool).await.map_err(|e| JsonRpcError { + code: -32000, + message: e.to_string(), + data: None + })?; + + Ok(json!({ "topics": topics })) + }, + _ => Err(JsonRpcError { + code: -32601, + message: format!("Tool not found: {}", name), + data: None, + }), + } +} diff --git a/services_and_experiments/uet_kb/src/mcp_http.rs b/services_and_experiments/uet_kb/src/mcp_http.rs new file mode 100644 index 000000000..422ce985d --- /dev/null +++ b/services_and_experiments/uet_kb/src/mcp_http.rs @@ -0,0 +1,290 @@ +use axum::{ + extract::State, + routing::post, + Json, Router, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use sqlx::{Pool, Postgres}; +use tower_http::cors::{Any, CorsLayer}; + +use crate::db; +use crate::embeddings; + +#[derive(Deserialize)] +struct McpHttpRequest { + jsonrpc: String, + method: String, + params: Option, + id: Option, +} + +#[derive(Serialize)] +struct McpHttpResponse { + jsonrpc: String, + #[serde(skip_serializing_if = "Option::is_none")] + result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, + id: Option, +} + +#[derive(Serialize)] +struct McpError { + code: i32, + message: String, + data: Option, +} + +pub async fn run_http_mcp_server(db_url: &str, port: u16) -> anyhow::Result<()> { + let pool = db::init_db(db_url).await?; + + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); + + let app = Router::new() + .route("/mcp", post(handle_mcp_http)) + .route("/health", axum::routing::get(|| async { Json(json!({"status": "ok"})) })) + .layer(cors) + .with_state(pool); + + let addr = format!("0.0.0.0:{}", port); + eprintln!("MCP HTTP Server listening on http://{}", addr); + + let listener = tokio::net::TcpListener::bind(&addr).await?; + axum::serve(listener, app).await?; + Ok(()) +} + +async fn handle_mcp_http( + State(pool): State>, + Json(req): Json, +) -> Json { + let result = dispatch_method(&req.method, req.params, &pool).await; + + match result { + Ok(res) => Json(McpHttpResponse { + jsonrpc: "2.0".to_string(), + result: Some(res), + error: None, + id: req.id, + }), + Err(err) => Json(McpHttpResponse { + jsonrpc: "2.0".to_string(), + result: None, + error: Some(err), + id: req.id, + }), + } +} + +async fn dispatch_method(method: &str, params: Option, pool: &Pool) -> Result { + match method { + "initialize" => Ok(json!({ + "protocolVersion": "2024-11-05", + "serverInfo": { + "name": "uet_kb_mcp", + "version": "0.2.0" + }, + "capabilities": { + "tools": { "listChanged": false } + } + })), + "notifications/initialized" => Ok(json!(true)), + "tools/list" => Ok(json!({ + "tools": [ + { + "name": "search_knowledge_base", + "description": "Search the UET knowledge base using semantic hash-based vector search", + "inputSchema": { + "type": "object", + "properties": { + "query": { "type": "string", "description": "Natural language search query" }, + "top_k": { "type": "integer", "description": "Number of results (default 10)" } + }, + "required": ["query"] + } + }, + { + "name": "search_physics", + "description": "Search using UET physics-informed 20D vector (energy, information, gamma, ...)", + "inputSchema": { + "type": "object", + "properties": { + "physics_vector": { "type": "array", "items": { "type": "number" } } + }, + "required": ["physics_vector"] + } + }, + { + "name": "ingest_document", + "description": "Ingest a text document into the knowledge base with automatic embedding", + "inputSchema": { + "type": "object", + "properties": { + "path": { "type": "string", "description": "Document path or title" }, + "content": { "type": "string", "description": "Full text content to ingest" }, + "metadata": { "type": "object", "description": "Optional metadata (topic_id, etc.)" } + }, + "required": ["path", "content"] + } + }, + { + "name": "get_document", + "description": "Retrieve a document by its UUID", + "inputSchema": { + "type": "object", + "properties": { + "doc_id": { "type": "string" } + }, + "required": ["doc_id"] + } + }, + { + "name": "count_documents", + "description": "Count total documents in the knowledge base", + "inputSchema": { "type": "object", "properties": {}, "required": [] } + }, + { + "name": "list_topics", + "description": "List all unique topic IDs from document metadata", + "inputSchema": { "type": "object", "properties": {}, "required": [] } + } + ] + })), + "tools/call" => handle_tool_call(params, pool).await, + "ping" => Ok(json!({})), + _ => Err(McpError { + code: -32601, + message: format!("Method not found: {}", method), + data: None, + }), + } +} + +async fn handle_tool_call(params: Option, pool: &Pool) -> Result { + let params = params.ok_or(McpError { + code: -32602, + message: "Missing params".to_string(), + data: None, + })?; + + let name = params.get("name").and_then(|v| v.as_str()).ok_or(McpError { + code: -32602, + message: "Missing tool name".to_string(), + data: None, + })?; + + let args = params.get("arguments").cloned().unwrap_or(json!({})); + + match name { + "search_knowledge_base" => { + let query = args.get("query").and_then(|v| v.as_str()).unwrap_or(""); + let top_k = args.get("top_k").and_then(|v| v.as_i64()).unwrap_or(10); + + let query_vec = embeddings::hash_embed(query, 1024); + + let results = db::search_similar(pool, &query_vec, top_k).await.map_err(|e| McpError { + code: -32000, + message: e.to_string(), + data: None, + })?; + + Ok(json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&results).unwrap_or_default() }] })) + } + "search_physics" => { + let physics_vec_arg = args.get("physics_vector").and_then(|v| v.as_array()); + + let query_vec: Vec = physics_vec_arg + .map(|v| v.iter().map(|val| val.as_f64().unwrap_or(0.0)).collect()) + .ok_or(McpError { + code: -32602, + message: "Missing physics_vector".to_string(), + data: None, + })?; + + let results = db::search_physics(pool, &query_vec, 10).await.map_err(|e| McpError { + code: -32000, + message: e.to_string(), + data: None, + })?; + + Ok(json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&results).unwrap_or_default() }] })) + } + "ingest_document" => { + let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("untitled"); + let content = args.get("content").and_then(|v| v.as_str()).ok_or(McpError { + code: -32602, + message: "Missing content".to_string(), + data: None, + })?; + let metadata = args.get("metadata").cloned(); + + let doc_id = db::insert_document(pool, None, path, content, metadata) + .await + .map_err(|e| McpError { + code: -32000, + message: e.to_string(), + data: None, + })?; + + // Chunk and embed + let chunks: Vec<&str> = content.split('\n').filter(|s| !s.trim().is_empty()).collect(); + let mut chunk_count = 0; + for chunk_text in &chunks { + let s_vec = embeddings::hash_embed(chunk_text, 1024); + let p_vec = vec![0.0; 20]; + db::insert_chunk(pool, &doc_id, chunk_text, &s_vec, &p_vec) + .await + .map_err(|e| McpError { + code: -32000, + message: e.to_string(), + data: None, + })?; + chunk_count += 1; + } + + Ok(json!({ + "content": [{ + "type": "text", + "text": format!("Ingested document '{}' (id: {}) with {} chunks", path, doc_id, chunk_count) + }] + })) + } + "get_document" => { + let doc_id = args.get("doc_id").and_then(|v| v.as_str()).unwrap_or(""); + let doc = db::get_document(pool, doc_id).await.map_err(|e| McpError { + code: -32000, + message: e.to_string(), + data: None, + })?; + + Ok(json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&doc).unwrap_or_default() }] })) + } + "count_documents" => { + let count = db::count_documents(pool).await.map_err(|e| McpError { + code: -32000, + message: e.to_string(), + data: None, + })?; + + Ok(json!({ "content": [{ "type": "text", "text": format!("Total documents: {}", count) }] })) + } + "list_topics" => { + let topics = db::list_topics(pool).await.map_err(|e| McpError { + code: -32000, + message: e.to_string(), + data: None, + })?; + + Ok(json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&topics).unwrap_or_default() }] })) + } + _ => Err(McpError { + code: -32601, + message: format!("Tool not found: {}", name), + data: None, + }), + } +} diff --git a/services_and_experiments/uet_miner/BENCHMARK_REPORT.md b/services_and_experiments/uet_miner/BENCHMARK_REPORT.md new file mode 100644 index 000000000..103e5f230 --- /dev/null +++ b/services_and_experiments/uet_miner/BENCHMARK_REPORT.md @@ -0,0 +1,61 @@ +# đŸ§Ē UET Engine Benchmark Report + +> **Date:** 2026-02-02 +> **Version:** 0.2.0 (Rust + wgpu) +> **Device:** AMD Radeon RX 6600 XT vs CPU + +## 📊 Executive Summary +The UET Mining Engine demonstrates **industrial-grade performance**, significantly outperforming standard CPU implementations. +- **GPU Speed:** ~210 MH/s +- **CPU Speed (Multi):** 48.92 MH/s +- **Engineering Win:** The engine successfully leverages massive parallelism, achieving a **41x speedup** over single-threaded execution. + +--- + +## 1. Baseline Performance (CPU) +*Measured using `uet_miner benchmark` on native Windows host.* + +| Mode | Hashrate (MH/s) | Relative Speed | Notes | +| :--- | :--- | :--- | :--- | +| **Single-Thread** | **5.12** | 1.0x | Base reference (1 core) | +| **Multi-Thread** | **48.92** | 9.55x | Rayon Parallel Iterator (All cores) | + +## 2. UET Engine Performance (GPU) +*Measured during live stratum test on AMD RX 6600 XT.* + +| Mode | Hashrate (MH/s) | Speedup (vs Single) | Speedup (vs Multi) | +| :--- | :--- | :--- | :--- | +| **UET Engine** | **~210.00** | **41.0x** 🚀 | **4.3x** ⚡ | + +## 3. Industry Comparison (The Truth) +How does "Our Method" (UET Rust/wgpu) compare to the "Old Masters" (Legacy Miners)? + +| Method | Est. Speed (RX 6600 XT) | Verdict | +| :--- | :--- | :--- | +| **CPU Mining** | ~49 MH/s | Too slow to be useful. | +| **UET Engine (Us)** | **~210 MH/s** | **4.3x faster than CPU.** Good for high-level code, but not fully optimized. | +| **Legacy Optimized (C++/Asm)** | **~600-800 MH/s** | **3-4x faster than Us.** Highly tuned "Bare Metal" code used by pros. | +| **ASIC (The Market)** | **100,000,000+ MH/s** | **500,000x faster.** Specialized hardware that rules the world. | + +## 4. Technical Analysis & Conclusion +**Did we beat the market?** +**No.** We used modern, safe tools (Rust + WebGPU) which prioritize reliability over raw speed. The "Old Masters" use dangerous, complex code (Assembly/OpenCL) to squeeze every last drop of performance (~3x faster than us). + +**What did we achieve?** +We proved that we can build a **Parallel Compute Engine** from scratch in 2 days that beats a CPU by 41x. +- **Limit Found:** To beat the Legacy Miners, we would need to abandon safe code and write raw Assembly language (Project Level: Extreme). +- **Limit Found:** To beat ASICs, Software is not enough; we need our own Hardware manufacturing. + +## 5. Future Outlook: The UET Paradigm Shift +**The Philosophical Discovery** +This research highlights a fundamental conflict between "Current Crypto" and the "UET Vision": + +| **Paradigm** | **Current Market (Bitcoin/ASIC)** | **UET Vision (Future)** | +| :--- | :--- | :--- | +| **Core Principle** | **Probability (Gambling)** | **Determinism (Fairness)** | +| **Mechanism** | Random Guessing (Rolling Dice) | Logic & Verification (Solving Truth) | +| **Reward Nature** | **High Variance:** You can have 100k GPUs and still lose to luck. | **Linear Justice:** 1 Unit of Energy = 1 Unit of Reward. No luck involved. | +| **Result** | Capitalist Competition (Winner Takes All) | True Meritocracy (You get exactly what you give) | + +**Conclusion:** +The current system forces us to "gamble" against massive factories, where the outcome is uncertain and requires averaging over decades. The UET paradigm aims to build a system where **Calculation equals Value** directly. Whether you have 1 CPU or 1,000,000 CPUs, you receive returns exactly proportional to your contribution—no gambling, no variance, just pure physics-based justice. diff --git a/services_and_experiments/uet_miner/Cargo.lock b/services_and_experiments/uet_miner/Cargo.lock new file mode 100644 index 000000000..ed128afbd --- /dev/null +++ b/services_and_experiments/uet_miner/Cargo.lock @@ -0,0 +1,1664 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.10.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.10.0", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "metal" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" +dependencies = [ + "bitflags 2.10.0", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "naga" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e380993072e52eef724eddfcde0ed013b0c023c3f0417336ed041aa9f076994e" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.10.0", + "cfg_aliases", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash", + "spirv", + "strum", + "termcolor", + "thiserror 2.0.18", + "unicode-xid", +] + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "range-alloc" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uet_miner" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytemuck", + "futures", + "hex", + "pollster", + "rayon", + "serde", + "serde_json", + "sha2", + "thiserror 1.0.69", + "tokio", + "tracing", + "tracing-subscriber", + "wgpu", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wgpu" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0b3436f0729f6cdf2e6e9201f3d39dc95813fad61d826c1ed07918b4539353" +dependencies = [ + "arrayvec", + "bitflags 2.10.0", + "cfg_aliases", + "document-features", + "js-sys", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "24.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f0aa306497a238d169b9dc70659105b4a096859a34894544ca81719242e1499" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.10.0", + "cfg_aliases", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash", + "smallvec", + "thiserror 2.0.18", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "24.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112f464674ca69f3533248508ee30cb84c67cf06c25ff6800685f5e0294e259" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.10.0", + "block", + "bytemuck", + "cfg_aliases", + "core-graphics-types", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys", + "objc", + "once_cell", + "ordered-float", + "parking_lot", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash", + "smallvec", + "thiserror 2.0.18", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows", + "windows-core", +] + +[[package]] +name = "wgpu-types" +version = "24.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ac044c0e76c03a0378e7786ac505d010a873665e2d51383dcff8dd227dc69c" +dependencies = [ + "bitflags 2.10.0", + "js-sys", + "log", + "web-sys", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "zmij" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1966f8ac2c1f76987d69a74d0e0f929241c10e78136434e3be70ff7f58f64214" diff --git a/services_and_experiments/uet_miner/Cargo.toml b/services_and_experiments/uet_miner/Cargo.toml new file mode 100644 index 000000000..16084d38d --- /dev/null +++ b/services_and_experiments/uet_miner/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "uet_miner" +version = "0.1.0" +edition = "2021" +authors = ["UET Research Team"] +description = "UET GPU Bitcoin Miner with Stratum V1 support" + +[dependencies] +# Async runtime +tokio = { version = "1", features = ["full"] } + +# Serialization +anyhow = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# Crypto +thiserror = "1.0" +sha2 = "0.10" +hex = "0.4" +sha3 = "0.10" +blake3 = "1.5" + +# Logging +tracing = "0.1" +tracing-subscriber = "0.3" + +# Utils +futures = "0.3" +rayon = "1.10" + +# GPU (Phase 2 - AMD RX 6600 XT via Vulkan) +wgpu = "0.20" +bytemuck = { version = "1", features = ["derive"] } +pollster = "0.3" + +# UET Core for equation solving +uet_core = { path = "../uet_core" } +ndarray = "0.15" +uet_security = { path = "../uet_security" } + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 diff --git a/services_and_experiments/uet_miner/NICEHASH_SETUP.md b/services_and_experiments/uet_miner/NICEHASH_SETUP.md new file mode 100644 index 000000000..5c3f8277b --- /dev/null +++ b/services_and_experiments/uet_miner/NICEHASH_SETUP.md @@ -0,0 +1,85 @@ +# 🔧 NiceHash Setup Guide + +ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šāšƒā¸Šāš‰ā¸ā¸ąā¸š UET Rust Bitcoin Miner + +``` +wsl -d Ubuntu -- bash -c "cd /mnt/c/Users/santa/Desktop/lad/Lab_uet_harness_v0.9.0/docs/topics/0.18_Mathnicry/rust_miner && ./target/release/uet_miner" +``` + +--- + +## 📝 ā¸‚ā¸ąāš‰ā¸™ā¸•ā¸­ā¸™ā¸ā¸˛ā¸Ŗ Setup NiceHash + +### 1. ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡/Login NiceHash Account + +1. āš„ā¸›ā¸—ā¸ĩāšˆ https://www.nicehash.com/ +2. ⏄ā¸Ĩ⏴⏁ **Register** ā¸Ģ⏪⏎⏭ **Login** +3. ⏗⏺ **KYC/KYB Verification** (ā¸•āš‰ā¸­ā¸‡ā¸—ā¸ŗā¸āšˆā¸­ā¸™ā¸–ā¸ļā¸‡ā¸ˆā¸° mine āš„ā¸”āš‰!) + - āšƒā¸Šāš‰ Passport ā¸Ģā¸Ŗā¸ˇā¸­ā¸šā¸ąā¸•ā¸Ŗā¸›ā¸Ŗā¸°ā¸Šā¸˛ā¸Šā¸™ + - ā¸­ā¸˛ā¸ˆāšƒā¸Šāš‰āš€ā¸§ā¸Ĩ⏞ 1-3 ā¸§ā¸ąā¸™ + +### 2. āšƒā¸Šāš‰ Stratum Generator (ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸š Custom Miner) + +⏈⏞⏁ā¸Ģā¸™āš‰ā¸˛ā¸—ā¸ĩāšˆāš€ā¸Ģāš‡ā¸™ āšƒā¸Ģāš‰āš€ā¸Ĩ⏎⏭⏁ **"Stratum generator"** (ā¸Ĩāšˆā¸˛ā¸‡ā¸‹āš‰ā¸˛ā¸ĸ) + +![NiceHash Options](uploaded_media_1769947280829.png) + +**Stratum Generator** ā¸ˆā¸°āšƒā¸Ģāš‰: +- Pool URL: `sha256.auto.nicehash.com` +- Port: `3334` (ā¸Ģ⏪⏎⏭ 443 ā¸–āš‰ā¸˛āšƒā¸Šāš‰ SSL) +- Username format: `YOUR_WALLET.WORKER_NAME` + +### 3. ā¸Ģ⏞ Wallet Address + +1. āš„ā¸›ā¸—ā¸ĩāšˆ https://www.nicehash.com/my/wallet +2. ⏄ā¸Ĩ⏴⏁ **Deposit** +3. Copy **BTC Wallet Address** ⏂⏭⏇⏄⏏⏓ +4. āšƒā¸Ēāšˆāšƒā¸™ config: + +```rust +// src/main.rs - āšā¸āš‰āš„ā¸‚ā¸•ā¸Ŗā¸‡ā¸™ā¸ĩāš‰ +wallet: "YOUR_BTC_WALLET_HERE".to_string(), +worker: "RX6600XT".to_string(), // ā¸Šā¸ˇāšˆā¸­ worker +``` + +### 4. Verify Mining + +1. Run miner +2. āš„ā¸›ā¸—ā¸ĩāšˆ https://www.nicehash.com/my/rig-manager +3. ā¸„ā¸§ā¸Ŗāš€ā¸Ģāš‡ā¸™ worker ⏂⏭⏇⏄⏏⏓ online + +--- + +## âš ī¸ Important Notes + +| āš€ā¸Ŗā¸ˇāšˆā¸­ā¸‡ | ⏪⏞ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸ⏔ | +|--------|------------| +| **KYC Required** | NiceHash ā¸•āš‰ā¸­ā¸‡ verify ā¸•ā¸ąā¸§ā¸•ā¸™ā¸āšˆā¸­ā¸™ mine āš„ā¸”āš‰ | +| **Minimum Payout** | 0.001 BTC (~$50-100) | +| **SHA256 GPU** | āš„ā¸Ąāšˆ profitable (ASIC ā¸Šā¸™ā¸°) āšā¸•āšˆāš€ā¸Ŗā¸ĩā¸ĸā¸™ā¸Ŗā¸šāš‰āš„ā¸”āš‰! | +| **Alternative** | āšƒā¸Šāš‰ Testnet pool ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸š test ⏟⏪ā¸ĩ | + +--- + +## đŸ§Ē ⏗⏔ā¸Ēā¸­ā¸šā¸ā¸ąā¸š Testnet Pool (⏟⏪ā¸ĩ) + +ā¸–āš‰ā¸˛āš„ā¸Ąāšˆā¸­ā¸ĸ⏞⏁⏗⏺ KYC ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āšƒā¸Šāš‰ Testnet: + +```rust +pool_url: "solo.ckpool.org".to_string(), +pool_port: 3333, +wallet: "YOUR_TESTNET_ADDRESS".to_string(), +``` + +--- + +## 🔗 Quick Links + +- [NiceHash Dashboard](https://www.nicehash.com/my/dashboard) +- [Rig Manager](https://www.nicehash.com/my/rig-manager) +- [Mining Calculator](https://www.nicehash.com/profitability-calculator) +- [Stratum Protocol Docs](https://www.nicehash.com/blog/post/blockchain-basics-mining-job) + +--- + +*📅 ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āš€ā¸Ąā¸ˇāšˆā¸­: 2026-02-01 | ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸š: UET Rust Bitcoin Miner* diff --git a/services_and_experiments/uet_miner/UET_NETWORK_WHITEPAPER.md.resolved b/services_and_experiments/uet_miner/UET_NETWORK_WHITEPAPER.md.resolved new file mode 100644 index 000000000..fd9edf9ac --- /dev/null +++ b/services_and_experiments/uet_miner/UET_NETWORK_WHITEPAPER.md.resolved @@ -0,0 +1,55 @@ +# 📜 UET Network Whitepaper (Draft v0.1) +**Title:** The Scientific Sovereign Currency (SSC) +**Concept:** A decentralized global fund backed by useful scientific work. + +## 1. The Core Philosophy (ā¸›ā¸Ŗā¸ąā¸Šā¸ā¸˛ā¸Ģā¸Ĩā¸ąā¸) +**"Everyone is the Whale."** +* **Old World:** Money is printed by debt (Fiat) or mined by factories (Bitcoin). Wealth concentrates at the top. +* **UET World:** Money is generated by **"Useful Work"** (solving physics/science problems) from the CPU of every human. +* **Goal:** To create a "Global Sovereign Wealth Fund" owned by the public, not a government. + +## 2. Monetary Policy (ā¸™āš‚ā¸ĸ⏚⏞ā¸ĸā¸ā¸˛ā¸Ŗāš€ā¸‡ā¸´ā¸™) +**"The Vampire Backing Strategy" (⏁ā¸Ĩā¸ĸā¸¸ā¸—ā¸˜āšŒā¸ā¸Ĩ⏎⏙⏁⏴⏙⏗⏏⏙⏙⏴ā¸ĸā¸Ą)** +To prevent inflation and ensure stability, UET Coin is not just "air money"; it is **fully backed** by existing global assets. + +1. **Mining (Generation):** + * Citizens contribute CPU power to solve Global Challenges (Proof of Useful Work). + * **Reward:** They receive **UET Coins**. + +2. **The Black Hole Mechanism (Reserve Fund):** + * The value generated from solving these problems (IP, Scientific Data, Optimization Services) is sold to the market. + * Revenue helps buy **Bitcoin, Gold, and Stocks** from the open market. + * These assets are locked in the **"UET Global Reserve"**. + +3. **Stability (Pegging):** + * UET Coin value is backed by this growing basket of assets. + * As UET grows, it "absorbs" the value of Bitcoin and traditional markets, effectively transferring ownership from elite holders to the UET public miners. + +## 3. Technology (āš€ā¸—ā¸„āš‚ā¸™āš‚ā¸Ĩā¸ĸā¸ĩ) +* **Consensus:** **Proof of Useful Work (PoUW)**. + * No random guessing (No SHA-256 gambling). + * Work = Solving Physics/UET Equations. + * Hardware Neutrality: Linear scaling. 1 core = 1 unit. No ASIC advantage. +* **Security:** Backed by **Memory Safety (Rust)** to ensure network stability and protect host devices. + +## 4. Economic Impact (⏜ā¸Ĩā¸ā¸Ŗā¸°ā¸—ā¸šāš€ā¸¨ā¸Ŗā¸Šā¸ā¸ā¸´ā¸ˆ) +* **Inflation Control:** Unlike Fiat which prints infinite money, UET is backed by real finite assets + real scientific output. +* **Wealth Redistribution:** The "Reserve Fund" acts like a global ETF. By mining UET, a farmer in Thailand effectively owns a fraction of the world's Bitcoin and Gold. +* **The End Game:** A transition from "Financial Capitalism" (Money chasing Money) to **"Scientific Meritocracy"** (Wealth from Wisdom). + +## 5. Strategic & Evolutionary Roadmap (ā¸ĸā¸¸ā¸—ā¸˜ā¸¨ā¸˛ā¸Ēā¸•ā¸ŖāšŒāšā¸Ĩā¸°ā¸§ā¸´ā¸§ā¸ąā¸’ā¸™ā¸˛ā¸ā¸˛ā¸Ŗ) +**1. The Self-Evolving Equation (ā¸Ēā¸Ąā¸ā¸˛ā¸Ŗā¸—ā¸ĩāšˆā¸§ā¸´ā¸§ā¸ąā¸’ā¸™ā¸˛ā¸ā¸˛ā¸Ŗāš„ā¸”āš‰āš€ā¸­ā¸‡)** +* **Concept:** The UET equation is not static. As millions of people use it to solve problems, the system identifies "Unsolvable Nodes". +* **Rule 12:** The system learns from these failures. It is a "Generic AI Scientist" that evolves its own logic. The more it is used, the smarter it becomes. + +**2. Topological Unity (āš€ā¸­ā¸ā¸ ā¸˛ā¸žā¸—ā¸˛ā¸‡ā¸—ā¸­ā¸žā¸­āš‚ā¸Ĩā¸ĸā¸ĩ)** +* **Concept:** Just as different geometric shapes (triangle, circle) can exist in the same space without conflict, different stakeholders can join UET. +* **Harmony:** Capitalists (Investors), Socialists (Workers), and Scientists (Developers) work together. The logic doesn't conflict; it integrates. + +**3. Geopolitical Neutrality (ā¸„ā¸§ā¸˛ā¸Ąāš€ā¸›āš‡ā¸™ā¸ā¸Ĩā¸˛ā¸‡ā¸—ā¸˛ā¸‡ā¸ ā¸šā¸Ąā¸´ā¸Ŗā¸ąā¸ā¸¨ā¸˛ā¸Ēā¸•ā¸ŖāšŒ)** +* **The "China Option":** A centralized power (like China) could adopt UET to instantly scale the network (Unity Command). +* **The "Democratic Option":** Free nations adopt it as a tool for public wealth distribution. +* **Conclusion:** The UET algorithm is **Neutral Truth**. It serves whoever respects the laws of physics, making it the ultimate tool for global balance. + +--- +> *"We do not aim to destroy the old currencies. We aim to buy them all, and give them back to everyone."* diff --git a/services_and_experiments/uet_miner/production_miner_plan.txt b/services_and_experiments/uet_miner/production_miner_plan.txt new file mode 100644 index 000000000..b655df6c4 --- /dev/null +++ b/services_and_experiments/uet_miner/production_miner_plan.txt @@ -0,0 +1,13 @@ +# production_miner.py (Internal Implementation Plan) + +This script will be the final bridge to financial sovereignty. + +## Proposed Strategy +1. **Network Layer:** Use `asyncio` for non-blocking Stratum communication. +2. **Resonance Layer:** Integrate the `field_resonance_score` from Alpha with the `prime_anchors` from Siege. +3. **Hardware Layer:** Implement multi-processing to maximize hashrate. + +## Configuration Details +- **Username:** `santazazagamer` (Derived from email/default) +- **Worker ID:** `Santa001` +- **Pool URL:** `stratum+tcp://btc.viabtc.io:3333` diff --git a/services_and_experiments/uet_miner/src/anti_cheat.rs b/services_and_experiments/uet_miner/src/anti_cheat.rs new file mode 100644 index 000000000..907d0134c --- /dev/null +++ b/services_and_experiments/uet_miner/src/anti_cheat.rs @@ -0,0 +1,172 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Anti-cheat controls for Uet-Cash mining +pub struct AntiCheatController { + used_nonces: HashSet, + epoch_start: u64, + epoch_duration: u64, +} + +#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct UsedNonce { + pub task_id: String, + pub node_id: String, + pub nonce: u64, +} + +impl AntiCheatController { + pub fn new(epoch_duration_seconds: u64) -> Self { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + Self { + used_nonces: HashSet::new(), + epoch_start: now / epoch_duration_seconds * epoch_duration_seconds, + epoch_duration: epoch_duration_seconds, + } + } + + /// Check if nonce has been used (replay protection) + pub fn is_nonce_used(&self, task_id: &str, node_id: &str, nonce: u64) -> bool { + let used_nonce = UsedNonce { + task_id: task_id.to_string(), + node_id: node_id.to_string(), + nonce, + }; + + self.used_nonces.contains(&used_nonce) + } + + /// Mark nonce as used + pub fn mark_nonce_used(&mut self, task_id: &str, node_id: &str, nonce: u64) { + let used_nonce = UsedNonce { + task_id: task_id.to_string(), + node_id: node_id.to_string(), + nonce, + }; + + self.used_nonces.insert(used_nonce); + } + + /// Check if we're in a new epoch and reset if needed + pub fn check_epoch(&mut self) { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let current_epoch_start = now / self.epoch_duration * self.epoch_duration; + + if current_epoch_start != self.epoch_start { + self.epoch_start = current_epoch_start; + self.used_nonces.clear(); + } + } + + /// Get current epoch start time + pub fn epoch_start(&self) -> u64 { + self.epoch_start + } + + /// Get epoch duration + pub fn epoch_duration(&self) -> u64 { + self.epoch_duration + } +} + +/// Fraud proof for challenging invalid proofs +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FraudProof { + pub block_hash: String, + pub proof_index: usize, + pub reason: FraudReason, + pub challenger_id: String, + pub timestamp: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum FraudReason { + InvalidDifficulty, + InvalidSignature, + ReplayAttack, + StolenProof, +} + +impl FraudProof { + pub fn new( + block_hash: String, + proof_index: usize, + reason: FraudReason, + challenger_id: String, + ) -> Self { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + Self { + block_hash, + proof_index, + reason, + challenger_id, + timestamp, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_replay_protection() { + let mut controller = AntiCheatController::new(3600); // 1 hour epoch + + // First use should be allowed + assert!(!controller.is_nonce_used("task1", "node1", 100)); + controller.mark_nonce_used("task1", "node1", 100); + + // Second use should be blocked + assert!(controller.is_nonce_used("task1", "node1", 100)); + + // Different nonce should be allowed + assert!(!controller.is_nonce_used("task1", "node1", 101)); + + // Different node should be allowed + assert!(!controller.is_nonce_used("task1", "node2", 100)); + } + + #[test] + fn test_epoch_reset() { + let mut controller = AntiCheatController::new(1); // 1 second epoch for testing + + controller.mark_nonce_used("task1", "node1", 100); + assert!(controller.is_nonce_used("task1", "node1", 100)); + + // Wait for epoch to pass + std::thread::sleep(std::time::Duration::from_millis(1100)); + controller.check_epoch(); + + // After epoch reset, nonce should be available again + assert!(!controller.is_nonce_used("task1", "node1", 100)); + } + + #[test] + fn test_fraud_proof() { + let fraud_proof = FraudProof::new( + "block_hash".to_string(), + 0, + FraudReason::InvalidDifficulty, + "challenger".to_string(), + ); + + assert_eq!(fraud_proof.block_hash, "block_hash"); + assert_eq!(fraud_proof.proof_index, 0); + assert!(matches!(fraud_proof.reason, FraudReason::InvalidDifficulty)); + assert_eq!(fraud_proof.challenger_id, "challenger"); + } +} diff --git a/services_and_experiments/uet_miner/src/benchmark.rs b/services_and_experiments/uet_miner/src/benchmark.rs new file mode 100644 index 000000000..060ef2ce7 --- /dev/null +++ b/services_and_experiments/uet_miner/src/benchmark.rs @@ -0,0 +1,54 @@ +use crate::hash::sha256::sha256d; +use rayon::prelude::*; +use std::time::Instant; +use tracing::info; + +pub fn run_cpu_benchmark() { + info!("========================================"); + info!("đŸ§Ē CPU BENCHMARK MODE"); + info!("========================================"); + + // 1. Single Threaded + info!("Running Single-Threaded Benchmark (5s)..."); + let start = Instant::now(); + let mut hashes_single = 0u64; + let duration = 5.0; // seconds + let mut data = [0u8; 80]; // Mock block header + + while start.elapsed().as_secs_f64() < duration { + // Simple mutation to prevent optimization + data[0] = data[0].wrapping_add(1); + let _ = sha256d(&data); + hashes_single += 1; + } + + let elapsed_single = start.elapsed().as_secs_f64(); + let mh_single = (hashes_single as f64 / elapsed_single) / 1_000_000.0; + info!("Single-Threaded Speed: {:.2} MH/s", mh_single); + + // 2. Multi-Threaded + info!("Running Multi-Threaded Benchmark (Rayon)..."); + + // Estimate count for decent duration (aim for ~100M hashes to stress test) + // If single thread is ~2MH/s, multi on 8 core might be ~16MH/s. + // Let's try to run for a fixed huge amount. + let batch_size = 10_000_000; + + let start_multi = Instant::now(); + (0..batch_size).into_par_iter().for_each(|i| { + let mut d = [0u8; 80]; + d[0] = (i % 255) as u8; + let _ = sha256d(&d); + }); + let elapsed_multi = start_multi.elapsed().as_secs_f64(); + + let mh_multi = (batch_size as f64 / elapsed_multi) / 1_000_000.0; + info!("Multi-Threaded Speed: {:.2} MH/s (Sample: 10M hashes)", mh_multi); + + info!("========================================"); + info!("RESULTS:"); + info!("Single-Core: {:.2} MH/s", mh_single); + info!("Multi-Core: {:.2} MH/s", mh_multi); + info!("Speedup: {:.2}x", mh_multi / mh_single); + info!("========================================"); +} diff --git a/services_and_experiments/uet_miner/src/gpu/compute.rs b/services_and_experiments/uet_miner/src/gpu/compute.rs new file mode 100644 index 000000000..29ff782d6 --- /dev/null +++ b/services_and_experiments/uet_miner/src/gpu/compute.rs @@ -0,0 +1,260 @@ +//! GPU compute engine for SHA256 mining. +//! +//! Uses wgpu for cross-platform GPU compute. +//! Optimized for AMD RDNA2 (RX 6600 XT) via Vulkan backend. + +use std::sync::Arc; +use bytemuck::{Pod, Zeroable}; +use wgpu::util::DeviceExt; +use tracing::{info, warn, debug}; + +use super::shader::SHA256_SHADER; + +/// Mining job data sent to GPU +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +pub struct GpuMiningJob { + pub header_base: [u32; 19], // 76 bytes (block header without nonce) + pub start_nonce: u32, + pub target_hi: u32, // High bits of target + pub target_lo: u32, // Low bits of target +} + +/// Mining result from GPU +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable, Debug)] +pub struct GpuMiningResult { + pub found: u32, + pub nonce: u32, + pub hash: [u32; 8], +} + +impl Default for GpuMiningResult { + fn default() -> Self { + Self { + found: 0, + nonce: 0, + hash: [0; 8], + } + } +} + +/// GPU compute context +pub struct GpuMiner { + device: wgpu::Device, + queue: wgpu::Queue, + pipeline: wgpu::ComputePipeline, + bind_group_layout: wgpu::BindGroupLayout, + workgroup_size: u32, +} + +impl GpuMiner { + /// Initialize GPU miner with wgpu + pub async fn new() -> Result> { + // Request GPU instance (Vulkan for AMD) + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::VULKAN | wgpu::Backends::DX12, + ..Default::default() + }); + + // Get adapter (prefer high-performance GPU) + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: None, + force_fallback_adapter: false, + }) + .await + .ok_or("Failed to find GPU adapter")?; + + let adapter_info = adapter.get_info(); + info!("[GPU] Found: {} ({:?})", adapter_info.name, adapter_info.backend); + info!("[GPU] Driver: {}", adapter_info.driver); + + // Request device + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: Some("UET Miner"), + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::default(), + memory_hints: wgpu::MemoryHints::Performance, + }, + None, + ) + .await?; + + // Create shader module + let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("SHA256 Shader"), + source: wgpu::ShaderSource::Wgsl(SHA256_SHADER.into()), + }); + + // Create bind group layout + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Mining Layout"), + entries: &[ + // Job input buffer + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + // Result output buffer + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::COMPUTE, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: false }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + }); + + // Create compute pipeline + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Mining Pipeline Layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { + label: Some("SHA256 Mining Pipeline"), + layout: Some(&pipeline_layout), + module: &shader_module, + entry_point: Some("main"), + compilation_options: Default::default(), + cache: None, + }); + + // Workgroup size matches shader (128 threads) + let workgroup_size = 128u32; + + info!("[GPU] Pipeline created successfully"); + + Ok(Self { + device, + queue, + pipeline, + bind_group_layout, + workgroup_size, + }) + } + + /// Run GPU mining for a batch of nonces + /// + /// # Arguments + /// * `job` - Mining job data + /// * `num_nonces` - Number of nonces to search + /// + /// # Returns + /// Mining result (if found) and hashes computed + pub fn mine_batch(&self, job: GpuMiningJob, num_nonces: u32) -> (GpuMiningResult, u32) { + // Create input buffer + let job_buffer = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Job Buffer"), + contents: bytemuck::bytes_of(&job), + usage: wgpu::BufferUsages::STORAGE, + }); + + // Create result buffer + let result_size = std::mem::size_of::() as u64; + let result_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Result Buffer"), + size: result_size, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC, + mapped_at_creation: false, + }); + + // Create staging buffer for reading results + let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Staging Buffer"), + size: result_size, + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + // Create bind group + let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Mining Bind Group"), + layout: &self.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: job_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: result_buffer.as_entire_binding(), + }, + ], + }); + + // Create command encoder + let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Mining Encoder"), + }); + + // Dispatch compute + { + let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: Some("Mining Pass"), + timestamp_writes: None, + }); + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &bind_group, &[]); + + // Dispatch workgroups + let num_workgroups = (num_nonces + self.workgroup_size - 1) / self.workgroup_size; + pass.dispatch_workgroups(num_workgroups, 1, 1); + } + + // Copy result to staging buffer + encoder.copy_buffer_to_buffer(&result_buffer, 0, &staging_buffer, 0, result_size); + + // Submit commands + self.queue.submit(Some(encoder.finish())); + + // Read result + let buffer_slice = staging_buffer.slice(..); + let (tx, rx) = std::sync::mpsc::channel(); + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + tx.send(result).unwrap(); + }); + self.device.poll(wgpu::Maintain::Wait); + rx.recv().unwrap().unwrap(); + + let data = buffer_slice.get_mapped_range(); + let result: GpuMiningResult = *bytemuck::from_bytes(&data); + drop(data); + staging_buffer.unmap(); + + (result, num_nonces) + } + + /// Get device info + pub fn device_name(&self) -> String { + // Device name is stored in adapter, we keep it simple here + "AMD RX 6600 XT (Vulkan)".to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_gpu_job_size() { + // Verify struct sizes for GPU alignment + assert_eq!(std::mem::size_of::(), 88); // 19*4 + 4 + 4 + 4 + assert_eq!(std::mem::size_of::(), 40); // 4 + 4 + 8*4 + } +} diff --git a/services_and_experiments/uet_miner/src/gpu/mod.rs b/services_and_experiments/uet_miner/src/gpu/mod.rs new file mode 100644 index 000000000..5e2f5b763 --- /dev/null +++ b/services_and_experiments/uet_miner/src/gpu/mod.rs @@ -0,0 +1,6 @@ +//! GPU compute module for SHA256 mining. +//! +//! Uses wgpu for cross-platform GPU compute (Vulkan for AMD). + +pub mod compute; +pub mod shader; diff --git a/services_and_experiments/uet_miner/src/gpu/shader.rs b/services_and_experiments/uet_miner/src/gpu/shader.rs new file mode 100644 index 000000000..52809859b --- /dev/null +++ b/services_and_experiments/uet_miner/src/gpu/shader.rs @@ -0,0 +1,4 @@ +//! WGSL shader source for compilation. + +/// SHA256 double-hash compute shader source +pub const SHA256_SHADER: &str = include_str!("shader.wgsl"); diff --git a/services_and_experiments/uet_miner/src/gpu/shader.wgsl b/services_and_experiments/uet_miner/src/gpu/shader.wgsl new file mode 100644 index 000000000..b5cd76b22 --- /dev/null +++ b/services_and_experiments/uet_miner/src/gpu/shader.wgsl @@ -0,0 +1,221 @@ +// SHA256 Double Hash - WGSL Compute Shader +// Optimized for GPU parallel mining +// Works with AMD RX 6600 XT via Vulkan backend + +// Round constants for SHA256 +var K: array = array( + 0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, + 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u, + 0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, + 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u, + 0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, + 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau, + 0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, + 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u, + 0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u, + 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u, + 0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, + 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u, + 0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, + 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u, + 0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, + 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u +); + +// Initial hash values +const H0: u32 = 0x6a09e667u; +const H1: u32 = 0xbb67ae85u; +const H2: u32 = 0x3c6ef372u; +const H3: u32 = 0xa54ff53au; +const H4: u32 = 0x510e527fu; +const H5: u32 = 0x9b05688cu; +const H6: u32 = 0x1f83d9abu; +const H7: u32 = 0x5be0cd19u; + +// Input block header (80 bytes = 20 u32s) +struct BlockHeader { + data: array, +} + +// Mining job input +struct MiningJob { + header_base: array, // 76 bytes without nonce + start_nonce: u32, + target_hi: u32, // Upper 32 bits of target for quick check + target_lo: u32, // Lower 32 bits +} + +// Mining result output +struct MiningResult { + found: u32, + nonce: u32, + hash: array, +} + +@group(0) @binding(0) var job: MiningJob; +@group(0) @binding(1) var result: MiningResult; + +// Bitwise rotation right +fn rotr(x: u32, n: u32) -> u32 { + return (x >> n) | (x << (32u - n)); +} + +// SHA256 compression functions +fn ch(x: u32, y: u32, z: u32) -> u32 { + return (x & y) ^ (~x & z); +} + +fn maj(x: u32, y: u32, z: u32) -> u32 { + return (x & y) ^ (x & z) ^ (y & z); +} + +fn sigma0(x: u32) -> u32 { + return rotr(x, 2u) ^ rotr(x, 13u) ^ rotr(x, 22u); +} + +fn sigma1(x: u32) -> u32 { + return rotr(x, 6u) ^ rotr(x, 11u) ^ rotr(x, 25u); +} + +fn gamma0(x: u32) -> u32 { + return rotr(x, 7u) ^ rotr(x, 18u) ^ (x >> 3u); +} + +fn gamma1(x: u32) -> u32 { + return rotr(x, 17u) ^ rotr(x, 19u) ^ (x >> 10u); +} + +// SHA256 hash of 80-byte block (block header) +fn sha256_80(header: array) -> array { + var h = array(H0, H1, H2, H3, H4, H5, H6, H7); + var w: array; + + // First block (64 bytes) + for (var i = 0u; i < 16u; i++) { + w[i] = header[i]; + } + + // Message schedule + for (var i = 16u; i < 64u; i++) { + w[i] = gamma1(w[i-2u]) + w[i-7u] + gamma0(w[i-15u]) + w[i-16u]; + } + + // Compression + var a = h[0]; var b = h[1]; var c = h[2]; var d = h[3]; + var e = h[4]; var f = h[5]; var g = h[6]; var hh = h[7]; + + for (var i = 0u; i < 64u; i++) { + let t1 = hh + sigma1(e) + ch(e, f, g) + K[i] + w[i]; + let t2 = sigma0(a) + maj(a, b, c); + hh = g; g = f; f = e; e = d + t1; + d = c; c = b; b = a; a = t1 + t2; + } + + h[0] += a; h[1] += b; h[2] += c; h[3] += d; + h[4] += e; h[5] += f; h[6] += g; h[7] += hh; + + // Second block (16 bytes of header + padding) + // header[16..19] + 0x80 padding + length + var block2: array; + block2[0] = header[16]; + block2[1] = header[17]; + block2[2] = header[18]; + block2[3] = header[19]; + block2[4] = 0x80000000u; // Padding bit + for (var i = 5u; i < 15u; i++) { + block2[i] = 0u; + } + block2[15] = 640u; // 80 bytes * 8 bits = 640 bits + + // Message schedule for block 2 + for (var i = 0u; i < 16u; i++) { + w[i] = block2[i]; + } + for (var i = 16u; i < 64u; i++) { + w[i] = gamma1(w[i-2u]) + w[i-7u] + gamma0(w[i-15u]) + w[i-16u]; + } + + // Compression for block 2 + a = h[0]; b = h[1]; c = h[2]; d = h[3]; + e = h[4]; f = h[5]; g = h[6]; hh = h[7]; + + for (var i = 0u; i < 64u; i++) { + let t1 = hh + sigma1(e) + ch(e, f, g) + K[i] + w[i]; + let t2 = sigma0(a) + maj(a, b, c); + hh = g; g = f; f = e; e = d + t1; + d = c; c = b; b = a; a = t1 + t2; + } + + h[0] += a; h[1] += b; h[2] += c; h[3] += d; + h[4] += e; h[5] += f; h[6] += g; h[7] += hh; + + return h; +} + +// SHA256 hash of 32-byte input (the first hash result) +fn sha256_32(input: array) -> array { + var h = array(H0, H1, H2, H3, H4, H5, H6, H7); + var w: array; + + // Single block: 32 bytes of input + padding + for (var i = 0u; i < 8u; i++) { + w[i] = input[i]; + } + w[8] = 0x80000000u; // Padding + for (var i = 9u; i < 15u; i++) { + w[i] = 0u; + } + w[15] = 256u; // 32 bytes * 8 bits = 256 bits + + // Message schedule + for (var i = 16u; i < 64u; i++) { + w[i] = gamma1(w[i-2u]) + w[i-7u] + gamma0(w[i-15u]) + w[i-16u]; + } + + // Compression + var a = h[0]; var b = h[1]; var c = h[2]; var d = h[3]; + var e = h[4]; var f = h[5]; var g = h[6]; var hh = h[7]; + + for (var i = 0u; i < 64u; i++) { + let t1 = hh + sigma1(e) + ch(e, f, g) + K[i] + w[i]; + let t2 = sigma0(a) + maj(a, b, c); + hh = g; g = f; f = e; e = d + t1; + d = c; c = b; b = a; a = t1 + t2; + } + + h[0] += a; h[1] += b; h[2] += c; h[3] += d; + h[4] += e; h[5] += f; h[6] += g; h[7] += hh; + + return h; +} + +@compute @workgroup_size(128) +fn main(@builtin(global_invocation_id) global_id: vec3) { + let nonce = job.start_nonce + global_id.x; + + // Build full 80-byte header with this nonce + var header: array; + for (var i = 0u; i < 19u; i++) { + header[i] = job.header_base[i]; + } + header[19] = nonce; // Nonce at end (little-endian) + + // Double SHA256 + let hash1 = sha256_80(header); + let hash2 = sha256_32(hash1); + + // Check if hash meets target (quick check on first word - big endian) + // Bitcoin hash comparison: hash must be <= target + // Hash is in big-endian, so we compare from hash[0] (most significant) + if (hash2[0] < job.target_hi || + (hash2[0] == job.target_hi && hash2[1] <= job.target_lo)) { + // Found a valid nonce! + // Simple assignment - race condition acceptable for mining + // (any valid nonce works, doesn't matter which thread wins) + result.found = 1u; + result.nonce = nonce; + for (var i = 0u; i < 8u; i++) { + result.hash[i] = hash2[i]; + } + } +} diff --git a/services_and_experiments/uet_miner/src/hash/mod.rs b/services_and_experiments/uet_miner/src/hash/mod.rs new file mode 100644 index 000000000..64c2e994b --- /dev/null +++ b/services_and_experiments/uet_miner/src/hash/mod.rs @@ -0,0 +1,3 @@ +//! SHA256 double-hash implementation for Bitcoin mining. + +pub mod sha256; diff --git a/services_and_experiments/uet_miner/src/hash/sha256.rs b/services_and_experiments/uet_miner/src/hash/sha256.rs new file mode 100644 index 000000000..9146e1548 --- /dev/null +++ b/services_and_experiments/uet_miner/src/hash/sha256.rs @@ -0,0 +1,113 @@ +//! SHA256 double-hash (SHA256d) implementation for Bitcoin. +//! +//! Bitcoin uses double SHA256 (SHA256(SHA256(data))) for: +//! - Block header hashing +//! - Merkle tree computation +//! - Coinbase transaction hashing + +use sha2::{Sha256, Digest}; + +/// Compute double SHA256 hash (Bitcoin standard). +/// +/// # Arguments +/// * `data` - Raw bytes to hash +/// +/// # Returns +/// 32-byte hash result +pub fn sha256d(data: &[u8]) -> [u8; 32] { + let first_hash = Sha256::digest(data); + let second_hash = Sha256::digest(&first_hash); + + let mut result = [0u8; 32]; + result.copy_from_slice(&second_hash); + result +} + +/// Reverse bytes in a 32-byte array (for endianness conversion). +/// +/// Used for byte-swapping previous hash and merkle root in block header. +pub fn reverse_bytes_32(input: &[u8; 32]) -> [u8; 32] { + let mut result = *input; + result.reverse(); + result +} + +/// Decode hex string to bytes. +pub fn hex_to_bytes(hex: &str) -> Vec { + hex::decode(hex).unwrap_or_default() +} + +/// Encode bytes to hex string. +pub fn bytes_to_hex(bytes: &[u8]) -> String { + hex::encode(bytes) +} + +/// Reverse bytes in a hex string. +/// +/// Example: "aabbccdd" -> "ddccbbaa" +pub fn reverse_hex(hex: &str) -> String { + let bytes = hex_to_bytes(hex); + let reversed: Vec = bytes.into_iter().rev().collect(); + bytes_to_hex(&reversed) +} + +/// Check if hash meets difficulty target. +/// +/// Hash is interpreted as little-endian integer and compared. +/// +/// # Arguments +/// * `hash` - 32-byte hash +/// * `target` - Target value (hash must be <= target) +pub fn hash_meets_target(hash: &[u8; 32], target: u128) -> bool { + // Compare first 16 bytes (most significant in little-endian) + // This is a simplified check for pool difficulty + let hash_val = u128::from_le_bytes(hash[0..16].try_into().unwrap()); + hash_val <= target +} + +/// Convert pool difficulty to target value. +/// +/// Target = (0xFFFF << 208) / difficulty +/// Simplified for pool shares (not full 256-bit precision). +pub fn difficulty_to_target(difficulty: f64) -> u128 { + if difficulty <= 0.0 { + return u128::MAX; + } + + // Simplified target for pool shares + // Full precision would require 256-bit arithmetic + let base_target: f64 = 0xFFFF as f64 * (2.0_f64.powi(48)); + (base_target / difficulty) as u128 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sha256d() { + let data = b"hello bitcoin"; + let hash = sha256d(data); + assert_eq!(hash.len(), 32); + + // Verify deterministic + let hash2 = sha256d(data); + assert_eq!(hash, hash2); + } + + #[test] + fn test_reverse_hex() { + assert_eq!(reverse_hex("aabbccdd"), "ddccbbaa"); + assert_eq!(reverse_hex("0102030405060708"), "0807060504030201"); + } + + #[test] + fn test_difficulty_to_target() { + let target = difficulty_to_target(1.0); + assert!(target > 0); + + // Higher difficulty = lower target + let target_high = difficulty_to_target(10.0); + assert!(target_high < target); + } +} diff --git a/services_and_experiments/uet_miner/src/lib.rs b/services_and_experiments/uet_miner/src/lib.rs new file mode 100644 index 000000000..7ef0055dd --- /dev/null +++ b/services_and_experiments/uet_miner/src/lib.rs @@ -0,0 +1,8 @@ +pub mod hash; +pub mod mining; +pub mod uet_cash_block; +pub mod anti_cheat; + +pub use mining::uet_cash::{MiningTask, TaskFamily, ProofOfWork, UetCashMiner}; +pub use uet_cash_block::{UetCashBlock, BlockHeader, BlockBody, Transaction}; +pub use anti_cheat::{AntiCheatController, UsedNonce, FraudProof, FraudReason}; diff --git a/services_and_experiments/uet_miner/src/main.rs b/services_and_experiments/uet_miner/src/main.rs new file mode 100644 index 000000000..e7312ac39 --- /dev/null +++ b/services_and_experiments/uet_miner/src/main.rs @@ -0,0 +1,104 @@ +//! # UET GPU Bitcoin Miner +//! +//! A Rust implementation of a Bitcoin miner with Stratum V1 protocol support. +//! Designed for educational purposes and UET research. +//! +//! ## Architecture +//! - `stratum/` - Stratum protocol client (TCP + JSON-RPC) +//! - `mining/` - Block header construction +//! - `hash/` - SHA256 double-hash implementation (CPU baseline) +//! - `gpu/` - GPU compute module (wgpu/Vulkan for AMD RX 6600 XT) + +mod stratum; +mod mining; +mod hash; +mod gpu; +mod benchmark; + +use anyhow::Result; +use tracing::{info, Level}; +use tracing_subscriber::FmtSubscriber; + +use crate::stratum::StratumClient; + +/// Configuration for the miner +#[derive(Debug, Clone)] +pub struct MinerConfig { + pub pool_url: String, + pub pool_port: u16, + pub wallet: String, + pub worker: String, +} + +impl Default for MinerConfig { + fn default() -> Self { + Self { + // ViaBTC BCH Pool + pool_url: "bch.viabtc.io".to_string(), + pool_port: 3333, + wallet: "Santa001".to_string(), // ViaBTC account name + worker: "001".to_string(), // Worker ID + } + } +} + +#[tokio::main] +async fn main() -> Result<()> { + // Initialize logging + let subscriber = FmtSubscriber::builder() + .with_max_level(Level::INFO) + .with_target(false) + .finish(); + tracing::subscriber::set_global_default(subscriber)?; + + // Parse CLI arguments + let args: Vec = std::env::args().collect(); + + // Check for benchmark mode + if args.len() > 1 && args[1] == "benchmark" { + benchmark::run_cpu_benchmark(); + return Ok(()); + } + + info!("========================================"); + info!(" đŸĻ€ UET RUST BITCOIN MINER v0.2.0"); + info!(" Research Project - Topic 0.18"); + info!(" GPU Accelerated (AMD RX 6600 XT)"); + info!("========================================"); + let config = if args.len() >= 4 { + MinerConfig { + pool_url: args[1].clone(), + pool_port: args[2].parse().unwrap_or(3333), + wallet: args[3].clone(), + worker: if args.len() > 4 { args[4].clone() } else { "x".to_string() }, + } + } else { + info!("Usage: uet_miner [pass/worker]"); + info!("No args provided, using default (ViaBTC BCH)..."); + MinerConfig::default() + }; + + info!("Pool: {}:{}", config.pool_url, config.pool_port); + info!("Wallet: {}.{}", config.wallet, config.worker); + info!(""); + + // Initialize GPU + info!("[GPU] Initializing..."); + let gpu_miner = match gpu::compute::GpuMiner::new().await { + Ok(miner) => { + info!("[GPU] ✅ Initialized: {}", miner.device_name()); + Some(std::sync::Arc::new(miner)) + } + Err(e) => { + info!("[GPU] âš ī¸ GPU not available, using CPU only: {}", e); + None + } + }; + info!(""); + + // Create and run the stratum client + let mut client = StratumClient::new(config, gpu_miner); + client.run().await?; + + Ok(()) +} diff --git a/services_and_experiments/uet_miner/src/mining/block.rs b/services_and_experiments/uet_miner/src/mining/block.rs new file mode 100644 index 000000000..df20ae397 --- /dev/null +++ b/services_and_experiments/uet_miner/src/mining/block.rs @@ -0,0 +1,160 @@ +//! Bitcoin block header construction for mining. +//! +//! Block Header Structure (80 bytes): +//! - Version: 4 bytes (little-endian) +//! - Previous Hash: 32 bytes (byte-swapped) +//! - Merkle Root: 32 bytes (byte-swapped) +//! - Timestamp: 4 bytes (little-endian) +//! - Bits (difficulty): 4 bytes (little-endian) +//! - Nonce: 4 bytes (little-endian) + +use crate::hash::sha256::{sha256d, hex_to_bytes, reverse_hex}; + +/// Represents a mining job from the pool. +#[derive(Debug, Clone)] +pub struct MiningJob { + pub job_id: String, + pub prev_hash: String, + pub coinbase1: String, + pub coinbase2: String, + pub merkle_branches: Vec, + pub version: String, + pub nbits: String, + pub ntime: String, + pub clean_jobs: bool, +} + +/// Build coinbase transaction from parts. +/// +/// Coinbase = coinbase1 + extranonce1 + extranonce2 + coinbase2 +pub fn build_coinbase( + coinbase1: &str, + coinbase2: &str, + extranonce1: &str, + extranonce2: &str, +) -> Vec { + let coinbase_hex = format!("{}{}{}{}", coinbase1, extranonce1, extranonce2, coinbase2); + hex_to_bytes(&coinbase_hex) +} + +/// Calculate merkle root from coinbase hash and merkle branches. +pub fn calculate_merkle_root(coinbase: &[u8], merkle_branches: &[String]) -> [u8; 32] { + let mut merkle = sha256d(coinbase); + + for branch in merkle_branches { + let branch_bytes = hex_to_bytes(branch); + + // Concatenate merkle + branch and hash + let mut combined = Vec::with_capacity(64); + combined.extend_from_slice(&merkle); + combined.extend_from_slice(&branch_bytes); + + merkle = sha256d(&combined); + } + + merkle +} + +/// Build 80-byte block header. +/// +/// # Arguments +/// * `version` - Block version (hex, 8 chars) +/// * `prev_hash` - Previous block hash (hex, 64 chars) +/// * `merkle_root` - Merkle root (32 bytes) +/// * `ntime` - Timestamp (hex, 8 chars) +/// * `nbits` - Difficulty bits (hex, 8 chars) +/// * `nonce` - Nonce value +/// +/// # Returns +/// 80-byte block header +pub fn build_block_header( + version: &str, + prev_hash: &str, + merkle_root: &[u8; 32], + ntime: &str, + nbits: &str, + nonce: u32, +) -> [u8; 80] { + let mut header = [0u8; 80]; + + // Version: little-endian + let version_bytes = hex_to_bytes(&reverse_hex(version)); + header[0..4].copy_from_slice(&version_bytes[0..4]); + + // Previous hash: byte-swapped + let prev_hash_bytes = hex_to_bytes(&reverse_hex(prev_hash)); + header[4..36].copy_from_slice(&prev_hash_bytes); + + // Merkle root: byte-swapped + let reversed_merkle: Vec = merkle_root.iter().rev().copied().collect(); + header[36..68].copy_from_slice(&reversed_merkle); + + // nTime: little-endian + let ntime_bytes = hex_to_bytes(&reverse_hex(ntime)); + header[68..72].copy_from_slice(&ntime_bytes[0..4]); + + // nBits: little-endian + let nbits_bytes = hex_to_bytes(&reverse_hex(nbits)); + header[72..76].copy_from_slice(&nbits_bytes[0..4]); + + // Nonce: little-endian + header[76..80].copy_from_slice(&nonce.to_le_bytes()); + + header +} + +/// Hash block header and check if it meets target. +pub fn mine_single_nonce(header_base: &[u8; 76], nonce: u32, target: u128) -> Option<[u8; 32]> { + let mut header = [0u8; 80]; + header[0..76].copy_from_slice(header_base); + header[76..80].copy_from_slice(&nonce.to_le_bytes()); + + let hash = sha256d(&header); + + // Check if hash meets target (simplified) + let hash_val = u128::from_le_bytes(hash[0..16].try_into().unwrap()); + if hash_val <= target { + Some(hash) + } else { + None + } +} + +/// Format nonce for Stratum submission. +/// +/// Returns 8-character hex string (big-endian format). +pub fn format_nonce_for_submit(nonce: u32) -> String { + format!("{:08x}", nonce.swap_bytes()) +} + +/// Format extranonce2 for submission. +pub fn format_extranonce2(value: u32, size: usize) -> String { + format!("{:0width$x}", value, width = size * 2) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_coinbase() { + let cb = build_coinbase("01000000", "ffffffff", "08000001", "00000000"); + assert!(!cb.is_empty()); + } + + #[test] + fn test_format_nonce() { + let nonce = 0x12345678u32; + let formatted = format_nonce_for_submit(nonce); + assert_eq!(formatted.len(), 8); + } + + #[test] + fn test_format_extranonce2() { + let en2 = format_extranonce2(0, 4); + assert_eq!(en2, "00000000"); + + let en2 = format_extranonce2(255, 4); + assert_eq!(en2, "000000ff"); + } +} diff --git a/services_and_experiments/uet_miner/src/mining/mod.rs b/services_and_experiments/uet_miner/src/mining/mod.rs new file mode 100644 index 000000000..f813da859 --- /dev/null +++ b/services_and_experiments/uet_miner/src/mining/mod.rs @@ -0,0 +1,4 @@ +//! Mining block header construction. + +pub mod block; +pub mod uet_cash; diff --git a/services_and_experiments/uet_miner/src/mining/uet_cash.rs b/services_and_experiments/uet_miner/src/mining/uet_cash.rs new file mode 100644 index 000000000..d5d5125ef --- /dev/null +++ b/services_and_experiments/uet_miner/src/mining/uet_cash.rs @@ -0,0 +1,163 @@ +use ndarray::Array1; +use serde::{Deserialize, Serialize}; +use sha3::{Digest, Sha3_256}; +use blake3; +use std::time::Instant; +use uet_security::{CryptoSuite, HashAlgorithm, SignatureAlgorithm, Signer, Verifier, SignedEnvelope}; + +/// Task for Uet-Cash mining +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MiningTask { + pub task_id: String, + pub family: TaskFamily, + pub input_seed: Vec, + pub difficulty: u64, + pub created_at: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TaskFamily { + DeterministicSimulation, + OptimizationBounded, + EquilibriumCertificate, +} + +/// Proof of Work for Uet-Cash mining +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProofOfWork { + pub task_id: String, + pub node_id: String, + pub result_hash_hex: String, + pub verification_artifact: Vec, + pub runtime_ms: u64, + pub nonce: u64, + pub suite: CryptoSuite, + pub signature_hex: String, +} + +/// Miner for Uet-Cash +pub struct UetCashMiner { + task: MiningTask, + node_id: String, + suite: CryptoSuite, +} + +impl UetCashMiner { + pub fn new(task: MiningTask, node_id: String, suite: CryptoSuite) -> Self { + Self { task, node_id, suite } + } + + /// Mine by solving UET equation with nonce search + pub fn mine(&self, max_nonce: u64, signer: &S) -> Option { + let start = Instant::now(); + + for nonce in 0..max_nonce { + let proof = self.solve_with_nonce(nonce, signer); + + if self.verify_difficulty(&proof) { + let runtime = start.elapsed().as_millis() as u64; + + return Some(ProofOfWork { + task_id: self.task.task_id.clone(), + node_id: self.node_id.clone(), + result_hash_hex: proof.result_hash_hex, + verification_artifact: proof.verification_artifact, + nonce, + runtime_ms: runtime, + suite: self.suite.clone(), + signature_hex: proof.signature_hex, + }); + } + } + + None + } + + /// Solve UET equation with given nonce + fn solve_with_nonce(&self, nonce: u64, signer: &S) -> ProofOfWork { + // Combine input_seed with nonce + let mut seed = self.task.input_seed.clone(); + seed.push(nonce as f64); + + // Solve UET equation (simplified for now) + let result = self.solve_equation(&seed); + + // Create hash using SHA3 + let hash = self.create_hash(&result, nonce); + + // Create signature + let signature = self.create_signature(&hash, signer); + + ProofOfWork { + task_id: self.task.task_id.clone(), + node_id: self.node_id.clone(), + result_hash_hex: hash, + verification_artifact: result, + nonce, + runtime_ms: 0, + suite: self.suite.clone(), + signature_hex: signature, + } + } + + /// Solve UET equation (placeholder - will integrate with uet_core) + fn solve_equation(&self, seed: &[f64]) -> Vec { + // Simplified: hash the seed using SHA3 + let mut hasher = Sha3_256::new(); + for val in seed { + hasher.update(val.to_le_bytes()); + } + hasher.finalize().to_vec() + } + + /// Create hash from result and nonce using SHA3 + fn create_hash(&self, result: &[u8], nonce: u64) -> String { + let mut hasher = Sha3_256::new(); + hasher.update(result); + hasher.update(nonce.to_le_bytes()); + hex::encode(hasher.finalize()) + } + + /// Create signature for hash + fn create_signature(&self, hash: &str, signer: &S) -> String { + let sig = signer.sign(hash.as_bytes()).unwrap(); + hex::encode(sig) + } + + /// Verify if proof meets difficulty requirement + fn verify_difficulty(&self, proof: &ProofOfWork) -> bool { + // Check if hash starts with enough zeros (difficulty) + let leading_zeros = proof.result_hash_hex + .chars() + .take_while(|c| *c == '0') + .count(); + + // Difficulty = number of leading zeros required + leading_zeros >= (self.task.difficulty as usize).min(16) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use uet_security::{MockSigner, SignatureAlgorithm}; + + #[test] + fn test_mining() { + let task = MiningTask { + task_id: "test_task".to_string(), + family: TaskFamily::EquilibriumCertificate, + input_seed: vec![1.0, 2.0, 3.0], + difficulty: 2, // Require 2 leading zeros (easy for test) + created_at: 0, + }; + + let suite = CryptoSuite::default(); + let signer = MockSigner::new("node-a", SignatureAlgorithm::Dilithium3); + + let miner = UetCashMiner::new(task, "node-a".to_string(), suite); + let proof = miner.mine(1000000, &signer); // Higher max_nonce + + assert!(proof.is_some(), "Mining should find a valid proof"); + } +} diff --git a/services_and_experiments/uet_miner/src/stratum/client.rs b/services_and_experiments/uet_miner/src/stratum/client.rs new file mode 100644 index 000000000..7d50a5d06 --- /dev/null +++ b/services_and_experiments/uet_miner/src/stratum/client.rs @@ -0,0 +1,508 @@ +//! Stratum V1 TCP client implementation. +//! +//! Handles connection, authentication, job notification, and share submission. + +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::TcpStream; +use tokio::sync::Mutex; +use anyhow::{anyhow, Result}; +use tracing::{info, warn, error, debug}; + +use crate::MinerConfig; +use crate::hash::sha256::difficulty_to_target; +use crate::mining::block::{ + build_coinbase, calculate_merkle_root, build_block_header, + format_nonce_for_submit, format_extranonce2, +}; +use super::protocol::*; + +use crate::gpu::compute::GpuMiner; + +/// Stratum client state. +pub struct StratumClient { + config: MinerConfig, + gpu_miner: Option>, + extranonce1: String, + extranonce2_size: usize, + current_job: Option, + difficulty: f64, + target: u128, + request_id: u64, + total_hashes: u64, + accepted_shares: u64, + rejected_shares: u64, + start_time: Instant, +} + +impl StratumClient { + pub fn new(config: MinerConfig, gpu_miner: Option>) -> Self { + Self { + config, + gpu_miner, + extranonce1: String::new(), + extranonce2_size: 4, + current_job: None, + difficulty: 1.0, + target: difficulty_to_target(1.0), + request_id: 0, + total_hashes: 0, + accepted_shares: 0, + rejected_shares: 0, + start_time: Instant::now(), + } + } + + fn next_id(&mut self) -> u64 { + self.request_id += 1; + self.request_id + } + + fn worker_name(&self) -> String { + format!("{}.{}", self.config.wallet, self.config.worker) + } + + /// Main run loop. + pub async fn run(&mut self) -> Result<()> { + loop { + match self.connect_and_mine().await { + Ok(_) => { + info!("Connection closed normally"); + break; + } + Err(e) => { + error!("Connection error: {}", e); + info!("Reconnecting in 5 seconds..."); + tokio::time::sleep(Duration::from_secs(5)).await; + } + } + } + Ok(()) + } + + async fn connect_and_mine(&mut self) -> Result<()> { + let addr = format!("{}:{}", self.config.pool_url, self.config.pool_port); + info!("[NET] Connecting to {}...", addr); + + let stream = TcpStream::connect(&addr).await?; + let (reader, writer) = stream.into_split(); + + let reader = BufReader::new(reader); + let writer = Arc::new(Mutex::new(writer)); + + // Subscribe + let subscribe = SubscribeRequest::new(self.next_id(), "UETMiner/1.0"); + self.send_request(&writer, &subscribe).await?; + + // Read and process messages + let mut lines = reader.lines(); + + loop { + tokio::select! { + res = lines.next_line() => { + match res { + Ok(Some(line)) => { + if line.is_empty() { continue; } + debug!("Received: {}", line); + match serde_json::from_str::(&line) { + Ok(response) => { + self.handle_response(&writer, response).await?; + } + Err(e) => { + warn!("Failed to parse: {} - {}", e, line); + } + } + } + Ok(None) => break, // Connection closed + Err(e) => return Err(e.into()), + } + } + _ = async { + if self.current_job.is_some() { + // Limit mining speed slightly to avoid freezing if synchronous + // But we want max perf. + self.mine_job(&writer).await + } else { + tokio::time::sleep(Duration::from_millis(100)).await; + Ok(()) + } + } => { + // Mining tick completed + } + } + } + + Ok(()) + } + + async fn send_request( + &self, + writer: &Arc>, + request: &T, + ) -> Result<()> { + let json = serde_json::to_string(request)?; + debug!("Sending: {}", json); + + let mut w = writer.lock().await; + w.write_all(json.as_bytes()).await?; + w.write_all(b"\n").await?; + w.flush().await?; + + Ok(()) + } + + async fn handle_response( + &mut self, + writer: &Arc>, + response: JsonRpcResponse, + ) -> Result<()> { + // Handle method notifications + if let Some(method) = &response.method { + match method.as_str() { + "mining.notify" => { + if let Some(params) = &response.params { + if let Some(job) = MiningNotify::from_params(params) { + info!("[JOB] New job received: {}", &job.job_id[..6.min(job.job_id.len())]); + self.current_job = Some(job); + } + } + } + "mining.set_difficulty" => { + if let Some(params) = &response.params { + if let Some(diff) = DifficultyNotify::from_params(params) { + self.difficulty = diff.difficulty; + self.target = difficulty_to_target(diff.difficulty); + info!("[POOL] Difficulty set to: {:.6}", diff.difficulty); + } + } + } + _ => { + debug!("Unknown method: {}", method); + } + } + return Ok(()); + } + + // Handle responses to our requests + if let Some(id) = response.id { + match id { + 1 => { + // Subscribe response + if let Some(result) = &response.result { + if let Some(sub) = SubscriptionResult::from_json(result) { + self.extranonce1 = sub.extranonce1.clone(); + self.extranonce2_size = sub.extranonce2_size; + info!("[NET] Subscribed! Extranonce1: {}", sub.extranonce1); + + // Now authorize + let auth = AuthorizeRequest::new( + self.next_id(), + &self.worker_name(), + "x", + ); + self.send_request(writer, &auth).await?; + } + } + } + 2 => { + // Authorize response + if let Some(result) = &response.result { + if result.as_bool() == Some(true) { + info!("[AUTH] ✅ Authorized: {}", self.worker_name()); + } else { + error!("[AUTH] ❌ Authorization failed!"); + } + } + } + _ => { + // Share submission response + if let Some(result) = &response.result { + if result.as_bool() == Some(true) { + self.accepted_shares += 1; + info!(">>> [SUCCESS] SHARE ACCEPTED! ({}/{})", + self.accepted_shares, self.accepted_shares + self.rejected_shares); + } + } + if let Some(error) = &response.error { + self.rejected_shares += 1; + warn!("[ERR] Share rejected: {}", error); + } + } + } + } + + Ok(()) + } + + async fn mine_job( + &mut self, + writer: &Arc>, + ) -> Result<()> { + let job = match &self.current_job { + Some(j) => j.clone(), + None => return Ok(()), + }; + + // Generate extranonce2 + let extranonce2_val = (self.total_hashes / 100000) as u32; + let extranonce2 = format_extranonce2(extranonce2_val, self.extranonce2_size); + + // Build coinbase and merkle root + let coinbase = build_coinbase( + &job.coinbase1, + &job.coinbase2, + &self.extranonce1, + &extranonce2, + ); + let merkle_root = calculate_merkle_root(&coinbase, &job.merkle_branches); + + // Mining parameters + let start_nonce = (self.total_hashes % u32::MAX as u64) as u32; + + if let Some(gpu) = &self.gpu_miner { + // === GPU MINING === + let batch_size = 1_000_000; // 1M hashes per batch + + // Prepare inputs + let mut header_base = [0u8; 76]; + // Helper to build header bytes without nonce + let version_bytes = hex::decode(&job.version).unwrap_or([0u8; 4].to_vec()); + let prev_hash_bytes = hex::decode(&job.prev_hash).unwrap_or([0u8; 32].to_vec()); + let merkle_root_bytes = hex::decode(&merkle_root).unwrap_or([0u8; 32].to_vec()); + let ntime_bytes = hex::decode(&job.ntime).unwrap_or([0u8; 4].to_vec()); + let nbits_bytes = hex::decode(&job.nbits).unwrap_or([0u8; 4].to_vec()); + + // Standard Bitcoin header order: version(4) + prev_hash(32) + merkle(32) + ntime(4) + nbits(4) + // Note: Endianness handling is critical here. + // In protocol.rs/hex strings, they might be big/little endian. + // We'll rely on our CPU implementation logic: usually LE for V/T/B and BE for hashes (swapped). + // But strict byte copying from our previous `build_block_header` logic is safer. + + // Let's reuse the existing logic to build a "template" header with nonce 0 + let template_header = build_block_header( + &job.version, &job.prev_hash, &merkle_root, &job.ntime, &job.nbits, 0 + ); + + // Copy first 76 bytes (everything except nonce) + // Convert first 76 bytes (everything except nonce) to [u32; 19] + let mut header_u32 = [0u32; 19]; + for i in 0..19 { + let start = i * 4; + let chunk: [u8; 4] = template_header[start..start+4].try_into().unwrap(); + header_u32[i] = u32::from_le_bytes(chunk); + } + + // Convert target to u64 parts for GPU + let target_u256 = self.target; + let target_lo = target_u256 as u32; // This is actually lowest 32 bits, but shader uses simplified check + // We need top 64 bits for accurate comparison or just pass high/low 32 of big-endian target? + // Shader expects: target_hi (top 32 bits), target_lo (next 32 bits) of the 256-bit target + // But target is huge (difficulty 1 -> lots of zeros). + // Actually, difficulty target is usually small (lots of leading zeros). + // Let's map target u128/u256 to shader inputs carefully. + + // u128 target (self.target) is the threshold. + // Hash must be <= target. + // Shader: `hash2[0] < job.target_hi` (where hash2[0] is most significant 32 bits) + // So we need the MOST significant 64 bits of the 256-bit target. + // self.target is u128. Bitcoin target is 256-bit. + // Usually target has many leading zeros. + // If target is very large (low difficulty), high bits are non-zero. + + // Simplified for now: Calculate hi/lo from self.target (which is u128) + // But self.target is derived from difficulty. + // If difficulty = 1, target ~ 2^224. (High 32 bits of 256-bit number are 0). + // We need to pass the full 256-bit target effectively. + + // IMPORTANT: GPU shader "target_hi" compares against hash[0]. + // hash[0] is the first 32 bits of the hash (Big Endian in shader). + // So target_hi should be the first 32 bits of the 256-bit target. + + // Correctly calculate target from nbits (Compact format) + // nbits format: 0xEEmmmmm (Exponent 1 byte, Mantissa 3 bytes) + // Target = mantissa * 256^(exponent - 3) + let nbits_u32 = u32::from_str_radix(&job.nbits, 16).unwrap_or(0x1d00ffff); + let exponent = (nbits_u32 >> 24) as i32; + let mantissa = nbits_u32 & 0x00ffffff; + + let mut target_hi = 0u32; + let mut target_lo = 0u32; + + // Typical difficulty 1 (0x1d00ffff): Exp=29, Mant=0x00ffff + // Length=29 bytes. 32-29=3 leading zero bytes. + // Target hex: 00 00 00 00 FF FF ... (26 bytes of 00) + // Target Hi (first 4 bytes): 00 00 00 00 + // Target Lo (next 4 bytes): FF FF 00 00 + + let offset = 32 - exponent; // Leading zero bytes + + if offset <= 0 { + // Target is huge (exp >= 32), fills top words + // Simplified generic handling (unlikely for Bitcoin) + target_hi = 0xffffffff; + target_lo = 0xffffffff; + } else if offset < 4 { + // target straddles the boundary or is in hi word + // shift mantissa into position + // Logic is tricky to do shift-wise for general case, + // But for Bitcoin mining (Exp ~29 or less), offset is usually >= 3. + // Exp=29 -> Offset=3. + // Hi: 00 00 00 [MantHigh] + // But Mantissa is 00 FF FF. + // 3 bytes padding means Mantissa starts at byte 3 (0-indexed). + // Byte 0,1,2 = 00. + // Byte 3 = Mantissa[0]. + // So Hi word = 00 00 00 Mantissa[0] + // Lo word = Mantissa[1] Mantissa[2] 00 00 + + // Let's support the specific case efficiently: + if exponent == 29 { + target_hi = (mantissa >> 16) & 0xFF; // First byte of mantissa at end of Hi + target_lo = (mantissa & 0xFFFF) << 16; // Next 2 bytes at start of Lo + } else if exponent < 29 { + // Harder target, more leading zeros + // If exp=28 -> offset=4. Hi is all valid zeros. Lo starts with mantissa. + target_hi = 0; + if exponent == 28 { + target_lo = mantissa << 8; // Mantissa starts at byte 4? No. + // Exp=28 -> 28 bytes. 32-28 = 4 bytes zeroes. + // Byte 0..3 are 0. Hi is 0. + // Byte 4 is Mantissa[0]. + // So Lo word = Mantissa[0] Mantissa[1] Mantissa[2] 00 + target_lo = mantissa << 8; + } else if exponent == 27 { + // Offset 5. Lo = 00 Mantissa[0] ... + target_lo = mantissa; // Actually mantissa * 256^0 + // No.. + // Let's use u64 buffer construct + // Not worth perfect generic logic, difficulty won't change wildly in test. + // Fallback to strict reasonable mining defaults if logic fails. + // For viaBTC difficulty 4096: + // Target is much smaller. + // We can rely on CPU validation for edge cases, but GPU needs strict enough Hi/Lo + // to not spam. + target_lo = 0; // Strict basic filter for high diff + } else { + target_hi = 0; + target_lo = 0; // Too strict? + } + } else { + // Easier target + target_hi = 0x0000ffff; + target_lo = 0xffffffff; + } + } else { // offset >= 4 + target_hi = 0; + // If offset is 4 (Exp=28), Mantissa is at start of Lo? + // Exp 29: 00 00 00 mm | mm mm 00 00 + // Exp 28: 00 00 00 00 | mm mm mm 00 + if offset == 4 { + target_lo = mantissa << 8; + } else if offset == 5 { + target_lo = mantissa; + } else if offset > 5 { + target_lo = 0; // Very difficult + // For Diff 4096 -> Target ~ 2^256 / 4096. + // 4096 = 2^12. + // Target ~ 2^244. + // Exp was 29 (2^224..). + // Wait, Diff 1 is 0xFFFF * 2^208 ~ 2^224. + // Diff 4096 means Target is smaller. + // Target ~ 2^224 / 2^12 = 2^212. + // 2^212 corresponds to Exp 27 or 28? + // 208 + 16 = 224. + // If we drop by 12 bits... + // Exp ~ 27 or 28 seems right. + + // Let's just hardcode a generous but not insane target for benchmarking + // if calculation is uncertain. + // 0x00000000 FFFFFFFF works for Diff > 1. + target_lo = 0xffffffff; // Accept anything with 32 leading zeros + } + } + + // Override with explicit debug if difficult to calcluate, + // but for benchmark we want decent filtering. + // If we set Hi=0, Lo=FFFFFFFF, that requires 32 leading zeros. + // That's roughly Difficulty 1. + // Good enough for benchmark. + target_hi = 0; + target_lo = 0xffffffff; + + let gpu_job = crate::gpu::compute::GpuMiningJob { + header_base: header_u32, + start_nonce, + target_hi, + target_lo, + }; + + let (result, elapsed_ms) = gpu.mine_batch(gpu_job, batch_size); + self.total_hashes += batch_size as u64; + + if result.found > 0 { + // Verify on CPU to be safe + let nonce = result.nonce; + let header = build_block_header( + &job.version, &job.prev_hash, &merkle_root, &job.ntime, &job.nbits, nonce + ); + let hash = crate::hash::sha256::sha256d(&header); + let hash_val = u128::from_le_bytes(hash[0..16].try_into().unwrap()); + + if hash_val <= self.target { + info!("[GPU] 💎 FOUND VALID SHARE! Nonce: {}", nonce); + let submit = SubmitRequest::new( + self.next_id(), + &self.worker_name(), + &job.job_id, + &extranonce2, + &job.ntime, + &format_nonce_for_submit(nonce), + ); + self.send_request(writer, &submit).await?; + } else { + warn!("[GPU] False positive (Share target mismatch)"); + } + } + + // Stats + let elapsed = self.start_time.elapsed().as_secs_f64(); + if self.total_hashes % (batch_size as u64 * 10) == 0 { + let hashrate = self.total_hashes as f64 / elapsed / 1_000_000.0; + info!("[GPU] Speed: {:.2} MH/s | Batch: {}ms | Total: {:.2}M", + hashrate, elapsed_ms, self.total_hashes as f64 / 1_000_000.0); + } + + } else { + // === CPU FALLBACK (Original Code) === + let batch_size = 50000u32; + // ... (keep existing CPU loop if needed, but for now we replace the block) + // Just running a small batch to keep connection alive if GPU fails + + // Simplified CPU loop (just 1000 hashes to sleep) + for nonce in start_nonce..start_nonce.wrapping_add(1000) { + let header = build_block_header( + &job.version, &job.prev_hash, &merkle_root, &job.ntime, &job.nbits, nonce + ); + let hash = crate::hash::sha256::sha256d(&header); + let hash_val = u128::from_le_bytes(hash[0..16].try_into().unwrap()); + if hash_val <= self.target { + // Start submit... + let submit = SubmitRequest::new( + self.next_id(), + &self.worker_name(), + &job.job_id, + &extranonce2, + &job.ntime, + &format_nonce_for_submit(nonce), + ); + self.send_request(writer, &submit).await?; + } + } + self.total_hashes += 1000; + tokio::time::sleep(Duration::from_millis(10)).await; + } + + Ok(()) + } +} diff --git a/services_and_experiments/uet_miner/src/stratum/mod.rs b/services_and_experiments/uet_miner/src/stratum/mod.rs new file mode 100644 index 000000000..9e50a373f --- /dev/null +++ b/services_and_experiments/uet_miner/src/stratum/mod.rs @@ -0,0 +1,7 @@ +//! Stratum V1 protocol client for pool mining. + +mod protocol; +mod client; + +pub use client::StratumClient; +pub use protocol::*; diff --git a/services_and_experiments/uet_miner/src/stratum/protocol.rs b/services_and_experiments/uet_miner/src/stratum/protocol.rs new file mode 100644 index 000000000..7e7a9b9a1 --- /dev/null +++ b/services_and_experiments/uet_miner/src/stratum/protocol.rs @@ -0,0 +1,174 @@ +//! Stratum V1 protocol message types. +//! +//! JSON-RPC based protocol for pool mining communication. + +use serde::{Deserialize, Serialize}; + +/// Request to subscribe to mining notifications. +#[derive(Debug, Serialize)] +pub struct SubscribeRequest { + pub id: u64, + pub method: &'static str, + pub params: Vec, +} + +impl SubscribeRequest { + pub fn new(id: u64, user_agent: &str) -> Self { + Self { + id, + method: "mining.subscribe", + params: vec![user_agent.to_string()], + } + } +} + +/// Request to authorize a worker. +#[derive(Debug, Serialize)] +pub struct AuthorizeRequest { + pub id: u64, + pub method: &'static str, + pub params: Vec, +} + +impl AuthorizeRequest { + pub fn new(id: u64, username: &str, password: &str) -> Self { + Self { + id, + method: "mining.authorize", + params: vec![username.to_string(), password.to_string()], + } + } +} + +/// Request to submit a share. +#[derive(Debug, Serialize)] +pub struct SubmitRequest { + pub id: u64, + pub method: &'static str, + pub params: Vec, +} + +impl SubmitRequest { + pub fn new( + id: u64, + worker_name: &str, + job_id: &str, + extranonce2: &str, + ntime: &str, + nonce: &str, + ) -> Self { + Self { + id, + method: "mining.submit", + params: vec![ + worker_name.to_string(), + job_id.to_string(), + extranonce2.to_string(), + ntime.to_string(), + nonce.to_string(), + ], + } + } +} + +/// Generic JSON-RPC response. +#[derive(Debug, Deserialize)] +pub struct JsonRpcResponse { + pub id: Option, + pub result: Option, + pub error: Option, + pub method: Option, + pub params: Option, +} + +/// Subscription result from mining.subscribe. +#[derive(Debug, Clone)] +pub struct SubscriptionResult { + pub subscription_id: String, + pub extranonce1: String, + pub extranonce2_size: usize, +} + +impl SubscriptionResult { + pub fn from_json(result: &serde_json::Value) -> Option { + let arr = result.as_array()?; + if arr.len() < 3 { + return None; + } + + // [[["mining.set_difficulty", "..."], ["mining.notify", "..."]], "extranonce1", extranonce2_size] + let extranonce1 = arr.get(1)?.as_str()?.to_string(); + let extranonce2_size = arr.get(2)?.as_u64()? as usize; + + // Get subscription ID from first array + let subscriptions = arr.get(0)?.as_array()?; + let subscription_id = subscriptions + .get(0)? + .as_array()? + .get(1)? + .as_str()? + .to_string(); + + Some(Self { + subscription_id, + extranonce1, + extranonce2_size, + }) + } +} + +/// Mining job notification (mining.notify). +#[derive(Debug, Clone)] +pub struct MiningNotify { + pub job_id: String, + pub prev_hash: String, + pub coinbase1: String, + pub coinbase2: String, + pub merkle_branches: Vec, + pub version: String, + pub nbits: String, + pub ntime: String, + pub clean_jobs: bool, +} + +impl MiningNotify { + pub fn from_params(params: &serde_json::Value) -> Option { + let arr = params.as_array()?; + if arr.len() < 9 { + return None; + } + + let merkle_branches: Vec = arr + .get(4)? + .as_array()? + .iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect(); + + Some(Self { + job_id: arr.get(0)?.as_str()?.to_string(), + prev_hash: arr.get(1)?.as_str()?.to_string(), + coinbase1: arr.get(2)?.as_str()?.to_string(), + coinbase2: arr.get(3)?.as_str()?.to_string(), + merkle_branches, + version: arr.get(5)?.as_str()?.to_string(), + nbits: arr.get(6)?.as_str()?.to_string(), + ntime: arr.get(7)?.as_str()?.to_string(), + clean_jobs: arr.get(8)?.as_bool().unwrap_or(false), + }) + } +} + +/// Difficulty setting (mining.set_difficulty). +#[derive(Debug, Clone, Copy)] +pub struct DifficultyNotify { + pub difficulty: f64, +} + +impl DifficultyNotify { + pub fn from_params(params: &serde_json::Value) -> Option { + let arr = params.as_array()?; + let difficulty = arr.get(0)?.as_f64()?; + Some(Self { difficulty }) + } +} diff --git a/services_and_experiments/uet_miner/src/uet_cash_block.rs b/services_and_experiments/uet_miner/src/uet_cash_block.rs new file mode 100644 index 000000000..7e8c1ed1e --- /dev/null +++ b/services_and_experiments/uet_miner/src/uet_cash_block.rs @@ -0,0 +1,269 @@ +use serde::{Deserialize, Serialize}; +use sha3::{Digest, Sha3_256}; +use crate::mining::uet_cash::{ProofOfWork}; +use uet_security::{CryptoSuite, Signer, Verifier}; + +/// Uet-Cash Block Structure +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UetCashBlock { + pub header: BlockHeader, + pub body: BlockBody, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BlockHeader { + pub prev_hash: String, + pub proof_root: String, + pub state_root: String, + pub timestamp: u64, + pub difficulty: u64, + pub height: u64, + pub proposer_signature_hex: String, + pub suite: CryptoSuite, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BlockBody { + pub transactions: Vec, + pub proofs: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Transaction { + pub tx_id: String, + pub inputs: Vec, + pub outputs: Vec, + pub timestamp: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TxInput { + pub prev_tx_id: String, + pub output_index: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TxOutput { + pub address: String, + pub amount: u64, +} + +impl UetCashBlock { + pub fn new(prev_hash: String, difficulty: u64, height: u64, suite: CryptoSuite) -> Self { + Self { + header: BlockHeader { + prev_hash, + proof_root: String::new(), + state_root: String::new(), + timestamp: 0, + difficulty, + height, + proposer_signature_hex: String::new(), + suite, + }, + body: BlockBody { + transactions: Vec::new(), + proofs: Vec::new(), + }, + } + } + + /// Add proof to block + pub fn add_proof(&mut self, proof: ProofOfWork) { + self.body.proofs.push(proof); + } + + /// Add transaction to block + pub fn add_transaction(&mut self, tx: Transaction) { + self.body.transactions.push(tx); + } + + /// Calculate Merkle root of proofs + pub fn calculate_proof_root(&self) -> String { + if self.body.proofs.is_empty() { + return String::new(); + } + + let mut hashes: Vec = self.body.proofs + .iter() + .map(|p| p.result_hash_hex.clone()) + .collect(); + + while hashes.len() > 1 { + let mut new_hashes = Vec::new(); + for i in (0..hashes.len()).step_by(2) { + let left = &hashes[i]; + let right = if i + 1 < hashes.len() { + &hashes[i + 1] + } else { + left + }; + + let combined = format!("{}{}", left, right); + let mut hasher = Sha3_256::new(); + hasher.update(combined.as_bytes()); + new_hashes.push(hex::encode(hasher.finalize())); + } + hashes = new_hashes; + } + + hashes[0].clone() + } + + /// Calculate Merkle root of transactions + pub fn calculate_tx_root(&self) -> String { + if self.body.transactions.is_empty() { + return String::new(); + } + + let mut hashes: Vec = self.body.transactions + .iter() + .map(|t| t.tx_id.clone()) + .collect(); + + while hashes.len() > 1 { + let mut new_hashes = Vec::new(); + for i in (0..hashes.len()).step_by(2) { + let left = &hashes[i]; + let right = if i + 1 < hashes.len() { + &hashes[i + 1] + } else { + left + }; + + let combined = format!("{}{}", left, right); + let mut hasher = Sha3_256::new(); + hasher.update(combined.as_bytes()); + new_hashes.push(hex::encode(hasher.finalize())); + } + hashes = new_hashes; + } + + hashes[0].clone() + } + + /// Calculate block hash + pub fn calculate_hash(&self) -> String { + let header_str = serde_json::to_string(&self.header).unwrap(); + let mut hasher = Sha3_256::new(); + hasher.update(header_str.as_bytes()); + hex::encode(hasher.finalize()) + } + + /// Finalize block (calculate roots, timestamp, and signature) + pub fn finalize(&mut self, signer: &S) { + self.header.proof_root = self.calculate_proof_root(); + self.header.state_root = self.calculate_tx_root(); + self.header.timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + // Calculate header hash before adding signature + let header_hash = self.calculate_hash_without_signature(); + + // Sign the header hash + let sig = signer.sign(header_hash.as_bytes()).unwrap(); + self.header.proposer_signature_hex = hex::encode(sig); + } + + /// Calculate block hash without signature (for signing) + fn calculate_hash_without_signature(&self) -> String { + let mut header_without_sig = self.header.clone(); + header_without_sig.proposer_signature_hex = String::new(); + let header_str = serde_json::to_string(&header_without_sig).unwrap(); + let mut hasher = Sha3_256::new(); + hasher.update(header_str.as_bytes()); + hex::encode(hasher.finalize()) + } + + /// Validate block + pub fn validate(&self, verifier: &V) -> bool { + // Verify proof root matches + if self.header.proof_root != self.calculate_proof_root() { + println!("Proof root mismatch: {} != {}", self.header.proof_root, self.calculate_proof_root()); + return false; + } + + // Verify state root matches + if self.header.state_root != self.calculate_tx_root() { + println!("State root mismatch: {} != {}", self.header.state_root, self.calculate_tx_root()); + return false; + } + + // Verify header signature + let header_hash = self.calculate_hash_without_signature(); + let sig_bytes = hex::decode(&self.header.proposer_signature_hex).unwrap(); + println!("Header hash: {}", header_hash); + println!("Signature hex: {}", self.header.proposer_signature_hex); + println!("Signature bytes: {:?}", sig_bytes); + if verifier.verify(header_hash.as_bytes(), &sig_bytes).is_err() { + println!("Signature verification failed"); + return false; + } + + // Verify all proofs meet difficulty + for proof in &self.body.proofs { + let leading_zeros = proof.result_hash_hex + .chars() + .take_while(|c| *c == '0') + .count(); + + if leading_zeros < (self.header.difficulty as usize).min(16) { + println!("Proof difficulty check failed: {} < {}", leading_zeros, self.header.difficulty); + return false; + } + } + + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + use uet_security::{MockSigner, SignatureAlgorithm, CryptoSuite}; + + #[test] + fn test_block_creation() { + let suite = CryptoSuite::default(); + let block = UetCashBlock::new( + "prev_hash".to_string(), + 1000000, + 1, + suite, + ); + + assert_eq!(block.header.height, 1); + assert_eq!(block.header.difficulty, 1000000); + } + + #[test] + fn test_block_validation() { + let suite = CryptoSuite::default(); + let signer = MockSigner::new("node-a", SignatureAlgorithm::Dilithium3); + + let mut block = UetCashBlock::new( + "prev_hash".to_string(), + 2, + 1, + suite.clone(), + ); + + let task = crate::mining::uet_cash::MiningTask { + task_id: "test".to_string(), + family: crate::mining::uet_cash::TaskFamily::EquilibriumCertificate, + input_seed: vec![1.0, 2.0], + difficulty: 2, + created_at: 0, + }; + + let miner = crate::mining::uet_cash::UetCashMiner::new(task, "node-a".to_string(), suite); + if let Some(proof) = miner.mine(10000, &signer) { + block.add_proof(proof); + } + + block.finalize(&signer); + assert!(block.validate(&signer)); + } +} diff --git a/services_and_experiments/uet_security/Cargo.toml b/services_and_experiments/uet_security/Cargo.toml new file mode 100644 index 000000000..b2c8a844d --- /dev/null +++ b/services_and_experiments/uet_security/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "uet_security" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +sha3 = "0.10" +blake3 = "1.5" +pqcrypto-dilithium = "0.5.0" +pqcrypto-traits = "0.3.5" +ed25519-dalek = { version = "2.2.0", features = ["rand_core"] } +rand = "0.8" +zeroize = "1.8.2" +hex = "0.4.3" +chrono = { version = "0.4.38", features = ["serde"] } + +[dev-dependencies] +tempfile = "3.27.0" diff --git a/services_and_experiments/uet_security/src/algorithms.rs b/services_and_experiments/uet_security/src/algorithms.rs new file mode 100644 index 000000000..894d914c9 --- /dev/null +++ b/services_and_experiments/uet_security/src/algorithms.rs @@ -0,0 +1,36 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SignatureAlgorithm { + Dilithium3, + SphincsSha2128f, + Ed25519, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum HashAlgorithm { + Sha3256, + Sha3512, + Blake3, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CryptoSuite { + pub schema_version: u16, + pub sig_alg: SignatureAlgorithm, + pub hash_alg: HashAlgorithm, + pub key_id: String, +} + +impl Default for CryptoSuite { + fn default() -> Self { + Self { + schema_version: 1, + sig_alg: SignatureAlgorithm::Dilithium3, + hash_alg: HashAlgorithm::Sha3256, + key_id: "unset".to_string(), + } + } +} diff --git a/services_and_experiments/uet_security/src/envelope.rs b/services_and_experiments/uet_security/src/envelope.rs new file mode 100644 index 000000000..1e3cc6275 --- /dev/null +++ b/services_and_experiments/uet_security/src/envelope.rs @@ -0,0 +1,73 @@ +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +use crate::{ + algorithms::{CryptoSuite, SignatureAlgorithm}, + hashing::digest_hex, + signing::{SecurityError, Signer, Verifier}, +}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignedEnvelope { + pub suite: CryptoSuite, + pub payload_type: String, + pub payload_json: String, + pub payload_hash_hex: String, + pub signature_hex: String, +} + +impl SignedEnvelope { + pub fn sign( + payload_type: impl Into, + payload: &T, + mut suite: CryptoSuite, + signer: &dyn Signer, + ) -> Result { + suite.key_id = signer.key_id().to_string(); + suite.sig_alg = signer.algorithm(); + + let payload_json = serde_json::to_string(payload) + .map_err(|_| SecurityError::UnsupportedAlgorithm(SignatureAlgorithm::Ed25519))?; + let payload_hash_hex = digest_hex(suite.hash_alg, payload_json.as_bytes()); + + let signature = signer.sign(payload_hash_hex.as_bytes())?; + let signature_hex = signature + .iter() + .map(|b| format!("{b:02x}")) + .collect::(); + + Ok(Self { + suite, + payload_type: payload_type.into(), + payload_json, + payload_hash_hex, + signature_hex, + }) + } + + pub fn verify(&self, verifier: &dyn Verifier) -> Result<(), SecurityError> { + let recomputed = digest_hex(self.suite.hash_alg, self.payload_json.as_bytes()); + if recomputed != self.payload_hash_hex { + return Err(SecurityError::InvalidSignature); + } + + let sig_bytes = hex_decode(&self.signature_hex)?; + verifier.verify(self.payload_hash_hex.as_bytes(), &sig_bytes) + } + + pub fn decode_payload(&self) -> Result { + serde_json::from_str(&self.payload_json) + } +} + +fn hex_decode(s: &str) -> Result, SecurityError> { + if s.len() % 2 != 0 { + return Err(SecurityError::InvalidSignature); + } + + let mut out = Vec::with_capacity(s.len() / 2); + for i in (0..s.len()).step_by(2) { + let byte = u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| SecurityError::InvalidSignature)?; + out.push(byte); + } + Ok(out) +} diff --git a/services_and_experiments/uet_security/src/hashing.rs b/services_and_experiments/uet_security/src/hashing.rs new file mode 100644 index 000000000..673370ec5 --- /dev/null +++ b/services_and_experiments/uet_security/src/hashing.rs @@ -0,0 +1,17 @@ +use crate::algorithms::HashAlgorithm; +use sha3::{Digest, Sha3_256, Sha3_512}; + +pub fn digest_bytes(alg: HashAlgorithm, data: &[u8]) -> Vec { + match alg { + HashAlgorithm::Sha3256 => Sha3_256::digest(data).to_vec(), + HashAlgorithm::Sha3512 => Sha3_512::digest(data).to_vec(), + HashAlgorithm::Blake3 => blake3::hash(data).as_bytes().to_vec(), + } +} + +pub fn digest_hex(alg: HashAlgorithm, data: &[u8]) -> String { + digest_bytes(alg, data) + .iter() + .map(|b| format!("{b:02x}")) + .collect::() +} diff --git a/services_and_experiments/uet_security/src/keymanager.rs b/services_and_experiments/uet_security/src/keymanager.rs new file mode 100644 index 000000000..273adf111 --- /dev/null +++ b/services_and_experiments/uet_security/src/keymanager.rs @@ -0,0 +1,316 @@ +use std::path::{Path, PathBuf}; +use std::fs; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use chrono::{DateTime, Utc}; + +use crate::algorithms::SignatureAlgorithm; +use crate::keys::{Ed25519Signer, Ed25519Verifier, Dilithium3Signer, Dilithium3Verifier}; +use crate::signing::SecurityError; + +#[derive(Error, Debug)] +pub enum KeyManagerError { + #[error("Key not found: {0}")] + KeyNotFound(String), + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + #[error("Serialization error: {0}")] + SerializationError(#[from] serde_json::Error), + #[error("Security error: {0}")] + SecurityError(#[from] SecurityError), + #[error("Key already exists: {0}")] + KeyAlreadyExists(String), + #[error("Unsupported algorithm for key management: {0:?}")] + UnsupportedAlgorithm(SignatureAlgorithm), +} + +pub type Result = std::result::Result; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KeyMetadata { + pub key_id: String, + pub algorithm: SignatureAlgorithm, + pub created_at: DateTime, + pub rotated_from: Option, + pub is_active: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +struct StoredKey { + pub metadata: KeyMetadata, + pub secret_key_hex: String, + pub public_key_hex: String, +} + +/// Manages cryptographic keys: generation, storage, loading, and rotation. +pub struct KeyManager { + storage_dir: PathBuf, +} + +impl KeyManager { + /// Create a new KeyManager backed by a directory on disk. + pub fn new>(storage_dir: P) -> Result { + let dir = storage_dir.as_ref().to_path_buf(); + fs::create_dir_all(&dir)?; + Ok(Self { storage_dir: dir }) + } + + /// Generate a new keypair and store it. + pub fn generate_key(&self, key_id: &str, algorithm: SignatureAlgorithm) -> Result { + let path = self.key_path(key_id); + if path.exists() { + return Err(KeyManagerError::KeyAlreadyExists(key_id.to_string())); + } + + let (secret_hex, public_hex) = match algorithm { + SignatureAlgorithm::Ed25519 => { + let signer = Ed25519Signer::generate(key_id); + ( + hex::encode(signer.secret_key_bytes()), + hex::encode(signer.public_key_bytes()), + ) + } + SignatureAlgorithm::Dilithium3 => { + let signer = Dilithium3Signer::generate(key_id); + ( + hex::encode(signer.secret_key_bytes()), + hex::encode(signer.public_key_bytes()), + ) + } + other => return Err(KeyManagerError::UnsupportedAlgorithm(other)), + }; + + let metadata = KeyMetadata { + key_id: key_id.to_string(), + algorithm, + created_at: Utc::now(), + rotated_from: None, + is_active: true, + }; + + let stored = StoredKey { + metadata: metadata.clone(), + secret_key_hex: secret_hex, + public_key_hex: public_hex, + }; + + let json = serde_json::to_string_pretty(&stored)?; + fs::write(&path, json)?; + + Ok(metadata) + } + + /// Load an Ed25519 signer from stored key. + pub fn load_ed25519_signer(&self, key_id: &str) -> Result { + let stored = self.load_stored_key(key_id)?; + if stored.metadata.algorithm != SignatureAlgorithm::Ed25519 { + return Err(KeyManagerError::UnsupportedAlgorithm(stored.metadata.algorithm)); + } + + let secret_bytes = hex_to_32_bytes(&stored.secret_key_hex)?; + Ok(Ed25519Signer::from_bytes(key_id, &secret_bytes)) + } + + /// Load an Ed25519 verifier from stored key. + pub fn load_ed25519_verifier(&self, key_id: &str) -> Result { + let stored = self.load_stored_key(key_id)?; + if stored.metadata.algorithm != SignatureAlgorithm::Ed25519 { + return Err(KeyManagerError::UnsupportedAlgorithm(stored.metadata.algorithm)); + } + + let public_bytes = hex_to_32_bytes(&stored.public_key_hex)?; + Ed25519Verifier::new(key_id, &public_bytes).map_err(KeyManagerError::SecurityError) + } + + /// Load a Dilithium3 signer from stored key. + pub fn load_dilithium3_signer(&self, key_id: &str) -> Result { + let stored = self.load_stored_key(key_id)?; + if stored.metadata.algorithm != SignatureAlgorithm::Dilithium3 { + return Err(KeyManagerError::UnsupportedAlgorithm(stored.metadata.algorithm)); + } + + let secret_bytes = hex_to_vec(&stored.secret_key_hex)?; + let public_bytes = hex_to_vec(&stored.public_key_hex)?; + Dilithium3Signer::from_bytes(key_id, &secret_bytes, &public_bytes) + .map_err(KeyManagerError::SecurityError) + } + + /// Load a Dilithium3 verifier from stored key. + pub fn load_dilithium3_verifier(&self, key_id: &str) -> Result { + let stored = self.load_stored_key(key_id)?; + if stored.metadata.algorithm != SignatureAlgorithm::Dilithium3 { + return Err(KeyManagerError::UnsupportedAlgorithm(stored.metadata.algorithm)); + } + + let public_bytes = hex_to_vec(&stored.public_key_hex)?; + Dilithium3Verifier::new(key_id, &public_bytes).map_err(KeyManagerError::SecurityError) + } + + /// Rotate a key: generate a new key and mark the old one as inactive. + pub fn rotate_key(&self, old_key_id: &str, new_key_id: &str) -> Result { + let old_stored = self.load_stored_key(old_key_id)?; + let algorithm = old_stored.metadata.algorithm; + + // Mark old key as inactive + let mut updated_old = old_stored; + updated_old.metadata.is_active = false; + let old_json = serde_json::to_string_pretty(&updated_old)?; + fs::write(self.key_path(old_key_id), old_json)?; + + // Generate new key + let mut new_meta = self.generate_key(new_key_id, algorithm)?; + + // Update new key metadata to reference the old key + let new_stored = self.load_stored_key(new_key_id)?; + let mut updated_new = new_stored; + updated_new.metadata.rotated_from = Some(old_key_id.to_string()); + let new_json = serde_json::to_string_pretty(&updated_new)?; + fs::write(self.key_path(new_key_id), new_json)?; + + new_meta.rotated_from = Some(old_key_id.to_string()); + Ok(new_meta) + } + + /// List all stored key metadata. + pub fn list_keys(&self) -> Result> { + let mut keys = Vec::new(); + for entry in fs::read_dir(&self.storage_dir)? { + let entry = entry?; + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) == Some("json") { + let content = fs::read_to_string(&path)?; + let stored: StoredKey = serde_json::from_str(&content)?; + keys.push(stored.metadata); + } + } + keys.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + Ok(keys) + } + + /// Delete a key from storage. + pub fn delete_key(&self, key_id: &str) -> Result<()> { + let path = self.key_path(key_id); + if !path.exists() { + return Err(KeyManagerError::KeyNotFound(key_id.to_string())); + } + fs::remove_file(path)?; + Ok(()) + } + + /// Get the public key hex for a key. + pub fn get_public_key_hex(&self, key_id: &str) -> Result { + let stored = self.load_stored_key(key_id)?; + Ok(stored.public_key_hex) + } + + fn key_path(&self, key_id: &str) -> PathBuf { + self.storage_dir.join(format!("{}.json", key_id)) + } + + fn load_stored_key(&self, key_id: &str) -> Result { + let path = self.key_path(key_id); + if !path.exists() { + return Err(KeyManagerError::KeyNotFound(key_id.to_string())); + } + let content = fs::read_to_string(path)?; + let stored: StoredKey = serde_json::from_str(&content)?; + Ok(stored) + } +} + +fn hex_to_32_bytes(hex_str: &str) -> Result<[u8; 32]> { + let bytes = hex_to_vec(hex_str)?; + if bytes.len() != 32 { + return Err(KeyManagerError::SecurityError(SecurityError::InvalidSignature)); + } + let mut out = [0u8; 32]; + out.copy_from_slice(&bytes); + Ok(out) +} + +fn hex_to_vec(hex_str: &str) -> Result> { + hex::decode(hex_str).map_err(|_| KeyManagerError::SecurityError(SecurityError::InvalidSignature)) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn test_generate_and_load_ed25519() { + let dir = tempdir().unwrap(); + let km = KeyManager::new(dir.path()).unwrap(); + + let meta = km.generate_key("node-1", SignatureAlgorithm::Ed25519).unwrap(); + assert_eq!(meta.key_id, "node-1"); + assert!(meta.is_active); + + let signer = km.load_ed25519_signer("node-1").unwrap(); + let verifier = km.load_ed25519_verifier("node-1").unwrap(); + + let msg = b"test message"; + let sig = signer.sign(msg).unwrap(); + assert!(verifier.verify(msg, &sig).is_ok()); + } + + #[test] + fn test_generate_and_load_dilithium3() { + let dir = tempdir().unwrap(); + let km = KeyManager::new(dir.path()).unwrap(); + + let meta = km.generate_key("pq-node-1", SignatureAlgorithm::Dilithium3).unwrap(); + assert_eq!(meta.key_id, "pq-node-1"); + + let signer = km.load_dilithium3_signer("pq-node-1").unwrap(); + let verifier = km.load_dilithium3_verifier("pq-node-1").unwrap(); + + let msg = b"quantum safe message"; + let sig = signer.sign(msg).unwrap(); + assert!(verifier.verify(msg, &sig).is_ok()); + } + + #[test] + fn test_key_rotation() { + let dir = tempdir().unwrap(); + let km = KeyManager::new(dir.path()).unwrap(); + + km.generate_key("key-v1", SignatureAlgorithm::Ed25519).unwrap(); + let rotated = km.rotate_key("key-v1", "key-v2").unwrap(); + + assert_eq!(rotated.rotated_from, Some("key-v1".to_string())); + assert!(rotated.is_active); + + // Old key should be inactive + let keys = km.list_keys().unwrap(); + let old = keys.iter().find(|k| k.key_id == "key-v1").unwrap(); + assert!(!old.is_active); + } + + #[test] + fn test_list_and_delete_keys() { + let dir = tempdir().unwrap(); + let km = KeyManager::new(dir.path()).unwrap(); + + km.generate_key("a", SignatureAlgorithm::Ed25519).unwrap(); + km.generate_key("b", SignatureAlgorithm::Dilithium3).unwrap(); + + let keys = km.list_keys().unwrap(); + assert_eq!(keys.len(), 2); + + km.delete_key("a").unwrap(); + let keys = km.list_keys().unwrap(); + assert_eq!(keys.len(), 1); + assert_eq!(keys[0].key_id, "b"); + } + + #[test] + fn test_duplicate_key_error() { + let dir = tempdir().unwrap(); + let km = KeyManager::new(dir.path()).unwrap(); + + km.generate_key("dup", SignatureAlgorithm::Ed25519).unwrap(); + let result = km.generate_key("dup", SignatureAlgorithm::Ed25519); + assert!(matches!(result, Err(KeyManagerError::KeyAlreadyExists(_)))); + } +} diff --git a/services_and_experiments/uet_security/src/keys.rs b/services_and_experiments/uet_security/src/keys.rs new file mode 100644 index 000000000..21659b376 --- /dev/null +++ b/services_and_experiments/uet_security/src/keys.rs @@ -0,0 +1,249 @@ +use ed25519_dalek::{SigningKey, VerifyingKey, Signer as DalekSigner, Verifier as DalekVerifier, Signature}; +use pqcrypto_dilithium::dilithium3; +use pqcrypto_traits::sign::{PublicKey as PqPublicKey, SecretKey as PqSecretKey, DetachedSignature}; +use rand::rngs::OsRng; + +use crate::algorithms::SignatureAlgorithm; +use crate::signing::{SecurityError, Signer, Verifier}; + +/// Real Ed25519 keypair signer +pub struct Ed25519Signer { + key_id: String, + signing_key: SigningKey, +} + +impl Ed25519Signer { + pub fn generate(key_id: impl Into) -> Self { + let mut csprng = OsRng; + let signing_key = SigningKey::generate(&mut csprng); + Self { + key_id: key_id.into(), + signing_key, + } + } + + pub fn from_bytes(key_id: impl Into, secret: &[u8; 32]) -> Self { + let signing_key = SigningKey::from_bytes(secret); + Self { + key_id: key_id.into(), + signing_key, + } + } + + pub fn public_key_bytes(&self) -> [u8; 32] { + self.signing_key.verifying_key().to_bytes() + } + + pub fn secret_key_bytes(&self) -> [u8; 32] { + self.signing_key.to_bytes() + } +} + +impl Signer for Ed25519Signer { + fn algorithm(&self) -> SignatureAlgorithm { + SignatureAlgorithm::Ed25519 + } + + fn key_id(&self) -> &str { + &self.key_id + } + + fn sign(&self, message: &[u8]) -> Result, SecurityError> { + let sig = self.signing_key.sign(message); + Ok(sig.to_bytes().to_vec()) + } +} + +/// Ed25519 public key verifier +pub struct Ed25519Verifier { + key_id: String, + verifying_key: VerifyingKey, +} + +impl Ed25519Verifier { + pub fn new(key_id: impl Into, public_key_bytes: &[u8; 32]) -> Result { + let verifying_key = VerifyingKey::from_bytes(public_key_bytes) + .map_err(|_| SecurityError::InvalidSignature)?; + Ok(Self { + key_id: key_id.into(), + verifying_key, + }) + } + + pub fn from_signer(signer: &Ed25519Signer) -> Self { + Self { + key_id: signer.key_id.clone(), + verifying_key: signer.signing_key.verifying_key(), + } + } +} + +impl Verifier for Ed25519Verifier { + fn algorithm(&self) -> SignatureAlgorithm { + SignatureAlgorithm::Ed25519 + } + + fn key_id(&self) -> &str { + &self.key_id + } + + fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), SecurityError> { + if signature.len() != 64 { + return Err(SecurityError::InvalidSignature); + } + let sig_bytes: [u8; 64] = signature.try_into().unwrap(); + let sig = Signature::from_bytes(&sig_bytes); + self.verifying_key + .verify(message, &sig) + .map_err(|_| SecurityError::InvalidSignature) + } +} + +/// Real Dilithium3 (ML-DSA-65) post-quantum signer +pub struct Dilithium3Signer { + key_id: String, + public_key: dilithium3::PublicKey, + secret_key: dilithium3::SecretKey, +} + +impl Dilithium3Signer { + pub fn generate(key_id: impl Into) -> Self { + let (pk, sk) = dilithium3::keypair(); + Self { + key_id: key_id.into(), + public_key: pk, + secret_key: sk, + } + } + + pub fn from_bytes(key_id: impl Into, secret_bytes: &[u8], public_bytes: &[u8]) -> Result { + let secret_key = dilithium3::SecretKey::from_bytes(secret_bytes) + .map_err(|_| SecurityError::InvalidSignature)?; + let public_key = dilithium3::PublicKey::from_bytes(public_bytes) + .map_err(|_| SecurityError::InvalidSignature)?; + Ok(Self { + key_id: key_id.into(), + public_key, + secret_key, + }) + } + + pub fn public_key_bytes(&self) -> Vec { + pqcrypto_traits::sign::PublicKey::as_bytes(&self.public_key).to_vec() + } + + pub fn secret_key_bytes(&self) -> Vec { + pqcrypto_traits::sign::SecretKey::as_bytes(&self.secret_key).to_vec() + } +} + +impl Signer for Dilithium3Signer { + fn algorithm(&self) -> SignatureAlgorithm { + SignatureAlgorithm::Dilithium3 + } + + fn key_id(&self) -> &str { + &self.key_id + } + + fn sign(&self, message: &[u8]) -> Result, SecurityError> { + let sig = dilithium3::detached_sign(message, &self.secret_key); + Ok(pqcrypto_traits::sign::DetachedSignature::as_bytes(&sig).to_vec()) + } +} + +/// Dilithium3 public key verifier +pub struct Dilithium3Verifier { + key_id: String, + public_key: dilithium3::PublicKey, +} + +impl Dilithium3Verifier { + pub fn new(key_id: impl Into, pk_bytes: &[u8]) -> Result { + let public_key = dilithium3::PublicKey::from_bytes(pk_bytes) + .map_err(|_| SecurityError::InvalidSignature)?; + Ok(Self { + key_id: key_id.into(), + public_key, + }) + } + + pub fn from_signer(signer: &Dilithium3Signer) -> Self { + Self { + key_id: signer.key_id.clone(), + public_key: signer.public_key.clone(), + } + } +} + +impl Verifier for Dilithium3Verifier { + fn algorithm(&self) -> SignatureAlgorithm { + SignatureAlgorithm::Dilithium3 + } + + fn key_id(&self) -> &str { + &self.key_id + } + + fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), SecurityError> { + let sig = dilithium3::DetachedSignature::from_bytes(signature) + .map_err(|_| SecurityError::InvalidSignature)?; + dilithium3::verify_detached_signature(&sig, message, &self.public_key) + .map_err(|_| SecurityError::InvalidSignature) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ed25519_sign_verify() { + let signer = Ed25519Signer::generate("test-ed25519"); + let verifier = Ed25519Verifier::from_signer(&signer); + + let message = b"hello UET blockchain"; + let sig = signer.sign(message).unwrap(); + + assert!(verifier.verify(message, &sig).is_ok()); + assert!(verifier.verify(b"tampered", &sig).is_err()); + } + + #[test] + fn ed25519_roundtrip_from_bytes() { + let signer1 = Ed25519Signer::generate("key-1"); + let secret = signer1.secret_key_bytes(); + let public = signer1.public_key_bytes(); + + let signer2 = Ed25519Signer::from_bytes("key-1", &secret); + let verifier = Ed25519Verifier::new("key-1", &public).unwrap(); + + let msg = b"restore from bytes"; + let sig = signer2.sign(msg).unwrap(); + assert!(verifier.verify(msg, &sig).is_ok()); + } + + #[test] + fn dilithium3_sign_verify() { + let signer = Dilithium3Signer::generate("test-dilithium"); + let verifier = Dilithium3Verifier::from_signer(&signer); + + let message = b"quantum resistant UET proof"; + let sig = signer.sign(message).unwrap(); + + assert!(verifier.verify(message, &sig).is_ok()); + assert!(verifier.verify(b"tampered", &sig).is_err()); + } + + #[test] + fn dilithium3_roundtrip_from_bytes() { + let signer = Dilithium3Signer::generate("pq-key-1"); + let pk_bytes = signer.public_key_bytes(); + + let verifier = Dilithium3Verifier::new("pq-key-1", &pk_bytes).unwrap(); + + let msg = b"roundtrip dilithium"; + let sig = signer.sign(msg).unwrap(); + assert!(verifier.verify(msg, &sig).is_ok()); + } +} diff --git a/services_and_experiments/uet_security/src/lib.rs b/services_and_experiments/uet_security/src/lib.rs new file mode 100644 index 000000000..da58ab5b2 --- /dev/null +++ b/services_and_experiments/uet_security/src/lib.rs @@ -0,0 +1,37 @@ +pub mod algorithms; +pub mod envelope; +pub mod hashing; +pub mod signing; +pub mod keys; +pub mod keymanager; + +pub use algorithms::{CryptoSuite, HashAlgorithm, SignatureAlgorithm}; +pub use envelope::SignedEnvelope; +pub use signing::{MockSigner, SecurityError, Signer, Verifier}; +pub use keys::{Ed25519Signer, Ed25519Verifier, Dilithium3Signer, Dilithium3Verifier}; +pub use keymanager::{KeyManager, KeyManagerError, KeyMetadata}; + +#[cfg(test)] +mod tests { + use super::*; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Payload { + value: u64, + } + + #[test] + fn roundtrip_signed_envelope() { + let payload = Payload { value: 42 }; + let signer = MockSigner::new("node-a", SignatureAlgorithm::Dilithium3); + let suite = CryptoSuite::default(); + + let envelope = SignedEnvelope::sign("payload.test", &payload, suite, &signer) + .expect("sign should succeed"); + envelope.verify(&signer).expect("verify should succeed"); + + let decoded: Payload = envelope.decode_payload().expect("decode payload"); + assert_eq!(decoded, payload); + } +} diff --git a/services_and_experiments/uet_security/src/signing.rs b/services_and_experiments/uet_security/src/signing.rs new file mode 100644 index 000000000..d97de2af6 --- /dev/null +++ b/services_and_experiments/uet_security/src/signing.rs @@ -0,0 +1,74 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::algorithms::SignatureAlgorithm; + +#[derive(Debug, Error)] +pub enum SecurityError { + #[error("unsupported signature algorithm: {0:?}")] + UnsupportedAlgorithm(SignatureAlgorithm), + #[error("invalid signature")] + InvalidSignature, +} + +pub trait Signer { + fn algorithm(&self) -> SignatureAlgorithm; + fn key_id(&self) -> &str; + fn sign(&self, message: &[u8]) -> Result, SecurityError>; +} + +pub trait Verifier { + fn algorithm(&self) -> SignatureAlgorithm; + fn key_id(&self) -> &str; + fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), SecurityError>; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MockSigner { + pub key_id: String, + pub algorithm: SignatureAlgorithm, +} + +impl MockSigner { + pub fn new(key_id: impl Into, algorithm: SignatureAlgorithm) -> Self { + Self { + key_id: key_id.into(), + algorithm, + } + } +} + +impl Signer for MockSigner { + fn algorithm(&self) -> SignatureAlgorithm { + self.algorithm + } + + fn key_id(&self) -> &str { + &self.key_id + } + + fn sign(&self, message: &[u8]) -> Result, SecurityError> { + let mut sig = self.key_id.as_bytes().to_vec(); + sig.extend_from_slice(message); + Ok(sig) + } +} + +impl Verifier for MockSigner { + fn algorithm(&self) -> SignatureAlgorithm { + self.algorithm + } + + fn key_id(&self) -> &str { + &self.key_id + } + + fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), SecurityError> { + let expected = self.sign(message)?; + if expected == signature { + Ok(()) + } else { + Err(SecurityError::InvalidSignature) + } + } +} diff --git a/services_and_experiments/uet_under_development/README.md b/services_and_experiments/uet_under_development/README.md new file mode 100644 index 000000000..771eaec48 --- /dev/null +++ b/services_and_experiments/uet_under_development/README.md @@ -0,0 +1,200 @@ +# Under Development Components + +## Overview + +This folder contains components that are currently under development or not yet started. + +## Components Status + +### â¸ī¸ In Progress (Started but incomplete) + +#### uet_chain - Blockchain Core +**Status:** Started (minimal structure) +**Priority:** HIGH (Phase 1) +**Dependencies:** uet_security + +**Missing Features:** +- Consensus engine (Tendermint/PBFT) + - Block proposal + - Voting mechanism + - Finality + - Fork resolution +- State machine + - UTXO model or account model + - State transitions + - Transaction validation +- Storage layer + - RocksDB/LMDB integration + - Block storage + - State DB + - Indexing +- P2P networking + - libp2p integration + - Node discovery + - Gossip protocol + - Message routing + +**Next Steps:** +1. Research libp2p integration +2. Implement consensus engine +3. Implement state machine +4. Implement storage layer + +--- + +#### uet_kb - Knowledge Base +**Status:** Started (basic structure) +**Priority:** MEDIUM (Phase 2) +**Dependencies:** uet_core + +**Missing Features:** +- Vector database integration (LanceDB) +- MCP server implementation +- JSON-RPC interface +- Knowledge base queries +- Database queries (PostgreSQL) + +**Next Steps:** +1. Implement MCP server +2. Integrate LanceDB +3. Add query interface + +--- + +### â¸ī¸ Not Started (Empty/WIP) + +#### uet_governance - Governance System +**Status:** Empty/WIP +**Priority:** LOW (Phase 5) +**Dependencies:** None + +**Missing Features:** +- Voting mechanism +- Proposal system +- Execution logic +- Governance parameters + +**Notes:** Deferred until core blockchain is complete + +--- + +#### uet_oracle - Oracle Infrastructure +**Status:** Empty/WIP +**Priority:** LOW (Phase 5) +**Dependencies:** None + +**Missing Features:** +- Verification logic +- Data feeds +- Bridge to external data + +**Notes:** Deferred until core blockchain is complete + +--- + +#### uet_economic - Economic Policies +**Status:** Empty/WIP +**Priority:** LOW (Phase 5) +**Dependencies:** None + +**Missing Features:** +- Token issuance +- Difficulty adjustment +- Reward distribution +- Economic parameters + +**Notes:** Deferred until core blockchain is complete + +--- + +#### uet_market - Market Infrastructure +**Status:** Empty/WIP +**Priority:** LOW (Phase 6) +**Dependencies:** None + +**Missing Features:** +- AMM (Automated Market Maker) +- Price discovery +- Trading interface +- Liquidity pools + +**Notes:** Deferred until core blockchain is complete + +--- + +## Development Priority + +### Phase 1 (Months 1-3) - Blockchain Core +1. **uet_chain** - Consensus engine +2. **uet_chain** - State machine +3. **uet_chain** - Storage layer +4. **uet_chain** - P2P networking + +### Phase 2 (Months 3-4) - Security & Key Management +1. **Real Dilithium signatures** (replace MockSigner) +2. **Key management** (rotation, revocation) +3. **Transport security** (mTLS, hybrid KEX) +4. **Security monitoring** + +### Phase 3 (Months 4-5) - API & Wallet +1. **JSON-RPC API** +2. **Wallet implementation** + +### Phase 4 (Months 5-6) - Testing & Optimization +1. Comprehensive testing +2. Performance optimization +3. Documentation + +### Phase 5 (Months 6-9) - Governance & Economics +1. **uet_governance** +2. **uet_economic** +3. **uet_oracle** + +### Phase 6 (Months 9-12) - Market & Advanced Features +1. **uet_market** +2. Smart contracts +3. Cross-chain bridges + +--- + +## Required Libraries + +### For uet_chain +```toml +libp2p = "0.53" # P2P networking +tokio = "1.0" # Async runtime +rocksdb = "0.22" # Embedded database +lmdb = "0.9" # Alternative storage +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +``` + +### For uet_kb +```toml +lancedb = "0.4" # Vector database +arrow-array = "50" +sqlx = { version = "0.7", features = ["postgres"] } +crossbeam-channel = "0.5" +``` + +--- + +## Reference Projects + +| Project | Purpose | Key Learnings | +|---------|---------|---------------| +| Solana | High-performance blockchain | Parallel processing, account model | +| Polkadot | Multi-chain architecture | Substrate framework, XCMP | +| Cosmos SDK | Tendermint-based chains | ABCI, IBC, governance | +| Ethereum | Smart contract platform | EVM, state management | +| Tendermint | BFT consensus | Consensus engine | + +--- + +## Notes + +- All components in this folder are work-in-progress +- Focus on uet_chain first (Phase 1) +- Governance/Oracle/Economic/Market are deferred +- See `../STATUS_REPORT.md` for detailed status +- See `../PRODUCTION_ROADMAP.md` for full roadmap diff --git a/services_and_experiments/uet_under_development/uet_economic/Cargo.toml b/services_and_experiments/uet_under_development/uet_economic/Cargo.toml new file mode 100644 index 000000000..a7deb2cfe --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_economic/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "uet_economic" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +chrono = { version = "0.4", features = ["serde"] } +thiserror = "1.0" +uuid = { version = "1.0", features = ["v4", "serde"] } +uet_chain = { path = "../../uet_chain" } + +[dev-dependencies] +tokio = { version = "1.0", features = ["full"] } diff --git a/services_and_experiments/uet_under_development/uet_economic/README.md b/services_and_experiments/uet_under_development/uet_economic/README.md new file mode 100644 index 000000000..b41cbdb74 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_economic/README.md @@ -0,0 +1,124 @@ +# UET Economic + +Economic policy engine for difficulty adjustment, issuance budget calculation, and portfolio rotation. + +## Overview + +`uet_economic` provides the core economic policy logic for UET, including: +- Difficulty adjustment based on work unit completion rates +- Issuance budget calculation based on energy input +- Portfolio rotation to maintain task family diversity + +## Features + +- **Difficulty Adjustment**: Adaptive difficulty based on completion rates +- **Issuance Budget**: Energy-based minting with clamping +- **Portfolio Rotation**: Automatic task family weight adjustment +- **Concentration Limits**: Prevent dominance of single task families + +## Usage + +### Difficulty Adjustment + +```rust +use uet_economic::*; + +let config = EconomicConfig::default(); +let engine = DifficultyAdjustmentEngine::new(config); + +// Calculate difficulty adjustment +let result = engine.calculate_adjustment( + "cosmology", // task family + 1.0, // current difficulty + 0.9, // completion rate (90%) +)?; + +println!("Difficulty: {} -> {}", result.old_difficulty, result.new_difficulty); +``` + +### Issuance Budget + +```rust +let engine = IssuanceBudgetEngine::new(config); + +// Calculate issuance budget for epoch +let result = engine.calculate_budget( + 1, // epoch number + 500_000.0, // energy input (kWh) + 100_000, // work units completed +)?; + +println!("Issuance: {} UET Coin", result.total_issuance); +``` + +### Portfolio Rotation + +```rust +let engine = PortfolioRotationEngine::new(config); + +// Rotate portfolio based on performance +let result = engine.rotate_portfolio( + ¤t_portfolio, + vec!["neutrino".to_string()], // new families to add +); + +println!("Added: {:?}", result.added_families); +println!("Removed: {:?}", result.removed_families); +``` + +## Economic Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `target_completion_rate` | 0.8 | Target work unit completion rate (80%) | +| `difficulty_sensitivity` | 0.1 | Difficulty adjustment sensitivity | +| `max_difficulty_multiplier` | 10.0 | Maximum difficulty multiplier | +| `min_difficulty_multiplier` | 0.1 | Minimum difficulty multiplier | +| `epoch_duration_hours` | 24 | Epoch duration in hours | +| `max_issuance_per_epoch` | 1,000,000 | Maximum issuance per epoch | +| `min_issuance_per_epoch` | 100,000 | Minimum issuance per epoch | +| `max_task_family_concentration` | 0.5 | Maximum task family concentration (50%) | +| `min_task_family_weight` | 0.05 | Minimum task family weight | +| `max_task_family_weight` | 0.5 | Maximum task family weight | + +## Difficulty Adjustment Logic + +``` +if completion_rate > target: + new_difficulty = current * (1 + sensitivity * (completion_rate - target)) +elif completion_rate < target: + new_difficulty = current * (1 - sensitivity * (target - completion_rate)) +else: + new_difficulty = current +``` + +## Issuance Budget Logic + +``` +issuance_per_kwh = max_issuance / 1,000,000 +calculated_issuance = energy_input_kwh * issuance_per_kwh +clamped_issuance = clamp(calculated, min_issuance, max_issuance) +``` + +## Portfolio Rotation Logic + +- **Add**: New families if portfolio not full +- **Remove**: Families with <10% completion rate or >max weight +- **Adjust**: Decrease weight if completion rate >90% (too easy) or <50% (too hard) + +## Integration + +Economic engine integrates with: +- `uet_governance`: For policy execution when proposals pass +- `uet_oracle`: For energy input verification +- `uet_chain`: For recording economic policy changes + +## Testing + +```bash +cargo test --package uet_economic +``` + +## License + +MIT diff --git a/services_and_experiments/uet_under_development/uet_economic/src/difficulty.rs b/services_and_experiments/uet_under_development/uet_economic/src/difficulty.rs new file mode 100644 index 000000000..05a6c757a --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_economic/src/difficulty.rs @@ -0,0 +1,135 @@ +use crate::types::*; +use chrono::Utc; +use std::collections::HashMap; + +/// Difficulty adjustment engine +pub struct DifficultyAdjustmentEngine { + config: EconomicConfig, +} + +impl DifficultyAdjustmentEngine { + /// Create a new difficulty adjustment engine + pub fn new(config: EconomicConfig) -> Self { + Self { config } + } + + /// Calculate difficulty adjustment for a task family + pub fn calculate_adjustment( + &self, + task_family: &str, + current_difficulty: f64, + completion_rate: f64, + ) -> Result { + if current_difficulty <= 0.0 { + return Err(EconomicError::InvalidDifficulty(current_difficulty)); + } + + if completion_rate < 0.0 || completion_rate > 1.0 { + return Err(EconomicError::CalculationFailed( + "Invalid completion rate".to_string(), + )); + } + + let target_rate = self.config.target_completion_rate; + let sensitivity = self.config.difficulty_sensitivity; + + // Calculate adjustment factor + let adjustment_factor = if completion_rate > target_rate { + // Too easy: increase difficulty + 1.0 + sensitivity * (completion_rate - target_rate) + } else if completion_rate < target_rate { + // Too hard: decrease difficulty + 1.0 - sensitivity * (target_rate - completion_rate) + } else { + 1.0 // No adjustment needed + }; + + // Apply adjustment + let new_difficulty = current_difficulty * adjustment_factor; + + // Clamp to limits + let clamped_difficulty = new_difficulty + .max(self.config.min_difficulty_multiplier) + .min(self.config.max_difficulty_multiplier); + + let reason = format!( + "Completion rate {:.1}% vs target {:.1}%", + completion_rate * 100.0, + target_rate * 100.0 + ); + + Ok(DifficultyAdjustmentResult { + task_family: task_family.to_string(), + old_difficulty: current_difficulty, + new_difficulty: clamped_difficulty, + adjustment_factor, + reason, + timestamp: Utc::now(), + }) + } + + /// Batch calculate difficulty adjustments for multiple task families + pub fn calculate_batch_adjustments( + &self, + task_families: &HashMap, + ) -> Vec { + task_families + .iter() + .filter_map(|(name, info)| { + self.calculate_adjustment(name, info.difficulty, info.completion_rate) + .ok() + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_difficulty_adjustment_increase() { + let config = EconomicConfig::default(); + let engine = DifficultyAdjustmentEngine::new(config); + + let result = engine + .calculate_adjustment("test", 1.0, 0.9) // 90% completion, target 80% + .unwrap(); + + assert!(result.new_difficulty > result.old_difficulty); + println!("✅ Difficulty increase test passed: {} -> {}", result.old_difficulty, result.new_difficulty); + } + + #[test] + fn test_difficulty_adjustment_decrease() { + let config = EconomicConfig::default(); + let engine = DifficultyAdjustmentEngine::new(config); + + let result = engine + .calculate_adjustment("test", 1.0, 0.6) // 60% completion, target 80% + .unwrap(); + + assert!(result.new_difficulty < result.old_difficulty); + println!("✅ Difficulty decrease test passed: {} -> {}", result.old_difficulty, result.new_difficulty); + } + + #[test] + fn test_difficulty_clamping() { + let config = EconomicConfig::default(); + let engine = DifficultyAdjustmentEngine::new(config); + + // Test max clamp + let result = engine + .calculate_adjustment("test", 100.0, 1.0) // 100% completion + .unwrap(); + assert!(result.new_difficulty <= config.max_difficulty_multiplier); + + // Test min clamp + let result = engine + .calculate_adjustment("test", 0.01, 0.0) // 0% completion + .unwrap(); + assert!(result.new_difficulty >= config.min_difficulty_multiplier); + + println!("✅ Difficulty clamping test passed"); + } +} diff --git a/services_and_experiments/uet_under_development/uet_economic/src/governance_integration.rs b/services_and_experiments/uet_under_development/uet_economic/src/governance_integration.rs new file mode 100644 index 000000000..bcf4a1805 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_economic/src/governance_integration.rs @@ -0,0 +1,174 @@ +// Economic-Governance Integration +// +// This module integrates the economic policy engine with governance, +// registering policy handlers and connecting economic policies to governance. + +use crate::*; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use chrono::{DateTime, Utc}; + +/// Economic policy action types +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum EconomicPolicyAction { + DifficultyAdjustment { + task_family: String, + old_difficulty: f64, + new_difficulty: f64, + }, + IssuanceBudgetUpdate { + epoch: u64, + total_issuance: u64, + energy_input_kwh: f64, + }, + PortfolioRotation { + added_families: Vec, + removed_families: Vec, + adjusted_weights: Vec<(String, f64)>, + }, +} + +/// Economic policy handler for governance +pub struct EconomicPolicyHandler { + difficulty_engine: DifficultyAdjustmentEngine, + issuance_engine: IssuanceBudgetEngine, + portfolio_engine: PortfolioRotationEngine, +} + +impl EconomicPolicyHandler { + /// Create a new economic policy handler + pub fn new(config: EconomicConfig) -> Self { + Self { + difficulty_engine: DifficultyAdjustmentEngine::new(config.clone()), + issuance_engine: IssuanceBudgetEngine::new(config.clone()), + portfolio_engine: PortfolioRotationEngine::new(config), + } + } + + /// Register this handler with the governance system + pub fn register_with_governance( + &self, + _governance: &mut (), + ) -> Result<(), ()> { + println!("Registered economic policy handler with governance"); + Ok(()) + } + + /// Execute a difficulty adjustment + pub fn execute_difficulty_adjustment( + &self, + task_family: String, + current_difficulty: f64, + completion_rate: f64, + ) -> Result { + let result = self + .difficulty_engine + .calculate_adjustment(&task_family, current_difficulty, completion_rate)?; + + Ok(EconomicPolicyAction::DifficultyAdjustment { + task_family, + old_difficulty: result.old_difficulty, + new_difficulty: result.new_difficulty, + }) + } + + /// Execute an issuance budget update + pub fn execute_issuance_budget( + &self, + epoch: u64, + energy_input_kwh: f64, + work_units_completed: u64, + ) -> Result { + let result = self + .issuance_engine + .calculate_budget(epoch, energy_input_kwh, work_units_completed)?; + + Ok(EconomicPolicyAction::IssuanceBudgetUpdate { + epoch, + total_issuance: result.total_issuance, + energy_input_kwh: result.energy_input_kwh, + }) + } + + /// Execute a portfolio rotation + pub fn execute_portfolio_rotation( + &self, + current_portfolio: &std::collections::HashMap, + new_families: Vec, + ) -> Result { + let result = self + .portfolio_engine + .rotate_portfolio(current_portfolio, new_families); + + Ok(EconomicPolicyAction::PortfolioRotation { + added_families: result.added_families, + removed_families: result.removed_families, + adjusted_weights: result.adjusted_weights, + }) + } +} + +// TODO: Implement PolicyHandler when circular dependency is resolved +/* +impl PolicyHandler for EconomicPolicyHandler { + fn execute(&self, action: &PolicyAction) -> Result { + match action { + PolicyAction::DifficultyAdjustment { task_family, difficulty, completion_rate } => { + let result = self.execute_difficulty_adjustment( + task_family.clone(), + *difficulty, + *completion_rate, + )?; + + Ok(ExecutionResult { + success: true, + message: format!("Difficulty adjusted: {:?}", result), + data: Some(serde_json::to_value(result).unwrap_or_default()), + }) + } + PolicyAction::AddTaskFamily { family_name } => { + Ok(ExecutionResult { + success: true, + message: format!("Task family {} added", family_name), + data: None, + }) + } + _ => Ok(ExecutionResult { + success: false, + message: "Unsupported policy action".to_string(), + data: None, + }), + } + } +} +*/ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_economic_policy_handler() { + let config = EconomicConfig::default(); + let handler = EconomicPolicyHandler::new(config); + + // Execute difficulty adjustment + let result = handler + .execute_difficulty_adjustment( + "cosmology".to_string(), + 1.0, + 0.9, + ) + .unwrap(); + + match result { + EconomicPolicyAction::DifficultyAdjustment { task_family, old_difficulty, new_difficulty } => { + assert_eq!(task_family, "cosmology"); + assert!(new_difficulty > old_difficulty); + } + _ => panic!("Expected DifficultyAdjustment result"), + } + + println!("✅ Economic policy handler test passed"); + } +} diff --git a/services_and_experiments/uet_under_development/uet_economic/src/issuance.rs b/services_and_experiments/uet_under_development/uet_economic/src/issuance.rs new file mode 100644 index 000000000..6d70a8ee1 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_economic/src/issuance.rs @@ -0,0 +1,115 @@ +use crate::types::*; +use chrono::Utc; + +/// Issuance budget engine +pub struct IssuanceBudgetEngine { + config: EconomicConfig, +} + +impl IssuanceBudgetEngine { + /// Create a new issuance budget engine + pub fn new(config: EconomicConfig) -> Self { + Self { config } + } + + /// Calculate issuance budget for an epoch + pub fn calculate_budget( + &self, + epoch: u64, + energy_input_kwh: f64, + work_units_completed: u64, + ) -> Result { + if energy_input_kwh <= 0.0 { + return Err(EconomicError::InsufficientData); + } + + if work_units_completed == 0 { + return Err(EconomicError::InsufficientData); + } + + // Calculate issuance based on energy input + // More energy = more issuance (up to max) + let issuance_per_kwh = self.config.max_issuance_per_epoch as f64 / 1_000_000.0; // Base rate + let calculated_issuance = (energy_input_kwh * issuance_per_kwh) as u64; + + // Clamp to limits + let clamped_issuance = calculated_issuance + .max(self.config.min_issuance_per_epoch) + .min(self.config.max_issuance_per_epoch); + + Ok(IssuanceBudgetResult { + epoch, + total_issuance: clamped_issuance, + energy_input_kwh, + issuance_per_kwh, + timestamp: Utc::now(), + }) + } + + /// Calculate issuance based on work units + pub fn calculate_by_work_units( + &self, + epoch: u64, + work_units: u64, + base_issuance_per_unit: f64, + ) -> Result { + if work_units == 0 { + return Err(EconomicError::InsufficientData); + } + + let calculated_issuance = (work_units as f64 * base_issuance_per_unit) as u64; + + // Clamp to limits + let clamped_issuance = calculated_issuance + .max(self.config.min_issuance_per_epoch) + .min(self.config.max_issuance_per_epoch); + + Ok(IssuanceBudgetResult { + epoch, + total_issuance: clamped_issuance, + energy_input_kwh: 0.0, + issuance_per_kwh: base_issuance_per_unit, + timestamp: Utc::now(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_issuance_calculation() { + let config = EconomicConfig::default(); + let engine = IssuanceBudgetEngine::new(config); + + let result = engine + .calculate_budget(1, 500_000.0, 100_000) // 500k kWh, 100k work units + .unwrap(); + + assert!(result.total_issuance >= config.min_issuance_per_epoch); + assert!(result.total_issuance <= config.max_issuance_per_epoch); + + println!("✅ Issuance calculation test passed: {} UET Coin", result.total_issuance); + } + + #[test] + fn test_issuance_clamping() { + let config = EconomicConfig::default(); + let engine = IssuanceBudgetEngine::new(config); + + // Test max clamp (extreme energy input) + let result = engine + .calculate_budget(1, 10_000_000.0, 1_000_000) + .unwrap(); + assert!(result.total_issuance <= config.max_issuance_per_epoch); + + // Test min clamp (minimal energy input) + let result = engine + .calculate_budget(1, 1.0, 1) + .unwrap(); + assert!(result.total_issuance >= config.min_issuance_per_epoch); + + println!("✅ Issuance clamping test passed"); + } +} diff --git a/services_and_experiments/uet_under_development/uet_economic/src/lib.rs b/services_and_experiments/uet_under_development/uet_economic/src/lib.rs new file mode 100644 index 000000000..7e8720beb --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_economic/src/lib.rs @@ -0,0 +1,59 @@ +// UET Economic - Economic Policy Engine +// +// This crate provides economic policy engines for difficulty adjustment, +// issuance budget calculation, and portfolio rotation. + +pub mod types; +pub mod difficulty; +pub mod issuance; +pub mod portfolio; +pub mod governance_integration; + +pub use types::*; +pub use difficulty::DifficultyAdjustmentEngine; +pub use issuance::IssuanceBudgetEngine; +pub use portfolio::PortfolioRotationEngine; +pub use governance_integration::{EconomicPolicyHandler, EconomicPolicyAction}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_economic_roundtrip() { + let config = EconomicConfig::default(); + + // Difficulty adjustment + let diff_engine = DifficultyAdjustmentEngine::new(config.clone()); + let diff_result = diff_engine + .calculate_adjustment("cosmology", 1.0, 0.9) + .unwrap(); + assert!(diff_result.new_difficulty > diff_result.old_difficulty); + + // Issuance budget + let issuance_engine = IssuanceBudgetEngine::new(config.clone()); + let issuance_result = issuance_engine + .calculate_budget(1, 500_000.0, 100_000) + .unwrap(); + assert!(issuance_result.total_issuance > 0); + + // Portfolio rotation + let portfolio_engine = PortfolioRotationEngine::new(config); + let mut portfolio = HashMap::new(); + portfolio.insert( + "cosmology".to_string(), + TaskFamilyInfo { + name: "cosmology".to_string(), + weight: 0.3, + difficulty: 1.0, + completion_rate: 0.9, + work_units_completed: 1000, + work_units_assigned: 1111, + }, + ); + let rotation_result = portfolio_engine.rotate_portfolio(&portfolio, vec!["neutrino".to_string()]); + assert!(rotation_result.added_families.contains(&"neutrino".to_string())); + + println!("✅ Economic roundtrip test passed"); + } +} diff --git a/services_and_experiments/uet_under_development/uet_economic/src/portfolio.rs b/services_and_experiments/uet_under_development/uet_economic/src/portfolio.rs new file mode 100644 index 000000000..a5f598906 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_economic/src/portfolio.rs @@ -0,0 +1,211 @@ +use crate::types::*; +use std::collections::HashMap; +use chrono::Utc; + +/// Portfolio rotation engine +pub struct PortfolioRotationEngine { + config: EconomicConfig, +} + +impl PortfolioRotationEngine { + /// Create a new portfolio rotation engine + pub fn new(config: EconomicConfig) -> Self { + Self { config } + } + + /// Check if a task family should be added to the portfolio + pub fn should_add_family( + &self, + family_name: &str, + current_portfolio: &HashMap, + ) -> bool { + // Check if already exists + if current_portfolio.contains_key(family_name) { + return false; + } + + // Check concentration limit + let total_weight: f64 = current_portfolio.values().map(|f| f.weight).sum(); + if total_weight >= 0.95 { + return false; // Portfolio nearly full + } + + true + } + + /// Check if a task family should be removed from the portfolio + pub fn should_remove_family( + &self, + family_name: &str, + task_family: &TaskFamilyInfo, + ) -> bool { + // Remove if completion rate is too low (too hard) + if task_family.completion_rate < 0.1 { + return true; + } + + // Remove if concentration is too high + if task_family.weight > self.config.max_task_family_weight { + return true; + } + + false + } + + /// Calculate weight adjustments for task families + pub fn calculate_weight_adjustments( + &self, + task_families: &HashMap, + ) -> Vec<(String, f64)> { + let mut adjustments = Vec::new(); + + for (name, info) in task_families { + let new_weight = if info.completion_rate > 0.9 { + // Too easy: decrease weight + (info.weight * 0.9).max(self.config.min_task_family_weight) + } else if info.completion_rate < 0.5 { + // Too hard: decrease weight + (info.weight * 0.8).max(self.config.min_task_family_weight) + } else { + // Optimal: maintain weight + info.weight + }; + + if (new_weight - info.weight).abs() > 0.01 { + adjustments.push((name.clone(), new_weight)); + } + } + + adjustments + } + + /// Execute portfolio rotation + pub fn rotate_portfolio( + &self, + current_portfolio: &HashMap, + new_families: Vec, + ) -> PortfolioRotationResult { + let mut added_families = Vec::new(); + let mut removed_families = Vec::new(); + let mut adjusted_weights = Vec::new(); + + // Check for families to add + for family_name in new_families { + if self.should_add_family(&family_name, current_portfolio) { + added_families.push(family_name); + } + } + + // Check for families to remove + for (name, info) in current_portfolio { + if self.should_remove_family(name, info) { + removed_families.push(name.clone()); + } + } + + // Calculate weight adjustments + let adjustments = self.calculate_weight_adjustments(current_portfolio); + adjusted_weights = adjustments; + + PortfolioRotationResult { + added_families, + removed_families, + adjusted_weights, + timestamp: Utc::now(), + } + } + + /// Check concentration limits + pub fn check_concentration( + &self, + task_families: &HashMap, + ) -> Result<(), EconomicError> { + for (name, info) in task_families { + if info.weight > self.config.max_task_family_weight { + return Err(EconomicError::ConcentrationLimitExceeded( + info.weight * 100.0, + self.config.max_task_family_weight * 100.0, + )); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_portfolio_rotation() { + let config = EconomicConfig::default(); + let engine = PortfolioRotationEngine::new(config); + + let mut portfolio = HashMap::new(); + portfolio.insert( + "cosmology".to_string(), + TaskFamilyInfo { + name: "cosmology".to_string(), + weight: 0.3, + difficulty: 1.0, + completion_rate: 0.9, + work_units_completed: 1000, + work_units_assigned: 1111, + }, + ); + + let result = engine.rotate_portfolio(&portfolio, vec!["neutrino".to_string()]); + + assert!(result.added_families.contains(&"neutrino".to_string())); + println!("✅ Portfolio rotation test passed: added {:?}", result.added_families); + } + + #[test] + fn test_concentration_check() { + let config = EconomicConfig::default(); + let engine = PortfolioRotationEngine::new(config); + + let mut portfolio = HashMap::new(); + portfolio.insert( + "test".to_string(), + TaskFamilyInfo { + name: "test".to_string(), + weight: 0.6, // Above max 0.5 + difficulty: 1.0, + completion_rate: 0.8, + work_units_completed: 1000, + work_units_assigned: 1250, + }, + ); + + let result = engine.check_concentration(&portfolio); + assert!(result.is_err()); + + println!("✅ Concentration check test passed"); + } + + #[test] + fn test_weight_adjustments() { + let config = EconomicConfig::default(); + let engine = PortfolioRotationEngine::new(config); + + let mut portfolio = HashMap::new(); + portfolio.insert( + "easy_task".to_string(), + TaskFamilyInfo { + name: "easy_task".to_string(), + weight: 0.4, + difficulty: 1.0, + completion_rate: 0.95, // Too easy + work_units_completed: 950, + work_units_assigned: 1000, + }, + ); + + let adjustments = engine.calculate_weight_adjustments(&portfolio); + assert!(!adjustments.is_empty()); + + println!("✅ Weight adjustments test passed: {:?}", adjustments); + } +} diff --git a/services_and_experiments/uet_under_development/uet_economic/src/types.rs b/services_and_experiments/uet_under_development/uet_economic/src/types.rs new file mode 100644 index 000000000..20c3ebd43 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_economic/src/types.rs @@ -0,0 +1,108 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc, Duration}; +use uuid::Uuid; + +/// Economic policy parameters +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EconomicConfig { + /// Target work unit completion rate (percentage) + pub target_completion_rate: f64, + /// Difficulty adjustment sensitivity (0.0 to 1.0) + pub difficulty_sensitivity: f64, + /// Maximum difficulty multiplier + pub max_difficulty_multiplier: f64, + /// Minimum difficulty multiplier + pub min_difficulty_multiplier: f64, + /// Epoch duration in hours + pub epoch_duration_hours: u64, + /// Maximum issuance per epoch + pub max_issuance_per_epoch: u64, + /// Minimum issuance per epoch + pub min_issuance_per_epoch: u64, + /// Maximum task family concentration (percentage) + pub max_task_family_concentration: f64, + /// Minimum task family weight + pub min_task_family_weight: f64, + /// Maximum task family weight + pub max_task_family_weight: f64, +} + +impl Default for EconomicConfig { + fn default() -> Self { + Self { + target_completion_rate: 0.8, // 80% target completion + difficulty_sensitivity: 0.1, + max_difficulty_multiplier: 10.0, + min_difficulty_multiplier: 0.1, + epoch_duration_hours: 24, // 24 hours per epoch + max_issuance_per_epoch: 1_000_000, + min_issuance_per_epoch: 100_000, + max_task_family_concentration: 0.5, // Max 50% concentration + min_task_family_weight: 0.05, + max_task_family_weight: 0.5, + } + } +} + +/// Task family information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaskFamilyInfo { + pub name: String, + pub weight: f64, + pub difficulty: f64, + pub completion_rate: f64, + pub work_units_completed: u64, + pub work_units_assigned: u64, +} + +/// Difficulty adjustment result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DifficultyAdjustmentResult { + pub task_family: String, + pub old_difficulty: f64, + pub new_difficulty: f64, + pub adjustment_factor: f64, + pub reason: String, + pub timestamp: DateTime, +} + +/// Issuance budget calculation result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IssuanceBudgetResult { + pub epoch: u64, + pub total_issuance: u64, + pub energy_input_kwh: f64, + pub issuance_per_kwh: f64, + pub timestamp: DateTime, +} + +/// Portfolio rotation result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PortfolioRotationResult { + pub added_families: Vec, + pub removed_families: Vec, + pub adjusted_weights: Vec<(String, f64)>, + pub timestamp: DateTime, +} + +/// Economic error types +#[derive(Debug, thiserror::Error)] +pub enum EconomicError { + #[error("Task family not found: {0}")] + TaskFamilyNotFound(String), + + #[error("Invalid difficulty value: {0}")] + InvalidDifficulty(f64), + + #[error("Invalid weight value: {0}")] + InvalidWeight(f64), + + #[error("Concentration limit exceeded: {0}% > {1}%")] + ConcentrationLimitExceeded(f64, f64), + + #[error("Insufficient data for calculation")] + InsufficientData, + + #[error("Calculation failed: {0}")] + CalculationFailed(String), +} diff --git a/services_and_experiments/uet_under_development/uet_governance/Cargo.toml b/services_and_experiments/uet_under_development/uet_governance/Cargo.toml new file mode 100644 index 000000000..1030b83ee --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_governance/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "uet_governance" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +chrono = { version = "0.4", features = ["serde"] } +thiserror = "1.0" +uuid = { version = "1.0", features = ["v4", "serde"] } +uet_security = { path = "../../uet_security" } +uet_chain = { path = "../../uet_chain" } + +[dev-dependencies] +tokio = { version = "1.0", features = ["full"] } diff --git a/services_and_experiments/uet_under_development/uet_governance/README.md b/services_and_experiments/uet_under_development/uet_governance/README.md new file mode 100644 index 000000000..d61aae8e4 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_governance/README.md @@ -0,0 +1,130 @@ +# UET Governance + +Decentralized governance system for managing UET's economic policy, including difficulty adjustment, issuance budget, task portfolio rotation, and other economic parameters. + +## Overview + +`uet_governance` provides a complete governance framework for the UET ecosystem, enabling decentralized decision-making for economic policy changes. It supports multiple voting strategies (1-person-1-vote, token-weighted, node-weighted, quadratic voting) and includes a proposal lifecycle management system with policy execution. + +## Features + +- **Voting Protocol**: Flexible voting power calculation strategies +- **Proposal Lifecycle**: Draft → Voting → Passed/Failed → Executed +- **Policy Execution**: Extensible handler system for policy changes +- **Emergency Veto**: Emergency authority can veto proposals +- **Quorum & Approval Thresholds**: Configurable governance parameters +- **Time Lock**: Delayed execution to prevent rushed changes + +## Usage + +```rust +use uet_governance::*; +use uet_governance::policy::{DifficultyAdjustmentHandler, TaskFamilyAdditionHandler}; + +// Setup governance system +let config = GovernanceConfig::default(); +let protocol = VotingProtocol::new(config, VotingPowerStrategy::OnePersonOneVote); +let manager = ProposalManager::new(protocol); +let mut engine = PolicyEngine::new(manager); + +// Register policy handlers +engine.register_handler( + "difficulty_adjustment".to_string(), + Box::new(DifficultyAdjustmentHandler), +); +engine.register_handler( + "task_family_addition".to_string(), + Box::new(TaskFamilyAdditionHandler), +); + +// Add voters +engine.manager().add_voter(VoterInfo { + id: voter_id, + name: "University Node".to_string(), + voting_weight: 1000, + is_active: true, +}); + +// Create a proposal +let proposal_id = engine.manager().create_proposal( + voter_id, + ProposalType::DifficultyAdjustment { + task_family: "cosmology".to_string(), + new_difficulty: 1.5, + }, + "Increase Cosmology Difficulty".to_string(), + "Adjust difficulty to maintain optimal work unit completion rate".to_string(), +)?; + +// Submit for voting +engine.manager().submit_proposal(proposal_id)?; + +// Cast votes +engine.manager().cast_vote(proposal_id, voter_id, Vote::Approve)?; + +// Finalize voting +engine.manager().finalize_voting(proposal_id)?; + +// Execute the proposal +engine.execute_proposal(proposal_id)?; +``` + +## Voting Strategies + +### One Person One Vote +Each voter gets exactly 1 vote, regardless of their weight. + +### Token Weighted +Voting power is proportional to token holdings. + +### Node Weighted +Voting power is proportional to node contribution (work units completed). + +### Quadratic +Voting power = sqrt(weight), reducing whale influence. + +### Hybrid +Combines token and node weights with configurable ratios. + +## Proposal Types + +- `DifficultyAdjustment`: Adjust difficulty parameters for a task family +- `IssuanceBudgetAdjustment`: Adjust issuance budget for an epoch +- `TaskFamilyAddition`: Add a new task family to the portfolio +- `TaskFamilyRemoval`: Remove a task family from the portfolio +- `TaskFamilyWeightAdjustment`: Adjust task family weights +- `EmergencyDisable`: Emergency disable of a compromised task family +- `OracleConfigUpdate`: Update oracle configuration +- `GovernanceParameterUpdate`: Update governance parameters +- `MintingPolicyUpdate`: Minting policy changes + +## Governance Configuration + +```rust +GovernanceConfig { + min_voting_period_hours: 24, + max_voting_period_hours: 168, // 7 days + min_quorum_percentage: 30.0, + min_approval_percentage: 60.0, + time_lock_hours: 24, + emergency_authority_id: Some(authority_id), +} +``` + +## Integration + +Governance integrates with: +- `uet_chain`: For ledger-based proposal recording +- `uet_security`: For signing proposals and votes +- `uet_oracle` (planned): For oracle verification in policy execution + +## Testing + +Run tests: +```bash +cargo test --package uet_governance +``` + +## License + +MIT diff --git a/services_and_experiments/uet_under_development/uet_governance/src/ledger_integration.rs b/services_and_experiments/uet_under_development/uet_governance/src/ledger_integration.rs new file mode 100644 index 000000000..0c4780c03 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_governance/src/ledger_integration.rs @@ -0,0 +1,188 @@ +// Governance-Chain Integration +// +// This module integrates the governance system with the ledger, +// recording proposals, votes, and execution results on-chain. + +use crate::*; +use uet_chain::*; +use uet_security::*; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use chrono::{DateTime, Utc}; + +/// Governance event types for ledger recording +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum GovernanceEvent { + ProposalCreated { + proposal_id: Uuid, + proposer_id: Uuid, + proposal_type: ProposalType, + title: String, + description: String, + timestamp: DateTime, + }, + VoteCast { + proposal_id: Uuid, + voter_id: Uuid, + vote: Vote, + voting_power: u64, + timestamp: DateTime, + }, + ProposalFinalized { + proposal_id: Uuid, + status: ProposalStatus, + approval_votes: u64, + total_votes: u64, + timestamp: DateTime, + }, + PolicyExecuted { + proposal_id: Uuid, + policy_type: String, + result: ExecutionResult, + timestamp: DateTime, + }, +} + +/// Governance ledger recorder +pub struct GovernanceLedgerRecorder { + // In a real implementation, this would hold a reference to the chain +} + +impl GovernanceLedgerRecorder { + /// Create a new governance ledger recorder + pub fn new() -> Self { + Self {} + } + + /// Record a governance event to the ledger + pub fn record_event(&self, event: GovernanceEvent) -> Result<(), GovernanceError> { + // In a real implementation, this would: + // 1. Serialize the event + // 2. Create a transaction + // 3. Submit to the chain + // 4. Wait for confirmation + + println!("Recording governance event: {:?}", event); + Ok(()) + } + + /// Create a proposal and record it + pub fn create_and_record_proposal( + &self, + proposer_id: Uuid, + proposal_type: ProposalType, + title: String, + description: String, + ) -> Result { + let proposal_id = Uuid::new_v4(); + + let event = GovernanceEvent::ProposalCreated { + proposal_id, + proposer_id, + proposal_type, + title, + description, + timestamp: Utc::now(), + }; + + self.record_event(event)?; + Ok(proposal_id) + } + + /// Record a vote + pub fn record_vote( + &self, + proposal_id: Uuid, + voter_id: Uuid, + vote: Vote, + voting_power: u64, + ) -> Result<(), GovernanceError> { + let event = GovernanceEvent::VoteCast { + proposal_id, + voter_id, + vote, + voting_power, + timestamp: Utc::now(), + }; + + self.record_event(event) + } + + /// Record proposal finalization + pub fn record_finalization( + &self, + proposal_id: Uuid, + status: ProposalStatus, + approval_votes: u64, + total_votes: u64, + ) -> Result<(), GovernanceError> { + let event = GovernanceEvent::ProposalFinalized { + proposal_id, + status, + approval_votes, + total_votes, + timestamp: Utc::now(), + }; + + self.record_event(event) + } + + /// Record policy execution + pub fn record_execution( + &self, + proposal_id: Uuid, + policy_type: String, + result: ExecutionResult, + ) -> Result<(), GovernanceError> { + let event = GovernanceEvent::PolicyExecuted { + proposal_id, + policy_type, + result, + timestamp: Utc::now(), + }; + + self.record_event(event) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_governance_ledger_recorder() { + let recorder = GovernanceLedgerRecorder::new(); + + // Create proposal + let proposal_id = recorder + .create_and_record_proposal( + Uuid::new_v4(), + ProposalType::DifficultyAdjustment, + "Adjust Cosmology Difficulty".to_string(), + "Increase difficulty to 1.1".to_string(), + ) + .unwrap(); + + // Record vote + recorder + .record_vote( + proposal_id, + Uuid::new_v4(), + Vote::Approve, + 1000, + ) + .unwrap(); + + // Record finalization + recorder + .record_finalization( + proposal_id, + ProposalStatus::Passed, + 800, + 1000, + ) + .unwrap(); + + println!("✅ Governance ledger recorder test passed"); + } +} diff --git a/services_and_experiments/uet_under_development/uet_governance/src/lib.rs b/services_and_experiments/uet_under_development/uet_governance/src/lib.rs new file mode 100644 index 000000000..30f51d64d --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_governance/src/lib.rs @@ -0,0 +1,192 @@ +// UET Governance - Decentralized Governance System +// +// This crate provides voting protocols, proposal lifecycle management, +// and policy execution for the UET economic system. + +pub mod types; +pub mod voting; +pub mod proposal; +pub mod policy; +pub mod ledger_integration; + +pub use types::*; +pub use voting::VotingProtocol; +pub use proposal::ProposalManager; +pub use policy::{PolicyEngine, PolicyHandler}; +pub use ledger_integration::{GovernanceEvent, GovernanceLedgerRecorder}; + +#[cfg(test)] +mod tests { + use super::*; + use crate::policy::{DifficultyAdjustmentHandler, TaskFamilyAdditionHandler}; + + #[test] + fn test_governance_roundtrip() { + // Setup governance system + let config = GovernanceConfig::default(); + let protocol = VotingProtocol::new(config, VotingPowerStrategy::OnePersonOneVote); + let manager = ProposalManager::new(protocol); + let mut engine = PolicyEngine::new(manager); + + // Register policy handlers + engine.register_handler( + "difficulty_adjustment".to_string(), + Box::new(DifficultyAdjustmentHandler), + ); + engine.register_handler( + "task_family_addition".to_string(), + Box::new(TaskFamilyAdditionHandler), + ); + + // Add voters + let voter1 = Uuid::new_v4(); + let voter2 = Uuid::new_v4(); + let voter3 = Uuid::new_v4(); + + engine.manager().add_voter(VoterInfo { + id: voter1, + name: "Voter 1".to_string(), + voting_weight: 100, + is_active: true, + }); + engine.manager().add_voter(VoterInfo { + id: voter2, + name: "Voter 2".to_string(), + voting_weight: 100, + is_active: true, + }); + engine.manager().add_voter(VoterInfo { + id: voter3, + name: "Voter 3".to_string(), + voting_weight: 100, + is_active: true, + }); + + // Create a proposal + let proposal_id = engine + .manager() + .create_proposal( + voter1, + ProposalType::DifficultyAdjustment { + task_family: "cosmology".to_string(), + new_difficulty: 1.5, + }, + "Increase Cosmology Difficulty".to_string(), + "Adjust difficulty to maintain optimal work unit completion rate".to_string(), + ) + .unwrap(); + + // Submit for voting + engine.manager().submit_proposal(proposal_id).unwrap(); + + // Cast votes (2 approve, 1 reject) + engine.manager().cast_vote(proposal_id, voter1, Vote::Approve).unwrap(); + engine.manager().cast_vote(proposal_id, voter2, Vote::Approve).unwrap(); + engine.manager().cast_vote(proposal_id, voter3, Vote::Reject).unwrap(); + + // Finalize voting + let status = engine.manager().finalize_voting(proposal_id).unwrap(); + assert_eq!(status, ProposalStatus::Passed); + + // Execute the proposal + engine.execute_proposal(proposal_id).unwrap(); + + // Verify execution + let proposal = engine.manager().get_proposal(proposal_id).unwrap(); + assert_eq!(proposal.status, ProposalStatus::Executed); + assert!(proposal.execution_result.is_some()); + assert!(proposal.execution_result.as_ref().unwrap().success); + + println!("✅ Governance roundtrip test passed"); + println!(" Proposal ID: {}", proposal_id); + println!(" Votes: 2 approve, 1 reject"); + println!(" Status: Executed"); + } + + #[test] + fn test_quadratic_voting() { + let config = GovernanceConfig::default(); + let protocol = VotingProtocol::new(config, VotingPowerStrategy::Quadratic); + let manager = ProposalManager::new(protocol); + + // Add voters with different weights + let voter1 = Uuid::new_v4(); + let voter2 = Uuid::new_v4(); + let voter3 = Uuid::new_v4(); + + manager.add_voter(VoterInfo { + id: voter1, + name: "Voter 1".to_string(), + voting_weight: 100, // sqrt = 10 + is_active: true, + }); + manager.add_voter(VoterInfo { + id: voter2, + name: "Voter 2".to_string(), + voting_weight: 400, // sqrt = 20 + is_active: true, + }); + manager.add_voter(VoterInfo { + id: voter3, + name: "Voter 3".to_string(), + voting_weight: 900, // sqrt = 30 + is_active: true, + }); + + // Verify quadratic voting power + let power1 = protocol.calculate_voting_power(&voter1, &manager.state.voters).unwrap(); + let power2 = protocol.calculate_voting_power(&voter2, &manager.state.voters).unwrap(); + let power3 = protocol.calculate_voting_power(&voter3, &manager.state.voters).unwrap(); + + assert_eq!(power1, 10); + assert_eq!(power2, 20); + assert_eq!(power3, 30); + + println!("✅ Quadratic voting test passed"); + println!(" Voter 1 (weight 100): power = {}", power1); + println!(" Voter 2 (weight 400): power = {}", power2); + println!(" Voter 3 (weight 900): power = {}", power3); + } + + #[test] + fn test_emergency_veto() { + let config = GovernanceConfig::default(); + let protocol = VotingProtocol::new(config, VotingPowerStrategy::OnePersonOneVote); + let mut manager = ProposalManager::new(protocol); + + // Set emergency authority + let authority_id = Uuid::new_v4(); + manager.state.config.emergency_authority_id = Some(authority_id); + + // Add voters + let voter_id = Uuid::new_v4(); + manager.add_voter(VoterInfo { + id: voter_id, + name: "Test Voter".to_string(), + voting_weight: 100, + is_active: true, + }); + + // Create a proposal + let proposal_id = manager + .create_proposal( + voter_id, + ProposalType::DifficultyAdjustment { + task_family: "test".to_string(), + new_difficulty: 1.0, + }, + "Test Proposal".to_string(), + "Test Description".to_string(), + ) + .unwrap(); + + // Veto the proposal + manager.veto_proposal(proposal_id, authority_id).unwrap(); + + // Verify vetoed status + let proposal = manager.get_proposal(proposal_id).unwrap(); + assert_eq!(proposal.status, ProposalStatus::Vetoed); + + println!("✅ Emergency veto test passed"); + } +} diff --git a/services_and_experiments/uet_under_development/uet_governance/src/policy.rs b/services_and_experiments/uet_under_development/uet_governance/src/policy.rs new file mode 100644 index 000000000..3a8d93bc7 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_governance/src/policy.rs @@ -0,0 +1,190 @@ +use crate::types::*; +use crate::proposal::ProposalManager; +use std::collections::HashMap; + +/// Policy execution engine +pub struct PolicyEngine { + manager: ProposalManager, + policy_handlers: HashMap>, +} + +/// Trait for handling policy execution +pub trait PolicyHandler: Send + Sync { + fn execute(&self, proposal: &Proposal) -> Result; +} + +impl PolicyEngine { + /// Create a new policy engine + pub fn new(manager: ProposalManager) -> Self { + Self { + manager, + policy_handlers: HashMap::new(), + } + } + + /// Register a policy handler + pub fn register_handler(&mut self, policy_name: String, handler: Box) { + self.policy_handlers.insert(policy_name, handler); + } + + /// Execute a passed proposal + pub fn execute_proposal(&mut self, proposal_id: ProposalId) -> Result<(), GovernanceError> { + let proposal = self.manager.get_proposal(proposal_id) + .ok_or(GovernanceError::ProposalNotFound(proposal_id))?; + + if proposal.status != ProposalStatus::Passed { + return Err(GovernanceError::InvalidProposalType); + } + + // Get the appropriate handler based on proposal type + let handler_key = match &proposal.proposal_type { + ProposalType::DifficultyAdjustment { .. } => "difficulty_adjustment", + ProposalType::IssuanceBudgetAdjustment { .. } => "issuance_budget", + ProposalType::TaskFamilyAddition { .. } => "task_family_addition", + ProposalType::TaskFamilyRemoval { .. } => "task_family_removal", + ProposalType::TaskFamilyWeightAdjustment { .. } => "task_family_weight", + ProposalType::EmergencyDisable { .. } => "emergency_disable", + ProposalType::OracleConfigUpdate { .. } => "oracle_config", + ProposalType::GovernanceParameterUpdate { .. } => "governance_param", + ProposalType::MintingPolicyUpdate { .. } => "minting_policy", + }; + + let handler = self.policy_handlers.get(handler_key) + .ok_or(GovernanceError::ExecutionFailed( + "No handler registered for this proposal type".to_string() + ))?; + + // Execute the policy + let result = handler.execute(proposal)?; + + // Update proposal status + if let Some(p) = self.manager.get_proposal_mut(proposal_id) { + p.execution_result = Some(result.clone()); + p.status = if result.success { + ProposalStatus::Executed + } else { + ProposalStatus::ExecutionFailed + }; + } + + Ok(()) + } + + /// Get the proposal manager + pub fn manager(&mut self) -> &mut ProposalManager { + &mut self.manager + } +} + +/// Example handler for difficulty adjustment +pub struct DifficultyAdjustmentHandler; + +impl PolicyHandler for DifficultyAdjustmentHandler { + fn execute(&self, proposal: &Proposal) -> Result { + if let ProposalType::DifficultyAdjustment { task_family, new_difficulty } = &proposal.proposal_type { + // In a real implementation, this would update the difficulty in the chain + println!( + "Adjusting difficulty for task family '{}' to {}", + task_family, new_difficulty + ); + + Ok(ExecutionResult { + success: true, + message: format!( + "Difficulty adjusted for {} to {}", + task_family, new_difficulty + ), + executed_at: chrono::Utc::now(), + }) + } else { + Err(GovernanceError::InvalidProposalType) + } + } +} + +/// Example handler for task family addition +pub struct TaskFamilyAdditionHandler; + +impl PolicyHandler for TaskFamilyAdditionHandler { + fn execute(&self, proposal: &Proposal) -> Result { + if let ProposalType::TaskFamilyAddition { family_name, weight } = &proposal.proposal_type { + // In a real implementation, this would add the task family to the portfolio + println!( + "Adding task family '{}' with weight {}", + family_name, weight + ); + + Ok(ExecutionResult { + success: true, + message: format!( + "Task family {} added with weight {}", + family_name, weight + ), + executed_at: chrono::Utc::now(), + }) + } else { + Err(GovernanceError::InvalidProposalType) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::voting::VotingProtocol; + + #[test] + fn test_policy_execution() { + let config = GovernanceConfig::default(); + let protocol = VotingProtocol::new(config, VotingPowerStrategy::OnePersonOneVote); + let manager = ProposalManager::new(protocol); + let mut engine = PolicyEngine::new(manager); + + // Register handlers + engine.register_handler( + "difficulty_adjustment".to_string(), + Box::new(DifficultyAdjustmentHandler), + ); + engine.register_handler( + "task_family_addition".to_string(), + Box::new(TaskFamilyAdditionHandler), + ); + + // Add a voter + let voter_id = Uuid::new_v4(); + engine.manager().add_voter(VoterInfo { + id: voter_id, + name: "Test Voter".to_string(), + voting_weight: 100, + is_active: true, + }); + + // Create and submit a proposal + let proposal_id = engine + .manager() + .create_proposal( + voter_id, + ProposalType::DifficultyAdjustment { + task_family: "test".to_string(), + new_difficulty: 1.5, + }, + "Test Proposal".to_string(), + "Test Description".to_string(), + ) + .unwrap(); + + engine.manager().submit_proposal(proposal_id).unwrap(); + engine.manager().cast_vote(proposal_id, voter_id, Vote::Approve).unwrap(); + + // Finalize voting (manually set status for testing) + if let Some(p) = engine.manager().get_proposal_mut(proposal_id) { + p.status = ProposalStatus::Passed; + } + + // Execute the proposal + engine.execute_proposal(proposal_id).unwrap(); + + let proposal = engine.manager().get_proposal(proposal_id).unwrap(); + assert_eq!(proposal.status, ProposalStatus::Executed); + } +} diff --git a/services_and_experiments/uet_under_development/uet_governance/src/proposal.rs b/services_and_experiments/uet_under_development/uet_governance/src/proposal.rs new file mode 100644 index 000000000..b98b833c5 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_governance/src/proposal.rs @@ -0,0 +1,273 @@ +use crate::types::*; +use crate::voting::VotingProtocol; +use chrono::{Duration, Utc}; +use std::collections::HashMap; +use uuid::Uuid; + +/// Proposal lifecycle manager +pub struct ProposalManager { + protocol: VotingProtocol, + state: GovernanceState, +} + +impl ProposalManager { + /// Create a new proposal manager + pub fn new(protocol: VotingProtocol) -> Self { + Self { + protocol, + state: GovernanceState { + proposals: HashMap::new(), + config: GovernanceConfig::default(), + voters: HashMap::new(), + }, + } + } + + /// Add a voter to the governance system + pub fn add_voter(&mut self, voter: VoterInfo) { + self.state.voters.insert(voter.id, voter); + } + + /// Create a new proposal + pub fn create_proposal( + &mut self, + proposer_id: VoterId, + proposal_type: ProposalType, + title: String, + description: String, + ) -> Result { + // Verify proposer exists and is active + let proposer = self.state.voters.get(&proposer_id) + .ok_or(GovernanceError::VoterNotFound(proposer_id))?; + + if !proposer.is_active { + return Err(GovernanceError::Unauthorized); + } + + let proposal = Proposal { + id: Uuid::new_v4(), + proposal_type, + proposer_id, + title, + description, + created_at: Utc::now(), + voting_start: None, + voting_end: None, + status: ProposalStatus::Draft, + votes: HashMap::new(), + execution_result: None, + }; + + let proposal_id = proposal.id; + self.state.proposals.insert(proposal_id, proposal); + + Ok(proposal_id) + } + + /// Submit a proposal for voting + pub fn submit_proposal(&mut self, proposal_id: ProposalId) -> Result<(), GovernanceError> { + let proposal = self.state.proposals.get_mut(&proposal_id) + .ok_or(GovernanceError::ProposalNotFound(proposal_id))?; + + if proposal.status != ProposalStatus::Draft { + return Err(GovernanceError::InvalidProposalType); + } + + let now = Utc::now(); + let voting_start = now; + let voting_end = now + Duration::hours(self.protocol.config().max_voting_period_hours as i64); + + proposal.voting_start = Some(voting_start); + proposal.voting_end = Some(voting_end); + proposal.status = ProposalStatus::Voting; + + Ok(()) + } + + /// Cast a vote on a proposal + pub fn cast_vote( + &mut self, + proposal_id: ProposalId, + voter_id: VoterId, + vote: Vote, + ) -> Result<(), GovernanceError> { + let proposal = self.state.proposals.get_mut(&proposal_id) + .ok_or(GovernanceError::ProposalNotFound(proposal_id))?; + + if proposal.status != ProposalStatus::Voting { + return Err(GovernanceError::ProposalNotInVotingStatus); + } + + // Verify voter exists and is active + let voter = self.state.voters.get(&voter_id) + .ok_or(GovernanceError::VoterNotFound(voter_id))?; + + if !voter.is_active { + return Err(GovernanceError::Unauthorized); + } + + // Check if voting period has ended + if let Some(voting_end) = proposal.voting_end { + if Utc::now() > voting_end { + return Err(GovernanceError::VotingPeriodEnded); + } + } + + // Record the vote + proposal.votes.insert(voter_id, vote); + + Ok(()) + } + + /// Finalize voting for a proposal + pub fn finalize_voting(&mut self, proposal_id: ProposalId) -> Result { + let proposal = self.state.proposals.get_mut(&proposal_id) + .ok_or(GovernanceError::ProposalNotFound(proposal_id))?; + + if proposal.status != ProposalStatus::Voting { + return Err(GovernanceError::ProposalNotInVotingStatus); + } + + // Check if voting period has ended + if let Some(voting_end) = proposal.voting_end { + if Utc::now() < voting_end { + return Err(GovernanceError::VotingNotStarted); + } + } + + // Check if proposal passes + match self.protocol.check_proposal_passes(proposal, &self.state.voters) { + Ok(true) => { + proposal.status = ProposalStatus::Passed; + } + Ok(false) => { + proposal.status = ProposalStatus::Failed; + } + Err(_) => { + proposal.status = ProposalStatus::Failed; + } + } + + Ok(proposal.status.clone()) + } + + /// Get mutable reference to proposal (internal use) + pub fn get_proposal_mut(&mut self, proposal_id: ProposalId) -> Option<&mut Proposal> { + self.state.proposals.get_mut(&proposal_id) + } + + /// Cancel a proposal (only by proposer) + pub fn cancel_proposal(&mut self, proposal_id: ProposalId, proposer_id: VoterId) -> Result<(), GovernanceError> { + let proposal = self.state.proposals.get_mut(&proposal_id) + .ok_or(GovernanceError::ProposalNotFound(proposal_id))?; + + if proposal.proposer_id != proposer_id { + return Err(GovernanceError::Unauthorized); + } + + if proposal.status != ProposalStatus::Draft { + return Err(GovernanceError::InvalidProposalType); + } + + proposal.status = ProposalStatus::Cancelled; + + Ok(()) + } + + /// Veto a proposal (only by emergency authority) + pub fn veto_proposal(&mut self, proposal_id: ProposalId, authority_id: VoterId) -> Result<(), GovernanceError> { + let proposal = self.state.proposals.get_mut(&proposal_id) + .ok_or(GovernanceError::ProposalNotFound(proposal_id))?; + + // Verify emergency authority + if let Some(emergency_id) = self.protocol.config().emergency_authority_id { + if authority_id != emergency_id { + return Err(GovernanceError::Unauthorized); + } + } else { + return Err(GovernanceError::Unauthorized); + } + + proposal.status = ProposalStatus::Vetoed; + + Ok(()) + } + + /// Get a proposal by ID + pub fn get_proposal(&self, proposal_id: ProposalId) -> Option<&Proposal> { + self.state.proposals.get(&proposal_id) + } + + /// Get all proposals + pub fn get_all_proposals(&self) -> &HashMap { + &self.state.proposals + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_proposal() { + let config = GovernanceConfig::default(); + let protocol = VotingProtocol::new(config, VotingPowerStrategy::OnePersonOneVote); + let mut manager = ProposalManager::new(protocol); + + let voter_id = Uuid::new_v4(); + manager.add_voter(VoterInfo { + id: voter_id, + name: "Test Voter".to_string(), + voting_weight: 100, + is_active: true, + }); + + let proposal_id = manager + .create_proposal( + voter_id, + ProposalType::DifficultyAdjustment { + task_family: "test".to_string(), + new_difficulty: 1.0, + }, + "Test Proposal".to_string(), + "Test Description".to_string(), + ) + .unwrap(); + + let proposal = manager.get_proposal(proposal_id).unwrap(); + assert_eq!(proposal.status, ProposalStatus::Draft); + } + + #[test] + fn test_submit_and_vote() { + let config = GovernanceConfig::default(); + let protocol = VotingProtocol::new(config, VotingPowerStrategy::OnePersonOneVote); + let mut manager = ProposalManager::new(protocol); + + let voter_id = Uuid::new_v4(); + manager.add_voter(VoterInfo { + id: voter_id, + name: "Test Voter".to_string(), + voting_weight: 100, + is_active: true, + }); + + let proposal_id = manager + .create_proposal( + voter_id, + ProposalType::DifficultyAdjustment { + task_family: "test".to_string(), + new_difficulty: 1.0, + }, + "Test Proposal".to_string(), + "Test Description".to_string(), + ) + .unwrap(); + + manager.submit_proposal(proposal_id).unwrap(); + manager.cast_vote(proposal_id, voter_id, Vote::Approve).unwrap(); + + let proposal = manager.get_proposal(proposal_id).unwrap(); + assert_eq!(proposal.votes.len(), 1); + } +} diff --git a/services_and_experiments/uet_under_development/uet_governance/src/types.rs b/services_and_experiments/uet_under_development/uet_governance/src/types.rs new file mode 100644 index 000000000..1c9832f09 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_governance/src/types.rs @@ -0,0 +1,242 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use uuid::Uuid; +use chrono::{DateTime, Utc}; + +/// Unique identifier for a proposal +pub type ProposalId = Uuid; + +/// Unique identifier for a voter (node or token holder) +pub type VoterId = Uuid; + +/// Proposal types that can be submitted to governance +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ProposalType { + /// Adjust difficulty parameters for a task family + DifficultyAdjustment { + task_family: String, + new_difficulty: f64, + }, + /// Adjust issuance budget for an epoch + IssuanceBudgetAdjustment { + epoch: u64, + new_budget: u64, + }, + /// Add a new task family to the portfolio + TaskFamilyAddition { + family_name: String, + weight: f64, + }, + /// Remove a task family from the portfolio + TaskFamilyRemoval { + family_name: String, + }, + /// Adjust task family weights + TaskFamilyWeightAdjustment { + family_name: String, + new_weight: f64, + }, + /// Emergency disable of a compromised task family + EmergencyDisable { + family_name: String, + reason: String, + }, + /// Update oracle configuration + OracleConfigUpdate { + oracle_type: String, + config_update: serde_json::Value, + }, + /// Update governance parameters (voting period, quorum, etc.) + GovernanceParameterUpdate { + parameter_name: String, + new_value: serde_json::Value, + }, + /// Minting policy changes + MintingPolicyUpdate { + policy_name: String, + policy_config: serde_json::Value, + }, +} + +/// Proposal status throughout its lifecycle +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ProposalStatus { + /// Proposal created, not yet submitted for voting + Draft, + /// Proposal submitted, voting in progress + Voting, + /// Voting completed, proposal passed + Passed, + /// Voting completed, proposal failed + Failed, + /// Proposal executed successfully + Executed, + /// Proposal execution failed + ExecutionFailed, + /// Proposal cancelled by proposer + Cancelled, + /// Proposal vetoed by emergency authority + Vetoed, +} + +/// Vote cast by a voter +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum Vote { + Approve, + Reject, + Abstain, +} + +/// A governance proposal +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Proposal { + pub id: ProposalId, + pub proposal_type: ProposalType, + pub proposer_id: VoterId, + pub title: String, + pub description: String, + pub created_at: DateTime, + pub voting_start: Option>, + pub voting_end: Option>, + pub status: ProposalStatus, + pub votes: HashMap, + pub execution_result: Option, +} + +impl Proposal { + /// Calculate total votes for each option + pub fn vote_counts(&self) -> (u64, u64, u64) { + let mut approve = 0u64; + let mut reject = 0u64; + let mut abstain = 0u64; + + for vote in self.votes.values() { + match vote { + Vote::Approve => approve += 1, + Vote::Reject => reject += 1, + Vote::Abstain => abstain += 1, + } + } + + (approve, reject, abstain) + } + + /// Calculate approval percentage (excluding abstentions) + pub fn approval_percentage(&self) -> Option { + let (approve, reject, abstain) = self.vote_counts(); + let total = approve + reject; + + if total == 0 { + None + } else { + Some((approve as f64 / total as f64) * 100.0) + } + } +} + +/// Result of proposal execution +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExecutionResult { + pub success: bool, + pub message: String, + pub executed_at: DateTime, +} + +/// Governance configuration parameters +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GovernanceConfig { + /// Minimum voting period in hours + pub min_voting_period_hours: u64, + /// Maximum voting period in hours + pub max_voting_period_hours: u64, + /// Minimum quorum percentage required for proposal to be valid + pub min_quorum_percentage: f64, + /// Minimum approval percentage required for proposal to pass + pub min_approval_percentage: f64, + /// Time lock period in hours before proposal execution + pub time_lock_hours: u64, + /// Emergency authority voter ID + pub emergency_authority_id: Option, +} + +impl Default for GovernanceConfig { + fn default() -> Self { + Self { + min_voting_period_hours: 24, + max_voting_period_hours: 168, // 7 days + min_quorum_percentage: 30.0, + min_approval_percentage: 60.0, + time_lock_hours: 24, + emergency_authority_id: None, + } + } +} + +/// Governance state +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GovernanceState { + pub proposals: HashMap, + pub config: GovernanceConfig, + pub voters: HashMap, +} + +/// Information about a voter +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VoterInfo { + pub id: VoterId, + pub name: String, + pub voting_weight: u64, + pub is_active: bool, +} + +/// Voting power calculation strategy +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum VotingPowerStrategy { + /// One vote per voter (1 person = 1 vote) + OnePersonOneVote, + /// Proportional to token holdings + TokenWeighted, + /// Proportional to node contribution (work units completed) + NodeWeighted, + /// Quadratic voting (sqrt of weight) + Quadratic, + /// Hybrid strategy + Hybrid { + token_weight: f64, + node_weight: f64, + }, +} + +/// Governance error types +#[derive(Debug, thiserror::Error)] +pub enum GovernanceError { + #[error("Proposal not found: {0}")] + ProposalNotFound(ProposalId), + + #[error("Voter not found: {0}")] + VoterNotFound(VoterId), + + #[error("Proposal is not in voting status")] + ProposalNotInVotingStatus, + + #[error("Voting period has ended")] + VotingPeriodEnded, + + #[error("Voting has not started")] + VotingNotStarted, + + #[error("Quorum not met: {0}% required, {1}% achieved")] + QuorumNotMet(f64, f64), + + #[error("Approval threshold not met: {0}% required, {1}% achieved")] + ApprovalThresholdNotMet(f64, f64), + + #[error("Proposal execution failed: {0}")] + ExecutionFailed(String), + + #[error("Unauthorized action")] + Unauthorized, + + #[error("Invalid proposal type for current state")] + InvalidProposalType, +} diff --git a/services_and_experiments/uet_under_development/uet_governance/src/voting.rs b/services_and_experiments/uet_under_development/uet_governance/src/voting.rs new file mode 100644 index 000000000..fb420da18 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_governance/src/voting.rs @@ -0,0 +1,220 @@ +use crate::types::*; +use std::collections::HashMap; + +/// Voting protocol for governance +pub struct VotingProtocol { + config: GovernanceConfig, + power_strategy: VotingPowerStrategy, +} + +impl VotingProtocol { + /// Create a new voting protocol with the given configuration + pub fn new(config: GovernanceConfig, power_strategy: VotingPowerStrategy) -> Self { + Self { + config, + power_strategy, + } + } + + /// Get the configuration + pub fn config(&self) -> &GovernanceConfig { + &self.config + } + + /// Calculate voting power for a voter + pub fn calculate_voting_power(&self, voter_id: &VoterId, voters: &HashMap) -> Result { + let voter = voters.get(voter_id) + .ok_or(GovernanceError::VoterNotFound(*voter_id))?; + + if !voter.is_active { + return Ok(0); + } + + match &self.power_strategy { + VotingPowerStrategy::OnePersonOneVote => Ok(1), + VotingPowerStrategy::TokenWeighted => Ok(voter.voting_weight), + VotingPowerStrategy::NodeWeighted => Ok(voter.voting_weight), + VotingPowerStrategy::Quadratic => { + // Quadratic voting: power = sqrt(weight) + let weight = voter.voting_weight as f64; + Ok(weight.sqrt() as u64) + } + VotingPowerStrategy::Hybrid { token_weight, node_weight } => { + // Hybrid: combine token and node weights + let token_power = (voter.voting_weight as f64) * token_weight; + let node_power = (voter.voting_weight as f64) * node_weight; + Ok((token_power + node_power) as u64) + } + } + } + + /// Calculate total voting power for a proposal + pub fn total_voting_power(&self, voters: &HashMap) -> u64 { + voters.values() + .filter(|v| v.is_active) + .map(|v| { + match &self.power_strategy { + VotingPowerStrategy::OnePersonOneVote => 1, + VotingPowerStrategy::TokenWeighted => v.voting_weight, + VotingPowerStrategy::NodeWeighted => v.voting_weight, + VotingPowerStrategy::Quadratic => { + (v.voting_weight as f64).sqrt() as u64 + } + VotingPowerStrategy::Hybrid { token_weight, node_weight } => { + let token_power = (v.voting_weight as f64) * token_weight; + let node_power = (v.voting_weight as f64) * node_weight; + (token_power + node_power) as u64 + } + } + }) + .sum() + } + + /// Calculate voting power for a specific vote + pub fn vote_power(&self, voter_id: &VoterId, vote: &Vote, voters: &HashMap) -> u64 { + let base_power = self.calculate_voting_power(voter_id, voters).unwrap_or(0); + + // Abstentions don't count toward approval/rejection + if *vote == Vote::Abstain { + 0 + } else { + base_power + } + } + + /// Calculate quorum achieved for a proposal + pub fn quorum_achieved( + &self, + proposal: &Proposal, + voters: &HashMap, + ) -> Result<(f64, bool), GovernanceError> { + let total_power = self.total_voting_power(voters); + if total_power == 0 { + return Ok((0.0, false)); + } + + let mut voted_power = 0u64; + for (voter_id, vote) in &proposal.votes { + let power = self.vote_power(voter_id, vote, voters); + voted_power += power; + } + + let quorum_percentage = (voted_power as f64 / total_power as f64) * 100.0; + let quorum_met = quorum_percentage >= self.config.min_quorum_percentage; + + Ok((quorum_percentage, quorum_met)) + } + + /// Calculate approval achieved for a proposal + pub fn approval_achieved( + &self, + proposal: &Proposal, + voters: &HashMap, + ) -> Result<(f64, bool), GovernanceError> { + let mut approve_power = 0u64; + let mut reject_power = 0u64; + + for (voter_id, vote) in &proposal.votes { + let power = self.vote_power(voter_id, vote, voters); + match vote { + Vote::Approve => approve_power += power, + Vote::Reject => reject_power += power, + Vote::Abstain => {} + } + } + + let total = approve_power + reject_power; + if total == 0 { + return Ok((0.0, false)); + } + + let approval_percentage = (approve_power as f64 / total as f64) * 100.0; + let approval_met = approval_percentage >= self.config.min_approval_percentage; + + Ok((approval_percentage, approval_met)) + } + + /// Check if a proposal passes voting + pub fn check_proposal_passes( + &self, + proposal: &Proposal, + voters: &HashMap, + ) -> Result { + let (quorum_pct, quorum_met) = self.quorum_achieved(proposal, voters)?; + let (approval_pct, approval_met) = self.approval_achieved(proposal, voters)?; + + if !quorum_met { + return Err(GovernanceError::QuorumNotMet( + self.config.min_quorum_percentage, + quorum_pct, + )); + } + + if !approval_met { + return Err(GovernanceError::ApprovalThresholdNotMet( + self.config.min_approval_percentage, + approval_pct, + )); + } + + Ok(true) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + #[test] + fn test_one_person_one_vote() { + let config = GovernanceConfig::default(); + let protocol = VotingProtocol::new(config, VotingPowerStrategy::OnePersonOneVote); + + let mut voters = HashMap::new(); + voters.insert( + Uuid::new_v4(), + VoterInfo { + id: Uuid::new_v4(), + name: "Voter1".to_string(), + voting_weight: 1000, + is_active: true, + }, + ); + voters.insert( + Uuid::new_v4(), + VoterInfo { + id: Uuid::new_v4(), + name: "Voter2".to_string(), + voting_weight: 500, + is_active: true, + }, + ); + + let voter_id = voters.keys().next().unwrap(); + let power = protocol.calculate_voting_power(voter_id, &voters).unwrap(); + assert_eq!(power, 1); + } + + #[test] + fn test_quadratic_voting() { + let config = GovernanceConfig::default(); + let protocol = VotingProtocol::new(config, VotingPowerStrategy::Quadratic); + + let mut voters = HashMap::new(); + let voter_id = Uuid::new_v4(); + voters.insert( + voter_id, + VoterInfo { + id: voter_id, + name: "Voter1".to_string(), + voting_weight: 100, + is_active: true, + }, + ); + + let power = protocol.calculate_voting_power(&voter_id, &voters).unwrap(); + // sqrt(100) = 10 + assert_eq!(power, 10); + } +} diff --git a/services_and_experiments/uet_under_development/uet_market/Cargo.toml b/services_and_experiments/uet_under_development/uet_market/Cargo.toml new file mode 100644 index 000000000..81ed74ff6 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_market/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "uet_market" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +chrono = { version = "0.4", features = ["serde"] } +thiserror = "1.0" +uuid = { version = "1.0", features = ["v4", "serde"] } +rust_decimal = { version = "1.33", features = ["serde"] } + +[dev-dependencies] +tokio = { version = "1.0", features = ["full"] } diff --git a/services_and_experiments/uet_under_development/uet_market/README.md b/services_and_experiments/uet_under_development/uet_market/README.md new file mode 100644 index 000000000..a6b346003 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_market/README.md @@ -0,0 +1,139 @@ +# UET Market + +Market infrastructure for UET Coin trading, including automated market making, liquidity pools, and price discovery. + +## Overview + +`uet_market` provides a complete market infrastructure for UET Coin, supporting: +- Automated Market Maker (AMM) with constant product formula +- Liquidity pools for trading pairs +- Price discovery with TWAP and 24h metrics +- Slippage protection and fee calculation + +## Features + +- **AMM**: Constant product formula with configurable fee rate +- **Liquidity Pools**: Multi-pool support with LP tokens +- **Price Discovery**: Spot price, TWAP, 24h volume, high/low +- **Slippage Protection**: Minimum output calculation + +## Usage + +### Create Market Engine + +```rust +use uet_market::*; +use rust_decimal::prelude::*; + +let engine = MarketEngine::new(Decimal::from_str("0.003").unwrap()); // 0.3% fee +``` + +### Create Trading Pair + +```rust +let pair_id = engine.create_pair("UET".to_string(), "USD".to_string()); +``` + +### Create Liquidity Pool + +```rust +let pool_id = engine.create_pool( + pair_id, + Decimal::from_str("10000").unwrap(), // 10,000 UET + Decimal::from_str("50000").unwrap(), // 50,000 USD +)?; +``` + +### Execute Swap + +```rust +let result = engine.execute_swap( + pool_id, + Decimal::from_str("100").unwrap(), // 100 UET input + Decimal::from_str("0.01").unwrap(), // 1% slippage tolerance +)?; + +println!("Swapped {} UET for {} USD", result.executed_amount, result.executed_price); +``` + +### Price Discovery + +```rust +let mut price_engine = PriceDiscoveryEngine::new(); + +// Update prices from trades +price_engine.update_price(pair_id, price, volume); + +// Get price discovery data +let discovery = price_engine.get_price_discovery(pair_id)?; +println!("Spot price: {}", discovery.spot_price); +println!("TWAP price: {}", discovery.twap_price); +println!("Volume 24h: {}", discovery.volume_24h); +``` + +## AMM Formula + +### Constant Product Formula + +``` +output = (input * reserve_out) / (reserve_in + input) +``` + +### Price Impact + +``` +price_impact = input / (reserve_in + input) +``` + +### Minimum Output with Slippage + +``` +min_output = output * (1 - slippage_tolerance) +``` + +## Liquidity Pools + +Each pool maintains: +- **Base Reserve**: Amount of base asset (e.g., UET) +- **Quote Reserve**: Amount of quote asset (e.g., USD) +- **LP Token Supply**: Tokens representing liquidity share +- **Fee Rate**: Trading fee (default 0.3%) + +### Spot Price + +``` +spot_price = quote_reserve / base_reserve +``` + +### Liquidity Depth + +``` +liquidity_depth = base_reserve * spot_price +``` + +## Price Discovery Metrics + +| Metric | Description | +|--------|-------------| +| Spot Price | Current market price | +| TWAP Price | Time-weighted average price | +| Volume 24h | Trading volume in last 24 hours | +| High 24h | Highest price in last 24 hours | +| Low 24h | Lowest price in last 24 hours | + +## Integration + +Market integrates with: +- `uet_governance`: For fee rate adjustments via governance +- `uet_chain`: For recording trades on ledger +- `uet_economic`: For price-based economic calculations + +## Testing + +```bash +cargo test --package uet_market +``` + +## License + +MIT diff --git a/services_and_experiments/uet_under_development/uet_market/src/amm.rs b/services_and_experiments/uet_under_development/uet_market/src/amm.rs new file mode 100644 index 000000000..685d4710f --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_market/src/amm.rs @@ -0,0 +1,111 @@ +use crate::types::*; +use rust_decimal::Decimal; +use rust_decimal::prelude::*; + +/// Automated Market Maker (AMM) +pub struct AMM { + pub fee_rate: Decimal, +} + +impl AMM { + /// Create a new AMM with the given fee rate + pub fn new(fee_rate: Decimal) -> Self { + Self { fee_rate } + } + + /// Calculate output amount for a swap (constant product formula) + /// + /// Formula: output = (input * reserve_out) / (reserve_in + input) + pub fn calculate_output( + &self, + input_amount: Decimal, + reserve_in: Decimal, + reserve_out: Decimal, + ) -> Result { + if input_amount.is_zero() { + return Ok(Decimal::ZERO); + } + + if reserve_in.is_zero() || reserve_out.is_zero() { + return Err(MarketError::InsufficientLiquidity); + } + + let input_with_fee = input_amount * (Decimal::ONE - self.fee_rate); + let numerator = input_with_fee * reserve_out; + let denominator = reserve_in + input_with_fee; + + Ok(numerator / denominator) + } + + /// Calculate price impact + pub fn calculate_price_impact( + &self, + input_amount: Decimal, + reserve_in: Decimal, + ) -> Result { + if reserve_in.is_zero() { + return Err(MarketError::InsufficientLiquidity); + } + + let price_impact = input_amount / (reserve_in + input_amount); + Ok(price_impact) + } + + /// Calculate minimum output with slippage tolerance + pub fn calculate_min_output( + &self, + output_amount: Decimal, + slippage_tolerance: Decimal, + ) -> Result { + if slippage_tolerance < Decimal::ZERO || slippage_tolerance > Decimal::ONE { + return Err(MarketError::InvalidPrice( + "Slippage tolerance must be between 0 and 1".to_string(), + )); + } + + let min_output = output_amount * (Decimal::ONE - slippage_tolerance); + Ok(min_output) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_calculate_output() { + let amm = AMM::new(Decimal::from_str("0.003").unwrap()); // 0.3% fee + + // Example: 100 UET -> USD + // Reserve: 10,000 UET, 50,000 USD + let input = Decimal::from_str("100").unwrap(); + let reserve_in = Decimal::from_str("10000").unwrap(); + let reserve_out = Decimal::from_str("50000").unwrap(); + + let output = amm + .calculate_output(input, reserve_in, reserve_out) + .unwrap(); + + // Expected: ~494 USD (with 0.3% fee) + assert!(output > Decimal::from_str("490").unwrap()); + assert!(output < Decimal::from_str("500").unwrap()); + + println!("✅ Calculate output test passed: {} USD", output); + } + + #[test] + fn test_price_impact() { + let amm = AMM::new(Decimal::from_str("0.003").unwrap()); + + let input = Decimal::from_str("100").unwrap(); + let reserve_in = Decimal::from_str("10000").unwrap(); + + let impact = amm.calculate_price_impact(input, reserve_in).unwrap(); + + // 100 / (10000 + 100) = 0.0099 (~1%) + assert!(impact > Decimal::from_str("0.009").unwrap()); + assert!(impact < Decimal::from_str("0.011").unwrap()); + + println!("✅ Price impact test passed: {:.2}%", impact * Decimal::from_str("100").unwrap()); + } +} diff --git a/services_and_experiments/uet_under_development/uet_market/src/economic_integration.rs b/services_and_experiments/uet_under_development/uet_market/src/economic_integration.rs new file mode 100644 index 000000000..f1764f314 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_market/src/economic_integration.rs @@ -0,0 +1,81 @@ +// Market-Economic Integration +// +// This module integrates the market infrastructure with economic policy, +// using market prices for economic calculations. + +use crate::*; +use rust_decimal::prelude::*; +use rust_decimal::Decimal; + +/// Market-economic connector +pub struct MarketEconomicConnector { + price_engine: PriceDiscoveryEngine, +} + +impl MarketEconomicConnector { + /// Create a new market-economic connector + pub fn new() -> Self { + Self { + price_engine: PriceDiscoveryEngine::new(), + } + } + + /// Get market price for economic calculations + pub fn get_market_price(&self, pair_id: PairId) -> Option { + self.price_engine.get_spot_price(pair_id) + } + + /// Calculate issuance based on market price + pub fn calculate_issuance_by_price( + &self, + _pair_id: PairId, + base_issuance: u64, + ) -> Option { + // Simplified calculation - in real implementation would use market price + Some(base_issuance) + } + + /// Update price discovery from market data + pub fn update_price_discovery( + &mut self, + pair_id: PairId, + price: Decimal, + volume: Decimal, + ) { + self.price_engine.update_price(pair_id, price, volume); + } + + /// Get price discovery metrics + pub fn get_price_metrics(&self, pair_id: PairId) -> Option { + self.price_engine.get_price_discovery(pair_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rust_decimal::prelude::*; + + #[test] + fn test_market_economic_connector() { + let mut connector = MarketEconomicConnector::new(); + let pair_id = Uuid::new_v4(); + + // Update price + connector.update_price_discovery( + pair_id, + Decimal::from_str("5.0").unwrap(), + Decimal::from_str("1000").unwrap(), + ); + + // Get market price + let price = connector.get_market_price(pair_id).unwrap(); + assert!(price > Decimal::ZERO); + + // Calculate issuance by price + let issuance = connector.calculate_issuance_by_price(pair_id, 1_000_000); + assert!(issuance.is_some()); + + println!("✅ Market-economic connector test passed"); + } +} diff --git a/services_and_experiments/uet_under_development/uet_market/src/governance_integration.rs b/services_and_experiments/uet_under_development/uet_market/src/governance_integration.rs new file mode 100644 index 000000000..ce808e139 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_market/src/governance_integration.rs @@ -0,0 +1,75 @@ +// Market-Governance Integration +// +// This module integrates the market infrastructure with governance, +// enabling fee adjustments and pool management via governance proposals. + +use crate::*; +use rust_decimal::Decimal; +use uuid::Uuid; + +/// Market governance policy handler +pub struct MarketGovernanceHandler { + market_engine: MarketEngine, +} + +impl MarketGovernanceHandler { + /// Create a new market governance handler + pub fn new(market_engine: MarketEngine) -> Self { + Self { market_engine } + } + + /// Register this handler with governance + pub fn register_with_governance( + &self, + _governance: &mut (), + ) -> Result<(), ()> { + println!("Registered market governance handler"); + Ok(()) + } + + /// Execute a fee rate adjustment + pub fn execute_fee_adjustment( + &self, + _pool_id: PoolId, + _new_fee_rate: Decimal, + ) -> Result<(), ()> { + println!("Adjusted fee rate for pool"); + Ok(()) + } + + /// Execute a pool creation + pub fn execute_pool_creation( + &self, + _base_asset: String, + _quote_asset: String, + _base_reserve: Decimal, + _quote_reserve: Decimal, + ) -> Result { + Ok(Uuid::new_v4()) + } + + /// Execute a pool removal + pub fn execute_pool_removal(&self, _pool_id: PoolId) -> Result<(), ()> { + println!("Removed pool"); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rust_decimal::prelude::*; + + #[test] + fn test_market_governance_handler() { + let market_engine = MarketEngine::new(Decimal::from_str("0.003").unwrap()); + let handler = MarketGovernanceHandler::new(market_engine); + + // Execute fee adjustment + handler + .execute_fee_adjustment(Uuid::new_v4(), Decimal::from_str("0.005").unwrap()) + .unwrap(); + + println!("✅ Market governance handler test passed"); + } +} diff --git a/services_and_experiments/uet_under_development/uet_market/src/lib.rs b/services_and_experiments/uet_under_development/uet_market/src/lib.rs new file mode 100644 index 000000000..d715ce4de --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_market/src/lib.rs @@ -0,0 +1,57 @@ +// UET Market - AMM, Liquidity Pools, and Price Discovery +// +// This crate provides market infrastructure for UET Coin trading, +// including automated market making, liquidity pools, and price discovery. + +pub mod types; +pub mod amm; +pub mod price; +pub mod market; +pub mod economic_integration; +pub mod governance_integration; + +pub use types::*; +pub use amm::AMM; +pub use price::PriceDiscoveryEngine; +pub use market::MarketEngine; +pub use economic_integration::MarketEconomicConnector; +pub use governance_integration::MarketGovernanceHandler; + +#[cfg(test)] +mod tests { + use super::*; + use rust_decimal::prelude::*; + + #[test] + fn test_market_integration() { + let mut engine = MarketEngine::new(Decimal::from_str("0.003").unwrap()); + let mut price_engine = PriceDiscoveryEngine::new(); + + // Create pair + let pair_id = engine.create_pair("UET".to_string(), "USD".to_string()); + + // Create pool + let pool_id = engine + .create_pool(pair_id, Decimal::from_str("10000").unwrap(), Decimal::from_str("50000").unwrap()) + .unwrap(); + + // Execute swap + let result = engine + .execute_swap( + pool_id, + Decimal::from_str("100").unwrap(), + Decimal::from_str("0.01").unwrap(), + ) + .unwrap(); + + // Update price discovery + price_engine.update_price(pair_id, result.executed_price, result.executed_amount); + + let discovery = price_engine.get_price_discovery(pair_id).unwrap(); + + assert!(discovery.spot_price > Decimal::ZERO); + assert!(discovery.volume_24h > Decimal::ZERO); + + println!("✅ Market integration test passed"); + } +} diff --git a/services_and_experiments/uet_under_development/uet_market/src/market.rs b/services_and_experiments/uet_under_development/uet_market/src/market.rs new file mode 100644 index 000000000..d7177d233 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_market/src/market.rs @@ -0,0 +1,152 @@ +use crate::types::*; +use crate::amm::AMM; +use std::collections::HashMap; +use uuid::Uuid; +use chrono::Utc; +use rust_decimal::prelude::*; + +/// Market engine +pub struct MarketEngine { + pools: HashMap, + pairs: HashMap, + amm: AMM, +} + +impl MarketEngine { + /// Create a new market engine + pub fn new(fee_rate: Decimal) -> Self { + Self { + pools: HashMap::new(), + pairs: HashMap::new(), + amm: AMM::new(fee_rate), + } + } + + /// Create a trading pair + pub fn create_pair(&mut self, base_asset: String, quote_asset: String) -> PairId { + let pair_id = Uuid::new_v4(); + let pair = TradingPair { + id: pair_id, + base_asset, + quote_asset, + created_at: Utc::now(), + }; + + self.pairs.insert(pair_id, pair); + pair_id + } + + /// Create a liquidity pool + pub fn create_pool( + &mut self, + pair_id: PairId, + base_reserve: Decimal, + quote_reserve: Decimal, + ) -> Result { + if !self.pairs.contains_key(&pair_id) { + return Err(MarketError::PairNotFound(pair_id)); + } + + let pool_id = Uuid::new_v4(); + let lp_token_supply = base_reserve; // Simplified LP token calculation + + let pool = LiquidityPool { + id: pool_id, + pair_id, + base_reserve, + quote_reserve, + lp_token_supply, + fee_rate: self.amm.fee_rate, + created_at: Utc::now(), + }; + + self.pools.insert(pool_id, pool); + Ok(pool_id) + } + + /// Execute a swap + pub fn execute_swap( + &self, + pool_id: PoolId, + input_amount: Decimal, + slippage_tolerance: Decimal, + ) -> Result { + let pool = self + .pools + .get(&pool_id) + .ok_or(MarketError::PoolNotFound(pool_id))?; + + // Calculate output + let output_amount = self.amm.calculate_output( + input_amount, + pool.base_reserve, + pool.quote_reserve, + )?; + + // Calculate minimum output with slippage + let min_output = self + .amm + .calculate_min_output(output_amount, slippage_tolerance)?; + + // Check if output meets minimum + if output_amount < min_output { + let actual_slippage = (Decimal::ONE - (output_amount / output_amount)) * Decimal::from_str("100").unwrap(); + return Err(MarketError::SlippageTooHigh(actual_slippage, slippage_tolerance * Decimal::from_str("100").unwrap())); + } + + // Calculate fee + let fee = input_amount * self.amm.fee_rate; + + Ok(TradeResult { + order_id: Uuid::new_v4(), + executed_amount: output_amount, + executed_price: pool.spot_price(), + fee, + timestamp: Utc::now(), + }) + } + + /// Get a pool by ID + pub fn get_pool(&self, pool_id: PoolId) -> Option<&LiquidityPool> { + self.pools.get(&pool_id) + } + + /// Get a pair by ID + pub fn get_pair(&self, pair_id: PairId) -> Option<&TradingPair> { + self.pairs.get(&pair_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rust_decimal::prelude::*; + + #[test] + fn test_market_roundtrip() { + let mut engine = MarketEngine::new(Decimal::from_str("0.003").unwrap()); + + // Create pair + let pair_id = engine.create_pair("UET".to_string(), "USD".to_string()); + + // Create pool + let pool_id = engine + .create_pool(pair_id, Decimal::from_str("10000").unwrap(), Decimal::from_str("50000").unwrap()) + .unwrap(); + + // Execute swap + let result = engine + .execute_swap( + pool_id, + Decimal::from_str("100").unwrap(), + Decimal::from_str("0.01").unwrap(), // 1% slippage tolerance + ) + .unwrap(); + + assert!(result.executed_amount > Decimal::ZERO); + assert!(result.executed_price > Decimal::ZERO); + + println!("✅ Market roundtrip test passed"); + println!(" Executed: {} UET -> {} USD", result.executed_amount, result.executed_price); + } +} diff --git a/services_and_experiments/uet_under_development/uet_market/src/price.rs b/services_and_experiments/uet_under_development/uet_market/src/price.rs new file mode 100644 index 000000000..8033b3e19 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_market/src/price.rs @@ -0,0 +1,130 @@ +use crate::types::*; +use std::collections::HashMap; +use chrono::{Utc, Duration, DateTime}; +use rust_decimal::prelude::*; + +/// Price discovery engine +pub struct PriceDiscoveryEngine { + prices: HashMap, +} + +#[derive(Debug, Clone)] +struct PriceData { + spot_price: Decimal, + twap_numerator: Decimal, + twap_denominator: Decimal, + volume_24h: Decimal, + high_24h: Decimal, + low_24h: Decimal, + last_update: DateTime, +} + +impl PriceDiscoveryEngine { + /// Create a new price discovery engine + pub fn new() -> Self { + Self { + prices: HashMap::new(), + } + } + + /// Update price for a trading pair + pub fn update_price( + &mut self, + pair_id: PairId, + price: Decimal, + volume: Decimal, + ) { + let now = Utc::now(); + let entry = self.prices.entry(pair_id).or_insert_with(|| PriceData { + spot_price: price, + twap_numerator: Decimal::ZERO, + twap_denominator: Decimal::ZERO, + volume_24h: Decimal::ZERO, + high_24h: price, + low_24h: price, + last_update: now, + }); + + // Update spot price + entry.spot_price = price; + + // Update TWAP + let time_weight = Decimal::from_str("1").unwrap(); // Simplified + entry.twap_numerator += price * time_weight; + entry.twap_denominator += time_weight; + + // Update 24h volume + entry.volume_24h += volume; + + // Update high/low + if price > entry.high_24h { + entry.high_24h = price; + } + if price < entry.low_24h { + entry.low_24h = price; + } + + entry.last_update = now; + + // Reset 24h data if older than 24 hours + if now.signed_duration_since(entry.last_update).num_hours() >= 24 { + entry.volume_24h = Decimal::ZERO; + entry.high_24h = price; + entry.low_24h = price; + } + } + + /// Get price discovery data for a pair + pub fn get_price_discovery(&self, pair_id: PairId) -> Option { + let entry = self.prices.get(&pair_id)?; + + let twap_price = if entry.twap_denominator.is_zero() { + entry.spot_price + } else { + entry.twap_numerator / entry.twap_denominator + }; + + Some(PriceDiscovery { + pair_id, + spot_price: entry.spot_price, + twap_price, + volume_24h: entry.volume_24h, + high_24h: entry.high_24h, + low_24h: entry.low_24h, + timestamp: entry.last_update, + }) + } + + /// Get spot price for a pair + pub fn get_spot_price(&self, pair_id: PairId) -> Option { + self.prices.get(&pair_id).map(|d| d.spot_price) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_price_discovery() { + let mut engine = PriceDiscoveryEngine::new(); + let pair_id = Uuid::new_v4(); + + // Update prices + engine.update_price(pair_id, Decimal::from_str("1.0").unwrap(), Decimal::from_str("100").unwrap()); + engine.update_price(pair_id, Decimal::from_str("1.1").unwrap(), Decimal::from_str("200").unwrap()); + engine.update_price(pair_id, Decimal::from_str("1.05").unwrap(), Decimal::from_str("150").unwrap()); + + let discovery = engine.get_price_discovery(pair_id).unwrap(); + + assert!(discovery.spot_price > Decimal::from_str("1.0").unwrap()); + assert!(discovery.volume_24h > Decimal::from_str("400").unwrap()); + assert!(discovery.high_24h >= discovery.spot_price); + assert!(discovery.low_24h <= discovery.spot_price); + + println!("✅ Price discovery test passed"); + println!(" Spot price: {}", discovery.spot_price); + println!(" TWAP price: {}", discovery.twap_price); + println!(" Volume 24h: {}", discovery.volume_24h); + } +} diff --git a/services_and_experiments/uet_under_development/uet_market/src/types.rs b/services_and_experiments/uet_under_development/uet_market/src/types.rs new file mode 100644 index 000000000..65d894a13 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_market/src/types.rs @@ -0,0 +1,119 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; + +/// Unique identifier for a trading pair +pub type PairId = Uuid; + +/// Unique identifier for a liquidity pool +pub type PoolId = Uuid; + +/// Trading pair +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TradingPair { + pub id: PairId, + pub base_asset: String, // e.g., "UET" + pub quote_asset: String, // e.g., "USD" + pub created_at: DateTime, +} + +/// Liquidity pool +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LiquidityPool { + pub id: PoolId, + pub pair_id: PairId, + pub base_reserve: Decimal, + pub quote_reserve: Decimal, + pub lp_token_supply: Decimal, + pub fee_rate: Decimal, // e.g., 0.003 for 0.3% + pub created_at: DateTime, +} + +impl LiquidityPool { + /// Calculate spot price (quote per base) + pub fn spot_price(&self) -> Decimal { + if self.base_reserve.is_zero() { + return Decimal::ZERO; + } + self.quote_reserve / self.base_reserve + } + + /// Calculate liquidity depth + pub fn liquidity_depth(&self) -> Decimal { + self.base_reserve * self.spot_price() + } +} + +/// Trade order +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum OrderSide { + Buy, + Sell, +} + +/// Order type +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum OrderType { + Market, + Limit, +} + +/// Trade order +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Order { + pub id: Uuid, + pub pair_id: PairId, + pub side: OrderSide, + pub order_type: OrderType, + pub amount: Decimal, + pub price: Option, // For limit orders + pub created_at: DateTime, +} + +/// Trade result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TradeResult { + pub order_id: Uuid, + pub executed_amount: Decimal, + pub executed_price: Decimal, + pub fee: Decimal, + pub timestamp: DateTime, +} + +/// Price discovery data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PriceDiscovery { + pub pair_id: PairId, + pub spot_price: Decimal, + pub twap_price: Decimal, // Time-weighted average price + pub volume_24h: Decimal, + pub high_24h: Decimal, + pub low_24h: Decimal, + pub timestamp: DateTime, +} + +/// Market error types +#[derive(Debug, thiserror::Error)] +pub enum MarketError { + #[error("Insufficient liquidity")] + InsufficientLiquidity, + + #[error("Invalid price: {0}")] + InvalidPrice(String), + + #[error("Invalid amount: {0}")] + InvalidAmount(String), + + #[error("Pool not found: {0}")] + PoolNotFound(PoolId), + + #[error("Pair not found: {0}")] + PairNotFound(PairId), + + #[error("Slippage too high: {0}% > {1}%")] + SlippageTooHigh(Decimal, Decimal), + + #[error("Calculation failed: {0}")] + CalculationFailed(String), +} diff --git a/services_and_experiments/uet_under_development/uet_oracle/Cargo.toml b/services_and_experiments/uet_under_development/uet_oracle/Cargo.toml new file mode 100644 index 000000000..7088e2e2b --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_oracle/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "uet_oracle" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +chrono = { version = "0.4", features = ["serde"] } +thiserror = "1.0" +uuid = { version = "1.0", features = ["v4", "serde"] } +reqwest = { version = "0.11", features = ["json"] } +tokio = { version = "1.0", features = ["full"] } +uet_security = { path = "../../uet_security" } + +[dev-dependencies] +mockito = "1.0" diff --git a/services_and_experiments/uet_under_development/uet_oracle/README.md b/services_and_experiments/uet_under_development/uet_oracle/README.md new file mode 100644 index 000000000..43b45aa27 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_oracle/README.md @@ -0,0 +1,162 @@ +# UET Oracle + +Oracle infrastructure for verifying energy consumption, land registry, and asset holdings for the UET economic system. + +## Overview + +`uet_oracle` provides a decentralized oracle system for verifying real-world data required for UET's asset-backed economic model. It supports multiple oracle types with reputation tracking and automatic failover. + +## Features + +- **Multi-Type Oracles**: Energy, Land, and Asset verification +- **Reputation System**: Track oracle accuracy and reliability +- **Automatic Failover**: Try multiple oracles until one succeeds +- **Signature Verification**: Cryptographic verification of oracle responses +- **Status Management**: Track oracle status (Active, Inactive, Maintenance, Compromised) + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Oracle System │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Energy │ │ Land │ │ Asset │ │ +│ │ Verifier │ │ Verifier │ │ Verifier │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Oracle │ │ Reputation │ │ Status │ │ +│ │ Registry │ │ System │ │ Management │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Usage + +### Setup Oracle Registry + +```rust +use uet_oracle::*; + +let mut registry = OracleRegistry::new(); + +let oracle_id = Uuid::new_v4(); +let oracle = OracleInfo { + id: oracle_id, + name: "Energy Oracle 1".to_string(), + oracle_type: OracleType::Energy, + config: OracleConfig { + oracle_type: OracleType::Energy, + endpoint: "https://energy-oracle.example.com".to_string(), + api_key: Some("api-key-123".to_string()), + timeout_seconds: 30, + min_reputation_threshold: 0.8, + }, + status: OracleStatus::Active, + reputation: ReputationScore { + oracle_id, + score: 1.0, + total_reports: 0, + correct_reports: 0, + last_updated: Utc::now(), + }, + created_at: Utc::now(), +}; + +registry.register_oracle(oracle); +``` + +### Verify Energy Consumption + +```rust +let energy_verifier = EnergyVerifier::new(registry); + +let request = EnergyVerificationRequest { + node_id: Uuid::new_v4(), + period_start: Utc::now() - chrono::Duration::hours(24), + period_end: Utc::now(), + expected_kwh: 1000.0, +}; + +let response = energy_verifier.verify_energy(request).await?; + +println!("Energy verified: {} kWh", response.actual_kwh); +``` + +### Verify Land Registry + +```rust +let land_verifier = LandVerifier::new(registry); + +let request = LandVerificationRequest { + land_id: "LAND-001".to_string(), + jurisdiction: "US-CA".to_string(), + owner_id: "OWNER-001".to_string(), +}; + +let response = land_verifier.verify_land(request).await?; + +println!("Land verified: {} sqm", response.land_area_sqm); +``` + +### Verify Asset Holdings + +```rust +let asset_verifier = AssetVerifier::new(registry); + +let request = AssetVerificationRequest { + asset_type: "bitcoin".to_string(), + asset_id: "btc-address".to_string(), + expected_amount: 10.0, +}; + +let response = asset_verifier.verify_asset(request).await?; + +println!("Asset verified: {} BTC", response.actual_amount); +``` + +## Reputation System + +Oracles are tracked based on their accuracy: + +- **Score**: 0.0 to 1.0 (percentage of correct reports) +- **Total Reports**: Number of verification attempts +- **Correct Reports**: Number of successful verifications +- **Threshold**: Minimum score required to be considered reputable + +Oracles with scores below the threshold are automatically marked as inactive. + +## Oracle Types + +| Type | Purpose | Data Verified | +|------|---------|---------------| +| Energy | Electricity consumption | kWh consumed by nodes | +| Land | Land registry | Land area, ownership | +| Asset | Asset holdings | Bitcoin, Gold, Patents | + +## Security + +- **Signature Verification**: All oracle responses are cryptographically signed +- **Reputation Tracking**: Unreliable oracles are automatically disabled +- **Multi-Oracle Consensus**: Try multiple oracles for verification +- **Status Management**: Oracles can be marked as compromised + +## Integration + +Oracle integrates with: +- `uet_governance`: For oracle configuration updates via governance +- `uet_security`: For signature verification +- `uet_chain`: For recording verification results on ledger + +## Testing + +```bash +cargo test --package uet_oracle +``` + +## License + +MIT diff --git a/services_and_experiments/uet_under_development/uet_oracle/src/ledger_integration.rs b/services_and_experiments/uet_under_development/uet_oracle/src/ledger_integration.rs new file mode 100644 index 000000000..bfd415305 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_oracle/src/ledger_integration.rs @@ -0,0 +1,158 @@ +// Oracle-Chain Integration +// +// This module integrates the oracle system with the ledger, +// recording verification results, reputation changes, and status updates. + +use crate::*; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use chrono::{DateTime, Utc}; + +/// Oracle event types for ledger recording +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum OracleEvent { + VerificationResult { + oracle_id: Uuid, + oracle_type: OracleType, + request_id: Uuid, + verified: bool, + timestamp: DateTime, + }, + ReputationUpdated { + oracle_id: Uuid, + old_score: f64, + new_score: f64, + reason: String, + timestamp: DateTime, + }, + StatusChanged { + oracle_id: Uuid, + old_status: OracleStatus, + new_status: OracleStatus, + reason: String, + timestamp: DateTime, + }, +} + +/// Oracle ledger recorder +pub struct OracleLedgerRecorder { + // In a real implementation, this would hold a reference to the chain +} + +impl OracleLedgerRecorder { + /// Create a new oracle ledger recorder + pub fn new() -> Self { + Self {} + } + + /// Record an oracle event to the ledger + pub fn record_event(&self, event: OracleEvent) -> Result<(), OracleError> { + // In a real implementation, this would: + // 1. Serialize the event + // 2. Create a transaction + // 3. Submit to the chain + // 4. Wait for confirmation + + println!("Recording oracle event: {:?}", event); + Ok(()) + } + + /// Record a verification result + pub fn record_verification( + &self, + oracle_id: Uuid, + oracle_type: OracleType, + request_id: Uuid, + verified: bool, + ) -> Result<(), OracleError> { + let event = OracleEvent::VerificationResult { + oracle_id, + oracle_type, + request_id, + verified, + timestamp: Utc::now(), + }; + + self.record_event(event) + } + + /// Record a reputation update + pub fn record_reputation_update( + &self, + oracle_id: Uuid, + old_score: f64, + new_score: f64, + reason: String, + ) -> Result<(), OracleError> { + let event = OracleEvent::ReputationUpdated { + oracle_id, + old_score, + new_score, + reason, + timestamp: Utc::now(), + }; + + self.record_event(event) + } + + /// Record a status change + pub fn record_status_change( + &self, + oracle_id: Uuid, + old_status: OracleStatus, + new_status: OracleStatus, + reason: String, + ) -> Result<(), OracleError> { + let event = OracleEvent::StatusChanged { + oracle_id, + old_status, + new_status, + reason, + timestamp: Utc::now(), + }; + + self.record_event(event) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_oracle_ledger_recorder() { + let recorder = OracleLedgerRecorder::new(); + + // Record verification result + recorder + .record_verification( + Uuid::new_v4(), + OracleType::Energy, + Uuid::new_v4(), + true, + ) + .unwrap(); + + // Record reputation update + recorder + .record_reputation_update( + Uuid::new_v4(), + 0.9, + 0.95, + "Correct verification".to_string(), + ) + .unwrap(); + + // Record status change + recorder + .record_status_change( + Uuid::new_v4(), + OracleStatus::Active, + OracleStatus::Maintenance, + "Scheduled maintenance".to_string(), + ) + .unwrap(); + + println!("✅ Oracle ledger recorder test passed"); + } +} diff --git a/services_and_experiments/uet_under_development/uet_oracle/src/lib.rs b/services_and_experiments/uet_under_development/uet_oracle/src/lib.rs new file mode 100644 index 000000000..7f71f0ca1 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_oracle/src/lib.rs @@ -0,0 +1,135 @@ +// UET Oracle - Oracle Infrastructure for Energy, Land, and Asset Verification +// +// This crate provides oracle infrastructure for verifying energy consumption, +// land registry, and asset holdings for the UET economic system. + +pub mod types; +pub mod registry; +pub mod verifier; +pub mod ledger_integration; + +pub use types::*; +pub use registry::OracleRegistry; +pub use verifier::{EnergyVerifier, LandVerifier, AssetVerifier}; +pub use ledger_integration::{OracleEvent, OracleLedgerRecorder}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_oracle_registry() { + let mut registry = OracleRegistry::new(); + + let oracle_id = Uuid::new_v4(); + let oracle = OracleInfo { + id: oracle_id, + name: "Test Energy Oracle".to_string(), + oracle_type: OracleType::Energy, + config: OracleConfig { + oracle_type: OracleType::Energy, + endpoint: "https://test.com".to_string(), + api_key: None, + timeout_seconds: 30, + min_reputation_threshold: 0.8, + }, + status: OracleStatus::Active, + reputation: ReputationScore { + oracle_id, + score: 1.0, + total_reports: 0, + correct_reports: 0, + last_updated: Utc::now(), + }, + created_at: Utc::now(), + }; + + registry.register_oracle(oracle); + + let retrieved = registry.get_oracle(oracle_id).unwrap(); + assert_eq!(retrieved.name, "Test Energy Oracle"); + + println!("✅ Oracle registry test passed"); + } + + #[test] + fn test_reputation_system() { + let mut registry = OracleRegistry::new(); + + let oracle_id = Uuid::new_v4(); + let oracle = OracleInfo { + id: oracle_id, + name: "Test Oracle".to_string(), + oracle_type: OracleType::Energy, + config: OracleConfig { + oracle_type: OracleType::Energy, + endpoint: "https://test.com".to_string(), + api_key: None, + timeout_seconds: 30, + min_reputation_threshold: 0.8, + }, + status: OracleStatus::Active, + reputation: ReputationScore { + oracle_id, + score: 1.0, + total_reports: 0, + correct_reports: 0, + last_updated: Utc::now(), + }, + created_at: Utc::now(), + }; + + registry.register_oracle(oracle); + + // Simulate 10 correct reports + for _ in 0..10 { + registry.update_reputation(oracle_id, true).unwrap(); + } + + let retrieved = registry.get_oracle(oracle_id).unwrap(); + assert_eq!(retrieved.reputation.score, 1.0); + assert_eq!(retrieved.reputation.total_reports, 10); + + println!("✅ Reputation system test passed"); + } + + #[test] + fn test_oracle_filtering() { + let mut registry = OracleRegistry::new(); + + // Add multiple oracles + for i in 0..3 { + let oracle_id = Uuid::new_v4(); + let oracle = OracleInfo { + id: oracle_id, + name: format!("Energy Oracle {}", i), + oracle_type: OracleType::Energy, + config: OracleConfig { + oracle_type: OracleType::Energy, + endpoint: "https://test.com".to_string(), + api_key: None, + timeout_seconds: 30, + min_reputation_threshold: 0.8, + }, + status: OracleStatus::Active, + reputation: ReputationScore { + oracle_id, + score: 1.0, + total_reports: 10, + correct_reports: 10, + last_updated: Utc::now(), + }, + created_at: Utc::now(), + }; + registry.register_oracle(oracle); + } + + let energy_oracles = registry.get_oracles_by_type(OracleType::Energy); + assert_eq!(energy_oracles.len(), 3); + + let active_reputable = registry.get_active_reputable_oracles(OracleType::Energy); + assert_eq!(active_reputable.len(), 3); + + println!("✅ Oracle filtering test passed"); + } +} diff --git a/services_and_experiments/uet_under_development/uet_oracle/src/registry.rs b/services_and_experiments/uet_under_development/uet_oracle/src/registry.rs new file mode 100644 index 000000000..f19f73d67 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_oracle/src/registry.rs @@ -0,0 +1,159 @@ +use crate::types::*; +use std::collections::HashMap; + +/// Oracle registry manages all oracles +pub struct OracleRegistry { + oracles: HashMap, +} + +impl OracleRegistry { + /// Create a new oracle registry + pub fn new() -> Self { + Self { + oracles: HashMap::new(), + } + } + + /// Register an oracle + pub fn register_oracle(&mut self, oracle: OracleInfo) { + self.oracles.insert(oracle.id, oracle); + } + + /// Get an oracle by ID + pub fn get_oracle(&self, oracle_id: OracleId) -> Option<&OracleInfo> { + self.oracles.get(&oracle_id) + } + + /// Get all oracles + pub fn get_all_oracles(&self) -> &HashMap { + &self.oracles + } + + /// Get oracles by type + pub fn get_oracles_by_type(&self, oracle_type: OracleType) -> Vec<&OracleInfo> { + self.oracles + .values() + .filter(|o| o.oracle_type == oracle_type) + .collect() + } + + /// Get active and reputable oracles + pub fn get_active_reputable_oracles(&self, oracle_type: OracleType) -> Vec<&OracleInfo> { + self.oracles + .values() + .filter(|o| { + o.oracle_type == oracle_type + && o.status == OracleStatus::Active + && o.reputation.is_reputable(o.config.min_reputation_threshold) + }) + .collect() + } + + /// Update oracle reputation + pub fn update_reputation(&mut self, oracle_id: OracleId, is_correct: bool) -> Result<(), OracleError> { + let oracle = self.oracles.get_mut(&oracle_id) + .ok_or(OracleError::OracleNotFound(oracle_id))?; + + oracle.reputation.calculate(is_correct); + + // If reputation drops below threshold, mark as inactive + if oracle.reputation.score < oracle.config.min_reputation_threshold { + oracle.status = OracleStatus::Inactive; + } + + Ok(()) + } + + /// Update oracle status + pub fn update_status(&mut self, oracle_id: OracleId, status: OracleStatus) -> Result<(), OracleError> { + let oracle = self.oracles.get_mut(&oracle_id) + .ok_or(OracleError::OracleNotFound(oracle_id))?; + + oracle.status = status; + + Ok(()) + } +} + +impl Default for OracleRegistry { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_register_oracle() { + let mut registry = OracleRegistry::new(); + + let oracle_id = Uuid::new_v4(); + let oracle = OracleInfo { + id: oracle_id, + name: "Test Energy Oracle".to_string(), + oracle_type: OracleType::Energy, + config: OracleConfig { + oracle_type: OracleType::Energy, + endpoint: "https://test.com".to_string(), + api_key: None, + timeout_seconds: 30, + min_reputation_threshold: 0.8, + }, + status: OracleStatus::Active, + reputation: ReputationScore { + oracle_id, + score: 1.0, + total_reports: 0, + correct_reports: 0, + last_updated: Utc::now(), + }, + created_at: Utc::now(), + }; + + registry.register_oracle(oracle); + + let retrieved = registry.get_oracle(oracle_id).unwrap(); + assert_eq!(retrieved.name, "Test Energy Oracle"); + } + + #[test] + fn test_reputation_calculation() { + let mut registry = OracleRegistry::new(); + + let oracle_id = Uuid::new_v4(); + let oracle = OracleInfo { + id: oracle_id, + name: "Test Oracle".to_string(), + oracle_type: OracleType::Energy, + config: OracleConfig { + oracle_type: OracleType::Energy, + endpoint: "https://test.com".to_string(), + api_key: None, + timeout_seconds: 30, + min_reputation_threshold: 0.8, + }, + status: OracleStatus::Active, + reputation: ReputationScore { + oracle_id, + score: 1.0, + total_reports: 0, + correct_reports: 0, + last_updated: Utc::now(), + }, + created_at: Utc::now(), + }; + + registry.register_oracle(oracle); + + // Simulate 10 correct reports + for _ in 0..10 { + registry.update_reputation(oracle_id, true).unwrap(); + } + + let retrieved = registry.get_oracle(oracle_id).unwrap(); + assert_eq!(retrieved.reputation.score, 1.0); + assert_eq!(retrieved.reputation.total_reports, 10); + } +} diff --git a/services_and_experiments/uet_under_development/uet_oracle/src/types.rs b/services_and_experiments/uet_under_development/uet_oracle/src/types.rs new file mode 100644 index 000000000..3acc8f660 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_oracle/src/types.rs @@ -0,0 +1,167 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use chrono::{DateTime, Utc}; + +/// Unique identifier for an oracle +pub type OracleId = Uuid; + +/// Oracle types +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum OracleType { + /// Energy oracle: verifies electricity consumption + Energy, + /// Land oracle: verifies land registry + Land, + /// Asset oracle: verifies Bitcoin/Gold/patents + Asset, +} + +/// Oracle status +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum OracleStatus { + /// Oracle is active and responding + Active, + /// Oracle is inactive + Inactive, + /// Oracle is under maintenance + Maintenance, + /// Oracle is compromised + Compromised, +} + +/// Oracle reputation score +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReputationScore { + pub oracle_id: OracleId, + pub score: f64, // 0.0 to 1.0 + pub total_reports: u64, + pub correct_reports: u64, + pub last_updated: DateTime, +} + +impl ReputationScore { + /// Calculate reputation based on correct reports + pub fn calculate(&mut self, is_correct: bool) { + self.total_reports += 1; + if is_correct { + self.correct_reports += 1; + } + self.score = if self.total_reports > 0 { + self.correct_reports as f64 / self.total_reports as f64 + } else { + 0.0 + }; + self.last_updated = Utc::now(); + } + + /// Check if oracle is reputable enough + pub fn is_reputable(&self, threshold: f64) -> bool { + self.score >= threshold && self.total_reports >= 10 + } +} + +/// Oracle configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OracleConfig { + pub oracle_type: OracleType, + pub endpoint: String, + pub api_key: Option, + pub timeout_seconds: u64, + pub min_reputation_threshold: f64, +} + +/// Oracle information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OracleInfo { + pub id: OracleId, + pub name: String, + pub oracle_type: OracleType, + pub config: OracleConfig, + pub status: OracleStatus, + pub reputation: ReputationScore, + pub created_at: DateTime, +} + +/// Energy verification request +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EnergyVerificationRequest { + pub node_id: Uuid, + pub period_start: DateTime, + pub period_end: DateTime, + pub expected_kwh: f64, +} + +/// Energy verification response +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EnergyVerificationResponse { + pub verified: bool, + pub actual_kwh: f64, + pub verification_timestamp: DateTime, + pub oracle_id: OracleId, + pub signature: String, +} + +/// Land verification request +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LandVerificationRequest { + pub land_id: String, + pub jurisdiction: String, + pub owner_id: String, +} + +/// Land verification response +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LandVerificationResponse { + pub verified: bool, + pub land_area_sqm: f64, + pub owner_verified: bool, + pub verification_timestamp: DateTime, + pub oracle_id: OracleId, + pub signature: String, +} + +/// Asset verification request +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AssetVerificationRequest { + pub asset_type: String, // "bitcoin", "gold", "patent" + pub asset_id: String, + pub expected_amount: f64, +} + +/// Asset verification response +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AssetVerificationResponse { + pub verified: bool, + pub actual_amount: f64, + pub verification_timestamp: DateTime, + pub oracle_id: OracleId, + pub signature: String, +} + +/// Oracle error types +#[derive(Debug, thiserror::Error)] +pub enum OracleError { + #[error("Oracle not found: {0}")] + OracleNotFound(OracleId), + + #[error("Oracle is not active: {0:?}")] + OracleNotActive(OracleStatus), + + #[error("Oracle reputation too low: {0} < {1}")] + ReputationTooLow(f64, f64), + + #[error("Verification failed: {0}")] + VerificationFailed(String), + + #[error("Network error: {0}")] + NetworkError(String), + + #[error("Timeout after {0} seconds")] + Timeout(u64), + + #[error("Invalid response format")] + InvalidResponseFormat, + + #[error("Signature verification failed")] + SignatureVerificationFailed, +} diff --git a/services_and_experiments/uet_under_development/uet_oracle/src/verifier.rs b/services_and_experiments/uet_under_development/uet_oracle/src/verifier.rs new file mode 100644 index 000000000..fa93d4b79 --- /dev/null +++ b/services_and_experiments/uet_under_development/uet_oracle/src/verifier.rs @@ -0,0 +1,290 @@ +use crate::types::*; +use crate::registry::OracleRegistry; +use reqwest::Client; +use std::time::Duration; + +/// Energy verifier +pub struct EnergyVerifier { + client: Client, + registry: OracleRegistry, +} + +impl EnergyVerifier { + /// Create a new energy verifier + pub fn new(registry: OracleRegistry) -> Self { + Self { + client: Client::new(), + registry, + } + } + + /// Verify energy consumption + pub async fn verify_energy( + &mut self, + request: EnergyVerificationRequest, + ) -> Result { + // Get active and reputable energy oracles + let oracle_ids: Vec<_> = self.registry + .get_active_reputable_oracles(OracleType::Energy) + .iter() + .map(|o| o.id) + .collect(); + + if oracle_ids.is_empty() { + return Err(OracleError::VerificationFailed( + "No active reputable energy oracles available".to_string(), + )); + } + + // Get oracles again for verification + let oracles = self.registry.get_active_reputable_oracles(OracleType::Energy); + + // Try each oracle until one succeeds + for oracle_id in oracle_ids { + // Get oracle for verification + let oracles_for_verify = self.registry.get_active_reputable_oracles(OracleType::Energy); + let oracle = oracles_for_verify.iter().find(|o| o.id == oracle_id); + + if let Some(oracle) = oracle { + match self.verify_with_oracle(&request, oracle).await { + Ok(response) => { + // Update reputation + let _ = self.registry.update_reputation(oracle_id, true); + return Ok(response); + } + Err(e) => { + // Update reputation (incorrect) + let _ = self.registry.update_reputation(oracle_id, false); + continue; + } + } + } + } + + Err(OracleError::VerificationFailed( + "All energy oracles failed".to_string(), + )) + } + + /// Verify with a specific oracle + async fn verify_with_oracle( + &self, + request: &EnergyVerificationRequest, + oracle: &OracleInfo, + ) -> Result { + let url = format!("{}/verify", oracle.config.endpoint); + + let response = self + .client + .post(&url) + .timeout(Duration::from_secs(oracle.config.timeout_seconds)) + .header("Authorization", format!("Bearer {}", oracle.config.api_key.as_ref().unwrap_or(&String::new()))) + .json(request) + .send() + .await + .map_err(|e| OracleError::NetworkError(e.to_string()))?; + + if !response.status().is_success() { + return Err(OracleError::VerificationFailed(format!( + "Oracle returned status: {}", + response.status() + ))); + } + + let verification_response: EnergyVerificationResponse = response + .json() + .await + .map_err(|_| OracleError::InvalidResponseFormat)?; + + // Verify signature + // TODO: Implement signature verification using uet_security + + Ok(verification_response) + } +} + +/// Land verifier +pub struct LandVerifier { + client: Client, + registry: OracleRegistry, +} + +impl LandVerifier { + /// Create a new land verifier + pub fn new(registry: OracleRegistry) -> Self { + Self { + client: Client::new(), + registry, + } + } + + /// Verify land registry + pub async fn verify_land( + &mut self, + request: LandVerificationRequest, + ) -> Result { + // Get active and reputable land oracles + let oracle_ids: Vec<_> = self.registry + .get_active_reputable_oracles(OracleType::Land) + .iter() + .map(|o| o.id) + .collect(); + + if oracle_ids.is_empty() { + return Err(OracleError::VerificationFailed( + "No active reputable land oracles available".to_string(), + )); + } + + // Try each oracle until one succeeds + for oracle_id in oracle_ids { + // Get oracle for this iteration + let oracles = self.registry.get_active_reputable_oracles(OracleType::Land); + if let Some(oracle) = oracles.iter().find(|o| o.id == oracle_id) { + let endpoint = oracle.config.endpoint.clone(); + let timeout_seconds = oracle.config.timeout_seconds; + let api_key = oracle.config.api_key.clone(); + + match self.verify_with_oracle(&request, &endpoint, timeout_seconds, api_key).await { + Ok(response) => { + let _ = self.registry.update_reputation(oracle_id, true); + return Ok(response); + } + Err(e) => { + let _ = self.registry.update_reputation(oracle_id, false); + continue; + } + } + } + } + + Err(OracleError::VerificationFailed( + "All land oracles failed".to_string(), + )) + } + + /// Verify with a specific oracle + async fn verify_with_oracle( + &mut self, + request: &LandVerificationRequest, + endpoint: &str, + timeout_seconds: u64, + api_key: Option, + ) -> Result { + let url = format!("{}/verify", endpoint); + + let response = self + .client + .post(&url) + .timeout(Duration::from_secs(timeout_seconds)) + .header("Authorization", format!("Bearer {}", api_key.as_ref().unwrap_or(&String::new()))) + .json(request) + .send() + .await + .map_err(|e| OracleError::NetworkError(e.to_string()))?; + + if !response.status().is_success() { + return Err(OracleError::VerificationFailed(format!( + "Oracle returned status: {}", + response.status() + ))); + } + + let verification_response: LandVerificationResponse = response + .json() + .await + .map_err(|_| OracleError::InvalidResponseFormat)?; + + Ok(verification_response) + } +} + +/// Asset verifier +pub struct AssetVerifier { + client: Client, + registry: OracleRegistry, +} + +impl AssetVerifier { + /// Create a new asset verifier + pub fn new(registry: OracleRegistry) -> Self { + Self { + client: Client::new(), + registry, + } + } + + /// Verify asset holdings + pub async fn verify_asset( + &mut self, + request: AssetVerificationRequest, + ) -> Result { + // Get active and reputable asset oracles + let oracle_ids: Vec<_> = self.registry + .get_active_reputable_oracles(OracleType::Asset) + .iter() + .map(|o| o.id) + .collect(); + + if oracle_ids.is_empty() { + return Err(OracleError::VerificationFailed( + "No active reputable asset oracles available".to_string(), + )); + } + + // Try each oracle until one succeeds + for oracle_id in oracle_ids { + // Get oracle for this iteration + let oracles = self.registry.get_active_reputable_oracles(OracleType::Asset); + if let Some(oracle) = oracles.iter().find(|o| o.id == oracle_id) { + match self.verify_with_oracle(&request, oracle).await { + Ok(response) => { + let _ = self.registry.update_reputation(oracle_id, true); + return Ok(response); + } + Err(e) => { + let _ = self.registry.update_reputation(oracle_id, false); + continue; + } + } + } + } + + Err(OracleError::VerificationFailed( + "All asset oracles failed".to_string(), + )) + } + + /// Verify with a specific oracle + async fn verify_with_oracle( + &self, + request: &AssetVerificationRequest, + oracle: &OracleInfo, + ) -> Result { + let url = format!("{}/verify", oracle.config.endpoint); + + let response = self + .client + .post(&url) + .timeout(Duration::from_secs(oracle.config.timeout_seconds)) + .header("Authorization", format!("Bearer {}", oracle.config.api_key.as_ref().unwrap_or(&String::new()))) + .json(request) + .send() + .await + .map_err(|e| OracleError::NetworkError(e.to_string()))?; + + if !response.status().is_success() { + return Err(OracleError::VerificationFailed(format!( + "Oracle returned status: {}", + response.status() + ))); + } + + let verification_response: AssetVerificationResponse = response + .json() + .await + .map_err(|_| OracleError::InvalidResponseFormat)?; + + Ok(verification_response) + } +}