#!/usr/bin/env python3 """ Montana Protocol — Ascending Auction System Аукцион нарастания для всех сервисов Montana. Модель ценообразования: - 1-й сервис: 1 Ɉ - 2-й сервис: 2 Ɉ - 3-й сервис: 3 Ɉ - N-й сервис: N Ɉ (шаг 1 Ɉ) Применяется к: - Domains (alice@efir.org) - VPN subscriptions - Всем будущим сервисам """ import json import threading import fcntl import time from pathlib import Path from typing import Dict, Optional, List from datetime import datetime import logging log = logging.getLogger("montana_auction") # ═══════════════════════════════════════════════════════════════════════════════ # AUCTION SERVICE TYPES # ═══════════════════════════════════════════════════════════════════════════════ class ServiceType: """Типы сервисов Montana Protocol""" # Базовые сервисы DOMAIN = "domain" # alice@efir.org VPN = "vpn" # WireGuard VPN subscription STORAGE = "storage" # Future: distributed storage COMPUTE = "compute" # Future: compute credits # Телефония и коммуникации PHONE_NUMBER = "phone_number" # Виртуальный номер +montana-123456 AUDIO_SECOND = "audio_second" # Цена 1 секунды аудио звонка VIDEO_SECOND = "video_second" # Цена 1 секунды видео звонка @classmethod def all(cls) -> List[str]: return [ cls.DOMAIN, cls.VPN, cls.STORAGE, cls.COMPUTE, cls.PHONE_NUMBER, cls.AUDIO_SECOND, cls.VIDEO_SECOND ] @classmethod def communication_services(cls) -> List[str]: """Коммуникационные сервисы (телефония, звонки)""" return [cls.PHONE_NUMBER, cls.AUDIO_SECOND, cls.VIDEO_SECOND] # ═══════════════════════════════════════════════════════════════════════════════ # AUCTION COUNTER REGISTRY # ═══════════════════════════════════════════════════════════════════════════════ class AuctionRegistry: """ Реестр счетчиков аукциона для каждого типа сервиса. Консенсус между 3 узлами Montana Network. """ def __init__(self, data_dir: Path): self.data_dir = data_dir self.data_dir.mkdir(parents=True, exist_ok=True) self.auction_file = self.data_dir / "auction_counters.json" self.purchases_file = self.data_dir / "auction_purchases.json" self._lock = threading.Lock() self._ensure_files() def _ensure_files(self): """Создать файлы аукциона если не существуют""" if not self.auction_file.exists(): self._save_counters({service: 0 for service in ServiceType.all()}) if not self.purchases_file.exists(): self._save_purchases([]) def _load_counters(self) -> Dict[str, int]: """Загрузить счетчики аукциона (thread-safe)""" with open(self.auction_file, 'r') as f: fcntl.flock(f.fileno(), fcntl.LOCK_SH) try: data = json.load(f) finally: fcntl.flock(f.fileno(), fcntl.LOCK_UN) return data def _save_counters(self, counters: Dict[str, int]): """Сохранить счетчики аукциона (thread-safe)""" with open(self.auction_file, 'w') as f: fcntl.flock(f.fileno(), fcntl.LOCK_EX) try: json.dump(counters, f, indent=2) finally: fcntl.flock(f.fileno(), fcntl.LOCK_UN) def _load_purchases(self) -> List[Dict]: """Загрузить историю покупок""" with open(self.purchases_file, 'r') as f: fcntl.flock(f.fileno(), fcntl.LOCK_SH) try: data = json.load(f) finally: fcntl.flock(f.fileno(), fcntl.LOCK_UN) return data def _save_purchases(self, purchases: List[Dict]): """Сохранить историю покупок""" with open(self.purchases_file, 'w') as f: fcntl.flock(f.fileno(), fcntl.LOCK_EX) try: json.dump(purchases, f, indent=2) finally: fcntl.flock(f.fileno(), fcntl.LOCK_UN) def get_current_price(self, service_type: str) -> int: """ Получить текущую цену для следующей покупки. Args: service_type: Тип сервиса (domain, vpn, etc.) Returns: Цена в Ɉ (монетах времени) """ if service_type not in ServiceType.all(): raise ValueError(f"Unknown service type: {service_type}") with self._lock: counters = self._load_counters() count = counters.get(service_type, 0) # Следующая цена = текущий счетчик + 1 return count + 1 def get_total_sold(self, service_type: str) -> int: """ Получить количество проданных сервисов. Args: service_type: Тип сервиса Returns: Количество проданных сервисов """ if service_type not in ServiceType.all(): raise ValueError(f"Unknown service type: {service_type}") with self._lock: counters = self._load_counters() return counters.get(service_type, 0) def purchase( self, service_type: str, buyer_address: str, service_id: str, amount_paid: int ) -> Dict: """ Зарегистрировать покупку сервиса. Args: service_type: Тип сервиса (domain, vpn, etc.) buyer_address: Montana адрес покупателя service_id: Идентификатор сервиса (например, "alice" для alice@efir.org) amount_paid: Сумма оплаченная в Ɉ Returns: Dict с результатом покупки Raises: ValueError: Если цена не совпадает или недостаточно монет """ if service_type not in ServiceType.all(): raise ValueError(f"Unknown service type: {service_type}") with self._lock: counters = self._load_counters() current_count = counters.get(service_type, 0) expected_price = current_count + 1 if amount_paid < expected_price: raise ValueError( f"Insufficient payment: {amount_paid} Ɉ, expected {expected_price} Ɉ" ) # Инкремент счетчика counters[service_type] = current_count + 1 self._save_counters(counters) # Записать в историю покупок purchases = self._load_purchases() purchase = { "service_type": service_type, "service_id": service_id, "buyer_address": buyer_address, "price_paid": amount_paid, "purchase_number": current_count + 1, "timestamp": datetime.utcnow().isoformat() + "Z" } purchases.append(purchase) self._save_purchases(purchases) log.info( f"Auction purchase: {service_type} #{current_count + 1} " f"'{service_id}' by {buyer_address[:10]}... for {amount_paid} Ɉ" ) return { "success": True, "service_type": service_type, "service_id": service_id, "price_paid": amount_paid, "purchase_number": current_count + 1, "next_price": current_count + 2 } def get_purchase_history( self, service_type: Optional[str] = None, limit: int = 100 ) -> List[Dict]: """ Получить историю покупок. Args: service_type: Фильтр по типу сервиса (None = все) limit: Максимум записей Returns: Список покупок (последние первыми) """ with self._lock: purchases = self._load_purchases() if service_type: purchases = [p for p in purchases if p.get("service_type") == service_type] # Последние первыми purchases.reverse() return purchases[:limit] def get_stats(self) -> Dict: """ Получить статистику аукциона по всем сервисам. Returns: Dict со статистикой """ with self._lock: counters = self._load_counters() purchases = self._load_purchases() total_revenue = sum(p.get("price_paid", 0) for p in purchases) stats = { "total_services_sold": sum(counters.values()), "total_revenue": total_revenue, "services": {} } for service_type in ServiceType.all(): count = counters.get(service_type, 0) next_price = count + 1 service_purchases = [p for p in purchases if p.get("service_type") == service_type] revenue = sum(p.get("price_paid", 0) for p in service_purchases) stats["services"][service_type] = { "total_sold": count, "next_price": next_price, "revenue": revenue, "latest_purchase": service_purchases[-1] if service_purchases else None } return stats # ═══════════════════════════════════════════════════════════════════════════════ # GLOBAL INSTANCE # ═══════════════════════════════════════════════════════════════════════════════ _auction_registry: Optional[AuctionRegistry] = None def get_auction_registry(data_dir: Path = None) -> AuctionRegistry: """Получить глобальный экземпляр AuctionRegistry (singleton)""" global _auction_registry if _auction_registry is None: if data_dir is None: data_dir = Path(__file__).parent / "data" _auction_registry = AuctionRegistry(data_dir) return _auction_registry # ═══════════════════════════════════════════════════════════════════════════════ # DOMAIN SERVICE (Montana Name Service) # ═══════════════════════════════════════════════════════════════════════════════ class DomainService: """ Montana Name Service (MNS) — доменный слой поверх Montana Protocol. Домены вида: alice@efir.org → mt1234...5678 """ def __init__(self, data_dir: Path): self.data_dir = data_dir self.data_dir.mkdir(parents=True, exist_ok=True) self.domains_file = self.data_dir / "domains.json" self._lock = threading.Lock() self._ensure_file() def _ensure_file(self): """Создать файл доменов если не существует""" if not self.domains_file.exists(): self._save_domains({}) def _load_domains(self) -> Dict[str, Dict]: """Загрузить реестр доменов""" with open(self.domains_file, 'r') as f: fcntl.flock(f.fileno(), fcntl.LOCK_SH) try: data = json.load(f) finally: fcntl.flock(f.fileno(), fcntl.LOCK_UN) return data def _save_domains(self, domains: Dict[str, Dict]): """Сохранить реестр доменов""" with open(self.domains_file, 'w') as f: fcntl.flock(f.fileno(), fcntl.LOCK_EX) try: json.dump(domains, f, indent=2) finally: fcntl.flock(f.fileno(), fcntl.LOCK_UN) def is_available(self, domain: str) -> bool: """Проверить доступность домена""" with self._lock: domains = self._load_domains() return domain.lower() not in domains def register( self, domain: str, owner_address: str, price_paid: int ) -> Dict: """ Зарегистрировать домен через аукцион. Args: domain: Имя домена (например, "alice") owner_address: Montana адрес владельца price_paid: Цена оплаченная в Ɉ Returns: Dict с результатом регистрации """ domain = domain.lower() # Валидация домена if not self._validate_domain(domain): raise ValueError(f"Invalid domain name: {domain}") with self._lock: domains = self._load_domains() if domain in domains: raise ValueError(f"Domain already registered: {domain}@efir.org") # Регистрация через аукцион auction = get_auction_registry(self.data_dir.parent / "data") result = auction.purchase( service_type=ServiceType.DOMAIN, buyer_address=owner_address, service_id=domain, amount_paid=price_paid ) # Сохранить в реестре доменов domains[domain] = { "owner": owner_address, "registered": datetime.utcnow().isoformat() + "Z", "purchase_number": result["purchase_number"], "price_paid": price_paid } self._save_domains(domains) log.info(f"Domain registered: {domain}@efir.org → {owner_address[:10]}...") return { "success": True, "domain": f"{domain}@efir.org", "owner": owner_address, "price_paid": price_paid, "purchase_number": result["purchase_number"] } def lookup(self, domain: str) -> Optional[Dict]: """ Найти владельца домена. Args: domain: Имя домена (alice или alice@efir.org) Returns: Dict с информацией о домене или None """ # Убрать @efir.org если есть domain = domain.lower().replace("@efir.org", "") with self._lock: domains = self._load_domains() if domain in domains: info = domains[domain].copy() info["domain"] = f"{domain}@efir.org" return info return None def _validate_domain(self, domain: str) -> bool: """ Валидация имени домена. Правила: - 3-32 символа - Только a-z, 0-9, дефис - Не начинается и не заканчивается дефисом """ if not domain: return False if len(domain) < 3 or len(domain) > 32: return False if not domain[0].isalnum() or not domain[-1].isalnum(): return False if not all(c.isalnum() or c == '-' for c in domain): return False return True # ═══════════════════════════════════════════════════════════════════════════════ # GLOBAL DOMAIN SERVICE # ═══════════════════════════════════════════════════════════════════════════════ _domain_service: Optional[DomainService] = None def get_domain_service(data_dir: Path = None) -> DomainService: """Получить глобальный экземпляр DomainService (singleton)""" global _domain_service if _domain_service is None: if data_dir is None: data_dir = Path(__file__).parent / "data" _domain_service = DomainService(data_dir) return _domain_service # ═══════════════════════════════════════════════════════════════════════════════ # PHONE SERVICE — Virtual Phone Numbers # ═══════════════════════════════════════════════════════════════════════════════ class PhoneService: """ Montana Phone Service — виртуальные телефонные номера для аудио/видео звонков. Формат номера: +montana-NNNNNN (где N = sequential number) Пример: +montana-000001, +montana-000042 Возможности: - P2P аудио звонки (WebRTC) - P2P видео звонки (WebRTC) - Оплата за секунды через аукцион """ def __init__(self, data_dir: Path): self.data_dir = data_dir self.data_dir.mkdir(parents=True, exist_ok=True) self.phone_file = self.data_dir / "phone_numbers.json" self._lock = threading.Lock() self._ensure_file() def _ensure_file(self): """Создать файл номеров если не существует""" if not self.phone_file.exists(): self._save_numbers({}) def _load_numbers(self) -> Dict[str, Dict]: """Загрузить реестр номеров""" with open(self.phone_file, 'r') as f: fcntl.flock(f.fileno(), fcntl.LOCK_SH) try: data = json.load(f) finally: fcntl.flock(f.fileno(), fcntl.LOCK_UN) return data def _save_numbers(self, numbers: Dict[str, Dict]): """Сохранить реестр номеров""" with open(self.phone_file, 'w') as f: fcntl.flock(f.fileno(), fcntl.LOCK_EX) try: json.dump(numbers, f, indent=2) finally: fcntl.flock(f.fileno(), fcntl.LOCK_UN) def format_number(self, number: int) -> str: """Форматировать номер: 42 → +montana-000042""" return f"+montana-{number:06d}" def is_available(self, number: int) -> bool: """Проверить доступность номера""" with self._lock: numbers = self._load_numbers() key = self.format_number(number) return key not in numbers def register( self, number: int, owner_address: str, price_paid: int ) -> Dict: """ Зарегистрировать виртуальный номер через аукцион. Args: number: Номер (например, 42 для +montana-000042) owner_address: Montana адрес владельца price_paid: Цена оплаченная в Ɉ Returns: Dict с результатом регистрации """ formatted = self.format_number(number) with self._lock: numbers = self._load_numbers() if formatted in numbers: raise ValueError(f"Phone number already registered: {formatted}") # Регистрация через аукцион auction = get_auction_registry(self.data_dir.parent / "data") result = auction.purchase( service_type=ServiceType.PHONE_NUMBER, buyer_address=owner_address, service_id=formatted, amount_paid=price_paid ) # Сохранить в реестре номеров numbers[formatted] = { "owner": owner_address, "number": number, "registered": datetime.utcnow().isoformat() + "Z", "purchase_number": result["purchase_number"], "price_paid": price_paid, "audio_seconds_used": 0, "video_seconds_used": 0 } self._save_numbers(numbers) log.info(f"Phone number registered: {formatted} → {owner_address[:10]}...") return { "success": True, "phone_number": formatted, "owner": owner_address, "price_paid": price_paid, "purchase_number": result["purchase_number"] } def lookup(self, number: int) -> Optional[Dict]: """ Найти владельца номера. Args: number: Номер (42 или "+montana-000042") Returns: Dict с информацией о номере или None """ # Поддержка обоих форматов if isinstance(number, str) and number.startswith("+montana-"): formatted = number else: formatted = self.format_number(int(number)) with self._lock: numbers = self._load_numbers() if formatted in numbers: return numbers[formatted].copy() return None def record_call( self, phone_number: str, call_type: str, duration_seconds: int, price_per_second: int ) -> Dict: """ Записать использование звонка. Args: phone_number: Номер телефона call_type: "audio" или "video" duration_seconds: Длительность в секундах price_per_second: Цена за секунду в Ɉ Returns: Dict с информацией о звонке """ total_cost = duration_seconds * price_per_second with self._lock: numbers = self._load_numbers() if phone_number not in numbers: raise ValueError(f"Phone number not found: {phone_number}") if call_type == "audio": numbers[phone_number]["audio_seconds_used"] += duration_seconds elif call_type == "video": numbers[phone_number]["video_seconds_used"] += duration_seconds else: raise ValueError(f"Invalid call type: {call_type}") self._save_numbers(numbers) log.info( f"Call recorded: {phone_number} {call_type} {duration_seconds}s " f"({total_cost} Ɉ)" ) return { "phone_number": phone_number, "call_type": call_type, "duration_seconds": duration_seconds, "price_per_second": price_per_second, "total_cost": total_cost } # ═══════════════════════════════════════════════════════════════════════════════ # GLOBAL PHONE SERVICE # ═══════════════════════════════════════════════════════════════════════════════ _phone_service: Optional[PhoneService] = None def get_phone_service(data_dir: Path = None) -> PhoneService: """Получить глобальный экземпляр PhoneService (singleton)""" global _phone_service if _phone_service is None: if data_dir is None: data_dir = Path(__file__).parent / "data" _phone_service = PhoneService(data_dir) return _phone_service # ═══════════════════════════════════════════════════════════════════════════════ # CALL PRICING — Audio & Video Second Pricing # ═══════════════════════════════════════════════════════════════════════════════ class CallPricingService: """ Аукционная модель цены за секунду аудио/видео звонков. Принцип: N-я секунда всех звонков стоит N Ɉ - 1-я секунда аудио в истории Montana = 1 Ɉ - 2-я секунда аудио = 2 Ɉ - N-я секунда аудио = N Ɉ Аналогично для видео (отдельный счетчик). """ def __init__(self, data_dir: Path): self.data_dir = data_dir self.auction = get_auction_registry(data_dir) def get_current_audio_price(self) -> int: """Получить текущую цену за 1 секунду аудио""" return self.auction.get_current_price(ServiceType.AUDIO_SECOND) def get_current_video_price(self) -> int: """Получить текущую цену за 1 секунду видео""" return self.auction.get_current_price(ServiceType.VIDEO_SECOND) def calculate_audio_call_cost(self, duration_seconds: int) -> int: """ Рассчитать стоимость аудио звонка. Args: duration_seconds: Длительность звонка в секундах Returns: Общая стоимость в Ɉ """ base_price = self.get_current_audio_price() # Формула: сумма арифметической прогрессии # cost = base_price + (base_price+1) + ... + (base_price+duration-1) # = duration * base_price + (0+1+2+...+(duration-1)) # = duration * base_price + duration*(duration-1)/2 cost = duration_seconds * base_price + (duration_seconds * (duration_seconds - 1)) // 2 return cost def calculate_video_call_cost(self, duration_seconds: int) -> int: """ Рассчитать стоимость видео звонка. Args: duration_seconds: Длительность звонка в секундах Returns: Общая стоимость в Ɉ """ base_price = self.get_current_video_price() cost = duration_seconds * base_price + (duration_seconds * (duration_seconds - 1)) // 2 return cost def register_audio_seconds(self, seconds: int, caller_address: str) -> Dict: """ Зарегистрировать использование N секунд аудио. Args: seconds: Количество секунд caller_address: Montana адрес звонящего Returns: Dict с результатом регистрации """ total_cost = self.calculate_audio_call_cost(seconds) # Зарегистрировать каждую секунду в аукционе for i in range(seconds): current_price = self.auction.get_current_price(ServiceType.AUDIO_SECOND) self.auction.purchase( service_type=ServiceType.AUDIO_SECOND, buyer_address=caller_address, service_id=f"audio_{self.auction.get_total_sold(ServiceType.AUDIO_SECOND) + 1}", amount_paid=current_price ) return { "service_type": "audio", "seconds": seconds, "total_cost": total_cost, "caller": caller_address } def register_video_seconds(self, seconds: int, caller_address: str) -> Dict: """ Зарегистрировать использование N секунд видео. Args: seconds: Количество секунд caller_address: Montana адрес звонящего Returns: Dict с результатом регистрации """ total_cost = self.calculate_video_call_cost(seconds) # Зарегистрировать каждую секунду в аукционе for i in range(seconds): current_price = self.auction.get_current_price(ServiceType.VIDEO_SECOND) self.auction.purchase( service_type=ServiceType.VIDEO_SECOND, buyer_address=caller_address, service_id=f"video_{self.auction.get_total_sold(ServiceType.VIDEO_SECOND) + 1}", amount_paid=current_price ) return { "service_type": "video", "seconds": seconds, "total_cost": total_cost, "caller": caller_address } # ═══════════════════════════════════════════════════════════════════════════════ # GLOBAL CALL PRICING SERVICE # ═══════════════════════════════════════════════════════════════════════════════ _call_pricing_service: Optional[CallPricingService] = None def get_call_pricing_service(data_dir: Path = None) -> CallPricingService: """Получить глобальный экземпляр CallPricingService (singleton)""" global _call_pricing_service if _call_pricing_service is None: if data_dir is None: data_dir = Path(__file__).parent / "data" _call_pricing_service = CallPricingService(data_dir) return _call_pricing_service