Fix coordinator CORS function name and add marketplace matching endpoints with miner poll improvements
Some checks failed
Coverage Phase 1 (70% Target) / test-coverage-70 (push) Has been cancelled
Coverage Phase 2 (85% Target) / test-coverage-85 (push) Has been cancelled
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Integration Tests / test-service-integration (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Systemd Sync / sync-systemd (push) Has been cancelled
Some checks failed
Coverage Phase 1 (70% Target) / test-coverage-70 (push) Has been cancelled
Coverage Phase 2 (85% Target) / test-coverage-85 (push) Has been cancelled
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Integration Tests / test-service-integration (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Systemd Sync / sync-systemd (push) Has been cancelled
This commit is contained in:
@@ -21,7 +21,7 @@ def create_app() -> FastAPI:
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=_validated_cors_origins(settings.cors_origins),
|
||||
allow_origins=validated_cors_origins(settings.cors_origins),
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
|
||||
@@ -24,6 +24,7 @@ from aitbc import (
|
||||
|
||||
from .storage import init_db, get_session
|
||||
from .services.marketplace_service import MarketplaceService
|
||||
from .services.matching_service import MatchingService
|
||||
|
||||
# Configure structured logging
|
||||
configure_logging(level="INFO")
|
||||
@@ -122,6 +123,11 @@ async def get_marketplace_service(session: AsyncSession = Depends(get_session))
|
||||
return MarketplaceService(session)
|
||||
|
||||
|
||||
async def get_matching_service(session: AsyncSession = Depends(get_session)) -> MatchingService:
|
||||
"""Get matching service instance"""
|
||||
return MatchingService(session)
|
||||
|
||||
|
||||
@app.get("/v1/marketplace/offers")
|
||||
async def get_offers(
|
||||
status: str | None = None,
|
||||
@@ -256,6 +262,82 @@ async def get_analytics(
|
||||
return await svc.get_analytics(period_type=period_type)
|
||||
|
||||
|
||||
@app.post("/v1/marketplace/match")
|
||||
async def find_match(
|
||||
bid_requirements: dict,
|
||||
max_price: float | None = None,
|
||||
preferred_region: str | None = None,
|
||||
min_gpu_memory: int | None = None,
|
||||
required_gpu_model: str | None = None,
|
||||
matching_svc: MatchingService = Depends(get_matching_service),
|
||||
):
|
||||
"""Find best matching offer for bid requirements"""
|
||||
try:
|
||||
logger.info(f"POST /v1/marketplace/match called with requirements: {bid_requirements.keys()}")
|
||||
result = await matching_svc.find_best_match(
|
||||
bid_requirements=bid_requirements,
|
||||
max_price=max_price,
|
||||
preferred_region=preferred_region,
|
||||
min_gpu_memory=min_gpu_memory,
|
||||
required_gpu_model=required_gpu_model
|
||||
)
|
||||
logger.info(f"Match result: {result is not None}")
|
||||
return result or {"message": "No matching offer found"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error in POST /v1/marketplace/match: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@app.post("/v1/marketplace/matches")
|
||||
async def create_match(
|
||||
bid_id: str,
|
||||
offer_id: str,
|
||||
match_data: dict,
|
||||
matching_svc: MatchingService = Depends(get_matching_service),
|
||||
):
|
||||
"""Create a match between a bid and an offer"""
|
||||
try:
|
||||
logger.info(f"POST /v1/marketplace/matches called: bid_id={bid_id}, offer_id={offer_id}")
|
||||
result = await matching_svc.create_match(bid_id, offer_id, match_data)
|
||||
logger.info(f"Created match: {result['match_id']}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error in POST /v1/marketplace/matches: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@app.get("/v1/marketplace/matches")
|
||||
async def list_matches(
|
||||
status: str | None = None,
|
||||
provider: str | None = None,
|
||||
matching_svc: MatchingService = Depends(get_matching_service),
|
||||
):
|
||||
"""List all matches"""
|
||||
try:
|
||||
logger.info(f"GET /v1/marketplace/matches called with filters: status={status}, provider={provider}")
|
||||
result = await matching_svc.list_matches(status=status, provider=provider)
|
||||
logger.info(f"Found {len(result)} matches")
|
||||
return {"matches": result}
|
||||
except Exception as e:
|
||||
logger.error(f"Error in GET /v1/marketplace/matches: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@app.post("/v1/marketplace/matches/auto")
|
||||
async def auto_match(
|
||||
matching_svc: MatchingService = Depends(get_matching_service),
|
||||
):
|
||||
"""Automatically match all pending bids with available offers"""
|
||||
try:
|
||||
logger.info("POST /v1/marketplace/matches/auto called")
|
||||
result = await matching_svc.auto_match_pending_bids()
|
||||
logger.info(f"Auto-match complete: {result}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error in POST /v1/marketplace/matches/auto: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@app.get("/v1/marketplace/plugins")
|
||||
async def get_plugins(
|
||||
type: str | None = None,
|
||||
|
||||
@@ -3,5 +3,6 @@ Marketplace Service services
|
||||
"""
|
||||
|
||||
from .marketplace_service import MarketplaceService
|
||||
from .matching_service import MatchingService
|
||||
|
||||
__all__ = ["MarketplaceService"]
|
||||
__all__ = ["MarketplaceService", "MatchingService"]
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
Marketplace matching service for matching GPU providers with consumers
|
||||
"""
|
||||
|
||||
from typing import Any, Optional
|
||||
from datetime import datetime, timezone
|
||||
from sqlmodel import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from aitbc import get_logger
|
||||
from ..domain.marketplace import MarketplaceOffer, MarketplaceBid
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MatchingService:
|
||||
"""Service for matching GPU offers with consumer bids"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def find_best_match(
|
||||
self,
|
||||
bid_requirements: dict,
|
||||
max_price: Optional[float] = None,
|
||||
preferred_region: Optional[str] = None,
|
||||
min_gpu_memory: Optional[int] = None,
|
||||
required_gpu_model: Optional[str] = None
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
Find the best matching offer for a bid based on requirements
|
||||
|
||||
Args:
|
||||
bid_requirements: Bid requirements (capacity, duration, etc.)
|
||||
max_price: Maximum price per hour willing to pay
|
||||
preferred_region: Preferred region for GPU location
|
||||
min_gpu_memory: Minimum GPU memory in GB
|
||||
required_gpu_model: Specific GPU model required
|
||||
|
||||
Returns:
|
||||
Best matching offer as dict, or None if no match found
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Finding best match for bid requirements: {bid_requirements.keys()}")
|
||||
|
||||
# Build query for available offers
|
||||
stmt = select(MarketplaceOffer).where(MarketplaceOffer.status == "active")
|
||||
|
||||
# Apply filters
|
||||
if max_price:
|
||||
stmt = stmt.where(MarketplaceOffer.price_per_hour <= max_price)
|
||||
|
||||
if preferred_region:
|
||||
stmt = stmt.where(MarketplaceOffer.region == preferred_region)
|
||||
|
||||
if min_gpu_memory:
|
||||
stmt = stmt.where(MarketplaceOffer.gpu_memory_gb >= min_gpu_memory)
|
||||
|
||||
if required_gpu_model:
|
||||
stmt = stmt.where(MarketplaceOffer.gpu_model == required_gpu_model)
|
||||
|
||||
# Order by price (lowest first) and capacity (highest first)
|
||||
stmt = stmt.order_by(
|
||||
MarketplaceOffer.price_per_hour.asc(),
|
||||
MarketplaceOffer.capacity.desc()
|
||||
)
|
||||
|
||||
result = await self.session.execute(stmt)
|
||||
offers = result.scalars().all()
|
||||
|
||||
if not offers:
|
||||
logger.info("No matching offers found")
|
||||
return None
|
||||
|
||||
# Return best match (first result due to ordering)
|
||||
best_offer = offers[0]
|
||||
logger.info(f"Found best match: offer_id={best_offer.id}, price={best_offer.price_per_hour}")
|
||||
|
||||
return {
|
||||
'id': best_offer.id,
|
||||
'provider': best_offer.provider,
|
||||
'capacity': best_offer.capacity,
|
||||
'price': best_offer.price,
|
||||
'price_per_hour': best_offer.price_per_hour,
|
||||
'gpu_model': best_offer.gpu_model,
|
||||
'gpu_memory_gb': best_offer.gpu_memory_gb,
|
||||
'gpu_count': best_offer.gpu_count,
|
||||
'region': best_offer.region,
|
||||
'match_score': self._calculate_match_score(best_offer, bid_requirements),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in find_best_match: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
|
||||
def _calculate_match_score(self, offer: MarketplaceOffer, requirements: dict) -> float:
|
||||
"""
|
||||
Calculate a match score for an offer based on requirements
|
||||
|
||||
Args:
|
||||
offer: Marketplace offer
|
||||
requirements: Bid requirements
|
||||
|
||||
Returns:
|
||||
Match score between 0.0 and 1.0
|
||||
"""
|
||||
score = 1.0
|
||||
|
||||
# Penalize higher prices
|
||||
if requirements.get('max_price'):
|
||||
price_ratio = offer.price_per_hour / requirements['max_price']
|
||||
score *= (1.0 - price_ratio * 0.3) # Price contributes 30% to score
|
||||
|
||||
# Bonus for higher capacity
|
||||
if requirements.get('capacity'):
|
||||
capacity_ratio = min(offer.capacity / requirements['capacity'], 2.0)
|
||||
score *= (0.7 + capacity_ratio * 0.15) # Capacity contributes 15%
|
||||
|
||||
# Bonus for region match
|
||||
if requirements.get('preferred_region') and offer.region == requirements['preferred_region']:
|
||||
score *= 1.1 # 10% bonus for region match
|
||||
|
||||
return min(max(score, 0.0), 1.0)
|
||||
|
||||
async def create_match(
|
||||
self,
|
||||
bid_id: str,
|
||||
offer_id: str,
|
||||
match_data: dict
|
||||
) -> dict:
|
||||
"""
|
||||
Create a match between a bid and an offer
|
||||
|
||||
Args:
|
||||
bid_id: ID of the bid
|
||||
offer_id: ID of the offer
|
||||
match_data: Additional match data
|
||||
|
||||
Returns:
|
||||
Match record as dict
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Creating match: bid_id={bid_id}, offer_id={offer_id}")
|
||||
|
||||
# Get bid and offer
|
||||
bid_stmt = select(MarketplaceBid).where(MarketplaceBid.id == bid_id)
|
||||
bid_result = await self.session.execute(bid_stmt)
|
||||
bid = bid_result.scalar_one_or_none()
|
||||
|
||||
if not bid:
|
||||
raise ValueError(f"Bid not found: {bid_id}")
|
||||
|
||||
offer_stmt = select(MarketplaceOffer).where(MarketplaceOffer.id == offer_id)
|
||||
offer_result = await self.session.execute(offer_stmt)
|
||||
offer = offer_result.scalar_one_or_none()
|
||||
|
||||
if not offer:
|
||||
raise ValueError(f"Offer not found: {offer_id}")
|
||||
|
||||
# Update bid status to matched
|
||||
bid.status = "matched"
|
||||
bid.notes = f"Matched with offer {offer_id}"
|
||||
|
||||
# Update offer status if capacity is fully utilized
|
||||
if offer.capacity <= bid.capacity:
|
||||
offer.status = "booked"
|
||||
else:
|
||||
offer.capacity -= bid.capacity
|
||||
|
||||
await self.session.commit()
|
||||
|
||||
match_record = {
|
||||
'match_id': f"match_{bid_id[:8]}_{offer_id[:8]}",
|
||||
'bid_id': bid_id,
|
||||
'offer_id': offer_id,
|
||||
'provider': offer.provider,
|
||||
'consumer': bid.provider,
|
||||
'price_per_hour': offer.price_per_hour,
|
||||
'capacity': bid.capacity,
|
||||
'status': 'active',
|
||||
'created_at': datetime.now(timezone.utc).isoformat(),
|
||||
'match_score': match_data.get('match_score', 0.0),
|
||||
}
|
||||
|
||||
logger.info(f"Created match: {match_record['match_id']}")
|
||||
return match_record
|
||||
|
||||
except Exception as e:
|
||||
await self.session.rollback()
|
||||
logger.error(f"Error in create_match: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
|
||||
async def list_matches(
|
||||
self,
|
||||
status: Optional[str] = None,
|
||||
provider: Optional[str] = None
|
||||
) -> list[dict]:
|
||||
"""
|
||||
List all matches (derived from matched bids)
|
||||
|
||||
Args:
|
||||
status: Filter by match status
|
||||
provider: Filter by provider
|
||||
|
||||
Returns:
|
||||
List of match records
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Listing matches with filters: status={status}, provider={provider}")
|
||||
|
||||
# Query matched bids
|
||||
stmt = select(MarketplaceBid).where(MarketplaceBid.status == "matched")
|
||||
|
||||
if provider:
|
||||
stmt = stmt.where(MarketplaceBid.provider == provider)
|
||||
|
||||
result = await self.session.execute(stmt)
|
||||
bids = result.scalars().all()
|
||||
|
||||
matches = []
|
||||
for bid in bids:
|
||||
# Extract offer_id from notes
|
||||
offer_id = None
|
||||
if bid.notes and "Matched with offer" in bid.notes:
|
||||
offer_id = bid.notes.split()[-1]
|
||||
|
||||
matches.append({
|
||||
'bid_id': bid.id,
|
||||
'offer_id': offer_id,
|
||||
'provider': bid.provider,
|
||||
'capacity': bid.capacity,
|
||||
'price': bid.price,
|
||||
'status': bid.status,
|
||||
'created_at': bid.submitted_at.isoformat() if bid.submitted_at else None,
|
||||
})
|
||||
|
||||
logger.info(f"Found {len(matches)} matches")
|
||||
return matches
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in list_matches: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
|
||||
async def auto_match_pending_bids(self) -> dict:
|
||||
"""
|
||||
Automatically match all pending bids with available offers
|
||||
|
||||
Returns:
|
||||
Summary of matching results
|
||||
"""
|
||||
try:
|
||||
logger.info("Starting auto-match for pending bids")
|
||||
|
||||
# Get all pending bids
|
||||
stmt = select(MarketplaceBid).where(MarketplaceBid.status == "pending")
|
||||
result = await self.session.execute(stmt)
|
||||
pending_bids = result.scalars().all()
|
||||
|
||||
matched_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for bid in pending_bids:
|
||||
try:
|
||||
# Find best match
|
||||
match = await self.find_best_match(
|
||||
bid_requirements={
|
||||
'capacity': bid.capacity,
|
||||
'max_price': bid.price,
|
||||
}
|
||||
)
|
||||
|
||||
if match:
|
||||
# Create the match
|
||||
await self.create_match(
|
||||
bid_id=bid.id,
|
||||
offer_id=match['id'],
|
||||
match_data=match
|
||||
)
|
||||
matched_count += 1
|
||||
else:
|
||||
failed_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to match bid {bid.id}: {e}")
|
||||
failed_count += 1
|
||||
|
||||
summary = {
|
||||
'total_pending': len(pending_bids),
|
||||
'matched': matched_count,
|
||||
'failed': failed_count,
|
||||
'timestamp': datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
logger.info(f"Auto-match complete: {summary}")
|
||||
return summary
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in auto_match_pending_bids: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
@@ -8,6 +8,7 @@ import time
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Optional
|
||||
|
||||
@@ -188,12 +189,13 @@ def register_miner():
|
||||
|
||||
headers = {
|
||||
"X-Api-Key": AUTH_TOKEN,
|
||||
"X-Miner-ID": MINER_ID,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
client = AITBCHTTPClient(base_url=COORDINATOR_URL, headers=headers, timeout=10)
|
||||
response = client.post(f"/v1/miners/register?miner_id={MINER_ID}", json=register_data)
|
||||
response = client.post("/v1/miners/register", json=register_data)
|
||||
|
||||
if response:
|
||||
logger.info(f"Successfully registered miner: {response}")
|
||||
@@ -239,12 +241,13 @@ def send_heartbeat():
|
||||
|
||||
headers = {
|
||||
"X-Api-Key": AUTH_TOKEN,
|
||||
"X-Miner-ID": MINER_ID,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
client = AITBCHTTPClient(base_url=COORDINATOR_URL, headers=headers, timeout=5)
|
||||
response = client.post(f"/v1/miners/heartbeat?miner_id={MINER_ID}", json=heartbeat_data)
|
||||
response = client.post("/v1/miners/heartbeat", json=heartbeat_data)
|
||||
|
||||
if response:
|
||||
logger.info(f"Heartbeat sent (GPU: {gpu_info['utilization'] if gpu_info else 'N/A'}%)")
|
||||
@@ -374,21 +377,36 @@ def poll_for_jobs():
|
||||
|
||||
headers = {
|
||||
"X-Api-Key": AUTH_TOKEN,
|
||||
"X-Miner-ID": MINER_ID,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
client = AITBCHTTPClient(base_url=COORDINATOR_URL, headers=headers, timeout=10)
|
||||
response = client.post("/v1/miners/poll", json=poll_data)
|
||||
|
||||
if response:
|
||||
job = response
|
||||
# Use requests directly to handle 204 No Content properly
|
||||
import requests
|
||||
url = f"{COORDINATOR_URL}/v1/miners/poll"
|
||||
response = requests.post(url, json=poll_data, headers=headers, timeout=10)
|
||||
|
||||
if response.status_code == 204:
|
||||
# No jobs available
|
||||
return None
|
||||
|
||||
response.raise_for_status()
|
||||
job = response.json()
|
||||
|
||||
if job and job.get("job_id"):
|
||||
logger.info(f"Received job: {job}")
|
||||
return job
|
||||
else:
|
||||
return None
|
||||
|
||||
except NetworkError as e:
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 204:
|
||||
logger.debug("No jobs available (204 No Content)")
|
||||
return None
|
||||
logger.error(f"HTTP error polling for jobs: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error polling for jobs: {e}")
|
||||
return None
|
||||
|
||||
@@ -399,18 +417,23 @@ def main():
|
||||
# Check GPU availability
|
||||
gpu_info = get_gpu_info()
|
||||
if not gpu_info:
|
||||
logger.error("GPU not available, exiting")
|
||||
# sys.exit(1)
|
||||
|
||||
logger.info(f"GPU detected: {gpu_info['name']} ({gpu_info['memory_total']}MB)")
|
||||
logger.warning("GPU not available, running in CPU-only mode")
|
||||
gpu_info = {
|
||||
"name": "CPU-Only",
|
||||
"memory_total": 0,
|
||||
"memory_used": 0,
|
||||
"utilization": 0
|
||||
}
|
||||
else:
|
||||
logger.info(f"GPU detected: {gpu_info['name']} ({gpu_info['memory_total']}MB)")
|
||||
|
||||
# Check Ollama
|
||||
ollama_available, models = check_ollama()
|
||||
if not ollama_available:
|
||||
logger.error("Ollama not available - please install and start Ollama")
|
||||
# sys.exit(1)
|
||||
|
||||
logger.info(f"Ollama models available: {', '.join(models)}")
|
||||
logger.warning("Ollama not available - miner will not be able to execute inference jobs")
|
||||
models = []
|
||||
else:
|
||||
logger.info(f"Ollama models available: {', '.join(models)}")
|
||||
|
||||
# Wait for coordinator
|
||||
if not wait_for_coordinator():
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"0x1234567890123456789012345678901234567890": {
|
||||
"private_key_pem": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCc0VfRmLKclWkN\nYO5NCf7MT4ss8JMkRKMmpwUhEN/BnDyCGxgAo+po88KCHoHLm5JiWIDLTALnQkgQ\nXXL8DpCaXwMtBICzbq/zfDye7L114+lcm3CXSRhmELmLV+zo29PiF5UNV52m4ZfV\n3O1ophFDQ0XnGFoo1eG1eflzdMYCOSoxjrV/Z0ltloc3P+O1wSXcttNw4DQ9fqnp\nDRTjZWKyVSz2dI6vlnObJszfntykzWmJ2YU1TtFdh810SU3LT1fV72icbWm1KjF5\nO7T2++Wl+JJ13D8cIBiwVruWuCMno8D5YfKC35uRS8Ob4wmMDQEnQDACEKXmR4B5\neQ4SEtZ9AgMBAAECggEABj2OcRjSgsivVYj18rrjGN5Re4hXUqook/Exkw9I2DuP\nbN4HJn9fZK3On773C1M1kBRVi8GKnAlXNM+DM+SgfIQrbC8xr/JHrjjTcL+bCoX3\nU2gcIukVv3oK6DCnjNyyodyuYcmKzIlNsYUJLZDuPu7+aSPe8qEQSliARMfw2UW9\nJWQMLfiyvC4NDh6Wem4Cl+LmiKMtx1DNYe9rgSSt30XyeGopNGaxiMStOifKIo4g\nweqjIQXxyIgmkCGrIPC4NUVIltHFzp9YrNpxdsbR3Il4ycrLPVuwErF4y+/iJfj4\ncNSxGPkHZzfAOmfYEgGKfU+l7Z6pPsf3oiqTNgsWAQKBgQDK4/vXeUU6rozPYpSY\nSsymL9uE8Kw6Ves3sIr8XO3KisXaWrov33RTgrDV8hwc3Z3yDFz1vgHUVlo2sJoY\nl/8mjD+RHsAyQNIU8IQsxUSLo3HftmEUa0xz9LQuHgF1CXFsOIBlXGeaVFsyp40Q\nYzL9wjr9ooXreF0R5Np4I69taQKBgQDF3fKETeZOqPkbJsP43SowkZ41z1kKJomP\nyuImmt3G4hSmAiUCSqV4GWvz5rxwPCpuD//91KSp8DcZ4LeGnPKhx5PIKv2lWrWg\n1V4s+4Ea27hu5NKfrpOAxOgZgUjt748mPs6D9Mmi+w8/EvRbIzJO5M964wSOtHi4\n+OsDzyT59QKBgFPotd8HaHo8dj/OpWXWiYyxfjgc0R3PKth9Sv3T8QQzIGCN5TKn\nV5SyGDBjUP0fKpNQSaHYUyleDTFRGGnTctKebiu2bAZciIXgcsmRTCf0EMRUyRGI\nzrWmHl50SmX84cvAElnZPX+2I4Fvigec/xmzmnILJRedT+B2pWPKXmMBAoGAU7+k\ndWFveJ3Gijp3Oi+KOvJ3j3kKy+QR133dCNAFzLdGXBmORpEHxnSkH6Dq42pj3yAA\njxRg+djFybs2ktB9VgJeR5wCreld9Qw6hzmQpKiZQL6zc4j1v8wYHSt+jc8WvO5a\nhLmoWsZ+5oiESsrz8TahpvbNqAU1D72z43Hayb0CgYA8N+FkGN+FAAQ4Pahvgmb8\njeqHUUPRbbq2tvCknB0VBuU5WOstRFjJWADMDFevHvmx/32yhadQP8C2eS0OQOkW\n6uqIPrLfwfQNyQWtNi+q5+par23ITDjv1RlptgDcOT1IVoo4D9EpaLhzBFHYONYU\nakhZt0S6Rf0IVxot+q+oOg==\n-----END PRIVATE KEY-----\n",
|
||||
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNFX0ZiynJVpDWDuTQn+\nzE+LLPCTJESjJqcFIRDfwZw8ghsYAKPqaPPCgh6By5uSYliAy0wC50JIEF1y/A6Q\nml8DLQSAs26v83w8nuy9dePpXJtwl0kYZhC5i1fs6NvT4heVDVedpuGX1dztaKYR\nQ0NF5xhaKNXhtXn5c3TGAjkqMY61f2dJbZaHNz/jtcEl3LbTcOA0PX6p6Q0U42Vi\nslUs9nSOr5ZzmybM357cpM1pidmFNU7RXYfNdElNy09X1e9onG1ptSoxeTu09vvl\npfiSddw/HCAYsFa7lrgjJ6PA+WHygt+bkUvDm+MJjA0BJ0AwAhCl5keAeXkOEhLW\nfQIDAQAB\n-----END PUBLIC KEY-----\n",
|
||||
"created_at": 1776106994.919443,
|
||||
"last_rotated": 1776106994.9196188
|
||||
"private_key_pem": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDHN6OklsaBWneN\njGkYKG9/r/vRu4abdX3ahykmPd5I6QnXxobl85QUPKsMqPu6Rx+5l9KPW85kpXBm\n/QwZOxgukFqd3KZEoZ3yy0IHSIpv6vmr9mBBkLKyT2sdYS1gWuJ3F6v7leLmoaU5\nIDsFcMDn+SctOZI32Zf9xF4VkMJDEhx05xdIbumvIpSoj4MXyXQZLfsHyoOWrH5k\nOKYL/t/6gxeBfxy+Yw9AvkldLrqTnbx9x6ewvV+f9VciKT0h56BX/197KLCMlmZo\nTB5QpdUpFEnFwAQcC81Yzs0t77LrWoFgpAoYPU0lWHvr250+h6HGgFCPkjcNtjkJ\ndtvtnk5PAgMBAAECggEASVyd8JBtjVkJSaD5WqIZXUYrT7LlAP6lWAIKD0EdSHA4\n5bMAHlIyp7knrEPWX2SttCTKr2w5dyrNV7+74ta2Mv+JvzRwLjnt9mkPaas2/7vi\nMYdLLxngFHXWlj0g/qi5WO3osX8izZedRoot8fTxtPs1iBv5UoPYyuSzWPGz+Ape\nonX9I9/ILdPFiW2ruq1PB8anmplIZZ2YbaijLXnZBpx30HEssntrHGCfq3Q1hgU1\nd+LEtL9KA7T9H7hRmp82E3DZGJIzjuHVJO4Wcdb5f4U3xj28QISA7maqn5dA9NT+\nDRz+33tjD52o/V0ymIKo3u3WtNMLhwAl1AhY1Z6yQQKBgQDyBldIhulDNAExDrdg\nH9yZd6ZnavpAbdr+9Kdpra43Hwa1QCpqdsYbpiIg5vBxBCn749AIhWEHpAzO7puP\ns61sa7IiISU6OVlRCpzqqSrcY+GpFCIyMXoHZwQR90g7jghbFMRm1g53sBjmZYli\nl1lDOzKYiz2SNZa/MfCuUmXXHwKBgQDSuIKdwEK3f9yjbH7J106SqJj1SzYwdaij\nMNhGjORfnmRy66IpPDgvaS6gBzS1hT1ympnoykgN3q9OgrZKOETuHoHuhfT+Bnfp\nfEjN3bDHnDP4wzV1cBvuLfYrI/cUdbfzUN6Dc81CxIVPnFyKO7F2b7zo7nCEPIlU\nVQIgpPaS0QKBgQCT+fiH6aTZaASKgBrydMimNJfTh372wbQySlfJr11jal7plw/Y\nBELgSNV5FHpSP1+EGSfq7dIDn/QM2arXU95m+fnyEB342XOYr0p912zTT2Z7wEmg\nMswPlpbQfUb20sKdHbdvwNUbrNmslMxJMYxsJNesmQXOTWGcCObFTq/htQKBgQDR\nJtR2cbOG4UGFcBX0j2Fszi1sIyf5N3+X4s54UDYI9nUrX9iH5z65SERAEIbvuP1B\nuFQVrFmScro8Sh9XUbyRQPSkZI/EZ3Uz6el1dJqXteIcAt4X35vJcBNLxJnk0+cu\nedEyVomgwOC1ITT0+8TsEoJGDQzfJBsG+o1vC222UQKBgQCJwmf8+RIqKFP3Lwcz\nQ96diGn+QqvWakrlaWZ7sPZamjytpLBFPmApUX1Q5+m7XqwXzWncqZReOJL+3VUV\nOCKJw5z79e8wWDjAtgLbFzxBMX/o6BoECUa7Sd0l8XAzBawPRog0s+qyXjiY2Ksk\nMKR4nzxr6Gns6ID35/tIBSZojw==\n-----END PRIVATE KEY-----\n",
|
||||
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxzejpJbGgVp3jYxpGChv\nf6/70buGm3V92ocpJj3eSOkJ18aG5fOUFDyrDKj7ukcfuZfSj1vOZKVwZv0MGTsY\nLpBandymRKGd8stCB0iKb+r5q/ZgQZCysk9rHWEtYFridxer+5Xi5qGlOSA7BXDA\n5/knLTmSN9mX/cReFZDCQxIcdOcXSG7pryKUqI+DF8l0GS37B8qDlqx+ZDimC/7f\n+oMXgX8cvmMPQL5JXS66k528fcensL1fn/VXIik9IeegV/9feyiwjJZmaEweUKXV\nKRRJxcAEHAvNWM7NLe+y61qBYKQKGD1NJVh769udPoehxoBQj5I3DbY5CXbb7Z5O\nTwIDAQAB\n-----END PUBLIC KEY-----\n",
|
||||
"created_at": 1779874874.5963404,
|
||||
"last_rotated": 1779874874.596439
|
||||
}
|
||||
}
|
||||
21
systemd/aitbc-production-miner.service
Normal file
21
systemd/aitbc-production-miner.service
Normal file
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=AITBC Production GPU Miner
|
||||
After=network.target ollama.service aitbc-coordinator-api.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/aitbc/apps/miner
|
||||
Environment="PATH=/opt/aitbc/venv/bin"
|
||||
Environment="PYTHONPATH=/opt/aitbc"
|
||||
Environment="COORDINATOR_URL=http://localhost:8011"
|
||||
Environment="MINER_ID=aitbc-miner-1"
|
||||
Environment="AUTH_TOKEN=aitbc-miner-token-secure"
|
||||
ExecStart=/opt/aitbc/venv/bin/python /opt/aitbc/apps/miner/production_miner.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user