#!/usr/bin/env python3 """ test_timechain.py — 9 Проверок Готовности Таймчейна Montana Protocol v3.0 Каждый тест соответствует одной проверке из спецификации. Все 9 PASS = Таймчейн готов. Post-quantum: ML-DSA-65 (FIPS 204) — реальные подписи, не моки. """ import os import sys import time import json import hashlib import tempfile import unittest import shutil # Добавляем путь к модулям sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from node_crypto import ( generate_keypair, public_key_to_address, sign_message, verify_signature, validate_address, ) from transaction import ( Transaction, TxInput, TxOutput, UTXOSet, create_coinbase_tx, create_transfer_tx, validate_transaction, ) from timechain import ( TimeChain, Tau1Window, Tau2Window, merkle_root, GENESIS_HASH, TAU1_PER_TAU2, ) from nts_anchor import NTSAnchorService from presence_proof import ( PresenceChain, PresenceProof, compute_proof_hash, format_presence_message, parse_presence_message, PRESENCE_VERSION, ) class TestTimeChainReadiness(unittest.TestCase): """ 9 проверок готовности Таймчейна Montana Protocol. Каждый тест использует реальную ML-DSA-65 криптографию. Временные БД создаются в /tmp и удаляются после теста. """ @classmethod def setUpClass(cls): """Генерируем ключи один раз для всех тестов (ML-DSA-65 keygen медленный)""" print("\n🔑 Generating ML-DSA-65 keys for tests...") # Узел A (создатель окон) cls.priv_a, cls.pub_a = generate_keypair() cls.addr_a = public_key_to_address(cls.pub_a) # Узел B (получатель) cls.priv_b, cls.pub_b = generate_keypair() cls.addr_b = public_key_to_address(cls.pub_b) # Узел C (для double-spend теста) cls.priv_c, cls.pub_c = generate_keypair() cls.addr_c = public_key_to_address(cls.pub_c) print(f" Node A: {cls.addr_a}") print(f" Node B: {cls.addr_b}") print(f" Node C: {cls.addr_c}") def setUp(self): """Создаём временную директорию для каждого теста""" self.test_dir = tempfile.mkdtemp(prefix="montana_test_") self.db_path = os.path.join(self.test_dir, "timechain.db") def tearDown(self): """Удаляем временную директорию""" shutil.rmtree(self.test_dir, ignore_errors=True) def _create_chain(self, with_nts: bool = True) -> TimeChain: """Создать TimeChain с ключами узла A (strict_timestamps=False for rapid test execution)""" nts = NTSAnchorService(mock_mode=True) if with_nts else None chain = TimeChain( node_id=self.addr_a, private_key=self.priv_a, db_path=self.db_path, public_key=self.pub_a, strict_timestamps=False, nts_service=nts, ) return chain # ═══════════════════════════════════════════════════════════════════════════ # ПРОВЕРКА 1: Горизонтальная целостность τ₁ # ═══════════════════════════════════════════════════════════════════════════ def test_1_horizontal_integrity(self): """ Берём любой слайс τ₁, вычисляем хеш предыдущего, сравниваем с prev_tau1_hash. Проходим от последнего до генезиса. Все хеши сходятся — цепочка не нарушена. Изменил бит в старом слайсе — все хеши после него рассыпаются. """ print("\n═══ ПРОВЕРКА 1: Горизонтальная целостность τ₁ ═══") chain = self._create_chain() # Создаём генезис genesis = chain.create_genesis_window() self.assertEqual(genesis.prev_tau1_hash, GENESIS_HASH) self.assertEqual(genesis.window_number, 0) # Создаём 10 τ₁ окон for i in range(10): coinbase = create_coinbase_tx(self.addr_a, 60, t2_index=0) chain.create_tau1_window([coinbase]) # Верификация цепочки ok, msg = chain.verify_tau1_chain() self.assertTrue(ok, f"Chain verification failed: {msg}") print(f" ✅ {msg}") # Проверяем что prev_hash каждого окна = hash предыдущего prev_hash = GENESIS_HASH for i in range(11): # genesis + 10 window = chain.db.get_tau1(i) self.assertIsNotNone(window, f"Window #{i} not found") self.assertEqual( window.prev_tau1_hash, prev_hash, f"Window #{i}: prev_hash mismatch" ) prev_hash = window.window_hash() print(f" ✅ 11 окон: горизонтальная цепочка целостна") # ═══════════════════════════════════════════════════════════════════════════ # ПРОВЕРКА 2: Вертикальная матрёшка # ═══════════════════════════════════════════════════════════════════════════ def test_2_vertical_matryoshka(self): """ Берём слайс τ₂. В нём tau1_headers (10 хешей) и tau1_merkle_root. Строим Merkle tree из 10 хешей, вычисляем корень, сравниваем. Совпадает — матрёшка корректна. """ print("\n═══ ПРОВЕРКА 2: Вертикальная матрёшка τ₂→τ₁ ═══") chain = self._create_chain() # Genesis + 10 τ₁ окон chain.create_genesis_window() tau1_hashes = [] for i in range(TAU1_PER_TAU2): coinbase = create_coinbase_tx(self.addr_a, 60, t2_index=0) window = chain.create_tau1_window([coinbase]) tau1_hashes.append(window.window_hash()) # Финализация τ₂ coinbase_t2 = [create_coinbase_tx(self.addr_a, 600, t2_index=0)] tau2 = chain.finalize_tau2(coinbase_t2, halving_coefficient=1.0) self.assertIsNotNone(tau2, "τ₂ should be created") # Проверяем merkle root expected_root = merkle_root(tau2.tau1_headers) self.assertEqual(tau2.tau1_merkle_root, expected_root) print(f" ✅ τ₂ Merkle root совпадает: {expected_root[:16]}...") # Верификация через chain.verify_tau2_matryoshka ok, msg = chain.verify_tau2_matryoshka(0) self.assertTrue(ok, f"Matryoshka verification failed: {msg}") print(f" ✅ {msg}") # Проверяем что τ₁ headers в τ₂ совпадают с реальными хешами # (genesis не входит в первый набор τ₁ headers для τ₂, # потому что pending_tau1_headers начинает заполняться после genesis) self.assertEqual(len(tau2.tau1_headers), TAU1_PER_TAU2) print(f" ✅ τ₂ содержит {len(tau2.tau1_headers)} хешей τ₁") # ═══════════════════════════════════════════════════════════════════════════ # ПРОВЕРКА 3: Валидация транзакций # ═══════════════════════════════════════════════════════════════════════════ def test_3_transaction_validation(self): """ Для каждой транзакции в каждом τ₁: - Найти UTXO по (tx_hash, output_idx) - Проверить что не потрачен - Проверить address(input.pubkey) == UTXO.address - Проверить ML_DSA_65.verify(pubkey, tx_data, signature) - Проверить sum(inputs) ≥ sum(outputs) """ print("\n═══ ПРОВЕРКА 3: Валидация транзакций ═══") chain = self._create_chain() chain.create_genesis_window() # Эмиссия: A получает 350 Ɉ coinbase = create_coinbase_tx(self.addr_a, 350, t2_index=0) chain.create_tau1_window([coinbase]) # Проверяем coinbase валидация ok, msg = validate_transaction(coinbase, chain.utxo_set) self.assertTrue(ok, f"Coinbase validation failed: {msg}") print(f" ✅ Coinbase валидна: {msg}") # Transfer: A → B (100 Ɉ), сдача 250 на A utxos_a = chain.utxo_set.get_utxos_for_address(self.addr_a) self.assertTrue(len(utxos_a) > 0, "A should have UTXOs") transfer = create_transfer_tx( utxos_to_spend=[(utxos_a[0]["tx_hash"], utxos_a[0]["output_idx"], utxos_a[0]["amount"])], recipient=self.addr_b, amount=100, change_address=self.addr_a, private_key=self.priv_a, public_key=self.pub_a, ) # Валидация transfer ok, msg = validate_transaction(transfer, chain.utxo_set) self.assertTrue(ok, f"Transfer validation failed: {msg}") print(f" ✅ Transfer валидна: {msg}") # Проверяем что подпись ML-DSA-65 работает for inp in transfer.inputs: verified = verify_signature(inp.pubkey, transfer.tx_hash, inp.signature) self.assertTrue(verified, "ML-DSA-65 signature should be valid") print(f" ✅ ML-DSA-65 подписи валидны") # Проверяем address(pubkey) == UTXO.address derived = public_key_to_address(self.pub_a) self.assertEqual(derived, self.addr_a) print(f" ✅ address(pubkey) == UTXO.address") # Проверяем невалидную транзакцию (подделанный tx_hash) bad_tx = Transaction( inputs=transfer.inputs, outputs=transfer.outputs, timestamp=transfer.timestamp, tx_type="transfer", tx_hash="0" * 64, # Подделанный хеш ) ok, msg = validate_transaction(bad_tx, chain.utxo_set) self.assertFalse(ok, "Transaction with fake hash should be invalid") print(f" ✅ Подделанный tx_hash отвергнут: {msg}") # ═══════════════════════════════════════════════════════════════════════════ # ПРОВЕРКА 4: UTXO и балансы # ═══════════════════════════════════════════════════════════════════════════ def test_4_utxo_and_balances(self): """ Отправляешь 100 Ɉ с адреса A на адрес B. Вход — UTXO адреса A (350 Ɉ). Выходы — 100 Ɉ на B + 250 Ɉ сдача на A. Старый UTXO потрачен, два новых создаются. balance(A) = 250, balance(B) = 100. Инвариант: sum(all UTXO) = total emission. """ print("\n═══ ПРОВЕРКА 4: UTXO и балансы ═══") chain = self._create_chain() chain.create_genesis_window() # Эмиссия: A получает 350 Ɉ coinbase = create_coinbase_tx(self.addr_a, 350, t2_index=0) chain.create_tau1_window([coinbase]) # Проверяем баланс A balance_a = chain.utxo_set.get_balance(self.addr_a) self.assertEqual(balance_a, 350) print(f" Balance A после эмиссии: {balance_a} Ɉ ✅") # Transfer: A → B (100), сдача A (250) utxos_a = chain.utxo_set.get_utxos_for_address(self.addr_a) transfer = create_transfer_tx( utxos_to_spend=[(utxos_a[0]["tx_hash"], utxos_a[0]["output_idx"], utxos_a[0]["amount"])], recipient=self.addr_b, amount=100, change_address=self.addr_a, private_key=self.priv_a, public_key=self.pub_a, ) chain.create_tau1_window([transfer]) # Проверяем балансы balance_a = chain.utxo_set.get_balance(self.addr_a) balance_b = chain.utxo_set.get_balance(self.addr_b) self.assertEqual(balance_a, 250, f"Expected A=250, got {balance_a}") self.assertEqual(balance_b, 100, f"Expected B=100, got {balance_b}") print(f" Balance A: {balance_a} Ɉ ✅") print(f" Balance B: {balance_b} Ɉ ✅") # Инвариант: sum(all UTXO) = total emission total = chain.utxo_set.total_unspent() self.assertEqual(total, 350, f"Total UTXO should be 350 (emission), got {total}") print(f" Total supply: {total} Ɉ = emission ✅") # Supply invariant ok, msg = chain.utxo_set.verify_supply_invariant() self.assertTrue(ok, f"Supply invariant failed: {msg}") print(f" Supply invariant: {msg} ✅") # ═══════════════════════════════════════════════════════════════════════════ # ПРОВЕРКА 5: Эмиссия # ═══════════════════════════════════════════════════════════════════════════ def test_5_emission(self): """ Каждый τ₂: coinbase-транзакции, coins = seconds × halving_coefficient. TIME_BANK -= 600 секунд. После халвинга: тот же участник за 450 секунд получает 225 Ɉ (коэф 0.5). """ print("\n═══ ПРОВЕРКА 5: Эмиссия ═══") chain = self._create_chain() chain.create_genesis_window() # Создаём 10 τ₁ (для накопления headers) for i in range(TAU1_PER_TAU2): chain.create_tau1_window([]) # Эпоха 0: halving_coefficient = 1.0 # Участник A: 450 секунд → 450 Ɉ # Участник B: 600 секунд → 600 Ɉ coinbase_txs = [ create_coinbase_tx(self.addr_a, 450, t2_index=0), create_coinbase_tx(self.addr_b, 600, t2_index=0), ] tau2 = chain.finalize_tau2(coinbase_txs, halving_coefficient=1.0) self.assertIsNotNone(tau2) self.assertEqual(tau2.total_emissions, 1050) print(f" τ₂ #0: emissions = {tau2.total_emissions} Ɉ (450+600) ✅") # Балансы balance_a = chain.utxo_set.get_balance(self.addr_a) balance_b = chain.utxo_set.get_balance(self.addr_b) self.assertEqual(balance_a, 450) self.assertEqual(balance_b, 600) print(f" A = {balance_a} Ɉ, B = {balance_b} Ɉ ✅") # TIME_BANK self.assertEqual(chain.time_bank_spent, 600) print(f" TIME_BANK spent: {chain.time_bank_spent} sec ✅") # Эпоха 1: halving_coefficient = 0.5 # 10 ещё τ₁ for i in range(TAU1_PER_TAU2): chain.create_tau1_window([]) coinbase_txs_halved = [ create_coinbase_tx(self.addr_a, 225, t2_index=1), # 450 × 0.5 = 225 ] tau2_halved = chain.finalize_tau2(coinbase_txs_halved, halving_coefficient=0.5) self.assertIsNotNone(tau2_halved) self.assertEqual(tau2_halved.total_emissions, 225) self.assertEqual(tau2_halved.halving_coefficient, 0.5) print(f" τ₂ #1 (halved): emissions = {tau2_halved.total_emissions} Ɉ, coef = 0.5 ✅") # Проверяем что A теперь 450 + 225 = 675 balance_a = chain.utxo_set.get_balance(self.addr_a) self.assertEqual(balance_a, 675) print(f" A total = {balance_a} Ɉ (450 + 225) ✅") # ═══════════════════════════════════════════════════════════════════════════ # ПРОВЕРКА 6: Double Spend # ═══════════════════════════════════════════════════════════════════════════ def test_6_double_spend(self): """ Пытаешься потратить один UTXO дважды: Транзакция на B и одновременно на C, ссылаясь на тот же UTXO. Сеть принимает первую, вторую отвергает. """ print("\n═══ ПРОВЕРКА 6: Double Spend ═══") chain = self._create_chain() chain.create_genesis_window() # Эмиссия: A получает 500 Ɉ coinbase = create_coinbase_tx(self.addr_a, 500, t2_index=0) chain.create_tau1_window([coinbase]) utxos_a = chain.utxo_set.get_utxos_for_address(self.addr_a) self.assertEqual(len(utxos_a), 1) utxo = utxos_a[0] # TX1: A → B (300), сдача 200 на A tx1 = create_transfer_tx( utxos_to_spend=[(utxo["tx_hash"], utxo["output_idx"], utxo["amount"])], recipient=self.addr_b, amount=300, change_address=self.addr_a, private_key=self.priv_a, public_key=self.pub_a, ) # TX2: A → C (400), сдача 100 на A (тот же UTXO!) tx2 = create_transfer_tx( utxos_to_spend=[(utxo["tx_hash"], utxo["output_idx"], utxo["amount"])], recipient=self.addr_c, amount=400, change_address=self.addr_a, private_key=self.priv_a, public_key=self.pub_a, ) # Первая проходит chain.create_tau1_window([tx1]) balance_b = chain.utxo_set.get_balance(self.addr_b) self.assertEqual(balance_b, 300) print(f" TX1 (A→B 300): ACCEPTED ✅") # Вторая ДОЛЖНА быть отвергнута (UTXO уже потрачен) ok, msg = validate_transaction(tx2, chain.utxo_set) self.assertFalse(ok, "Double spend should be rejected") self.assertIn("already spent", msg) print(f" TX2 (A→C 400, same UTXO): REJECTED ✅ ({msg})") # Попытка включить в окно тоже должна провалиться with self.assertRaises(ValueError): chain.create_tau1_window([tx2]) print(f" Double spend в окне: REJECTED ✅") # ═══════════════════════════════════════════════════════════════════════════ # ПРОВЕРКА 7: Presence Proof Chain # ═══════════════════════════════════════════════════════════════════════════ def test_7_presence_proof_chain(self): """ Genesis → proof#1 → proof#2 → … Формат MONTANA_PRESENCE_V1 корректен. ML-DSA-65 подписи валидны. Временные метки строго вперёд. prev_hash совпадает с proof_hash предыдущего. """ print("\n═══ ПРОВЕРКА 7: Presence Proof Chain ═══") presence = PresenceChain( node_id=self.addr_a, private_key=self.priv_a, pubkey=self.pub_a, db_path=self.db_path, ) # Создаём 5 proofs proofs = [] for i in range(5): proof = presence.create_proof(t2_index=0) proofs.append(proof) time.sleep(0.01) # Гарантируем разные timestamps self.assertEqual(presence.proof_count, 5) print(f" Создано {presence.proof_count} proofs ✅") # Верификация цепочки ok, msg = presence.verify_chain() self.assertTrue(ok, f"Presence chain verification failed: {msg}") print(f" Верификация: {msg} ✅") # Проверяем формат каждого proof for proof in proofs: parsed = parse_presence_message(proof.message) self.assertIsNotNone(parsed, f"Proof #{proof.proof_number}: invalid format") self.assertEqual(parsed["version"], PRESENCE_VERSION) self.assertEqual(parsed["pubkey"], self.pub_a) print(f" Формат MONTANA_PRESENCE_V1: OK ✅") # Проверяем цепочку prev_hash prev_hash = GENESIS_HASH for proof in proofs: self.assertEqual(proof.prev_proof_hash, prev_hash) expected = compute_proof_hash(proof.message, proof.signature) self.assertEqual(proof.proof_hash, expected) prev_hash = proof.proof_hash print(f" Цепочка prev_hash: OK ✅") # Проверяем подписи ML-DSA-65 for proof in proofs: verified = verify_signature(proof.pubkey, proof.message, proof.signature) self.assertTrue(verified, f"Proof #{proof.proof_number}: invalid signature") print(f" ML-DSA-65 подписи: OK ✅") # ═══════════════════════════════════════════════════════════════════════════ # ПРОВЕРКА 8: Синхронизация узлов # ═══════════════════════════════════════════════════════════════════════════ def test_8_node_sync(self): """ Создаём chain на node A. Копируем БД на node B. Node B верифицирует от генезиса. Должен прийти к тому же UTXO set и тем же балансам. """ print("\n═══ ПРОВЕРКА 8: Синхронизация узлов ═══") # Node A создаёт chain chain_a = self._create_chain() chain_a.create_genesis_window() # Эмиссия + transfer coinbase = create_coinbase_tx(self.addr_a, 500, t2_index=0) chain_a.create_tau1_window([coinbase]) utxos = chain_a.utxo_set.get_utxos_for_address(self.addr_a) transfer = create_transfer_tx( utxos_to_spend=[(utxos[0]["tx_hash"], utxos[0]["output_idx"], utxos[0]["amount"])], recipient=self.addr_b, amount=200, change_address=self.addr_a, private_key=self.priv_a, public_key=self.pub_a, ) chain_a.create_tau1_window([transfer]) # Балансы на A balance_a_on_a = chain_a.utxo_set.get_balance(self.addr_a) balance_b_on_a = chain_a.utxo_set.get_balance(self.addr_b) print(f" Node A: A={balance_a_on_a}, B={balance_b_on_a}") # Node B: открываем ту же БД (симуляция синхронизации) chain_b = TimeChain( node_id=self.addr_b, private_key=self.priv_b, db_path=self.db_path, public_key=self.pub_b, strict_timestamps=False, ) # Регистрируем узел A для верификации его подписей chain_b.register_node(self.addr_a, self.pub_a) # Верификация на B ok, msg = chain_b.verify_full_chain() self.assertTrue(ok, f"Node B verification failed: {msg}") print(f" Node B verification: {msg} ✅") # Балансы совпадают balance_a_on_b = chain_b.utxo_set.get_balance(self.addr_a) balance_b_on_b = chain_b.utxo_set.get_balance(self.addr_b) self.assertEqual(balance_a_on_a, balance_a_on_b) self.assertEqual(balance_b_on_a, balance_b_on_b) print(f" Node B: A={balance_a_on_b}, B={balance_b_on_b} (identical) ✅") # UTXO counts self.assertEqual( chain_a.utxo_set.utxo_count(), chain_b.utxo_set.utxo_count(), ) print(f" UTXO count: {chain_a.utxo_set.utxo_count()} = {chain_b.utxo_set.utxo_count()} ✅") # ═══════════════════════════════════════════════════════════════════════════ # ПРОВЕРКА 9: API # ═══════════════════════════════════════════════════════════════════════════ def test_9_api(self): """ get_balance() возвращает confirmed, utxo_count, total_supply. get_stats() возвращает tau1_count, tau2_count, total_supply, utxo_count. """ print("\n═══ ПРОВЕРКА 9: API ═══") chain = self._create_chain() chain.create_genesis_window() # Эмиссия for i in range(TAU1_PER_TAU2): coinbase = create_coinbase_tx(self.addr_a, 60, t2_index=0) chain.create_tau1_window([coinbase]) # τ₂ finalize coinbase_t2 = [create_coinbase_tx(self.addr_a, 600, t2_index=0)] chain.finalize_tau2(coinbase_t2, halving_coefficient=1.0) # Balance API balance = chain.get_balance(self.addr_a) self.assertIn("confirmed", balance) self.assertIn("utxo_count", balance) self.assertIn("total_supply", balance) self.assertEqual(balance["confirmed"], 600 + 600) # 10 coinbase в τ₁ + 1 coinbase в τ₂ print(f" Balance API: {balance} ✅") # Stats API stats = chain.get_stats() self.assertEqual(stats["tau1_count"], 11) # genesis + 10 self.assertEqual(stats["tau2_count"], 1) self.assertGreater(stats["total_supply"], 0) self.assertGreater(stats["utxo_count"], 0) print(f" Stats API: τ₁={stats['tau1_count']}, τ₂={stats['tau2_count']}, supply={stats['total_supply']} ✅") # ═══════════════════════════════════════════════════════════════════════════ # БОНУС: Крипто-слой тест из спецификации # ═══════════════════════════════════════════════════════════════════════════ def test_crypto_layer(self): """ Генерируешь seed, выводишь ключи, получаешь адрес, подписываешь строку «Montana Genesis», верифицируешь подпись — работает. (Детерминистичность seed→keys требует BIP-39 + KDF, здесь тестируем ML-DSA-65 sign/verify цикл) """ print("\n═══ БОНУС: Крипто-слой ═══") # Генерация priv, pub = generate_keypair() addr = public_key_to_address(pub) self.assertTrue(validate_address(addr)) self.assertEqual(len(addr), 42) self.assertTrue(addr.startswith("mt")) print(f" Адрес: {addr} ✅") # Подпись message = "Montana Genesis" sig = sign_message(priv, message) self.assertTrue(len(sig) > 0) print(f" Подпись: {len(bytes.fromhex(sig))} bytes ✅") # Верификация verified = verify_signature(pub, message, sig) self.assertTrue(verified, "Signature should be valid") print(f" Верификация: OK ✅") # Изменение сообщения → верификация падает wrong_verified = verify_signature(pub, "Montana Genesis!", sig) self.assertFalse(wrong_verified, "Changed message should fail") print(f" Изменённое сообщение отвергнуто ✅") # Чужой ключ → верификация падает _, pub2 = generate_keypair() foreign_verified = verify_signature(pub2, message, sig) self.assertFalse(foreign_verified, "Foreign key should fail") print(f" Чужой ключ отвергнут ✅") # address(pubkey) → addr derived = public_key_to_address(pub) self.assertEqual(derived, addr) print(f" address(pubkey) = {derived} = {addr} ✅") # ═══════════════════════════════════════════════════════════════════════════════ # MAIN # ═══════════════════════════════════════════════════════════════════════════════ if __name__ == "__main__": print("=" * 70) print(" Montana Protocol — 9 Проверок Готовности Таймчейна") print(" ML-DSA-65 (FIPS 204) — Post-Quantum Security") print("=" * 70) unittest.main(verbosity=2)