nexaroa 0.0.111__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. neuroshard/__init__.py +93 -0
  2. neuroshard/__main__.py +4 -0
  3. neuroshard/cli.py +466 -0
  4. neuroshard/core/__init__.py +92 -0
  5. neuroshard/core/consensus/verifier.py +252 -0
  6. neuroshard/core/crypto/__init__.py +20 -0
  7. neuroshard/core/crypto/ecdsa.py +392 -0
  8. neuroshard/core/economics/__init__.py +52 -0
  9. neuroshard/core/economics/constants.py +387 -0
  10. neuroshard/core/economics/ledger.py +2111 -0
  11. neuroshard/core/economics/market.py +975 -0
  12. neuroshard/core/economics/wallet.py +168 -0
  13. neuroshard/core/governance/__init__.py +74 -0
  14. neuroshard/core/governance/proposal.py +561 -0
  15. neuroshard/core/governance/registry.py +545 -0
  16. neuroshard/core/governance/versioning.py +332 -0
  17. neuroshard/core/governance/voting.py +453 -0
  18. neuroshard/core/model/__init__.py +30 -0
  19. neuroshard/core/model/dynamic.py +4186 -0
  20. neuroshard/core/model/llm.py +905 -0
  21. neuroshard/core/model/registry.py +164 -0
  22. neuroshard/core/model/scaler.py +387 -0
  23. neuroshard/core/model/tokenizer.py +568 -0
  24. neuroshard/core/network/__init__.py +56 -0
  25. neuroshard/core/network/connection_pool.py +72 -0
  26. neuroshard/core/network/dht.py +130 -0
  27. neuroshard/core/network/dht_plan.py +55 -0
  28. neuroshard/core/network/dht_proof_store.py +516 -0
  29. neuroshard/core/network/dht_protocol.py +261 -0
  30. neuroshard/core/network/dht_service.py +506 -0
  31. neuroshard/core/network/encrypted_channel.py +141 -0
  32. neuroshard/core/network/nat.py +201 -0
  33. neuroshard/core/network/nat_traversal.py +695 -0
  34. neuroshard/core/network/p2p.py +929 -0
  35. neuroshard/core/network/p2p_data.py +150 -0
  36. neuroshard/core/swarm/__init__.py +106 -0
  37. neuroshard/core/swarm/aggregation.py +729 -0
  38. neuroshard/core/swarm/buffers.py +643 -0
  39. neuroshard/core/swarm/checkpoint.py +709 -0
  40. neuroshard/core/swarm/compute.py +624 -0
  41. neuroshard/core/swarm/diloco.py +844 -0
  42. neuroshard/core/swarm/factory.py +1288 -0
  43. neuroshard/core/swarm/heartbeat.py +669 -0
  44. neuroshard/core/swarm/logger.py +487 -0
  45. neuroshard/core/swarm/router.py +658 -0
  46. neuroshard/core/swarm/service.py +640 -0
  47. neuroshard/core/training/__init__.py +29 -0
  48. neuroshard/core/training/checkpoint.py +600 -0
  49. neuroshard/core/training/distributed.py +1602 -0
  50. neuroshard/core/training/global_tracker.py +617 -0
  51. neuroshard/core/training/production.py +276 -0
  52. neuroshard/governance_cli.py +729 -0
  53. neuroshard/grpc_server.py +895 -0
  54. neuroshard/runner.py +3223 -0
  55. neuroshard/sdk/__init__.py +92 -0
  56. neuroshard/sdk/client.py +990 -0
  57. neuroshard/sdk/errors.py +101 -0
  58. neuroshard/sdk/types.py +282 -0
  59. neuroshard/tracker/__init__.py +0 -0
  60. neuroshard/tracker/server.py +864 -0
  61. neuroshard/ui/__init__.py +0 -0
  62. neuroshard/ui/app.py +102 -0
  63. neuroshard/ui/templates/index.html +1052 -0
  64. neuroshard/utils/__init__.py +0 -0
  65. neuroshard/utils/autostart.py +81 -0
  66. neuroshard/utils/hardware.py +121 -0
  67. neuroshard/utils/serialization.py +90 -0
  68. neuroshard/version.py +1 -0
  69. nexaroa-0.0.111.dist-info/METADATA +283 -0
  70. nexaroa-0.0.111.dist-info/RECORD +78 -0
  71. nexaroa-0.0.111.dist-info/WHEEL +5 -0
  72. nexaroa-0.0.111.dist-info/entry_points.txt +4 -0
  73. nexaroa-0.0.111.dist-info/licenses/LICENSE +190 -0
  74. nexaroa-0.0.111.dist-info/top_level.txt +2 -0
  75. protos/__init__.py +0 -0
  76. protos/neuroshard.proto +651 -0
  77. protos/neuroshard_pb2.py +160 -0
  78. protos/neuroshard_pb2_grpc.py +1298 -0
