montana/Русский/Бот/time_bank.py

991 lines
41 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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}")