991 lines
41 KiB
Python
991 lines
41 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
TIME_BANK v3.0 — Протокол начисления монет времени Montana
|
||
===========================================================
|
||
|
||
ЭМИССИЯ:
|
||
- Динамическая: зависит от количества участников
|
||
- 1 секунда присутствия = 1 Ɉ × halving_coefficient
|
||
- Халвинг каждые τ₄ (4 года)
|
||
|
||
РЕЗЕРВ TIME_BANK:
|
||
- 21,000,000 минут (~40 лет)
|
||
- Банк тратит 10 мин/T2, подтверждая время
|
||
- После исчерпания → Oracle Mode
|
||
|
||
Привязка: Montana address
|
||
База данных: SQLite (montana.db)
|
||
"""
|
||
|
||
import time
|
||
import threading
|
||
import hashlib
|
||
from datetime import datetime, timezone
|
||
from typing import Dict, Optional, Any, List, Tuple
|
||
import logging
|
||
|
||
from montana_db import get_db, MontanaDB
|
||
|
||
# EVENT SOURCING — идеальная синхронизация между узлами
|
||
try:
|
||
from event_ledger import get_event_ledger, EventLedger
|
||
EVENT_SOURCING_ENABLED = True
|
||
except ImportError:
|
||
EVENT_SOURCING_ENABLED = False
|
||
|
||
# ML-DSA-65 для криптографических доказательств присутствия
|
||
try:
|
||
from node_crypto import sign_message, verify_signature, get_node_crypto_system
|
||
ML_DSA_AVAILABLE = True
|
||
except ImportError:
|
||
ML_DSA_AVAILABLE = False
|
||
|
||
# TIMECHAIN — Immutable Time Ledger (append-only, hash chaining)
|
||
try:
|
||
from timechain import get_timechain, TimeChain
|
||
TIMECHAIN_ENABLED = True
|
||
except ImportError:
|
||
TIMECHAIN_ENABLED = False
|
||
|
||
logging.basicConfig(level=logging.INFO)
|
||
logger = logging.getLogger("TIME_BANK")
|
||
|
||
|
||
# ============================================================
|
||
# КОНСТАНТЫ ПРОТОКОЛА v3.0
|
||
# ============================================================
|
||
|
||
class Protocol:
|
||
"""Константы протокола TIME_BANK v3.0"""
|
||
VERSION = "3.0"
|
||
|
||
# Сеть
|
||
NODES_COUNT = 5 # 5 узлов Montana
|
||
BANK_PRESENCE_PER_T2 = 600 # Банк всегда присутствует 600 сек (10 мин)
|
||
|
||
# КОГНИТИВНЫЙ ГЕНЕЗИС: 9 января 2026 00:00:00 МСК
|
||
# = 8 января 2026 21:00:00 UTC
|
||
GENESIS_YEAR = 2026
|
||
GENESIS_MONTH = 1
|
||
GENESIS_DAY = 9
|
||
GENESIS_HOUR = 0 # по МСК
|
||
GENESIS_MINUTE = 0
|
||
GENESIS_SECOND = 0
|
||
GENESIS_TIMEZONE = "Europe/Moscow"
|
||
|
||
# TIME_BANK RESERVE — ровно 40 лет в секундах (с реальными високосными)
|
||
# Високосные: 2028,2032,2036,2040,2044,2048,2052,2056,2060,2064 = 10 лет
|
||
# Обычные: 30 лет
|
||
# 30 × 365 × 86400 = 946,080,000
|
||
# 10 × 366 × 86400 = 316,224,000
|
||
# ИТОГО: 1,262,304,000 секунд
|
||
# Конец: 9 января 2066 00:00:00 МСК
|
||
BANK_TOTAL_SECONDS = 1_262_304_000 # Ровно 40 лет
|
||
BANK_TOTAL_MINUTES = BANK_TOTAL_SECONDS // 60 # 21,038,400 минут
|
||
|
||
# Эмиссия (динамическая, зависит от участников)
|
||
# Нет фиксированной эмиссии — каждый получает свои секунды × halving
|
||
|
||
# Временные координаты (Temporal Coordinates)
|
||
TAU1_INTERVAL_SEC = 60 # τ₁ = 1 минута — интервал подписи присутствия
|
||
T2_DURATION_SEC = 10 * 60 # τ₂ = 10 минут = 600 секунд (slice/block)
|
||
TAU3_DURATION_SEC = 14 * 24 * 60 * 60 # τ₃ = 14 дней = 1,209,600 сек (checkpoint)
|
||
TAU4_DURATION_SEC = 4 * 365 * 24 * 60 * 60 # τ₄ = 4 года = 126,144,000 сек (epoch)
|
||
|
||
# Иерархия
|
||
T2_PER_TAU3 = 2016 # 2016 × τ₂ в τ₃ (14 дней / 10 минут)
|
||
TAU3_PER_YEAR = 26 # 26 × τ₃ в году (365 / 14)
|
||
TAU3_PER_TAU4 = 104 # 104 × τ₃ в τ₄ (4 года)
|
||
|
||
# Другие временные параметры
|
||
INACTIVITY_LIMIT_SEC = 1 * 60 # τ₁ = 1 минута без активности = пауза
|
||
TICK_INTERVAL_SEC = 1 # Интервал обновления
|
||
|
||
# Монеты
|
||
COINS_PER_SECOND = 1 # 1 секунда = 1 монета (без лотереи)
|
||
|
||
# Presence Proof
|
||
PRESENCE_PROOF_VERSION = "MONTANA_PRESENCE_V1"
|
||
GENESIS_HASH = "0" * 64 # Genesis prev_hash
|
||
|
||
@classmethod
|
||
def get_genesis_datetime(cls) -> datetime:
|
||
"""Возвращает дату когнитивного генезиса (9 января 2026 00:00:00 МСК)"""
|
||
import pytz
|
||
tz = pytz.timezone(cls.GENESIS_TIMEZONE)
|
||
return tz.localize(datetime(
|
||
cls.GENESIS_YEAR, cls.GENESIS_MONTH, cls.GENESIS_DAY,
|
||
cls.GENESIS_HOUR, cls.GENESIS_MINUTE, cls.GENESIS_SECOND
|
||
))
|
||
|
||
@classmethod
|
||
def get_end_datetime(cls) -> datetime:
|
||
"""Возвращает дату конца эмиссии (9 января 2066 00:00:00 МСК)"""
|
||
from datetime import timedelta
|
||
return cls.get_genesis_datetime() + timedelta(seconds=cls.BANK_TOTAL_SECONDS)
|
||
|
||
@classmethod
|
||
def seconds_since_genesis(cls) -> float:
|
||
"""Секунды с момента генезиса"""
|
||
import pytz
|
||
now = datetime.now(pytz.timezone(cls.GENESIS_TIMEZONE))
|
||
genesis = cls.get_genesis_datetime()
|
||
return max(0, (now - genesis).total_seconds())
|
||
|
||
@classmethod
|
||
def seconds_until_end(cls) -> float:
|
||
"""Секунды до конца эмиссии"""
|
||
return max(0, cls.BANK_TOTAL_SECONDS - cls.seconds_since_genesis())
|
||
|
||
|
||
# ============================================================
|
||
# HALVING — Деление эмиссии на 2 каждые τ₄
|
||
# ============================================================
|
||
|
||
def halving_coefficient(tau4_count: int) -> float:
|
||
"""
|
||
Коэффициент халвинга — деление на 2 каждые τ₄ (4 года)
|
||
|
||
Эмиссия уменьшается в 2 раза каждую эпоху τ₄
|
||
|
||
Args:
|
||
tau4_count: Количество пройденных τ₄ эпох
|
||
|
||
Returns:
|
||
Коэффициент эмиссии (1.0, 0.5, 0.25, 0.125...)
|
||
|
||
Формула:
|
||
emission_per_second = 1.0 / (2 ** tau4_count)
|
||
|
||
Пример:
|
||
>>> halving_coefficient(0) # τ₄ #0 (первые 4 года)
|
||
1.0
|
||
>>> halving_coefficient(1) # τ₄ #1 (4-8 лет)
|
||
0.5
|
||
>>> halving_coefficient(2) # τ₄ #2 (8-12 лет)
|
||
0.25
|
||
>>> halving_coefficient(3) # τ₄ #3 (12-16 лет)
|
||
0.125
|
||
"""
|
||
return 1.0 / (2 ** tau4_count)
|
||
|
||
|
||
# ============================================================
|
||
# КЭШ СЕССИЙ
|
||
# ============================================================
|
||
|
||
class PresenceCache:
|
||
"""Кэш присутствия по адресам (address или ip)"""
|
||
|
||
def __init__(self):
|
||
self.entries: Dict[str, Dict[str, Any]] = {}
|
||
self._lock = threading.Lock()
|
||
|
||
def get(self, address: str) -> Optional[Dict[str, Any]]:
|
||
with self._lock:
|
||
return self.entries.get(address)
|
||
|
||
def set(self, address: str, data: Dict[str, Any]):
|
||
with self._lock:
|
||
self.entries[address] = data
|
||
|
||
def remove(self, address: str):
|
||
with self._lock:
|
||
self.entries.pop(address, None)
|
||
|
||
def all(self) -> Dict[str, Dict[str, Any]]:
|
||
"""Возвращает копию (для безопасного итерирования снаружи lock)"""
|
||
with self._lock:
|
||
return dict(self.entries)
|
||
|
||
def items_snapshot(self) -> list:
|
||
"""
|
||
ULTRA-LIGHTWEIGHT: Снимок items для итерации.
|
||
Создаёт список один раз, не копирует весь dict.
|
||
"""
|
||
with self._lock:
|
||
return list(self.entries.items())
|
||
|
||
def count_active(self) -> int:
|
||
with self._lock:
|
||
return sum(1 for e in self.entries.values() if e.get("is_active"))
|
||
|
||
|
||
# ============================================================
|
||
# ОСНОВНОЙ КЛАСС
|
||
# ============================================================
|
||
|
||
class TimeBank:
|
||
"""
|
||
TIME_BANK v3.0 — Банк Времени Montana
|
||
|
||
Эмиссия: динамическая, 1 сек = 1 Ɉ × halving
|
||
Резерв: 21,000,000 минут (~40 лет)
|
||
Халвинг: каждые τ₄ (4 года)
|
||
"""
|
||
|
||
def __init__(self, db: Optional[MontanaDB] = None):
|
||
self.db = db or get_db()
|
||
self.presence = PresenceCache() # Все адреса (tg_id или ip)
|
||
|
||
# EVENT SOURCING — идеальная синхронизация
|
||
self.ledger: Optional[EventLedger] = None
|
||
if EVENT_SOURCING_ENABLED:
|
||
self.ledger = get_event_ledger()
|
||
|
||
# TIMECHAIN — Immutable Time Ledger (append-only, hash chaining)
|
||
self.timechain: Optional[TimeChain] = None
|
||
if TIMECHAIN_ENABLED:
|
||
self.timechain = get_timechain()
|
||
logger.info("⛓️ TimeChain: ENABLED (immutable ledger)")
|
||
|
||
# Счётчики T2
|
||
self.current_t2_start = time.time()
|
||
self.t2_emission = 0
|
||
self.t2_distributed = 0
|
||
self.total_reserve = 0
|
||
self.total_emitted = 0
|
||
self.total_distributed = 0
|
||
self.t2_count = 0
|
||
|
||
# Счётчики τ₃ и τ₄
|
||
self.tau3_count = 0 # Количество пройденных τ₃ (14 дней)
|
||
self.tau4_count = 0 # Количество пройденных τ₄ (4 года)
|
||
self.current_halving_coefficient = 1.0 # Текущий коэффициент халвинга
|
||
|
||
# TIME_BANK RESERVE — отслеживание расхода 21 млн минут
|
||
self.bank_seconds_spent = 0 # Сколько секунд банк уже потратил
|
||
self.bank_exhausted = False # True когда 21 млн минут исчерпаны
|
||
|
||
self._running = False
|
||
self._thread: Optional[threading.Thread] = None
|
||
|
||
# ML-DSA-65 Presence Proof
|
||
self._presence_proofs: List[Dict[str, Any]] = [] # Подписанные доказательства
|
||
self._last_proof_hash = Protocol.GENESIS_HASH # Prev hash для цепочки
|
||
self._tau1_counter = 0 # Счётчик секунд до τ₁
|
||
self._node_private_key: Optional[str] = None # Private key узла
|
||
self._node_public_key: Optional[str] = None # Public key узла
|
||
|
||
logger.info(f"TIME_BANK v{Protocol.VERSION}")
|
||
logger.info(f"📡 Эмиссия: динамическая (1 сек = 1 Ɉ × halving)")
|
||
logger.info(f"⏳ Резерв: {Protocol.BANK_TOTAL_MINUTES:,} минут (~40 лет)")
|
||
logger.info(f"🔐 ML-DSA-65: {'✅' if ML_DSA_AVAILABLE else '❌'}")
|
||
logger.info(f"📊 Event Sourcing: {'✅' if EVENT_SOURCING_ENABLED else '❌'}")
|
||
logger.info(f"⛓️ TimeChain: {'✅ IMMUTABLE' if TIMECHAIN_ENABLED else '❌'}")
|
||
|
||
# --------------------------------------------------------
|
||
# ПРИСУТСТВИЕ (по адресу = ключу)
|
||
# --------------------------------------------------------
|
||
|
||
def start(self, address: str, addr_type: str = "unknown") -> Dict[str, Any]:
|
||
"""
|
||
Начинает присутствие по адресу.
|
||
address = address (str) или ip_address
|
||
"""
|
||
self.db.wallet(address, addr_type)
|
||
|
||
entry = {
|
||
"address": address,
|
||
"addr_type": addr_type,
|
||
"presence_seconds": 0,
|
||
"last_activity": time.time(),
|
||
"t2_seconds": 0,
|
||
"is_active": True
|
||
}
|
||
self.presence.set(address, entry)
|
||
|
||
logger.info(f"📍 Присутствие: {address} [{addr_type}]")
|
||
return entry
|
||
|
||
def activity(self, address: str, addr_type: str = "unknown") -> dict:
|
||
"""
|
||
Регистрирует активность по адресу.
|
||
|
||
Returns:
|
||
{
|
||
"is_new": True если новая сессия,
|
||
"was_paused": True если возобновлена после паузы,
|
||
"presence_seconds": текущие секунды присутствия
|
||
}
|
||
"""
|
||
entry = self.presence.get(address)
|
||
is_new = False
|
||
was_paused = False
|
||
|
||
if not entry:
|
||
self.start(address, addr_type)
|
||
entry = self.presence.get(address)
|
||
is_new = True
|
||
logger.info(f"📍 Новое присутствие: {address}")
|
||
|
||
entry["last_activity"] = time.time()
|
||
|
||
if not entry.get("is_active") and not is_new:
|
||
entry["is_active"] = True
|
||
was_paused = True
|
||
logger.info(f"▶️ Возобновлено: {address}")
|
||
|
||
return {
|
||
"is_new": is_new,
|
||
"was_paused": was_paused,
|
||
"presence_seconds": entry.get("presence_seconds", 0),
|
||
"t2_seconds": entry.get("t2_seconds", 0)
|
||
}
|
||
|
||
def end(self, address: str) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
Завершает присутствие.
|
||
|
||
Монеты НЕ начисляются сразу — они будут начислены при финализации T2.
|
||
Накопленные t2_seconds остаются в кэше до закрытия окна времени (раз в 10 минут).
|
||
"""
|
||
entry = self.presence.get(address)
|
||
if not entry:
|
||
return None
|
||
|
||
# Помечаем как неактивный, но НЕ удаляем из кэша
|
||
# t2_seconds будут начислены при следующей финализации T2
|
||
entry["is_active"] = False
|
||
|
||
logger.info(f"🏁 Завершено: {address}, {entry['presence_seconds']} сек, pending T2: {entry['t2_seconds']} сек")
|
||
return entry
|
||
|
||
def get(self, address: str) -> Optional[Dict[str, Any]]:
|
||
"""Информация о присутствии"""
|
||
entry = self.presence.get(address)
|
||
if not entry:
|
||
return None
|
||
|
||
return {
|
||
"address": address,
|
||
"presence_seconds": entry["presence_seconds"],
|
||
"t2_seconds": entry["t2_seconds"],
|
||
"is_active": entry["is_active"],
|
||
"balance": self.db.balance(address)
|
||
}
|
||
|
||
# --------------------------------------------------------
|
||
# ML-DSA-65 PRESENCE PROOF
|
||
# --------------------------------------------------------
|
||
|
||
def set_node_keys(self, private_key_hex: str, public_key_hex: str):
|
||
"""
|
||
Устанавливает ключи узла для подписи присутствия
|
||
|
||
POST-QUANTUM: ML-DSA-65 (FIPS 204)
|
||
|
||
Args:
|
||
private_key_hex: Приватный ключ (4032 байта в hex)
|
||
public_key_hex: Публичный ключ (1952 байта в hex)
|
||
"""
|
||
self._node_private_key = private_key_hex
|
||
self._node_public_key = public_key_hex
|
||
logger.info(f"🔑 Node keys set (ML-DSA-65)")
|
||
|
||
# Передаём ключи в TimeChain для подписи блоков
|
||
if self.timechain:
|
||
self.timechain.set_node_keys(private_key_hex, public_key_hex)
|
||
logger.info(f"⛓️🔐 TimeChain keys set (ML-DSA-65)")
|
||
|
||
def _sign_presence_proof(self) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
Подписывает доказательство присутствия каждую τ₁ (1 минуту)
|
||
|
||
Формат сообщения:
|
||
MONTANA_PRESENCE_V1:{timestamp}:{prev_hash}:{pubkey}:{t2_index}
|
||
|
||
Returns:
|
||
Signed proof dict или None если нет ключей
|
||
"""
|
||
if not ML_DSA_AVAILABLE:
|
||
logger.warning("ML-DSA-65 недоступен")
|
||
return None
|
||
|
||
if not self._node_private_key or not self._node_public_key:
|
||
logger.debug("Node keys не установлены, пропуск подписи")
|
||
return None
|
||
|
||
timestamp = int(time.time())
|
||
t2_index = self.t2_count
|
||
|
||
# Формируем сообщение для подписи
|
||
message = f"{Protocol.PRESENCE_PROOF_VERSION}:{timestamp}:{self._last_proof_hash}:{self._node_public_key}:{t2_index}"
|
||
|
||
# Подписываем ML-DSA-65
|
||
signature = sign_message(self._node_private_key, message)
|
||
|
||
# Вычисляем hash этого proof для цепочки
|
||
proof_hash = hashlib.sha256(
|
||
f"{message}:{signature}".encode('utf-8')
|
||
).hexdigest()
|
||
|
||
proof = {
|
||
"version": Protocol.PRESENCE_PROOF_VERSION,
|
||
"timestamp": timestamp,
|
||
"prev_hash": self._last_proof_hash,
|
||
"pubkey": self._node_public_key,
|
||
"t2_index": t2_index,
|
||
"message": message,
|
||
"signature": signature,
|
||
"proof_hash": proof_hash,
|
||
"active_addresses": self.presence.count_active()
|
||
}
|
||
|
||
# Обновляем prev_hash для следующего proof
|
||
self._last_proof_hash = proof_hash
|
||
|
||
# Сохраняем proof
|
||
self._presence_proofs.append(proof)
|
||
|
||
# Ограничиваем хранимые proofs (последние 100)
|
||
if len(self._presence_proofs) > 100:
|
||
self._presence_proofs = self._presence_proofs[-100:]
|
||
|
||
logger.info(f"✍️ Presence Proof #{len(self._presence_proofs)} signed (τ₁)")
|
||
|
||
return proof
|
||
|
||
def get_presence_proofs(self, limit: int = 10) -> List[Dict[str, Any]]:
|
||
"""
|
||
Получает последние подписанные доказательства присутствия
|
||
|
||
Returns:
|
||
List of signed proofs (newest first)
|
||
"""
|
||
return list(reversed(self._presence_proofs[-limit:]))
|
||
|
||
def verify_presence_proof(self, proof: Dict[str, Any]) -> bool:
|
||
"""
|
||
Верифицирует подпись доказательства присутствия ML-DSA-65
|
||
|
||
Args:
|
||
proof: Proof dict с message и signature
|
||
|
||
Returns:
|
||
True если подпись валидна
|
||
"""
|
||
if not ML_DSA_AVAILABLE:
|
||
return False
|
||
|
||
try:
|
||
pubkey = proof.get("pubkey")
|
||
message = proof.get("message")
|
||
signature = proof.get("signature")
|
||
|
||
if not all([pubkey, message, signature]):
|
||
return False
|
||
|
||
return verify_signature(pubkey, message, signature)
|
||
except Exception as e:
|
||
logger.error(f"Verify error: {e}")
|
||
return False
|
||
|
||
def get_proof_chain_status(self) -> Dict[str, Any]:
|
||
"""
|
||
Статус цепочки доказательств присутствия
|
||
|
||
Returns:
|
||
Chain status dict
|
||
"""
|
||
return {
|
||
"ml_dsa_available": ML_DSA_AVAILABLE,
|
||
"node_keys_set": bool(self._node_private_key),
|
||
"total_proofs": len(self._presence_proofs),
|
||
"last_proof_hash": self._last_proof_hash,
|
||
"tau1_interval_sec": Protocol.TAU1_INTERVAL_SEC,
|
||
"genesis_hash": Protocol.GENESIS_HASH
|
||
}
|
||
|
||
# --------------------------------------------------------
|
||
# КОШЕЛЁК API
|
||
# --------------------------------------------------------
|
||
|
||
def balance(self, address: str) -> int:
|
||
"""Баланс по адресу (ключу) — EVENT SOURCING приоритет"""
|
||
if self.ledger:
|
||
return self.ledger.balance(address)
|
||
return self.db.balance(address)
|
||
|
||
def timechain_balance(self, address: str) -> int:
|
||
"""
|
||
IMMUTABLE баланс из TimeChain (сумма всех секунд)
|
||
|
||
TimeChain — единственный источник истины.
|
||
Каждый блок хэшируется с предыдущим.
|
||
Без права перезаписи, отката, восстановления.
|
||
"""
|
||
if self.timechain:
|
||
return self.timechain.balance(address)
|
||
return 0
|
||
|
||
def verify_timechain(self, address: str) -> bool:
|
||
"""Проверяет целостность цепочки для адреса"""
|
||
if self.timechain:
|
||
return self.timechain.verify_chain(address)
|
||
return False
|
||
|
||
def get_timechain_blocks(self, address: str, limit: int = 100) -> list:
|
||
"""Получает блоки timechain для адреса"""
|
||
if self.timechain:
|
||
return self.timechain.get_blocks(address, limit)
|
||
return []
|
||
|
||
def get_balance_with_pending(self, address: str) -> Dict[str, Any]:
|
||
"""
|
||
Баланс с учётом pending монет (ещё не подтверждённых в T2)
|
||
|
||
Args:
|
||
address: Адрес кошелька
|
||
|
||
Returns:
|
||
Dict с тремя значениями:
|
||
- confirmed: Подтверждённый баланс (в DB)
|
||
- pending: Накапливается в текущем T2 (в cache)
|
||
- total: Сумма confirmed + pending
|
||
"""
|
||
# Подтверждённый баланс (в БД)
|
||
confirmed = self.db.balance(address)
|
||
|
||
# Pending монеты (в кэше присутствия)
|
||
entry = self.presence.get(address)
|
||
pending_seconds = entry.get("t2_seconds", 0) if entry else 0
|
||
|
||
# Умножаем на текущий коэффициент халвинга
|
||
pending = int(pending_seconds * self.current_halving_coefficient)
|
||
|
||
return {
|
||
"confirmed": confirmed,
|
||
"pending": pending,
|
||
"total": confirmed + pending
|
||
}
|
||
|
||
def send(self, from_addr: str, to_addr: str, amount: int) -> Dict[str, Any]:
|
||
"""Перевод — EVENT SOURCING приоритет"""
|
||
if self.ledger:
|
||
ok, msg, event = self.ledger.transfer(from_addr, to_addr, amount)
|
||
if ok:
|
||
return {"success": True, "proof": event.event_id, "event_hash": event.event_hash}
|
||
return {"success": False, "error": msg}
|
||
|
||
# Fallback на старый метод
|
||
proof = self.db.send(from_addr, to_addr, amount)
|
||
if proof:
|
||
return {"success": True, "proof": proof}
|
||
return {"success": False}
|
||
|
||
def tx_feed(self, limit: int = 50) -> List[Dict[str, Any]]:
|
||
"""Публичная лента TX"""
|
||
return self.db.tx_feed(limit)
|
||
|
||
def tx_verify(self, proof: str) -> Dict[str, Any]:
|
||
"""Верификация TX"""
|
||
return self.db.tx_verify(proof)
|
||
|
||
def my_txs(self, address: str, limit: int = 50) -> List[Dict[str, Any]]:
|
||
"""Личная история TX"""
|
||
return self.db.my_txs(address, limit)
|
||
|
||
def get_reserve_info(self) -> Dict[str, Any]:
|
||
"""
|
||
Информация о резерве TIME_BANK (21 млн минут).
|
||
|
||
Returns:
|
||
{
|
||
"reserve_minutes": int, # Осталось минут
|
||
"reserve_years": float, # Осталось лет (~40 изначально)
|
||
"total_minutes": int, # Всего (21,000,000)
|
||
"spent_minutes": int, # Потрачено
|
||
"halving_coefficient": float, # Текущий коэффициент халвинга
|
||
"exhausted": bool # True если резерв исчерпан
|
||
}
|
||
"""
|
||
spent_minutes = self.bank_seconds_spent // 60
|
||
remaining_minutes = max(0, Protocol.BANK_TOTAL_MINUTES - spent_minutes)
|
||
remaining_years = remaining_minutes / (60 * 24 * 365)
|
||
|
||
return {
|
||
"reserve_minutes": remaining_minutes,
|
||
"reserve_years": remaining_years,
|
||
"total_minutes": Protocol.BANK_TOTAL_MINUTES,
|
||
"spent_minutes": spent_minutes,
|
||
"halving_coefficient": self.current_halving_coefficient,
|
||
"exhausted": self.bank_exhausted
|
||
}
|
||
|
||
def wallets(self, addr_type: str = None) -> List[Dict[str, Any]]:
|
||
"""Все кошельки"""
|
||
return self.db.wallets(addr_type)
|
||
|
||
def stats(self) -> Dict[str, Any]:
|
||
"""Статистика TIME_BANK"""
|
||
t2_elapsed = int(time.time() - self.current_t2_start)
|
||
|
||
return {
|
||
"version": Protocol.VERSION,
|
||
"emission_model": "dynamic", # Динамическая эмиссия
|
||
"last_t2_emission": self.t2_emission,
|
||
"t2_count": self.t2_count,
|
||
"t2_elapsed_sec": t2_elapsed,
|
||
"t2_remaining_sec": max(0, Protocol.T2_DURATION_SEC - t2_elapsed),
|
||
"total_emitted": self.total_emitted,
|
||
"total_distributed": self.total_distributed,
|
||
"active_presence": self.presence.count_active(),
|
||
"wallets": len(self.db.wallets()),
|
||
# Temporal Coordinates
|
||
"tau3_count": self.tau3_count,
|
||
"tau4_count": self.tau4_count,
|
||
"current_year": self.tau3_count // Protocol.TAU3_PER_YEAR,
|
||
"halving_coefficient": self.current_halving_coefficient,
|
||
"t2_to_next_tau3": Protocol.T2_PER_TAU3 - (self.t2_count % Protocol.T2_PER_TAU3),
|
||
# TIME_BANK RESERVE (21 млн минут)
|
||
"bank_total_minutes": Protocol.BANK_TOTAL_MINUTES,
|
||
"bank_seconds_spent": self.bank_seconds_spent,
|
||
"bank_minutes_spent": self.bank_seconds_spent // 60,
|
||
"bank_minutes_remaining": max(0, Protocol.BANK_TOTAL_MINUTES - self.bank_seconds_spent // 60),
|
||
"bank_exhausted": self.bank_exhausted,
|
||
"bank_years_remaining": max(0, (Protocol.BANK_TOTAL_SECONDS - self.bank_seconds_spent) / (525600 * 60)),
|
||
# ML-DSA-65 Presence Proof
|
||
"ml_dsa_65": ML_DSA_AVAILABLE,
|
||
"presence_proofs": len(self._presence_proofs),
|
||
"tau1_counter": self._tau1_counter,
|
||
"node_keys_set": bool(self._node_private_key),
|
||
# Event Sourcing
|
||
"event_sourcing": EVENT_SOURCING_ENABLED,
|
||
"ledger_stats": self.ledger.stats() if self.ledger else None,
|
||
# TimeChain — Immutable Ledger
|
||
"timechain_enabled": TIMECHAIN_ENABLED,
|
||
"timechain_blocks": self.timechain.total_blocks() if self.timechain else 0
|
||
}
|
||
|
||
# --------------------------------------------------------
|
||
# ФОНОВЫЙ ПРОЦЕСС
|
||
# --------------------------------------------------------
|
||
|
||
def run(self):
|
||
"""Запускает фоновый процесс"""
|
||
if self._running:
|
||
return
|
||
|
||
self._running = True
|
||
self._thread = threading.Thread(target=self._tick_loop, daemon=True)
|
||
self._thread.start()
|
||
logger.info("⏱️ TIME_BANK запущен")
|
||
|
||
def stop(self):
|
||
"""Останавливает"""
|
||
self._running = False
|
||
if self._thread:
|
||
self._thread.join(timeout=2)
|
||
logger.info("⏹️ TIME_BANK остановлен")
|
||
|
||
def _tick_loop(self):
|
||
"""Основной цикл"""
|
||
while self._running:
|
||
self._tick()
|
||
time.sleep(Protocol.TICK_INTERVAL_SEC)
|
||
|
||
def _tick(self):
|
||
"""Обновление каждую секунду"""
|
||
now = time.time()
|
||
|
||
# Проверяем окончание T2
|
||
if now - self.current_t2_start >= Protocol.T2_DURATION_SEC:
|
||
self._finalize_t2()
|
||
|
||
# τ₁ — подпись присутствия каждую минуту (ML-DSA-65)
|
||
self._tau1_counter += 1
|
||
if self._tau1_counter >= Protocol.TAU1_INTERVAL_SEC:
|
||
self._sign_presence_proof()
|
||
self._tau1_counter = 0
|
||
|
||
# Обновляем все адреса
|
||
for address, entry in self.presence.items_snapshot():
|
||
inactive = now - entry["last_activity"]
|
||
|
||
if inactive > Protocol.INACTIVITY_LIMIT_SEC:
|
||
if entry["is_active"]:
|
||
entry["is_active"] = False
|
||
logger.debug(f"⏸️ Пауза: {address}")
|
||
else:
|
||
entry["presence_seconds"] += 1
|
||
entry["t2_seconds"] += 1
|
||
|
||
def _finalize_t2(self):
|
||
"""
|
||
Завершает T2, начисляет монеты с халвингом
|
||
|
||
Механизм эмиссии:
|
||
1. Считаем сумму всех секунд присутствия
|
||
2. Банк всегда присутствует 600 секунд (подтверждает что прошло 10 минут)
|
||
3. Эмиссия = (total_seconds - bank_seconds) × halving_coefficient
|
||
4. Распределяем каждому: user_seconds × halving_coefficient
|
||
"""
|
||
self.t2_count += 1
|
||
|
||
# Вычисляем коэффициент халвинга
|
||
self.current_halving_coefficient = halving_coefficient(self.tau4_count)
|
||
|
||
# Считаем общую сумму секунд присутствия всех участников
|
||
total_users_seconds = 0
|
||
for address, entry in self.presence.items_snapshot():
|
||
total_users_seconds += entry["t2_seconds"]
|
||
|
||
# Банк подтверждает что прошло 10 минут (600 секунд)
|
||
bank_seconds = Protocol.BANK_PRESENCE_PER_T2
|
||
|
||
# TIME_BANK RESERVE — отслеживание расхода 21 млн минут
|
||
# Банк всегда тратит 10 мин/T2, независимо от халвинга
|
||
self.bank_seconds_spent += bank_seconds
|
||
|
||
# Проверяем исчерпание резерва (21 млн минут = ~40 лет)
|
||
if not self.bank_exhausted and self.bank_seconds_spent >= Protocol.BANK_TOTAL_SECONDS:
|
||
self.bank_exhausted = True
|
||
logger.info(f"")
|
||
logger.info(f"╔═══════════════════════════════════════════════════════════╗")
|
||
logger.info(f"║ ⏳ TIME_BANK RESERVE EXHAUSTED — ORACLE MODE ║")
|
||
logger.info(f"╚═══════════════════════════════════════════════════════════╝")
|
||
logger.info(f"🏦 Банк потратил все 21 млн минут")
|
||
logger.info(f"📡 Теперь чистый оракул — продолжает верифицировать время")
|
||
|
||
# Эмиссия = сумма секунд участников × халвинг
|
||
# Банк НЕ получает монеты — только тратит резерв времени
|
||
emission = int(total_users_seconds * self.current_halving_coefficient)
|
||
|
||
self.t2_emission = emission
|
||
self.total_emitted += emission
|
||
|
||
# Распределяем по адресам (каждый получает свои секунды × halving)
|
||
# EVENT SOURCING: используем ledger.emit() для неизменяемого лога
|
||
distributed = 0
|
||
for address, entry in self.presence.items_snapshot():
|
||
if entry["t2_seconds"] > 0:
|
||
seconds_earned = entry["t2_seconds"]
|
||
coins = int(seconds_earned * self.current_halving_coefficient)
|
||
|
||
if self.ledger:
|
||
# EVENT SOURCING — создаём событие EMISSION
|
||
self.ledger.emit(
|
||
to_addr=address,
|
||
amount=coins,
|
||
metadata={
|
||
"t2_index": self.t2_count,
|
||
"seconds": seconds_earned,
|
||
"halving": self.current_halving_coefficient,
|
||
"addr_type": entry.get("addr_type", "unknown")
|
||
}
|
||
)
|
||
else:
|
||
# Fallback на старый метод
|
||
self.db.credit(address, coins, entry.get("addr_type", "unknown"))
|
||
|
||
# TIMECHAIN — IMMUTABLE LEDGER (append-only, hash chaining)
|
||
# Записываем секунды (не монеты) — это сырое время присутствия
|
||
if self.timechain:
|
||
self.timechain.append(address, seconds_earned)
|
||
|
||
distributed += coins
|
||
entry["t2_seconds"] = 0
|
||
|
||
self.t2_distributed = distributed
|
||
self.total_distributed += distributed
|
||
|
||
# Очищаем неактивные записи после финализации
|
||
inactive_addresses = [
|
||
addr for addr, entry in self.presence.items_snapshot()
|
||
if not entry.get("is_active") and entry.get("t2_seconds", 0) == 0
|
||
]
|
||
for addr in inactive_addresses:
|
||
self.presence.remove(addr)
|
||
logger.debug(f"🗑️ Очистка неактивной записи: {addr}")
|
||
|
||
# Проверяем τ₃ checkpoint (каждые 2016 T2 = 14 дней)
|
||
if self.t2_count % Protocol.T2_PER_TAU3 == 0:
|
||
self.tau3_count += 1
|
||
logger.info(f"")
|
||
logger.info(f"╔═══════════════════════════════════════════════════════════╗")
|
||
logger.info(f"║ τ₃ CHECKPOINT #{self.tau3_count} — 14 ДНЕЙ ПРОЙДЕНО ║")
|
||
logger.info(f"╚═══════════════════════════════════════════════════════════╝")
|
||
logger.info(f"⏰ τ₃ Index: {self.tau3_count}")
|
||
logger.info(f"📅 Year: {self.tau3_count // Protocol.TAU3_PER_YEAR}")
|
||
logger.info(f"📊 Halving: {self.current_halving_coefficient:.4f}x")
|
||
logger.info(f"💰 Total Emitted: {self.total_emitted:,} Ɉ")
|
||
|
||
# Проверяем τ₄ epoch (каждые 104 τ₃ = 4 года) — ХАЛВИНГ!
|
||
if self.tau3_count > 0 and self.tau3_count % Protocol.TAU3_PER_TAU4 == 0:
|
||
self.tau4_count += 1
|
||
logger.info(f"")
|
||
logger.info(f"╔═══════════════════════════════════════════════════════════╗")
|
||
logger.info(f"║ 🔥 τ₄ HALVING #{self.tau4_count} — ЭМИССИЯ ÷ 2 ║")
|
||
logger.info(f"╚═══════════════════════════════════════════════════════════╝")
|
||
logger.info(f"🎉 τ₄ Epoch: {self.tau4_count}")
|
||
logger.info(f"📊 Новый коэффициент: {halving_coefficient(self.tau4_count):.4f}x")
|
||
logger.info(f"💰 Total Emitted: {self.total_emitted:,} Ɉ")
|
||
|
||
logger.info(f"═══ T2 #{self.t2_count} ═══")
|
||
logger.info(f"👥 Участники: {total_users_seconds} сек")
|
||
logger.info(f"🏦 Банк: {bank_seconds} сек (из резерва 21M)")
|
||
logger.info(f"📊 Халвинг: {self.current_halving_coefficient:.4f}x")
|
||
logger.info(f"📡 Эмиссия: {emission} Ɉ")
|
||
logger.info(f"💰 Распределено: {distributed} Ɉ")
|
||
# TIME_BANK RESERVE status
|
||
bank_minutes_remaining = Protocol.BANK_TOTAL_MINUTES - self.bank_seconds_spent // 60
|
||
bank_years_remaining = bank_minutes_remaining / 525600
|
||
logger.info(f"⏳ Резерв: {bank_minutes_remaining:,} мин (~{bank_years_remaining:.1f} лет)")
|
||
|
||
self.current_t2_start = time.time()
|
||
|
||
# ============================================================
|
||
# SINGLETON
|
||
# ============================================================
|
||
|
||
_instance: Optional[TimeBank] = None
|
||
_lock = threading.Lock()
|
||
|
||
def get_time_bank() -> TimeBank:
|
||
"""Возвращает глобальный экземпляр TimeBank"""
|
||
global _instance
|
||
with _lock:
|
||
if _instance is None:
|
||
_instance = TimeBank()
|
||
_instance.run()
|
||
return _instance
|
||
|
||
|
||
# ============================================================
|
||
# CLI
|
||
# ============================================================
|
||
|
||
if __name__ == "__main__":
|
||
import sys
|
||
import json
|
||
|
||
bank = get_time_bank()
|
||
|
||
if len(sys.argv) < 2:
|
||
print(f"""
|
||
TIME_BANK v{Protocol.VERSION} — Банк Времени Montana
|
||
═══════════════════════════════════════════════════
|
||
|
||
ЭМИССИЯ:
|
||
• Динамическая: 1 сек = 1 Ɉ × halving
|
||
• T2 = {Protocol.T2_DURATION_SEC // 60} мин
|
||
• Халвинг каждые 4 года (τ₄)
|
||
|
||
РЕЗЕРВ:
|
||
• {Protocol.BANK_TOTAL_MINUTES:,} минут (~40 лет)
|
||
|
||
Команды:
|
||
balance <addr> — баланс
|
||
start <addr> — начать присутствие
|
||
activity <addr> — активность
|
||
end <addr> — завершить
|
||
send <from> <to> <amount> — перевод
|
||
wallets — все кошельки
|
||
stats — статистика
|
||
proofs — ML-DSA-65 presence proofs
|
||
proof-status — статус цепочки proofs
|
||
demo — демо
|
||
""")
|
||
sys.exit(0)
|
||
|
||
cmd = sys.argv[1]
|
||
|
||
if cmd == "demo":
|
||
print(f"🎬 Демо TIME_BANK v{Protocol.VERSION}")
|
||
print("=" * 50)
|
||
|
||
addr = "demo_123"
|
||
bank.start(addr, "demo")
|
||
|
||
print(f"▶️ Присутствие: {addr}")
|
||
print(f"💰 Баланс: {bank.balance(addr)} Ɉ")
|
||
|
||
print("\n⏱️ Симуляция 15 секунд...")
|
||
for i in range(15):
|
||
bank.activity(addr, "demo")
|
||
bank._tick()
|
||
time.sleep(0.1)
|
||
|
||
info = bank.get(addr)
|
||
print(f"📊 Присутствие: {info['presence_seconds']} сек")
|
||
print(f"📊 T2: {info['t2_seconds']} сек")
|
||
|
||
bank.end(addr)
|
||
print(f"\n🏁 Завершено")
|
||
print(f"💰 Итого: {bank.balance(addr)} Ɉ")
|
||
|
||
elif cmd == "stats":
|
||
s = bank.stats()
|
||
print("📊 Статистика TIME_BANK:")
|
||
print("=" * 50)
|
||
for k, v in s.items():
|
||
print(f"{k}: {v}")
|
||
|
||
elif cmd == "balance" and len(sys.argv) > 2:
|
||
addr = sys.argv[2]
|
||
print(f"💰 {addr}: {bank.balance(addr)} Ɉ")
|
||
|
||
elif cmd == "start" and len(sys.argv) > 2:
|
||
addr = sys.argv[2]
|
||
addr_type = sys.argv[3] if len(sys.argv) > 3 else "cli"
|
||
bank.start(addr, addr_type)
|
||
print(f"▶️ Присутствие: {addr}")
|
||
|
||
elif cmd == "activity" and len(sys.argv) > 2:
|
||
addr = sys.argv[2]
|
||
bank.activity(addr)
|
||
print(f"✓ Активность: {addr}")
|
||
|
||
elif cmd == "end" and len(sys.argv) > 2:
|
||
addr = sys.argv[2]
|
||
result = bank.end(addr)
|
||
if result:
|
||
print(f"🏁 Завершено")
|
||
print(f"💰 Баланс: {bank.balance(addr)} Ɉ")
|
||
else:
|
||
print(f"Нет присутствия: {addr}")
|
||
|
||
elif cmd == "send" and len(sys.argv) > 4:
|
||
from_addr = sys.argv[2]
|
||
to_addr = sys.argv[3]
|
||
amount = int(sys.argv[4])
|
||
result = bank.send(from_addr, to_addr, amount)
|
||
if result.get("success"):
|
||
print(f"✓ TX: {result['proof'][:16]}...")
|
||
else:
|
||
print("❌ Ошибка")
|
||
|
||
elif cmd == "wallets":
|
||
ws = bank.wallets()
|
||
print("💼 Кошельки:")
|
||
print("-" * 40)
|
||
for w in ws[:20]:
|
||
print(f"{w['address']}: {w['balance']} Ɉ [{w['address_type']}]")
|
||
|
||
elif cmd == "proofs":
|
||
proofs = bank.get_presence_proofs(10)
|
||
print("🔐 ML-DSA-65 Presence Proofs:")
|
||
print("=" * 50)
|
||
if not proofs:
|
||
print("Нет подписанных proofs")
|
||
for p in proofs:
|
||
print(f"\n#{p['t2_index']} @ {p['timestamp']}")
|
||
print(f" hash: {p['proof_hash'][:32]}...")
|
||
print(f" prev: {p['prev_hash'][:32]}...")
|
||
print(f" sig: {p['signature'][:32]}...")
|
||
print(f" active: {p['active_addresses']} addresses")
|
||
|
||
elif cmd == "proof-status":
|
||
status = bank.get_proof_chain_status()
|
||
print("🔐 Presence Proof Chain Status:")
|
||
print("=" * 50)
|
||
print(f"ML-DSA-65: {'✅' if status['ml_dsa_available'] else '❌'}")
|
||
print(f"Node keys: {'✅' if status['node_keys_set'] else '❌'}")
|
||
print(f"Total proofs: {status['total_proofs']}")
|
||
print(f"τ₁ interval: {status['tau1_interval_sec']} sec")
|
||
print(f"Last hash: {status['last_proof_hash'][:32]}...")
|
||
|
||
else:
|
||
print(f"Неизвестная команда: {cmd}")
|