@@ -0,0 +1,975 @@
1
+ """
2
+ Dynamic Inference Market - Request-Response Marketplace
3
+
4
+ This module implements a TRUE MARKETPLACE for inference where:
5
+ - Users submit requests with locked-in prices
6
+ - Nodes claim requests and process them
7
+ - Payment guaranteed at submission price (no timing attacks)
8
+ - Full attack resistance through request matching
9
+
10
+ Economic Principles:
11
+ 1. High demand + Low supply = High price (attracts nodes to serve)
12
+ 2. Low demand + High supply = Low price (nodes switch to training)
13
+ 3. Market finds equilibrium where: inference_profit ≈ training_profit × 0.5
14
+
15
+ Security Features:
16
+ 1. Request-Response Matching: Price locked at submission time
17
+ 2. Atomic Claiming: Prevents double-claim attacks
18
+ 3. Claim Timeouts: Squatting nodes slashed and requests re-queued
19
+ 4. ECDSA Signatures: User must sign request to authorize payment
20
+ 5. Capacity Timeout: Fake capacity removed after 60s
21
+ 6. Price Smoothing: EMA prevents flash crashes
22
+ 7. Rate Limiting: Per-node and per-user limits
23
+ """
24
+
25
+ import time
26
+ import math
27
+ import uuid
28
+ import logging
29
+ from dataclasses import dataclass, field
30
+ from typing import List, Optional, Tuple, Dict, Set
31
+ from collections import deque
32
+ from enum import Enum
33
+ import threading
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class RequestStatus(str, Enum):
39
+ """Request lifecycle states."""
40
+ PENDING = "pending" # Waiting for driver to claim
41
+ DRIVER_CLAIMED = "driver_claimed" # Driver claimed, starting pipeline
42
+ PROCESSING = "processing" # Pipeline actively processing
43
+ COMPLETED = "completed" # All proofs received, done
44
+ FAILED = "failed" # Timeout or error
45
+
46
+
47
+ @dataclass
48
+ class InferenceRequest:
49
+ """
50
+ Marketplace request (PUBLIC metadata - NO PROMPT!).
51
+
52
+ Privacy: Prompt is sent DIRECTLY to driver node (encrypted).
53
+ The marketplace only orchestrates pricing and pipeline coordination.
54
+ """
55
+ request_id: str # Unique ID (UUID)
56
+ user_id: str # User who submitted
57
+ driver_node_id: str # Which driver node will process (Layer 0)
58
+
59
+ # Pricing
60
+ tokens_requested: int # Max tokens to generate
61
+ max_price: float # Max NEURO per 1M tokens user will pay
62
+ locked_price: float # Actual price locked at submission time
63
+ priority: int # Higher = more urgent
64
+
65
+ # Pipeline state (distributed inference)
66
+ pipeline_session_id: Optional[str] = None # Tracks distributed session
67
+ status: RequestStatus = RequestStatus.PENDING
68
+ claimed_by: Optional[str] = None # Node ID that claimed this request
69
+
70
+ # Timestamps
71
+ timestamp: float = 0.0 # When submitted
72
+ claimed_at: Optional[float] = None # When driver claimed
73
+ completed_at: Optional[float] = None # When all proofs received
74
+
75
+ # Proofs received (distributed inference)
76
+ driver_proof_received: bool = False
77
+ worker_proofs_received: List[str] = field(default_factory=list) # Node IDs
78
+ validator_proof_received: bool = False
79
+
80
+ # Security
81
+ user_signature: str = "" # ECDSA signature authorizing payment
82
+
83
+ @property
84
+ def completed(self) -> bool:
85
+ """Check if request is completed (for ledger.py compatibility)."""
86
+ return self.status == RequestStatus.COMPLETED
87
+
88
+
89
+ @dataclass
90
+ class NodeCapacity:
91
+ """Available inference capacity from a node."""
92
+ node_id: str
93
+ tokens_per_second: int # Processing capacity
94
+ min_price: float # Minimum NEURO per 1M tokens node will accept
95
+ timestamp: float
96
+
97
+
98
+ @dataclass
99
+ class PipelineSession:
100
+ """
101
+ Tracks distributed inference pipeline across multiple nodes.
102
+
103
+ Privacy: Prompt is NEVER stored here - only driver knows it!
104
+ Workers only see activations (meaningless vectors).
105
+ """
106
+ session_id: str # UUID
107
+ request_id: str # Links to InferenceRequest
108
+ driver_node_id: str # Layer 0 (embedding)
109
+
110
+ # Pipeline participants (discovered dynamically via DHT)
111
+ worker_node_ids: List[str] = field(default_factory=list) # Layers 1-N
112
+ validator_node_id: Optional[str] = None # LM Head
113
+
114
+ # State tracking
115
+ current_layer: int = 0 # Which layer is processing now
116
+ tokens_generated: int = 0 # How many tokens generated so far
117
+
118
+ # Security: Verify pipeline integrity
119
+ activations_hashes: List[str] = field(default_factory=list) # Hash per layer
120
+
121
+ # Timestamps
122
+ started_at: float = 0.0
123
+ last_activity: float = 0.0
124
+
125
+
126
+ class InferenceMarket:
127
+ """
128
+ Real-time market for inference pricing.
129
+
130
+ Price Discovery:
131
+ - Measures real-time supply (nodes offering capacity)
132
+ - Measures real-time demand (pending requests)
133
+ - Adjusts price to clear the market
134
+
135
+ Properties:
136
+ - Self-regulating (no admin intervention)
137
+ - Attack-resistant (real supply/demand signals)
138
+ - Training-dominant (low prices push nodes to train)
139
+ """
140
+
141
+ def __init__(
142
+ self,
143
+ price_smoothing: float = 0.8, # Exponential moving average (prevents volatility)
144
+ capacity_timeout: int = 60, # Seconds before stale capacity expires
145
+ claim_timeout: int = 60, # Seconds before claimed request expires
146
+ base_price: float = 0.0001, # Bootstrap starting price (worthless model)
147
+ ):
148
+ self.price_smoothing = price_smoothing
149
+ self.capacity_timeout = capacity_timeout
150
+ self.claim_timeout = claim_timeout
151
+ self.base_price = base_price
152
+
153
+ # REQUEST QUEUES (marketplace state)
154
+ self.pending_requests: Dict[str, InferenceRequest] = {} # Unclaimed requests
155
+ self.claimed_requests: Dict[str, InferenceRequest] = {} # Claimed but incomplete
156
+ self.completed_requests: Dict[str, InferenceRequest] = {} # Done (audit trail)
157
+
158
+ # RESULTS STORAGE (user retrieval)
159
+ self.results: Dict[str, str] = {} # request_id -> output_text
160
+ self.result_timestamps: Dict[str, float] = {} # For cleanup
161
+
162
+ # PIPELINE SESSIONS (distributed inference tracking)
163
+ self.active_sessions: Dict[str, PipelineSession] = {} # session_id -> session
164
+ self.completed_sessions: Dict[str, PipelineSession] = {} # Audit trail
165
+
166
+ # Capacity tracking
167
+ self.available_capacity: List[NodeCapacity] = []
168
+
169
+ # Price tracking
170
+ self.current_price = base_price # Start near zero (model has no value yet)
171
+ self.price_history = deque(maxlen=100)
172
+
173
+ # Security: Prevent replay attacks
174
+ self.request_nonces: Set[str] = set() # Track used request IDs
175
+ self.last_capacity_update: Dict[str, float] = {} # Rate limit capacity updates
176
+
177
+ # Threading
178
+ self.lock = threading.Lock()
179
+
180
+ # Metrics
181
+ self.total_tokens_processed = 0
182
+ self.total_requests_served = 0
183
+ self.price_updates = 0
184
+
185
+ logger.info("InferenceMarket initialized with request-response matching")
186
+
187
+ # ========================================================================
188
+ # SUPPLY MEASUREMENT
189
+ # ========================================================================
190
+
191
+ def register_capacity(
192
+ self,
193
+ node_id: str,
194
+ tokens_per_second: int,
195
+ min_price: float
196
+ ):
197
+ """
198
+ Node announces available inference capacity.
199
+
200
+ Args:
201
+ node_id: Node identifier
202
+ tokens_per_second: How many tokens/sec this node can process
203
+ min_price: Minimum price node will accept (0 = any price)
204
+
205
+ Security:
206
+ - Rate limited to prevent spam
207
+ - Sanity check on capacity claims
208
+ - Expires after 60s (must refresh)
209
+ """
210
+ with self.lock:
211
+ now = time.time()
212
+
213
+ # SECURITY: Rate limit capacity updates (max 1 per 30 seconds)
214
+ if node_id in self.last_capacity_update:
215
+ if (now - self.last_capacity_update[node_id]) < 30:
216
+ logger.warning(f"Capacity update too frequent from {node_id[:16]}...")
217
+ return # Silently ignore spam
218
+
219
+ # SECURITY: Sanity check on capacity claim
220
+ MAX_CAPACITY_SINGLE_NODE = 50000 # 50k tokens/sec (~high-end GPU)
221
+ if tokens_per_second > MAX_CAPACITY_SINGLE_NODE:
222
+ logger.warning(f"Unrealistic capacity claim from {node_id[:16]}...: "
223
+ f"{tokens_per_second} t/s (max {MAX_CAPACITY_SINGLE_NODE})")
224
+ tokens_per_second = MAX_CAPACITY_SINGLE_NODE # Cap it
225
+
226
+ # Remove old capacity from this node
227
+ self.available_capacity = [
228
+ c for c in self.available_capacity
229
+ if c.node_id != node_id
230
+ ]
231
+
232
+ # Add new capacity
233
+ self.available_capacity.append(NodeCapacity(
234
+ node_id=node_id,
235
+ tokens_per_second=tokens_per_second,
236
+ min_price=min_price,
237
+ timestamp=now
238
+ ))
239
+
240
+ # Track last update time
241
+ self.last_capacity_update[node_id] = now
242
+
243
+ # Trigger price update
244
+ self._update_market_price()
245
+
246
+ def withdraw_capacity(self, node_id: str):
247
+ """Node withdraws from inference market (e.g., to focus on training)."""
248
+ with self.lock:
249
+ self.available_capacity = [
250
+ c for c in self.available_capacity
251
+ if c.node_id != node_id
252
+ ]
253
+ self._update_market_price()
254
+
255
+ def get_total_supply(self) -> int:
256
+ """
257
+ Calculate total available supply in tokens/sec.
258
+
259
+ Returns:
260
+ Total network inference capacity (tokens/sec)
261
+ """
262
+ now = time.time()
263
+
264
+ # Clean up stale capacity announcements
265
+ self.available_capacity = [
266
+ c for c in self.available_capacity
267
+ if (now - c.timestamp) < self.capacity_timeout
268
+ ]
269
+
270
+ return sum(c.tokens_per_second for c in self.available_capacity)
271
+
272
+ # ========================================================================
273
+ # MARKETPLACE: REQUEST SUBMISSION
274
+ # ========================================================================
275
+
276
+ def submit_request(
277
+ self,
278
+ user_id: str,
279
+ driver_node_id: str,
280
+ tokens_requested: int,
281
+ max_price: float,
282
+ user_signature: str = "",
283
+ priority: int = 0
284
+ ) -> Tuple[bool, str, float]:
285
+ """
286
+ Submit inference request to marketplace (DISTRIBUTED INFERENCE).
287
+
288
+ PRIVACY: Prompt is NOT sent to marketplace!
289
+ User sends encrypted prompt DIRECTLY to driver node.
290
+ Marketplace only handles pricing and pipeline coordination.
291
+
292
+ Args:
293
+ user_id: User submitting request
294
+ driver_node_id: Which driver node (Layer 0) will process
295
+ tokens_requested: Max tokens to generate
296
+ max_price: Max NEURO per 1M tokens user will pay
297
+ user_signature: ECDSA signature authorizing payment
298
+ priority: Request priority (higher = more urgent)
299
+
300
+ Returns:
301
+ (success, request_id, locked_price)
302
+
303
+ Security:
304
+ - User signature validates authorization
305
+ - Nonce prevents replay attacks
306
+ - Price locked at current market rate
307
+ - Driver-specific (user chooses who sees prompt)
308
+ """
309
+ try:
310
+ # Input validation
311
+ if not user_id or not driver_node_id:
312
+ logger.warning("Invalid request: missing user_id or driver_node_id")
313
+ return False, "", self.current_price
314
+
315
+ if tokens_requested <= 0:
316
+ logger.warning(f"Invalid tokens_requested: {tokens_requested}")
317
+ return False, "", self.current_price
318
+
319
+ if max_price < 0:
320
+ logger.warning(f"Invalid max_price: {max_price}")
321
+ return False, "", self.current_price
322
+
323
+ with self.lock:
324
+ # 1. Check current price vs user's max
325
+ current_price = self.current_price
326
+ if max_price < current_price:
327
+ logger.debug(f"Price too high for user: {current_price:.6f} > {max_price:.6f}")
328
+ return False, "", current_price # Price too high for user
329
+
330
+ # 2. Generate unique request ID
331
+ request_id = str(uuid.uuid4())
332
+
333
+ # 3. Check for replay attack
334
+ if request_id in self.request_nonces:
335
+ logger.warning(f"Duplicate request_id detected: {request_id}")
336
+ return False, "", current_price
337
+
338
+ # 4. Create request with LOCKED price (NO PROMPT - privacy!)
339
+ request = InferenceRequest(
340
+ request_id=request_id,
341
+ user_id=user_id,
342
+ driver_node_id=driver_node_id, # User chooses driver
343
+ tokens_requested=tokens_requested,
344
+ max_price=max_price,
345
+ locked_price=current_price, # LOCKED at submission time!
346
+ priority=priority,
347
+ timestamp=time.time(),
348
+ status=RequestStatus.PENDING,
349
+ user_signature=user_signature
350
+ )
351
+
352
+ # 5. Add to pending queue
353
+ self.pending_requests[request_id] = request
354
+ self.request_nonces.add(request_id)
355
+
356
+ # 6. Update market price (demand increased)
357
+ self._update_market_price()
358
+
359
+ logger.info(f"Request {request_id[:8]}... submitted: "
360
+ f"{tokens_requested} tokens @ {current_price:.6f} NEURO/1M")
361
+
362
+ return True, request_id, current_price
363
+
364
+ except Exception as e:
365
+ logger.error(f"Error submitting request: {e}")
366
+ return False, "", self.current_price
367
+
368
+ # ========================================================================
369
+ # MARKETPLACE: REQUEST CLAIMING
370
+ # ========================================================================
371
+
372
+ def claim_request(self, node_id: str) -> Optional[InferenceRequest]:
373
+ """
374
+ Driver node claims a pending request to start distributed pipeline.
375
+
376
+ DISTRIBUTED INFERENCE:
377
+ - Only the specified driver node can claim
378
+ - Driver starts the pipeline (Layer 0)
379
+ - Workers and validators join via pipeline session
380
+
381
+ Args:
382
+ node_id: Driver node ID attempting to claim
383
+
384
+ Returns:
385
+ InferenceRequest if claim successful, None if no requests available
386
+
387
+ Security:
388
+ - Atomic operation (thread-safe)
389
+ - Driver-specific (must match driver_node_id)
390
+ - Priority-based selection
391
+ """
392
+ try:
393
+ if not node_id:
394
+ logger.warning("claim_request called with empty node_id")
395
+ return None
396
+
397
+ with self.lock:
398
+ if not self.pending_requests:
399
+ return None # No requests available
400
+
401
+ # Find requests for THIS driver only
402
+ my_requests = [
403
+ r for r in self.pending_requests.values()
404
+ if r.driver_node_id == node_id
405
+ ]
406
+
407
+ if not my_requests:
408
+ return None # No requests for this driver
409
+
410
+ # Select best request (highest priority, oldest first)
411
+ sorted_requests = sorted(
412
+ my_requests,
413
+ key=lambda r: (-r.priority, r.timestamp)
414
+ )
415
+
416
+ request = sorted_requests[0]
417
+
418
+ # Claim the request (atomic operation)
419
+ request.claimed_at = time.time()
420
+ request.claimed_by = node_id # Track who claimed it (for ledger verification)
421
+ request.status = RequestStatus.DRIVER_CLAIMED
422
+
423
+ # Generate pipeline session ID
424
+ request.pipeline_session_id = str(uuid.uuid4())
425
+
426
+ # Move from pending to claimed
427
+ self.claimed_requests[request.request_id] = request
428
+ del self.pending_requests[request.request_id]
429
+
430
+ logger.info(f"Request {request.request_id[:8]}... claimed by DRIVER {node_id[:16]}... "
431
+ f"(session {request.pipeline_session_id[:8]}...) at locked price {request.locked_price:.6f} NEURO/1M")
432
+
433
+ return request
434
+
435
+ except Exception as e:
436
+ logger.error(f"Error claiming request for node {node_id[:16] if node_id else 'unknown'}...: {e}")
437
+ return None
438
+
439
+ # ========================================================================
440
+ # MARKETPLACE: REQUEST COMPLETION
441
+ # ========================================================================
442
+
443
+ def register_proof_received(
444
+ self,
445
+ request_id: str,
446
+ node_id: str,
447
+ is_driver: bool,
448
+ is_validator: bool
449
+ ) -> Tuple[bool, str]:
450
+ """
451
+ Track that a node has submitted proof for this request (DISTRIBUTED).
452
+
453
+ In distributed inference, multiple nodes process the same request:
454
+ - Driver (Layer 0)
455
+ - Workers (Layers 1-N)
456
+ - Validator (LM Head)
457
+
458
+ Each submits their own proof. Request is complete when all proofs received.
459
+
460
+ Args:
461
+ request_id: Which request
462
+ node_id: Which node submitted proof
463
+ is_driver: Has embedding layer
464
+ is_validator: Has LM head layer
465
+
466
+ Returns:
467
+ (is_request_complete, error_message)
468
+ """
469
+ try:
470
+ # Input validation
471
+ if not request_id:
472
+ return False, "Missing request_id"
473
+ if not node_id:
474
+ return False, "Missing node_id"
475
+
476
+ with self.lock:
477
+ # Look in both claimed and completed (proof might arrive late)
478
+ request = self.claimed_requests.get(request_id) or self.completed_requests.get(request_id)
479
+
480
+ if not request:
481
+ logger.warning(f"Proof received for unknown request {request_id[:8]}...")
482
+ return False, "Request not found"
483
+
484
+ # Update proof tracking
485
+ if is_driver:
486
+ request.driver_proof_received = True
487
+ logger.debug(f"DRIVER proof received for {request_id[:8]}...")
488
+ elif is_validator:
489
+ request.validator_proof_received = True
490
+ logger.debug(f"VALIDATOR proof received for {request_id[:8]}...")
491
+ else:
492
+ # Worker
493
+ if node_id not in request.worker_proofs_received:
494
+ request.worker_proofs_received.append(node_id)
495
+ logger.debug(f"WORKER proof received for {request_id[:8]}... from {node_id[:16]}...")
496
+
497
+ # Check if all proofs received (request complete)
498
+ # Minimum: driver + validator (workers optional if single-node has all layers)
499
+ all_proofs_received = (
500
+ request.driver_proof_received and
501
+ request.validator_proof_received
502
+ )
503
+
504
+ if all_proofs_received and request.status != RequestStatus.COMPLETED:
505
+ # Mark as completed
506
+ request.status = RequestStatus.COMPLETED
507
+ request.completed_at = time.time()
508
+
509
+ # Move to completed queue
510
+ if request_id in self.claimed_requests:
511
+ self.completed_requests[request_id] = request
512
+ del self.claimed_requests[request_id]
513
+
514
+ # Update metrics
515
+ self.total_requests_served += 1
516
+
517
+ # Update market price (demand decreased)
518
+ self._update_market_price()
519
+
520
+ logger.info(f"Request {request_id[:8]}... COMPLETE: "
521
+ f"driver + {len(request.worker_proofs_received)} workers + validator")
522
+
523
+ return True, ""
524
+
525
+ return False, "" # Not yet complete
526
+
527
+ except Exception as e:
528
+ logger.error(f"Error registering proof for request {request_id[:8] if request_id else 'unknown'}...: {e}")
529
+ return False, str(e)
530
+
531
+ def get_request(self, request_id: str) -> Optional[InferenceRequest]:
532
+ """Look up a request by ID (across all queues)."""
533
+ with self.lock:
534
+ if request_id in self.pending_requests:
535
+ return self.pending_requests[request_id]
536
+ elif request_id in self.claimed_requests:
537
+ return self.claimed_requests[request_id]
538
+ elif request_id in self.completed_requests:
539
+ return self.completed_requests[request_id]
540
+ return None
541
+
542
+ def store_result(self, request_id: str, output_text: str):
543
+ """
544
+ Store inference result for user retrieval.
545
+
546
+ Results are kept for 5 minutes after completion.
547
+ """
548
+ with self.lock:
549
+ self.results[request_id] = output_text
550
+ self.result_timestamps[request_id] = time.time()
551
+ logger.info(f"Stored result for request {request_id[:8]}... ({len(output_text)} chars)")
552
+
553
+ def get_result(self, request_id: str) -> Optional[str]:
554
+ """Retrieve inference result."""
555
+ with self.lock:
556
+ return self.results.get(request_id)
557
+
558
+ def cleanup_old_results(self):
559
+ """Remove results older than 5 minutes."""
560
+ with self.lock:
561
+ now = time.time()
562
+ to_remove = [
563
+ req_id for req_id, ts in self.result_timestamps.items()
564
+ if (now - ts) > 300 # 5 minutes
565
+ ]
566
+ for req_id in to_remove:
567
+ self.results.pop(req_id, None)
568
+ self.result_timestamps.pop(req_id, None)
569
+
570
+ if to_remove:
571
+ logger.info(f"Cleaned up {len(to_remove)} old results")
572
+
573
+ # ========================================================================
574
+ # PIPELINE SESSION MANAGEMENT (Distributed Inference)
575
+ # ========================================================================
576
+
577
+ def start_pipeline_session(
578
+ self,
579
+ request_id: str,
580
+ session_id: str,
581
+ driver_node_id: str
582
+ ) -> bool:
583
+ """
584
+ Start a distributed inference pipeline session.
585
+
586
+ Called by driver node after claiming request.
587
+ Workers and validator will join dynamically via DHT.
588
+
589
+ Args:
590
+ request_id: Which request this session serves
591
+ session_id: Unique session identifier
592
+ driver_node_id: Driver node starting the pipeline
593
+
594
+ Returns:
595
+ True if session started successfully
596
+ """
597
+ with self.lock:
598
+ if session_id in self.active_sessions:
599
+ logger.warning(f"Session {session_id[:8]}... already exists")
600
+ return False
601
+
602
+ session = PipelineSession(
603
+ session_id=session_id,
604
+ request_id=request_id,
605
+ driver_node_id=driver_node_id,
606
+ started_at=time.time(),
607
+ last_activity=time.time()
608
+ )
609
+
610
+ self.active_sessions[session_id] = session
611
+
612
+ logger.info(f"Pipeline session {session_id[:8]}... started for request {request_id[:8]}...")
613
+ return True
614
+
615
+ def update_pipeline_progress(
616
+ self,
617
+ session_id: str,
618
+ current_layer: int,
619
+ tokens_generated: int = 0
620
+ ):
621
+ """Update pipeline progress (called by processing nodes)."""
622
+ with self.lock:
623
+ session = self.active_sessions.get(session_id)
624
+ if session:
625
+ session.current_layer = current_layer
626
+ session.tokens_generated = tokens_generated
627
+ session.last_activity = time.time()
628
+
629
+ def register_pipeline_participant(
630
+ self,
631
+ session_id: str,
632
+ node_id: str,
633
+ is_worker: bool = False,
634
+ is_validator: bool = False
635
+ ):
636
+ """Register a node as participant in the pipeline."""
637
+ with self.lock:
638
+ session = self.active_sessions.get(session_id)
639
+ if not session:
640
+ logger.warning(f"Session {session_id[:8]}... not found")
641
+ return
642
+
643
+ if is_worker and node_id not in session.worker_node_ids:
644
+ session.worker_node_ids.append(node_id)
645
+ logger.debug(f"Worker {node_id[:16]}... joined session {session_id[:8]}...")
646
+ elif is_validator:
647
+ session.validator_node_id = node_id
648
+ logger.debug(f"Validator {node_id[:16]}... joined session {session_id[:8]}...")
649
+
650
+ def complete_pipeline_session(self, session_id: str):
651
+ """Mark pipeline session as complete."""
652
+ with self.lock:
653
+ session = self.active_sessions.get(session_id)
654
+ if session:
655
+ self.completed_sessions[session_id] = session
656
+ del self.active_sessions[session_id]
657
+ logger.info(f"Pipeline session {session_id[:8]}... completed")
658
+
659
+ def get_session(self, session_id: str) -> Optional[PipelineSession]:
660
+ """Look up a pipeline session."""
661
+ with self.lock:
662
+ return (self.active_sessions.get(session_id) or
663
+ self.completed_sessions.get(session_id))
664
+
665
+ # ========================================================================
666
+ # DEMAND MEASUREMENT (updated for marketplace)
667
+ # ========================================================================
668
+
669
+ def get_total_demand(self) -> int:
670
+ """
671
+ Calculate total pending demand in tokens.
672
+
673
+ Returns:
674
+ Total tokens waiting to be processed (pending + claimed but incomplete)
675
+ """
676
+ # Include both pending AND claimed (still being processed)
677
+ pending_tokens = sum(r.tokens_requested for r in self.pending_requests.values())
678
+ claimed_tokens = sum(r.tokens_requested for r in self.claimed_requests.values())
679
+ return pending_tokens + claimed_tokens
680
+
681
+ # ========================================================================
682
+ # PRICE DISCOVERY
683
+ # ========================================================================
684
+
685
+ def _update_market_price(self):
686
+ """
687
+ Update market price based on PURE supply/demand (no artificial caps).
688
+
689
+ Price Formula (Pure Market):
690
+ P = base_price × (1 + utilization)^2
691
+
692
+ Where:
693
+ utilization = demand_rate / supply_rate
694
+ base_price = Starting price for worthless model (0.0001)
695
+
696
+ Natural Dynamics:
697
+ - No demand → Price ≈ 0 (stupid model = worthless)
698
+ - Low util (0.1) → Price ≈ 0.00012 (cheap, encourages usage)
699
+ - Balanced (1.0) → Price ≈ 0.0004 (fair value)
700
+ - High util (10) → Price ≈ 0.012 (scarcity premium)
701
+ - Extreme (100) → Price ≈ 1.01 (severe scarcity)
702
+
703
+ Quality Emerges Naturally:
704
+ - Stupid model → Users don't use it → Demand = 0 → Price ≈ 0
705
+ - Good model → Users want it → Demand ↑ → Price ↑
706
+ - Excellent model → Viral usage → Demand >> Supply → Price spikes
707
+
708
+ No Caps Needed:
709
+ - Market finds true value
710
+ - High price attracts supply (nodes switch from training)
711
+ - Equilibrium naturally forms where: inference_profit ≈ training_profit
712
+ """
713
+ supply = self.get_total_supply() # tokens/sec
714
+ demand = self.get_total_demand() # total tokens waiting
715
+
716
+ if supply == 0:
717
+ if demand == 0:
718
+ # No supply, no demand → Keep current price
719
+ new_price = self.current_price
720
+ else:
721
+ # No supply but there's demand → Extreme scarcity
722
+ # Price spikes to attract supply (this is CORRECT market behavior!)
723
+ new_price = self.current_price * 10.0 # Dramatic signal
724
+ else:
725
+ # Calculate demand rate (tokens/sec needed to clear queue)
726
+ # Using TARGET_RESPONSE_TIME from economics
727
+ from neuroshard.core.economics.constants import INFERENCE_MARKET_TARGET_RESPONSE_TIME
728
+ demand_rate = demand / INFERENCE_MARKET_TARGET_RESPONSE_TIME
729
+
730
+ # Utilization ratio (pure supply/demand)
731
+ utilization = demand_rate / supply
732
+
733
+ # PURE MARKET FORMULA: Price grows with utilization squared
734
+ # This creates natural equilibrium:
735
+ # - Low utilization → Cheap (encourages usage)
736
+ # - High utilization → Expensive (attracts supply)
737
+ # - Quadratic ensures rapid price response to scarcity
738
+ new_price = self.base_price * math.pow(1 + utilization, 2)
739
+
740
+ # Smooth price changes (exponential moving average)
741
+ # Higher smoothing = more stability, less volatility
742
+ self.current_price = (
743
+ self.price_smoothing * self.current_price +
744
+ (1 - self.price_smoothing) * new_price
745
+ )
746
+
747
+ # Record for analysis
748
+ self.price_history.append({
749
+ 'timestamp': time.time(),
750
+ 'price': self.current_price,
751
+ 'supply': supply,
752
+ 'demand': demand,
753
+ 'utilization': demand_rate / supply if supply > 0 else 0
754
+ })
755
+
756
+ self.price_updates += 1
757
+
758
+ def get_current_price(self) -> float:
759
+ """
760
+ Get current market price for inference.
761
+
762
+ Returns:
763
+ NEURO per 1M tokens (current market rate)
764
+ """
765
+ with self.lock:
766
+ return self.current_price
767
+
768
+ # ========================================================================
769
+ # SECURITY: TIMEOUT HANDLING
770
+ # ========================================================================
771
+
772
+ def cleanup_stale_claims(self) -> int:
773
+ """
774
+ Re-queue requests that were claimed but not completed (timeout).
775
+
776
+ This prevents malicious nodes from "squatting" on requests.
777
+ Called periodically by background task.
778
+
779
+ Returns:
780
+ Number of requests re-queued
781
+ """
782
+ with self.lock:
783
+ now = time.time()
784
+ expired_requests = []
785
+
786
+ # Find expired claims
787
+ for req_id, request in self.claimed_requests.items():
788
+ try:
789
+ # Safety check for claimed_at
790
+ if request.claimed_at is None:
791
+ logger.warning(f"Request {req_id[:8]}... has no claimed_at timestamp, marking expired")
792
+ expired_requests.append(req_id)
793
+ elif (now - request.claimed_at) > self.claim_timeout:
794
+ expired_requests.append(req_id)
795
+ except Exception as e:
796
+ logger.error(f"Error checking claim expiry for {req_id[:8]}...: {e}")
797
+
798
+ # Re-queue them
799
+ for req_id in expired_requests:
800
+ try:
801
+ request = self.claimed_requests[req_id]
802
+
803
+ # Store info before reset
804
+ squatter_node_id = request.claimed_by or "unknown"
805
+ claim_duration = (now - request.claimed_at) if request.claimed_at else 0
806
+
807
+ # Reset claim state
808
+ request.status = RequestStatus.PENDING
809
+ request.claimed_by = None
810
+ request.claimed_at = None
811
+ request.pipeline_session_id = None
812
+
813
+ # Move back to pending
814
+ self.pending_requests[req_id] = request
815
+ del self.claimed_requests[req_id]
816
+
817
+ logger.warning(f"Request {req_id[:8]}... timed out "
818
+ f"(claimed by {squatter_node_id[:16]}... for {claim_duration:.1f}s), "
819
+ f"re-queued")
820
+ except Exception as e:
821
+ logger.error(f"Error re-queuing request {req_id[:8]}...: {e}")
822
+
823
+ return len(expired_requests)
824
+
825
+ def prune_completed_requests(self, max_age_seconds: int = 3600):
826
+ """
827
+ Remove old completed requests from audit trail.
828
+
829
+ Keeps memory usage bounded while maintaining recent history.
830
+
831
+ Args:
832
+ max_age_seconds: How long to keep completed requests (default 1 hour)
833
+ """
834
+ with self.lock:
835
+ now = time.time()
836
+ to_remove = []
837
+
838
+ for req_id, request in self.completed_requests.items():
839
+ if (now - request.completed_at) > max_age_seconds:
840
+ to_remove.append(req_id)
841
+
842
+ for req_id in to_remove:
843
+ del self.completed_requests[req_id]
844
+ # Also remove from nonce set to allow reuse after long time
845
+ self.request_nonces.discard(req_id)
846
+
847
+ if to_remove:
848
+ logger.debug(f"Pruned {len(to_remove)} old completed requests")
849
+
850
+ # ========================================================================
851
+ # ANALYTICS
852
+ # ========================================================================
853
+
854
+ def get_market_stats(self) -> dict:
855
+ """
856
+ Get current marketplace statistics.
857
+
858
+ Returns:
859
+ Dict with price, supply, demand, queue sizes, etc.
860
+ """
861
+ with self.lock:
862
+ supply = self.get_total_supply()
863
+ demand = self.get_total_demand()
864
+
865
+ return {
866
+ # Price
867
+ 'current_price': round(self.current_price, 6),
868
+
869
+ # Supply & Demand
870
+ 'supply_tokens_per_sec': supply,
871
+ 'demand_tokens_waiting': demand,
872
+ 'utilization': (demand / 60) / supply if supply > 0 else 0,
873
+
874
+ # Marketplace queues
875
+ 'pending_requests': len(self.pending_requests),
876
+ 'claimed_requests': len(self.claimed_requests),
877
+ 'completed_requests': len(self.completed_requests),
878
+
879
+ # Pipeline sessions (distributed inference)
880
+ 'active_sessions': len(self.active_sessions),
881
+ 'completed_sessions': len(self.completed_sessions),
882
+ 'pending_requests': len(self.pending_requests),
883
+ 'available_nodes': len(self.available_capacity),
884
+ 'total_processed': self.total_tokens_processed,
885
+ 'total_served': self.total_requests_served,
886
+ 'avg_price_24h': self._get_avg_price(24 * 3600)
887
+ }
888
+
889
+ def _get_avg_price(self, seconds: int) -> float:
890
+ """Calculate average price over last N seconds."""
891
+ if not self.price_history:
892
+ return self.current_price
893
+
894
+ cutoff = time.time() - seconds
895
+ recent = [p['price'] for p in self.price_history if p['timestamp'] > cutoff]
896
+
897
+ return sum(recent) / len(recent) if recent else self.current_price
898
+
899
+ def get_price_chart(self, last_n: int = 100) -> List[dict]:
900
+ """Get recent price history for charting."""
901
+ with self.lock:
902
+ return list(self.price_history)[-last_n:]
903
+
904
+
905
+ # ============================================================================
906
+ # INTEGRATION WITH LEDGER
907
+ # ============================================================================
908
+
909
+ def calculate_inference_reward(
910
+ market: InferenceMarket,
911
+ tokens_processed: int
912
+ ) -> float:
913
+ """
914
+ Calculate inference reward based on current market price.
915
+
916
+ This replaces the fixed INFERENCE_REWARD_PER_MILLION.
917
+
918
+ Args:
919
+ market: InferenceMarket instance
920
+ tokens_processed: Number of tokens processed
921
+
922
+ Returns:
923
+ NEURO reward based on market rate
924
+ """
925
+ market_price = market.get_current_price()
926
+ reward = (tokens_processed / 1_000_000.0) * market_price
927
+ return reward
928
+
929
+
930
+ # ============================================================================
931
+ # EXAMPLE USAGE
932
+ # ============================================================================
933
+
934
+ if __name__ == "__main__":
935
+ # Setup logging for demo
936
+ logging.basicConfig(level=logging.INFO, format='%(message)s')
937
+
938
+ # Create market
939
+ market = InferenceMarket()
940
+
941
+ # Scenario 1: Low demand (bootstrap phase)
942
+ logger.info("\n=== Scenario 1: Low Demand ===")
943
+ market.register_capacity("node1", tokens_per_second=1000, min_price=0.01)
944
+ market.register_capacity("node2", tokens_per_second=1000, min_price=0.01)
945
+ market.submit_request("user1", "driver1", tokens_requested=10000, max_price=1.0)
946
+
947
+ stats = market.get_market_stats()
948
+ logger.info(f"Supply: {stats['supply_tokens_per_sec']} t/s")
949
+ logger.info(f"Demand: {stats['demand_tokens_waiting']} tokens")
950
+ logger.info(f"Price: {stats['current_price']:.4f} NEURO per 1M tokens")
951
+ logger.info(f"Utilization: {stats['utilization']*100:.1f}%")
952
+
953
+ # Scenario 2: High demand
954
+ logger.info("\n=== Scenario 2: High Demand ===")
955
+ for i in range(10):
956
+ market.submit_request(f"user{i+2}", "driver1",
957
+ tokens_requested=50000, max_price=1.0)
958
+
959
+ stats = market.get_market_stats()
960
+ logger.info(f"Supply: {stats['supply_tokens_per_sec']} t/s")
961
+ logger.info(f"Demand: {stats['demand_tokens_waiting']} tokens")
962
+ logger.info(f"Price: {stats['current_price']:.4f} NEURO per 1M tokens")
963
+ logger.info(f"Utilization: {stats['utilization']*100:.1f}%")
964
+
965
+ # Scenario 3: Supply increases (more nodes join)
966
+ logger.info("\n=== Scenario 3: Supply Increases ===")
967
+ for i in range(10):
968
+ market.register_capacity(f"node{i+3}", tokens_per_second=1000, min_price=0.01)
969
+
970
+ stats = market.get_market_stats()
971
+ logger.info(f"Supply: {stats['supply_tokens_per_sec']} t/s")
972
+ logger.info(f"Demand: {stats['demand_tokens_waiting']} tokens")
973
+ logger.info(f"Price: {stats['current_price']:.4f} NEURO per 1M tokens")
974
+ logger.info(f"Utilization: {stats['utilization']*100:.1f}%")
975
+