diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 0000000..c73e032 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,23 @@ +name: Pylint + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Analysing the code with pylint + run: | + pylint $(git ls-files '*.py') diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..82f8dbd --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,70 @@ +# This workflow will upload a Python Package to PyPI when a release is created +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + release-build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Build release distributions + run: | + # NOTE: put your own distribution build steps here. + python -m pip install build + python -m build + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: release-dists + path: dist/ + + pypi-publish: + runs-on: ubuntu-latest + needs: + - release-build + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + + # Dedicated environments with protections for publishing are strongly recommended. + # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules + environment: + name: pypi + # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: + # url: https://pypi.org/p/YOURPROJECT + # + # ALTERNATIVE: if your GitHub Release name is the PyPI project version string + # ALTERNATIVE: exactly, uncomment the following line instead: + # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }} + + steps: + - name: Retrieve release distributions + uses: actions/download-artifact@v4 + with: + name: release-dists + path: dist/ + + - name: Publish release distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ diff --git a/Linux.server.Kert-One.py b/Linux.server.Kert-One.py deleted file mode 100644 index 6aa8111..0000000 --- a/Linux.server.Kert-One.py +++ /dev/null @@ -1,1445 +0,0 @@ -import hashlib -import json -import time -import threading -import sqlite3 -import os -from uuid import uuid4 -from flask import Flask, jsonify, request, send_file, render_template -import requests -from urllib.parse import urlparse -import socket -import ipaddress -import sys -from ecdsa import SigningKey, VerifyingKey, SECP256k1, BadSignatureError -import qrcode -from io import BytesIO -from datetime import datetime -import re -import shutil -from flask_cors import CORS - -# --- Configurações --- -DIFFICULTY = 1 # Dificuldade inicial para o bloco Gênese -MINING_REWARD = 50 # Recompensa padrão (será sobrescrita pela lógica de halving) -DATABASE = 'chain.db' -COIN_NAME = "Kert-One" -COIN_SYMBOL = "KERT" -PEERS_FILE = 'peers.json' -WALLET_FILE = "client_wallet.json" # Caminho para o arquivo da carteira do cliente - mantido para compatibilidade, mas não usado pela GUI - -# --- NÓS SEMENTES (SEED NODES) --- -# Importante: Se os nós semente usam HTTPS, seu nó local também deve ser acessível via HTTPS -# para comunicação bidirecional ideal em um ambiente de produção. -# Para testes locais, HTTP pode ser suficiente, mas pode haver problemas de conectividade -# com nós HTTPS públicos que tentam se conectar de volta ao seu nó HTTP. -SEED_NODES = [ - "https://seend.kert-one.com", - "https://seend2.kert-one.com", - "https://seend3.kert-one.com", -] - -app = Flask(__name__) -node_id = str(uuid4()).replace('-', '') -CORS(app) - -# Variáveis globais para mineração contínua -mining_active = False -miner_thread = None -miner_address_global = None # Endereço para onde as recompensas de mineração serão enviadas - -# --- Funções de Persistência de Peers --- -def salvar_peers(peers): - """Salva a lista de peers conhecidos em um arquivo JSON.""" - with open(PEERS_FILE, 'w') as f: - json.dump(list(peers), f) - print(f"[PEERS] Peers salvos: {len(peers)} peers.") - -def carregar_peers(): - """Carrega a lista de peers conhecidos de um arquivo JSON.""" - if not os.path.exists(PEERS_FILE): - print(f"[PEERS] Arquivo {PEERS_FILE} não encontrado. Iniciando com lista vazia.") - return [] - with open(PEERS_FILE, 'r') as f: - try: - peers = json.load(f) - print(f"[PEERS] {len(peers)} peers carregados de {PEERS_FILE}.") - return peers - except json.JSONDecodeError: - print(f"[ERRO] {PEERS_FILE} está corrompido ou vazio. Recriando.") - return [] - -known_nodes = set(carregar_peers()) - -blockchain = None -meu_url = None # Definido no main -meu_ip = None # Definido no main -port = None # Definido no main - -# --- Classe Blockchain --- -class Blockchain: - ADJUST_INTERVAL = 2016 # Blocos para recalcular dificuldade - TARGET_TIME = 600 # Tempo alvo entre blocos em segundos (10 minutos) - - def __init__(self, conn, node_id): - self.conn = conn - self.node_id = node_id - self._init_db() - self.chain = self._load_chain() - self.current_transactions = [] - - if not self.chain: - print("[BOOT] Criando bloco Gênese...") - genesis_difficulty = DIFFICULTY - self.new_block(proof=100, previous_hash='1', miner=self.node_id, initial_difficulty=genesis_difficulty) - - self.difficulty = self._calculate_difficulty_for_index(len(self.chain)) - print(f"[BOOT] Dificuldade inicial da cadeia: {self.difficulty}") - - @staticmethod - def hash(block): - """ - Cria um hash SHA-256 de um Bloco. - Ignora o campo 'transactions' e 'hash' (se presente) para hashing. - """ - block_string = json.dumps({k: v for k, v in block.items() if k not in ['transactions', 'hash']}, sort_keys=True) - return hashlib.sha256(block_string.encode()).hexdigest() - - def is_duplicate_transaction(self, new_tx): - """Verifica se uma transação já está na fila de transações pendentes ou em um bloco minerado.""" - # Verificar transações pendentes - for tx in self.current_transactions: - if tx.get('id') == new_tx.get('id'): - print(f"[DUPLICIDADE] Transação {new_tx.get('id')} já pendente.") - return True - # Verificação mais robusta para transações sem ID (embora todas devam ter) - if (tx.get('sender') == new_tx.get('sender') and - tx.get('recipient') == new_tx.get('recipient') and - tx.get('amount') == new_tx.get('amount') and - tx.get('fee') == new_tx.get('fee') and - tx.get('signature') == new_tx.get('signature')): - print(f"[DUPLICIDADE] Detectada transação pendente quase idêntica (sender={new_tx.get('sender')}, amount={new_tx.get('amount')}).") - return True - - # Verificar transações já mineradas - c = self.conn.cursor() - c.execute("SELECT 1 FROM txs WHERE id=?", (new_tx.get('id'),)) - if c.fetchone(): - print(f"[DUPLICIDADE] Transação {new_tx.get('id')} já minerada.") - return True - return False - - @staticmethod - def custom_asic_resistant_hash(data_bytes, nonce): - """Função de hash resistente a ASICs.""" - raw = data_bytes + str(nonce).encode() - h1 = hashlib.sha256(raw).digest() - h2 = hashlib.sha512(h1).digest() - h3 = hashlib.blake2b(h2).digest() - return hashlib.sha256(h3).hexdigest() - - def _init_db(self): - """Inicializa o esquema do banco de dados SQLite.""" - c = self.conn.cursor() - c.execute(''' - CREATE TABLE IF NOT EXISTS blocks( - index_ INTEGER PRIMARY KEY, - previous_hash TEXT, - proof INTEGER, - timestamp REAL, - miner TEXT, - difficulty INTEGER - ) - ''') - c.execute(''' - CREATE TABLE IF NOT EXISTS txs( - id TEXT PRIMARY KEY, - sender TEXT, - recipient TEXT, - amount TEXT, - fee TEXT, - signature TEXT, - block_index INTEGER, - public_key TEXT - ) - ''') - self.conn.commit() - print("[DB] Esquema do banco de dados inicializado/verificado.") - - def _load_chain(self): - """Carrega a cadeia de blocos do banco de dados.""" - c = self.conn.cursor() - c.execute("SELECT index_, previous_hash, proof, timestamp, miner, difficulty FROM blocks ORDER BY index_") - chain = [] - for idx, prev, proof, ts, miner, difficulty in c.fetchall(): - c.execute("SELECT id, sender, recipient, amount, fee, signature, public_key FROM txs WHERE block_index=?", (idx,)) - txs = [] - for r in c.fetchall(): - txs.append(dict(id=r[0], sender=r[1], recipient=r[2], - amount=r[3], - fee=r[4], - signature=r[5], public_key=r[6])) - block = { - 'index': idx, - 'previous_hash': prev, - 'proof': proof, - 'timestamp': ts, - 'miner': miner, - 'transactions': txs, - 'difficulty': difficulty - } - chain.append(block) - print(f"[DB] Cadeia carregada com {len(chain)} blocos.") - return chain - - def new_block(self, proof, previous_hash, miner, initial_difficulty=None): - """Cria um novo bloco e o adiciona à cadeia.""" - block_index = len(self.chain) + 1 - reward = self._get_mining_reward(block_index) - - difficulty = self._calculate_difficulty_for_index(block_index) if initial_difficulty is None else initial_difficulty - - # Adiciona a transação de recompensa (coinbase) ao início das transações do bloco - mining_reward_tx = { - 'id': str(uuid4()), 'sender': '0', 'recipient': miner, - 'amount': f"{reward:.8f}", 'fee': f"{0.0:.8f}", 'signature': '', 'public_key': '' - } - - # Cria uma cópia das transações pendentes para o novo bloco - transactions_for_block = list(self.current_transactions) - transactions_for_block.insert(0, mining_reward_tx) # Insere a recompensa - - block = { - 'index': block_index, - 'previous_hash': previous_hash, - 'proof': proof, - 'timestamp': time.time(), - 'miner': miner, - 'transactions': transactions_for_block, # Usa as transações preparadas - 'difficulty': difficulty - } - - self.chain.append(block) - - self._save_block(block) # Salva o novo bloco no DB - - # Remove as transações que foram incluídas no bloco da lista de transações pendentes - mined_tx_ids = {tx['id'] for tx in transactions_for_block if tx['sender'] != '0'} - self.current_transactions = [tx for tx in self.current_transactions if tx['id'] not in mined_tx_ids] - print(f"[BLOCK] Novo bloco {block['index']} forjado com {len(transactions_for_block)} transações.") - - return block - - def _save_block(self, block): - """Salva um bloco e suas transações no banco de dados.""" - c = self.conn.cursor() - c.execute("INSERT INTO blocks VALUES (?, ?, ?, ?, ?, ?)", - (block['index'], block['previous_hash'], block['proof'], - block['timestamp'], block['miner'], block['difficulty'])) - for t in block['transactions']: - c.execute("INSERT INTO txs VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - (t['id'], t['sender'], t['recipient'], t['amount'], - t['fee'], t['signature'], block['index'], t.get('public_key', ''))) - self.conn.commit() - - def new_tx(self, sender, recipient, amount_str, fee_str, signature, public_key): - """Adiciona uma nova transação à lista de transações pendentes. - amount_str e fee_str já devem ser strings formatadas.""" - tx = { - 'id': str(uuid4()), 'sender': sender, 'recipient': recipient, - 'amount': amount_str, 'fee': fee_str, 'signature': signature, 'public_key': public_key - } - if self.is_duplicate_transaction(tx): - print(f"[TX] Transação {tx.get('id', '')} já pendente ou minerada. Ignorando.") - return -1 - - self.current_transactions.append(tx) - print(f"[TX] Nova transação adicionada: {tx['id']}") - return self.last_block()['index'] + 1 if self.chain else 1 - - def _get_mining_reward(self, block_index): - """Calcula a recompensa de mineração com base no índice do bloco (halving).""" - if block_index <= 1200: - return 50.0 - elif block_index <= 2200: - return 25.0 - elif block_index <= 4000: - return 12.5 - elif block_index <= 5500: - return 6.5 - elif block_index <= 6200: - return 3.25 - elif block_index <= 20000: - return 1.25 - elif block_index <= 1000000: - return 0.03 - else: - halvings = (block_index - 1000000) // 2100000 - base_reward = 0.03 - reward = base_reward / (2 ** halvings) - return max(reward, 0.0) - - def last_block(self): - """Retorna o último bloco da cadeia.""" - return self.chain[-1] if self.chain else None - - def proof_of_work(self, last_proof): - """ - Encontra uma prova de trabalho que satisfaça os requisitos de dificuldade. - Retorna a prova (nonce) ou -1 se a mineração for abortada. - """ - difficulty_for_pow = self._calculate_difficulty_for_index(len(self.chain) + 1) - proof = 0 - print(f"Iniciando mineração com dificuldade {difficulty_for_pow}...") - start_time = time.time() - - while not self.valid_proof(last_proof, proof, difficulty_for_pow): - global mining_active # Usa a variável de controle da mineração contínua - if not mining_active: # Verifica o flag de mineração - print("[Miner] Sinal para parar recebido durante PoW. Abortando mineração.") - return -1 - - # Verifica se um novo bloco chegou enquanto estamos minerando - # Isso é crucial para evitar mineração em uma cadeia desatualizada - if self.last_block()['proof'] != last_proof: - print("[Miner] Outro bloco chegou na cadeia principal durante PoW. Abortando e reiniciando.") - return -1 - - if time.time() - start_time > 10 and proof % 100000 == 0: - print(f" Tentativa: {proof}") - proof += 1 - print(f"Mineração concluída: proof = {proof}") - return proof - - @staticmethod - def valid_proof(last_proof, proof, difficulty): - """ - Valida se um dado hash de prova satisfaz os requisitos de dificuldade. - """ - guess = f"{last_proof}{proof}".encode() - guess_hash = Blockchain.custom_asic_resistant_hash(guess, proof) - return guess_hash[:difficulty] == "0" * difficulty - - def tx_already_mined(self, tx_id): - """Verifica se uma transação com o dado ID já foi minerada em algum bloco.""" - c = self.conn.cursor() - c.execute("SELECT 1 FROM txs WHERE id=?", (tx_id,)) - return c.fetchone() is not None - - def valid_chain(self, chain): - """ - Determina se uma dada cadeia de blocos é válida. - Verifica hashes, provas de trabalho, transações e dificuldade. - """ - if not chain: - print("[VAL_CHAIN_ERRO] Cadeia vazia.") - return False - - # Verifica o bloco Gênese - if chain[0]['index'] != 1 or chain[0]['previous_hash'] != '1' or chain[0]['proof'] != 100: - print("[VAL_CHAIN_ERRO] Bloco Gênese inválido.") - return False - - for idx in range(1, len(chain)): - prev = chain[idx - 1] - curr = chain[idx] - - prev_hash = self.hash(prev) - - if curr['previous_hash'] != prev_hash: - print(f"[VAL_CHAIN_ERRO] Hash anterior incorreto no bloco {curr['index']}. Esperado: {prev_hash}, Obtido: {curr['previous_hash']}.") - return False - - block_declared_difficulty = curr.get('difficulty', DIFFICULTY) - - if not self.valid_proof(prev['proof'], curr['proof'], block_declared_difficulty): - hash_check = self.custom_asic_resistant_hash(f"{prev['proof']}{curr['proof']}".encode(), curr['proof']) - print(f"[VAL_CHAIN_ERRO] Proof of Work inválido no bloco {curr['index']} com dificuldade {block_declared_difficulty}. Hash: {hash_check}") - return False - - for tx in curr.get('transactions', []): - # Ignora transações de recompensa (coinbase) na validação de assinatura - if tx['sender'] == '0': - # Valida o destinatário e o valor da recompensa - if tx['recipient'] != curr['miner']: - print(f"[VAL_CHAIN_ERRO] TX de recompensa inválida no bloco {curr['index']}: Recipiente incorreto.") - return False - expected_reward = self._get_mining_reward(curr['index']) - # Comparar recompensas como floats, mas tx['amount'] é string - if abs(float(tx['amount']) - expected_reward) > 0.000001: # Usar tolerância para floats - print(f"[VAL_CHAIN_ERRO] TX de recompensa inválida no bloco {curr['index']}: Valor incorreto. Esperado: {expected_reward}, Obtido: {tx['amount']}") - return False - continue # Pula para a próxima transação se for de recompensa - - try: - # Deriva o endereço do remetente da chave pública para verificação - pk_for_address_derivation = tx['public_key'] - # Remove o prefixo '04' se presente, que indica chave pública não comprimida - if pk_for_address_derivation.startswith('04') and len(pk_for_address_derivation) == 130: - pk_for_address_derivation = pk_for_address_derivation[2:] - - derived_address = hashlib.sha256(bytes.fromhex(pk_for_address_derivation)).hexdigest()[:40] - if derived_address != tx['sender']: - print(f"[VAL_CHAIN_ERRO] Transação {tx['id']} no bloco {curr['index']}: Endereço ({tx['sender']}) não bate com o derivado da chave pública ({derived_address}).") - return False - - # CRÍTICO: Garantir que amount e fee são strings formatadas para a verificação - # Sempre converte para float primeiro, depois formata para string com .8f - amount_to_verify = f"{float(tx['amount']):.8f}" - fee_to_verify = f"{float(tx['fee']):.8f}" - - tx_copy_for_signature = { - 'amount': amount_to_verify, - 'fee': fee_to_verify, - 'recipient': tx['recipient'], - 'sender': tx['sender'] - } - message = json.dumps(tx_copy_for_signature, sort_keys=True, separators=(",", ":")).encode() - - # Verifica a assinatura da transação - vk = VerifyingKey.from_string(bytes.fromhex(tx['public_key']), curve=SECP256k1) - vk.verify_digest(bytes.fromhex(tx['signature']), hashlib.sha256(message).digest()) - - except BadSignatureError: - print(f"[VAL_CHAIN_ERRO] Transação {tx['id']} inválida no bloco {curr['index']}: Assinatura inválida.") - return False - except Exception as e: - print(f"[VAL_CHAIN_ERRO] Transação {tx['id']} inválida no bloco {curr['index']}: Erro inesperado durante validação: {e}") - return False - return True - - def _calculate_difficulty_for_index(self, target_block_index): - """ - Calcula a dificuldade esperada para um dado índice de bloco. - Implementa o ajuste de dificuldade do Bitcoin. - """ - if target_block_index <= self.ADJUST_INTERVAL: - return DIFFICULTY - - # Se a cadeia ainda não tem blocos suficientes para o intervalo de ajuste, - # usa a dificuldade do último bloco ou a dificuldade padrão. - if len(self.chain) < self.ADJUST_INTERVAL: - return self.chain[-1].get('difficulty', DIFFICULTY) if self.chain else DIFFICULTY - - # Índices dos blocos que definem a janela de tempo para o cálculo da dificuldade - start_block_for_calc_index = len(self.chain) - self.ADJUST_INTERVAL - end_block_for_calc_index = len(self.chain) - 1 - - # Garantir que os índices estão dentro dos limites da cadeia existente - if start_block_for_calc_index < 0 or end_block_for_calc_index >= len(self.chain): - # Isso pode acontecer se a cadeia for muito curta para o intervalo completo - # Neste caso, usamos a dificuldade do último bloco ou a dificuldade padrão. - return self.chain[-1].get('difficulty', DIFFICULTY) if self.chain else DIFFICULTY - - start_block_for_calc = self.chain[start_block_for_calc_index] - end_block_for_calc = self.chain[end_block_for_calc_index] - - actual_window_time = end_block_for_calc['timestamp'] - start_block_for_calc['timestamp'] - expected_time = self.TARGET_TIME * self.ADJUST_INTERVAL - - current_calculated_difficulty = end_block_for_calc.get('difficulty', DIFFICULTY) - - new_difficulty = current_calculated_difficulty - # Ajusta a dificuldade com base no tempo real vs. tempo esperado - if actual_window_time < expected_time / 4: - new_difficulty += 2 - elif actual_window_time < expected_time / 2: - new_difficulty += 1 - elif actual_window_time > expected_time * 4 and new_difficulty > 1: - new_difficulty -= 2 - elif actual_window_time > expected_time * 2 and new_difficulty > 1: - new_difficulty -= 1 - - return max(1, new_difficulty) # Dificuldade mínima é 1 - - def get_total_difficulty(self, chain_to_check): - """Calcula a dificuldade acumulada de uma cadeia.""" - total_difficulty = 0 - for block in chain_to_check: - total_difficulty += block.get('difficulty', DIFFICULTY) - return total_difficulty - - def resolve_conflicts(self): - """ - Implementa o algoritmo de consenso para resolver conflitos na cadeia. - Substitui a cadeia local pela mais longa e válida da rede. - """ - global known_nodes # Acessa a variável global known_nodes - neighbors = list(known_nodes) # Cria uma cópia para iterar - new_chain = None - current_total_difficulty = self.get_total_difficulty(self.chain) - - print(f"[CONSENSO] Tentando resolver conflitos com {len(neighbors)} vizinhos... Cadeia local dificuldade: {current_total_difficulty}") - - peers_to_remove_during_conflict_resolution = set() - - for node_url in neighbors: - if node_url == meu_url: - continue # Não tentar resolver conflito consigo mesmo - try: - print(f"[CONSENSO] Buscando cadeia de {node_url}...") - response = requests.get(f"{node_url}/chain", timeout=10) - if response.status_code == 200: - data = response.json() - peer_chain = data.get("chain") - - if not peer_chain: - print(f"[CONSENSO] Resposta malformada (sem 'chain') de {node_url}. Marcando peer para remoção.") - peers_to_remove_during_conflict_resolution.add(node_url) - continue - - peer_total_difficulty = self.get_total_difficulty(peer_chain) - - print(f"[CONSENSO] Node {node_url}: Dificuldade Total={peer_total_difficulty}, Comprimento={len(peer_chain)}. Local Comprimento={len(self.chain)}") - - # Prioriza a cadeia com maior dificuldade total - if peer_total_difficulty > current_total_difficulty and self.valid_chain(peer_chain): - current_total_difficulty = peer_total_difficulty - new_chain = peer_chain - print(f"[CONSENSO] ✔ Cadeia mais difícil e válida encontrada em {node_url} (Dificuldade: {peer_total_difficulty})") - else: - print(f"[CONSENSO] ✘ Cadeia de {node_url} (Dificuldade: {peer_total_difficulty}) não é mais difícil ou não é válida.") - else: - print(f"[CONSENSO] Resposta inválida de {node_url}: Status {response.status_code}. Marcando peer para remoção.") - peers_to_remove_during_conflict_resolution.add(node_url) - except requests.exceptions.RequestException as e: - print(f"[CONSENSO] Erro ao buscar cadeia de {node_url}: {e}. Marcando peer para remoção.") - peers_to_remove_during_conflict_resolution.add(node_url) - except Exception as e: - print(f"[CONSENSO] Erro inesperado ao processar cadeia de {node_url}: {e}. Marcando peer para remoção.") - peers_to_remove_during_conflict_resolution.add(node_url) - - # Remove peers problemáticos APÓS a iteração - if peers_to_remove_during_conflict_resolution: - for peer in peers_to_remove_during_conflict_resolution: - if peer not in SEED_NODES: # Não remove nós semente automaticamente - known_nodes.discard(peer) - print(f"[CONSENSO] Removido peer problemático: {peer}") - salvar_peers(known_nodes) - - if new_chain: - # Identifica transações da cadeia antiga que não estão na nova cadeia - old_chain_tx_ids = set() - for block in self.chain: - for tx in block.get('transactions', []): - old_chain_tx_ids.add(tx['id']) - - new_chain_tx_ids = set() - for block in new_chain: - for tx in block.get('transactions', []): - new_chain_tx_ids.add(tx['id']) - - re_add_txs = [] - # Adiciona transações da cadeia antiga que não foram incluídas na nova cadeia - for block in self.chain: - for tx in block.get('transactions', []): - if tx['id'] not in new_chain_tx_ids and tx['sender'] != '0': # Ignora TXs de recompensa - re_add_txs.append(tx) - - # Adiciona transações pendentes atuais que não foram incluídas na nova cadeia - for tx in self.current_transactions: - if tx['id'] not in new_chain_tx_ids: - re_add_txs.append(tx) - - # Limpa as transações pendentes e as re-adiciona (evitando duplicatas) - self.current_transactions = [] - for tx in re_add_txs: - temp_tx_for_duplicate_check = { - 'sender': tx['sender'], - 'recipient': tx['recipient'], - 'amount': tx['amount'], - 'fee': tx['fee'], - 'id': tx.get('id') - } - if not self.is_duplicate_transaction(temp_tx_for_duplicate_check): - self.current_transactions.append(tx) - - self.chain = new_chain - self._rebuild_db_from_chain() - print(f"[CONSENSO] ✅ Cadeia substituída com sucesso pela mais difícil e válida (Dificuldade: {current_total_difficulty}). {len(re_add_txs)} transações re-adicionadas à fila pendente.") - return True - - print("[CONSENSO] 🔒 Cadeia local continua sendo a mais difícil ou nenhuma cadeia mais difícil/válida foi encontrada.") - return False - - def _rebuild_db_from_chain(self): - """Reconstrói o banco de dados local a partir da cadeia atual (usado após consenso).""" - print("[REBUILD] Reconstruindo dados locais a partir da nova cadeia...") - try: - c = self.conn.cursor() - c.execute("DELETE FROM txs") # Deleta transações primeiro para evitar FK issues - c.execute("DELETE FROM blocks") - - - for block in self.chain: - difficulty_to_save = block.get('difficulty', DIFFICULTY) - c.execute("INSERT INTO blocks VALUES (?, ?, ?, ?, ?, ?)", - (block['index'], block['previous_hash'], block['proof'], - block['timestamp'], block['miner'], difficulty_to_save)) - for tx in block['transactions']: - c.execute("INSERT INTO txs VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - (tx['id'], tx['sender'], tx['recipient'], tx['amount'], - tx['fee'], tx['signature'], block['index'], tx.get('public_key', ''))) - self.conn.commit() - print("[REBUILD] Banco reconstruído com sucesso.") - except Exception as e: - print(f"[REBUILD] Erro ao reconstruir banco: {e}") - sys.exit(1) # Saída em caso de erro crítico na reconstrução do DB - - def balance(self, address): - """Calcula o saldo de um endereço, incluindo transações pendentes.""" - bal = 0.0 - for block in self.chain: - for t in block['transactions']: - if t['sender'] == address: - bal -= (float(t['amount']) + float(t['fee'])) - if t['recipient'] == address: - bal += float(t['amount']) - - for t in self.current_transactions: - if t['sender'] == address: - bal -= (float(t['amount']) + float(t['fee'])) - if t['recipient'] == address: - bal += float(t['amount']) - return bal - -# --- Funções de Criptografia e Carteira --- -def gerar_endereco(public_key_hex): - """Gera um endereço de carteira a partir de uma chave pública hexadecimal.""" - try: - if public_key_hex.startswith("04"): - public_key_hex = public_key_hex[2:] - public_key_bytes = bytes.fromhex(public_key_hex) - return hashlib.sha256(public_key_bytes).hexdigest()[:40] - except ValueError as e: - print(f"[ERRO] Falha ao gerar endereço: {e}") - return None - -def sign_transaction(private_key_hex, tx_data): - """ - Assina uma transação com a chave privada ECDSA (SECP256k1). - tx_data deve ter: 'sender', 'recipient', 'amount' (string), 'fee' (string). - Retorna a assinatura em hex. - """ - sk = SigningKey.from_string(bytes.fromhex(private_key_hex), curve=SECP256k1) - - message_data = { - 'amount': tx_data['amount'], - 'fee': tx_data['fee'], - 'recipient': tx_data['recipient'], - 'sender': tx_data['sender'] - } - - message_json = json.dumps( - message_data, - sort_keys=True, - separators=(',',':') - ).encode('utf-8') - - # print(f"DEBUG_SIGN: JSON da mensagem para assinatura (decodificado): {message_json.decode('utf-8')}") - # print(f"DEBUG_SIGN: Bytes da mensagem para assinatura (raw): {message_json}") - # print(f"DEBUG_SIGN: Hash da mensagem para assinatura (SHA256, HEX): {hashlib.sha256(message_json).hexdigest()}") - - message_hash = hashlib.sha256(message_json).digest() - return sk.sign_digest(message_hash).hex() - -def create_wallet(): - """Cria e retorna dados de uma nova carteira.""" - private_key_obj = SigningKey.generate(curve=SECP256k1) - public_key_obj = private_key_obj.get_verifying_key() - private_key_hex = private_key_obj.to_string().hex() - public_key_hex = "04" + public_key_obj.to_string().hex() - address = gerar_endereco(public_key_hex) - - if address is None: - print("[ERRO] Falha ao criar carteira: Endereço não pôde ser gerado.") - return None - - return { - 'private_key': private_key_hex, - 'public_key': public_key_hex, - 'address': address - } - -def load_wallet_file(filepath): - """Carrega dados da carteira de um arquivo JSON.""" - if os.path.exists(filepath): - try: - with open(filepath, 'r') as f: - wallet_data = json.load(f) - if 'public_key' in wallet_data: - derived_addr_check = gerar_endereco(wallet_data['public_key']) - if derived_addr_check and derived_addr_check != wallet_data.get('address'): - print(f"[WALLET] Endereço na carteira desatualizado. Atualizando de {wallet_data.get('address')} para {derived_addr_check}") - wallet_data['address'] = derived_addr_check - with open(filepath, "w") as fw: - json.dump(wallet_data, fw, indent=4) - return wallet_data - except (json.JSONDecodeError, FileNotFoundError) as e: - print(f"[ERRO] Falha ao carregar carteira de {filepath}: {e}") - return None - return None - -def save_wallet_file(wallet_data, filepath): - """Salva dados da carteira em um arquivo JSON.""" - try: - with open(filepath, 'w') as f: - json.dump(wallet_data, f, indent=4) - print(f"[WALLET] Carteira salva em {filepath}.") - except Exception as e: - print(f"[ERRO] Falha ao salvar carteira em {filepath}: {e}") - -# --- Flask Endpoints (do nó) --- -@app.route('/', methods=['GET']) -def index_web(): - return "Kert-One Blockchain Node is running!" - -@app.route('/miner') -def miner_web(): - return "Kert-One Miner Interface (via Web)" - -@app.route('/chain', methods=['GET']) -def chain_api(): - response = { - 'chain': blockchain.chain, - 'length': len(blockchain.chain), - 'pending_transactions': blockchain.current_transactions, - 'coin_name': COIN_NAME, - 'coin_symbol': COIN_SYMBOL, - 'node_id': node_id - } - return jsonify(response), 200 - -@app.route('/nodes/register', methods=['POST']) -def register_nodes_api(): - """ - Registra um novo nó na lista de peers conhecidos. - Espera a URL completa do nó no payload. - """ - data = request.get_json() - new_node_url = data.get('url') # Agora espera a URL completa - - if not new_node_url: - print(f"[ERRO 400] URL do nó ausente na requisição de registro.") - return jsonify({"message": "URL do nó inválida/ausente."}), 400 - - # Validação básica da URL - if not (new_node_url.startswith('http://') or new_node_url.startswith('https://')): - print(f"[ERRO 400] URL do nó inválida: {new_node_url}. Deve começar com http:// ou https://") - return jsonify({"message": "URL do nó inválida. Deve começar com http:// ou https://."}), 400 - - if new_node_url != meu_url: - if new_node_url not in known_nodes: - known_nodes.add(new_node_url) - salvar_peers(known_nodes) - print(f"[INFO] Peer {new_node_url} registrado.") - else: - print(f"[INFO] Peer {new_node_url} já estava registrado.") - else: - print(f"[INFO] Recebi meu próprio registro: {new_node_url}. Ignorando.") - - return jsonify({ - "message": f"Peer {new_node_url} registrado ou atualizado.", - "known_peers": list(known_nodes) - }), 200 - -@app.route('/nodes', methods=['GET']) -def get_nodes_api(): - return jsonify({'nodes': list(known_nodes)}), 200 - -@app.route('/nodes/resolve', methods=['GET']) -def resolve_api(): - replaced = blockchain.resolve_conflicts() - if replaced: - response = {'message': 'Nossa cadeia foi substituída pela mais longa e válida.'} - else: - response = {'message': 'Nossa cadeia é a mais longa ou nenhuma cadeia mais longa/válida foi encontrada.'} - return jsonify(response), 200 - -@app.route('/balance/', methods=['GET']) -def balance_api(addr): - return jsonify({ - 'address': addr, - 'balance': blockchain.balance(addr), - 'coin_name': COIN_NAME, - 'coin_symbol': COIN_SYMBOL - }), 200 - -@app.route('/transactions/pending', methods=['GET']) -def pending_transactions(): - """Retorna a lista de transações pendentes.""" - return jsonify(blockchain.current_transactions), 200 - -@app.route('/tx/new', methods=['POST']) -def new_transaction_api(): - """Recebe uma nova transação do cliente e a adiciona à fila pendente.""" - # print(f"DEBUG_SERVER: Requisição recebida para /tx/new") - # print(f"DEBUG_SERVER: Headers da requisição: {request.headers}") - # print(f"DEBUG_SERVER: Mimetype da requisição: {request.mimetype}") - # print(f"DEBUG_SERVER: Content-Type da requisição: {request.content_type}") - # print(f"DEBUG_SERVER: Dados da requisição (raw): {request.data}") - - raw_values = None - try: - raw_values = request.get_json(silent=True) - # print(f"DEBUG_SERVER: Payload JSON parseado (request.get_json()): {raw_values}") - except Exception as e: - print(f"DEBUG_SERVER: ERRO - Exceção durante o parsing JSON: {e}") - - if raw_values is None: - print(f"DEBUG_SERVER: ERRO - request.get_json() retornou None. Verifique o Content-Type ou a validade do JSON.") - return jsonify({'message': 'Erro: Não foi possível parsear o JSON da requisição. Verifique o Content-Type ou a validade do JSON.'}), 400 - - values = raw_values - - required = ['id', 'sender', 'recipient', 'amount', 'fee', 'public_key', 'signature'] - if not all(k in values for k in required): - missing = [k for k in required if k not in values] - print(f"[ERRO 400] Valores ausentes na transação: {missing}") - return jsonify({'message': f'Valores ausentes na requisição: {", ".join(missing)}'}), 400 - - try: - amount_float = float(values['amount']) - fee_float = float(values['fee']) - amount_str_formatted = f"{amount_float:.8f}" - fee_str_formatted = f"{fee_float:.8f}" - - if fee_float <= 0: - print(f"[ERRO 400] Taxa de transação inválida: {fee_float}. A taxa deve ser maior que 0.") - return jsonify({'message': 'Taxa de transação inválida. A taxa deve ser maior que 0.'}), 400 - - transaction = { - 'id': values['id'], - 'sender': values['sender'], - 'recipient': values['recipient'], - 'amount': amount_str_formatted, - 'fee': fee_str_formatted, - 'public_key': values['public_key'], - 'signature': values['signature'], - 'timestamp': values.get('timestamp', time.time()) # Usar timestamp fornecido ou atual - } - except ValueError as e: - print(f"[ERRO 400] Erro de conversão de tipo na transação: {e}") - return jsonify({'message': f'Erro ao processar dados numéricos da transação: {e}'}), 400 - except Exception as e: - print(f"[ERRO 400] Erro inesperado ao construir transação: {e}") - return jsonify({'message': f'Erro ao processar dados da transação: {e}'}), 400 - - temp_tx_for_duplicate_check = { - 'sender': transaction['sender'], - 'recipient': transaction['recipient'], - 'amount': transaction['amount'], - 'fee': transaction['fee'], - 'id': transaction.get('id') - } - if blockchain.is_duplicate_transaction(temp_tx_for_duplicate_check): - print(f"[AVISO] Transação duplicada detectada para {transaction['sender']} -> {transaction['recipient']}. Ignorando.") - return jsonify({'message': 'Transação duplicada detectada. Não adicionada novamente.'}), 200 - - try: - pk_for_address_derivation = transaction['public_key'] - if pk_for_address_derivation.startswith('04') and len(pk_for_address_derivation) == 130: - pk_for_address_derivation = pk_for_address_derivation[2:] - - derived_address = hashlib.sha256(bytes.fromhex(pk_for_address_derivation)).hexdigest()[:40] - if derived_address != transaction['sender']: - print(f"[ERRO 400] Assinatura inválida: Endereço do remetente ({transaction['sender']}) não corresponde à chave pública fornecida ({derived_address}).") - return jsonify({'message': 'Assinatura inválida: Endereço do remetente não corresponde à chave pública fornecida'}), 400 - - if not verify_signature(transaction['public_key'], transaction['signature'], transaction): - print(f"[ERRO 400] Assinatura inválida ou chave pública malformada para TX ID: {transaction.get('id')}") - return jsonify({'message': 'Assinatura inválida ou chave pública malformada: Falha na verificação da assinatura'}), 400 - - except Exception as e: - print(f"[ERRO 400] Erro inesperado na validação da assinatura: {e}. TX ID: {transaction.get('id')}") - return jsonify({'message': f'Erro inesperado na validação da transação: {e}'}), 400 - - current_balance = blockchain.balance(transaction['sender']) - required_amount = float(transaction['amount']) + float(transaction['fee']) - if current_balance < required_amount: - print(f"[ERRO 400] Saldo insuficiente para {transaction['sender']}: Necessário {required_amount}, Disponível {current_balance}. TX ID: {transaction.get('id')}") - return jsonify({'message': f'Saldo insuficiente para a transação. Saldo atual: {current_balance}, Necessário: {required_amount}'}), 400 - - blockchain.current_transactions.append(transaction) - - broadcast_tx_to_peers(transaction) - - response = {'message': f'Transação {transaction["id"]} adicionada à fila de transações pendentes.', - 'coin_name': COIN_NAME, - 'coin_symbol': COIN_SYMBOL, - 'transaction_id': transaction['id']} - return jsonify(response), 201 - -def broadcast_tx_to_peers(tx): - """Envia uma transação para todos os peers conhecidos.""" - print(f"[Broadcast TX] Enviando transação {tx.get('id')} para {len(known_nodes)} peers.") - peers_to_remove = set() - for peer in known_nodes.copy(): - if peer == meu_url: continue - try: - requests.post(f"{peer}/tx/receive", json=tx, timeout=3) - except requests.exceptions.RequestException as e: - print(f"[Broadcast TX] Erro ao enviar TX para {peer}: {e}. Marcando peer para remoção (se não for seed).") - if peer not in SEED_NODES: - peers_to_remove.add(peer) - except Exception as e: - print(f"[Broadcast TX] Erro inesperado ao enviar TX para {peer}: {e}. Marcando peer para remoção (se não for seed).") - if peer not in SEED_NODES: - peers_to_remove.add(peer) - - if peers_to_remove: - known_nodes.difference_update(peers_to_remove) - salvar_peers(known_nodes) - print(f"[Broadcast TX] Removidos {len(peers_to_remove)} peers problemáticos.") - -@app.route('/tx/receive', methods=['POST']) -def receive_transaction_api(): - """Recebe uma transação de outro nó e a adiciona à fila pendente após validação.""" - tx_data = request.get_json() - if not tx_data: - print("[RECEIVE_TX ERROR] Nenhum dado de transação recebido.") - return jsonify({"message": "Nenhum dado de transação recebido."}), 400 - - required = ['id', 'sender', 'recipient', 'amount', 'fee', 'public_key', 'signature'] - if not all(k in tx_data for k in required): - print(f"[RECEIVE_TX ERROR] Dados de transação incompletos: {tx_data}") - return jsonify({'message': 'Dados de transação incompletos.'}), 400 - - try: - amount_float = float(tx_data['amount']) - fee_float = float(tx_data['fee']) - amount_str_formatted = f"{amount_float:.8f}" - fee_str_formatted = f"{fee_float:.8f}" - - if fee_float <= 0: - print(f"[RECEIVE TX ERROR] Taxa de transação inválida: {fee_float}. A taxa deve ser maior que 0.") - return jsonify({'message': 'Transação inválida: A taxa deve ser maior que 0.'}), 400 - - temp_tx_for_duplicate_check = { - 'sender': tx_data['sender'], - 'recipient': tx_data['recipient'], - 'amount': amount_str_formatted, - 'fee': fee_str_formatted, - 'id': tx_data.get('id') - } - if blockchain.is_duplicate_transaction(temp_tx_for_duplicate_check): - print(f"[RECEIVE TX] Transação {tx_data.get('id')} já existe na fila pendente ou minerada. Ignorando.") - return jsonify({'message': 'Transação já conhecida.'}), 200 - - tx_for_verification = { - 'id': tx_data['id'], - 'sender': tx_data['sender'], - 'recipient': tx_data['recipient'], - 'amount': amount_str_formatted, - 'fee': fee_str_formatted, - 'public_key': tx_data['public_key'], - 'signature': tx_data['signature'], - 'timestamp': tx_data.get('timestamp', time.time()) - } - - if not verify_signature(tx_for_verification['public_key'], tx_for_verification['signature'], tx_for_verification): - print(f"[RECEIVE TX ERROR] TX {tx_data.get('id')}: Assinatura inválida ou chave pública malformada.") - return jsonify({'message': 'Transação inválida: Assinatura inválida ou chave pública malformada.'}), 400 - - current_balance = blockchain.balance(tx_data['sender']) - required_amount = float(tx_data['amount']) + float(tx_data['fee']) - if current_balance < required_amount: - print(f"[RECEIVE TX ERROR] TX {tx_data.get('id')}: Saldo insuficiente para {tx_data['sender']}. Necessário: {required_amount}, Disponível: {current_balance}") - return jsonify({'message': 'Transação inválida: Saldo insuficiente.'}), 400 - - blockchain.current_transactions.append(tx_for_verification) - print(f"[RECEIVE TX] Transação {tx_data.get('id')} recebida e adicionada à fila pendente.") - return jsonify({"message": "Transação recebida e adicionada com sucesso."}), 200 - - except ValueError as e: - print(f"[RECEIVE TX ERROR] Erro de conversão de tipo ao processar TX {tx_data.get('id')}: {e}") - return jsonify({'message': f'Erro ao processar dados numéricos da transação: {e}'}), 400 - except Exception as e: - print(f"[RECEIVE TX ERROR] Erro inesperado ao processar TX {tx_data.get('id')}: {e}") - return jsonify({'message': f'Erro interno ao processar transação: {e}'}), 500 - -def verify_signature(public_key_hex, signature_hex, tx_data): - """ - Verifica a assinatura de uma transação. - tx_data deve conter 'sender', 'recipient', 'amount', 'fee'. - 'amount' e 'fee' devem ser strings formatadas com 8 casas decimais. - """ - try: - vk = VerifyingKey.from_string(bytes.fromhex(public_key_hex), curve=SECP256k1) - - # Garantir que amount e fee são strings formatadas para a verificação - amount_to_verify = f"{float(tx_data['amount']):.8f}" - fee_to_verify = f"{float(tx_data['fee']):.8f}" - - prepared_message_data = { - 'amount': amount_to_verify, - 'fee': fee_to_verify, - 'recipient': tx_data['recipient'], - 'sender': tx_data['sender'] - } - - message = json.dumps(prepared_message_data, sort_keys=True, separators=(',', ':')).encode('utf-8') - - message_hash_bytes = hashlib.sha256(message).digest() - signature_bytes = bytes.fromhex(signature_hex) - - # print(f"DEBUG_VERIFY: Chave Pública recebida (hex): {public_key_hex}") - # print(f"DEBUG_VERIFY: Assinatura recebida (hex): {signature_hex}") - # print(f"DEBUG_VERIFY: Dados da mensagem para verificação (antes de json.dumps): {prepared_message_data}") - # print(f"DEBUG_VERIFY: JSON da mensagem para verificação (decodificado): {message.decode('utf-8')}") - # print(f"DEBUG_VERIFY: Bytes da mensagem para verificação (raw): {message}") - # print(f"DEBUG_VERIFY: Hash da mensagem para verificação (SHA256, HEX): {hashlib.sha256(message).hexdigest()}") - - vk.verify_digest(signature_bytes, message_hash_bytes) - return True - except BadSignatureError: - print("Falha na verificação da assinatura: BadSignatureError!") - return False - except ValueError as ve: - print(f"Falha na verificação da assinatura: ValueError (e.g., bad hex string or malformed key): {ve}") - return False - except Exception as e: - print(f"Erro durante a verificação da assinatura: {e}") - return False - -@app.route('/blocks/receive', methods=['POST']) -def receive_block_api(): - """Recebe um bloco de outro nó e tenta adicioná-lo à cadeia local.""" - block_data = request.get_json() - if not block_data: - print("[RECEIVE_BLOCK ERROR] Nenhum dado de bloco recebido.") - return jsonify({"message": "Nenhum dado de bloco recebido."}), 400 - - required_keys = ['index', 'previous_hash', 'proof', 'timestamp', 'miner', 'transactions', 'difficulty'] - if not all(k in block_data for k in required_keys): - print(f"[RECEIVE_BLOCK ERROR] Bloco recebido com chaves ausentes: {block_data}") - return jsonify({"message": "Dados de bloco incompletos ou malformados."}), 400 - - if not blockchain.chain: - print("[RECEIVE_BLOCK INFO] Cadeia local vazia. Iniciando resolução de conflitos para sincronização inicial.") - threading.Thread(target=blockchain.resolve_conflicts, daemon=True).start() - return jsonify({'message': 'Cadeia local vazia. Tentando sincronizar com a rede.'}), 202 - - last_local_block = blockchain.last_block() - - if block_data['index'] <= last_local_block['index']: - # Bloco duplicado ou mais antigo - if block_data['index'] == last_local_block['index'] and \ - block_data['previous_hash'] == last_local_block['previous_hash'] and \ - block_data['proof'] == last_local_block['proof'] and \ - block_data['miner'] == last_local_block['miner'] and \ - block_data['difficulty'] == last_local_block['difficulty']: - print(f"[RECEIVE_BLOCK INFO] Bloco {block_data['index']} já recebido e processado (duplicado).") - return jsonify({'message': 'Bloco já recebido e processado'}), 200 - else: - print(f"[RECEIVE_BLOCK INFO] Bloco {block_data['index']} é antigo ou de um fork mais curto/inválido (Local: {last_local_block['index']}). Ignorando.") - return jsonify({'message': 'Bloco antigo ou de um fork irrelevante.'}), 200 - - if block_data['index'] == last_local_block['index'] + 1: - # Próximo bloco na sequência - expected_previous_hash = blockchain.hash(last_local_block) - if block_data['previous_hash'] != expected_previous_hash: - print(f"[RECEIVE_BLOCK ERROR] Bloco {block_data['index']}: Hash anterior incorreto. Esperado: {expected_previous_hash}, Recebido: {block_data['previous_hash']}. Iniciando sincronização.") - threading.Thread(target=blockchain.resolve_conflicts, daemon=True).start() - return jsonify({'message': 'Hash anterior incorreto, resolução de conflitos iniciada'}), 400 - - if not blockchain.valid_proof(last_local_block['proof'], block_data['proof'], block_data['difficulty']): - print(f"[RECEIVE_BLOCK ERROR] Bloco {block_data['index']}: Prova de Trabalho inválida. Iniciando sincronização.") - threading.Thread(target=blockchain.resolve_conflicts, daemon=True).start() - return jsonify({'message': 'Prova inválida, resolução de conflitos iniciada'}), 400 - - # Valida as transações dentro do bloco recebido - for tx in block_data.get('transactions', []): - if tx['sender'] == '0': # Ignora transações de recompensa - continue - - try: - tx_for_verification = { - 'id': tx['id'], - 'sender': tx['sender'], - 'recipient': tx['recipient'], - 'amount': f"{float(tx['amount']):.8f}", - 'fee': f"{float(tx['fee']):.8f}", - 'public_key': tx['public_key'], - 'signature': tx['signature'], - 'timestamp': tx.get('timestamp', time.time()) - } - if not verify_signature(tx_for_verification['public_key'], tx_for_verification['signature'], tx_for_verification): - raise ValueError(f"Assinatura inválida para transação {tx.get('id', 'N/A')}") - - except Exception as e: - print(f"[RECEIVE_BLOCK ERROR] Transação inválida {tx.get('id', 'N/A')} no bloco {block_data['index']}: {e}. Iniciando sincronização.") - threading.Thread(target=blockchain.resolve_conflicts, daemon=True).start() - return jsonify({'message': f'Transação inválida no bloco: {e}'}), 400 - - print(f"[RECEIVE_BLOCK SUCCESS] Bloco {block_data['index']} aceito e adicionado localmente.") - blockchain.chain.append(block_data) - blockchain._save_block(block_data) - - # Remove transações que foram incluídas no novo bloco da fila de pendentes - mined_tx_ids = {t.get('id') for t in block_data.get('transactions', []) if t.get('id')} - blockchain.current_transactions = [ - tx for tx in blockchain.current_transactions if tx.get('id') not in mined_tx_ids - ] - print(f"[RECEIVE_BLOCK] Removidas {len(mined_tx_ids)} transações da fila pendente.") - - return jsonify({'message': 'Bloco aceito e adicionado'}), 200 - - elif block_data['index'] > last_local_block['index'] + 1: - # Bloco está muito à frente, provavelmente um fork mais longo - print(f"[RECEIVE_BLOCK INFO] Bloco {block_data['index']} está à frente da cadeia local ({last_local_block['index']}). Iniciando resolução de conflitos.") - threading.Thread(target=blockchain.resolve_conflicts, daemon=True).start() - return jsonify({'message': 'Bloco está à frente. Iniciando sincronização.'}), 202 - - print(f"[RECEIVE_BLOCK WARNING] Condição inesperada para o bloco {block_data['index']}. Iniciando resolução de conflitos.") - threading.Thread(target=blockchain.resolve_conflicts, daemon=True).start() - return jsonify({'message': 'Bloco com status inesperado, resolução de conflitos iniciada'}), 400 - -@app.route('/sync/check', methods=['GET']) -def check_sync_api(): - last = blockchain.last_block() - if not last: - return jsonify({'message': 'Blockchain não inicializada localmente.'}), 500 - local_hash = blockchain.hash(last) - return jsonify({ - 'index': last['index'], - 'hash': local_hash, - 'timestamp': last['timestamp'], - 'miner': last['miner'], - 'num_txs': len(last['transactions']) - }) - -@app.route('/miner/set_address', methods=['POST']) -def set_miner_address_api(): - """Define o endereço de mineração para o nó.""" - global miner_address_global # Usar a variável global consistente - data = request.get_json() - address = data.get('address') - if not address: - return jsonify({"message": "Endereço do minerador ausente."}), 400 - miner_address_global = address - print(f"[MINER] Endereço do minerador definido para {miner_address_global}") - return jsonify({"message": f"Endereço do minerador definido para {miner_address_global}"}), 200 - -@app.route('/mine', methods=['GET']) -def mine_api(): - """Inicia o processo de mineração de um novo bloco (manual).""" - global mining_active, miner_address_global - if not miner_address_global: - return jsonify({"message": "Endereço do minerador não definido. Por favor, defina um endereço primeiro usando /miner/set_address."}), 400 - - # Se a mineração contínua estiver ativa, não permitir mineração manual separada - if mining_active: - return jsonify({"message": "Mineração contínua já está em andamento. Pare-a para minerar manualmente."}), 409 - - last_block = blockchain.last_block() - if not last_block: - return jsonify({"message": "Blockchain não inicializada. Não é possível minerar."}), 500 - - last_proof = last_block['proof'] - - # Temporariamente ativar mining_active para que proof_of_work funcione - # e possa ser interrompido se necessário (embora esta rota não tenha um 'stop') - original_mining_active_state = mining_active - mining_active = True - proof = blockchain.proof_of_work(last_proof) - mining_active = original_mining_active_state # Restaurar estado - - if proof == -1: # Mineração foi abortada (por chegada de bloco ou outro motivo) - return jsonify({"message": "Mineração abortada ou interrompida (provavelmente um bloco foi encontrado por outro nó)."}), 200 - - previous_hash = blockchain.hash(last_block) - new_block = blockchain.new_block(proof, previous_hash, miner_address_global) - - broadcast_block(new_block) - - response = { - 'message': "Novo bloco forjado!", - 'index': new_block['index'], - 'transactions': new_block['transactions'], - 'proof': new_block['proof'], - 'previous_hash': new_block['previous_hash'], - 'difficulty': new_block['difficulty'] - } - return jsonify(response), 200 - -@app.route('/miner/start_continuous', methods=['GET']) -def start_continuous_mining(): - """Endpoint para iniciar a mineração contínua em um thread separado.""" - global mining_active, miner_thread, miner_address_global - if mining_active: - return jsonify({"message": "Mineração contínua já está em execução."}), 400 - - if not miner_address_global: - return jsonify({"message": "Endereço do minerador não definido. Defina um endereço primeiro usando /miner/set_address."}), 400 - - mining_active = True - miner_thread = threading.Thread(target=_continuous_mine, daemon=True) - miner_thread.start() - print("[MINER] Mineração contínua iniciada.") - return jsonify({"message": "Mineração contínua iniciada."}), 200 - -@app.route('/miner/stop_continuous', methods=['GET']) -def stop_continuous_mining(): - """Endpoint para parar a mineração contínua.""" - global mining_active, miner_thread - if not mining_active: - return jsonify({"message": "Mineração contínua não está em execução."}), 400 - - mining_active = False - # O thread irá parar por si só na próxima iteração do loop ou quando proof_of_work verificar `mining_active` - print("[MINER] Sinal para parar mineração contínua enviado.") - return jsonify({"message": "Sinal para parar mineração contínua enviado. Pode levar alguns segundos para parar o bloco atual."}), 200 - -def _continuous_mine(): - """Função que executa a mineração continuamente em um thread.""" - global mining_active, blockchain, miner_address_global - print("[MINER] Thread de mineração contínua iniciada.") - while mining_active: - try: - last_block = blockchain.last_block() - if not last_block: - print("[MINER ERROR] Blockchain não inicializada para mineração contínua. Tentando novamente em 5s.") - time.sleep(5) # Espera antes de tentar novamente - continue - - last_proof = last_block['proof'] - - proof = blockchain.proof_of_work(last_proof) - - if proof == -1: # Mineração foi abortada (novo bloco encontrado ou sinal para parar) - print("[MINER] Mineração de bloco abortada. Verificando novamente o estado.") - time.sleep(1) # Pequena pausa antes de tentar o próximo bloco - continue - - previous_hash = blockchain.hash(last_block) - new_block = blockchain.new_block(proof, previous_hash, miner_address_global) - print(f"[MINER] Bloco minerado continuamente: {new_block['index']}") - - broadcast_block(new_block) - time.sleep(1) # Pequena pausa para evitar loops muito rápidos - - except Exception as e: - print(f"[MINER ERROR] Erro na mineração contínua: {e}. Parando mineração.") - mining_active = False # Parar a mineração em caso de erro grave - break - print("[MINER] Thread de mineração contínua parada.") - - -# --- Funções de Peer-to-Peer (do nó) --- -def broadcast_block(block): - """Envia um bloco recém-minerado para todos os peers conhecidos.""" - print(f"[BROADCAST] Enviando bloco #{block['index']} para {len(known_nodes)} peers...") - peers_to_remove = set() - for peer in known_nodes.copy(): - if peer == meu_url: continue - try: - requests.post(f"{peer}/blocks/receive", json=block, timeout=5) - except requests.exceptions.RequestException as e: - print(f"[BROADCAST] Erro ao enviar bloco para {peer}: {e}. Marcando peer para remoção (se não for seed).") - if peer not in SEED_NODES: - peers_to_remove.add(peer) - except Exception as e: - print(f"[BROADCAST] Erro inesperado ao enviar bloco para {peer}: {e}. Marcando peer para remoção (se não for seed).") - if peer not in SEED_NODES: - peers_to_remove.add(peer) - - if peers_to_remove: - known_nodes.difference_update(peers_to_remove) - salvar_peers(known_nodes) - print(f"[BROADCAST] Removidos {len(peers_to_remove)} peers problemáticos.") - -def discover_peers(): - """ - Descobre e registra peers na rede. - Prioriza a conexão com os nós semente (SEED_NODES) para iniciar a descoberta. - """ - global known_nodes, meu_url - - # 1. Adiciona os nós semente à lista de peers conhecidos. - initial_known_nodes_count = len(known_nodes) - for seed in SEED_NODES: - if seed not in known_nodes and seed != meu_url: - known_nodes.add(seed) - print(f"[DISCOVERY] Adicionando nó semente: {seed}") - - if len(known_nodes) > initial_known_nodes_count: - salvar_peers(known_nodes) # Salva a lista atualizada de peers se houver novas adições - - # 2. Itera sobre a lista de peers conhecidos (incluindo os nós semente) - # para descobrir novos peers e registrar o nó local. - peers_to_check = list(known_nodes.copy()) # Cria uma cópia para iterar - - peers_to_remove_during_discovery = set() - new_peers_discovered = False - - for peer in peers_to_check: - if peer == meu_url: - continue # Não tentar conectar a si mesmo - try: - # Tenta obter a lista de nós conhecidos pelo peer - print(f"[DISCOVERY] Consultando peers de {peer}...") - r = requests.get(f"{peer}/nodes", timeout=5) - if r.status_code == 200: - raw_new_peers = r.json().get('nodes', []) - for np in raw_new_peers: - # Garante que 'np' é uma string de URL válida - if isinstance(np, str) and (np.startswith('http://') or np.startswith('https://')): - if np not in known_nodes and np != meu_url: - known_nodes.add(np) - print(f"[DISCOVERY] Descoberto novo peer {np} via {peer}") - new_peers_discovered = True - - # Tenta registrar o nó local com o novo peer descoberto - try: - print(f"[DISCOVERY] Registrando meu nó ({meu_url}) com o novo peer {np}...") - requests.post(f"{np}/nodes/register", json={'url': meu_url}, timeout=2) - except requests.exceptions.RequestException as e: - print(f"[DISCOVERY ERROR] Falha ao registrar em {np}: {e}") - except Exception as e: - print(f"[DISCOVERY ERROR] Erro inesperado ao registrar em {np}: {e}") - else: - print(f"[DISCOVERY WARNING] Peer {np} de {peer} não é uma URL válida. Ignorando.") - - # Tenta registrar o nó local com o peer atual (seja ele semente ou descoberto) - print(f"[DISCOVERY] Registrando meu nó ({meu_url}) com {peer}...") - requests.post(f"{peer}/nodes/register", json={'url': meu_url}, timeout=5) - - except requests.exceptions.RequestException as e: - print(f"[DISCOVERY ERROR] Falha ao conectar/descobrir peer {peer}: {e}. Marcando para remoção.") - if peer not in SEED_NODES: # Não remove nós semente automaticamente - peers_to_remove_during_discovery.add(peer) - except Exception as e: - print(f"[DISCOVERY ERROR] Erro inesperado durante descoberta com {peer}: {e}. Marcando para remoção.") - if peer not in SEED_NODES: # Não remove nós semente automaticamente - peers_to_remove_during_discovery.add(peer) - - # Salva a lista de peers após todas as operações de descoberta e remoção - if new_peers_discovered or peers_to_remove_during_discovery: - known_nodes.difference_update(peers_to_remove_during_discovery) - salvar_peers(known_nodes) - if peers_to_remove_during_discovery: - print(f"[DISCOVERY] Removidos {len(peers_to_remove_during_discovery)} peers problemáticos.") - -def get_my_ip(): - """Tenta obter o IP local do nó e avisa se for privado.""" - try: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(("8.8.8.8", 80)) # Conecta a um IP público para obter o IP de saída - ip = s.getsockname()[0] - s.close() - try: - if ipaddress.ip_address(ip).is_private: - print(f"[AVISO IP] Seu IP ({ip}) é privado. Para comunicação completa com peers públicos, configure o redirecionamento de portas (port forwarding) para a porta {port} no seu roteador e use um IP público ou serviço DDNS.") - except ValueError: - pass # Não é um IP válido para verificar se é privado - return ip - except Exception: - print("[AVISO IP] Não foi possível determinar o IP local. Usando 127.0.0.1 como fallback. A comunicação com peers externos pode ser limitada.") - return "127.0.0.1" - -def load_or_create_node_id(filename="node_id.txt"): - """Carrega ou cria um ID de nó único.""" - if os.path.exists(filename): - with open(filename, "r") as f: - node_id_loaded = f.read().strip() - print(f"[BOOT] ID do nó carregado: {node_id_loaded}") - return node_id_loaded - else: - new_id = str(uuid4()).replace("-", "")[:16] - with open(filename, "w") as f: - f.write(new_id) - print(f"[BOOT] Novo ID do nó criado: {new_id}") - return new_id - -# Funções auxiliares para auto_sync_checker -def auto_sync_checker(blockchain_instance): - """Verifica periodicamente a sincronização com os peers e inicia a resolução de conflitos se necessário.""" - while True: - try: - comparar_ultimos_blocos(blockchain_instance) - except Exception as e: - print(f"[SYNC_CHECKER ERROR] Erro no verificador de sincronização: {e}") - time.sleep(60) # Verifica a cada 60 segundos - -def comparar_ultimos_blocos(blockchain_instance): - """Compara o último bloco local com o dos peers e inicia a resolução de conflitos se houver diferença.""" - if blockchain_instance is None or blockchain_instance.last_block() is None: - print("[SYNC] Blockchain ainda não inicializada. Aguardando...") - return - - print("\n🔍 Verificando sincronização com os peers...") - local_block = blockchain_instance.last_block() - local_hash = blockchain_instance.hash(local_block) - - peers_to_remove_during_sync_check = set() - - for peer in known_nodes.copy(): - if peer == meu_url: - continue - try: - r = requests.get(f"{peer}/sync/check", timeout=5) - data = r.json() - peer_index = data.get('index') - peer_hash = data.get('hash') - - if peer_index is None or peer_hash is None: - print(f"[SYNC ⚠️] Resposta de sincronização malformada de {peer}. Marcando peer para remoção.") - peers_to_remove_during_sync_check.add(peer) - continue - - if peer_index == local_block['index'] and peer_hash == local_hash: - print(f"[SYNC ✅] {peer} está sincronizado com índice {peer_index}.") - else: - print(f"[SYNC ⚠️] {peer} DIFERENTE! Local: {local_block['index']} | Peer: {peer_index}. Iniciando resolução de conflitos.") - threading.Thread(target=blockchain_instance.resolve_conflicts, daemon=True).start() - except requests.exceptions.RequestException as e: - print(f"[SYNC ❌] Falha ao verificar {peer}: {e}. Marcando peer para remoção.") - if peer not in SEED_NODES: - peers_to_remove_during_sync_check.add(peer) - except Exception as e: - print(f"[SYNC ❌] Erro inesperado ao verificar {peer}: {e}. Marcando peer para remoção.") - if peer not in SEED_NODES: - peers_to_remove_during_sync_check.add(peer) - - if peers_to_remove_during_sync_check: - known_nodes.difference_update(peers_to_remove_during_sync_check) - salvar_peers(known_nodes) - print(f"[SYNC] Removidos {len(peers_to_remove_during_sync_check)} peers problemáticos durante a verificação de sincronização.") - -# --- Execução Principal --- -def run_server(): - global blockchain, meu_ip, meu_url, port - port = int(os.environ.get('PORT', 5000)) - - conn = sqlite3.connect(DATABASE, check_same_thread=False) - node_id_val = load_or_create_node_id() - blockchain = Blockchain(conn, node_id_val) # Inicializa blockchain aqui - - meu_ip = get_my_ip() - # Importante: Se você pretende que seu nó seja acessível publicamente via HTTPS, - # você precisará configurar o Flask para servir HTTPS e ajustar esta URL. - # Para uso local ou em redes privadas, HTTP geralmente é suficiente. - meu_url = f"http://{meu_ip}:{port}" - print(f"[INFO] Node URL: {meu_url}") - - # Inicia a descoberta de peers em um thread separado - threading.Thread(target=discover_peers, daemon=True).start() - - # Tenta resolver conflitos na inicialização para sincronizar com a rede - # Dá um pequeno tempo para a descoberta inicial de peers ocorrer - time.sleep(5) - if len(known_nodes) > 0: - print("[BOOT] Tentando resolver conflitos na inicialização com peers conhecidos...") - blockchain.resolve_conflicts() - else: - print("[BOOT] Nenhum peer conhecido. Operando de forma isolada inicialmente. Descoberta de peers continuará em segundo plano.") - - # Inicia o verificador de sincronização automática - threading.Thread(target=auto_sync_checker, args=(blockchain,), daemon=True).start() - - print(f"[INFO] Iniciando o nó em modo servidor (sem GUI) na porta {port}.") - app.run(host='0.0.0.0', port=port) - -if __name__ == "__main__": - run_server() diff --git a/WrappedKERT.sol b/WrappedKERT.sol new file mode 100644 index 0000000..4c0b944 --- /dev/null +++ b/WrappedKERT.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/* + * OpenZeppelin v5 FIXADO + */ +import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.1/contracts/token/ERC20/ERC20.sol"; +import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.1/contracts/access/Ownable.sol"; +import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.1/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.1/contracts/utils/Pausable.sol"; + +contract WKertOne is ERC20, Ownable, ERC20Burnable, Pausable { + uint256 public constant MAX_SUPPLY = 1_000_000 * 10**18; + + mapping(address => bool) public blacklist; + + event Blacklisted(address indexed user, bool status); + event BridgeMint(address indexed to, uint256 amount); + event BridgeBurn(address indexed from, uint256 amount); + + constructor() + ERC20("WKert-One", "WKERT") + Ownable(msg.sender) + { + // 🔥 MINT INICIAL (resolve 90% dos seus problemas) + _mint(msg.sender, 100_000 * 10**18); + } + + modifier notBlacklisted(address account) { + require(!blacklist[account], "Blacklisted"); + _; + } + + function _update( + address from, + address to, + uint256 amount + ) + internal + override + whenNotPaused + notBlacklisted(from) + notBlacklisted(to) + { + super._update(from, to, amount); + } + + // 🌉 Mint (bridge KERT → WKERT) + function mint(address to, uint256 amount) external onlyOwner { + require(totalSupply() + amount <= MAX_SUPPLY, "Max supply reached"); + _mint(to, amount); + emit BridgeMint(to, amount); + } + + // 🌉 Burn (WKERT → KERT real) + function burnFrom(address account, uint256 amount) + public + override + onlyOwner + { + _burn(account, amount); + emit BridgeBurn(account, amount); + } + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } + + function setBlacklist(address user, bool status) external onlyOwner { + blacklist[user] = status; + emit Blacklisted(user, status); + } +} diff --git a/kertone.png b/kertone.png new file mode 100644 index 0000000..c60483e Binary files /dev/null and b/kertone.png differ diff --git a/nodes.json b/nodes.json new file mode 100644 index 0000000..f9505f4 --- /dev/null +++ b/nodes.json @@ -0,0 +1,6 @@ +[ + "https://seend.kert-one.com", + "https://seend2.kert-one.com", + "http://seend3.kert-one.com:8001" +] + diff --git "a/n\303\263-completo.windows.py" "b/n\303\263-completo.windows.py" index abf4641..726a8dc 100644 --- "a/n\303\263-completo.windows.py" +++ "b/n\303\263-completo.windows.py" @@ -5,7 +5,7 @@ import sqlite3 import os from uuid import uuid4 -from flask import Flask, jsonify, request, send_file +from flask import Flask, jsonify, request, send_file, render_template import requests from urllib.parse import urlparse import socket @@ -17,1231 +17,714 @@ from datetime import datetime import re import shutil -from flask import Flask, render_template, flash from flask_cors import CORS -from PyQt5.QtCore import pyqtSlot - -# Importações PyQt5 +from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt, QObject, QMetaObject, Q_ARG, QMutex, QMutexLocker from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QTextEdit, QVBoxLayout, QWidget, QLabel, QLineEdit, QFormLayout, QGroupBox, QMessageBox, QHBoxLayout, QTabWidget, QStatusBar, QDialog, QDialogButtonBox, QPlainTextEdit, - QInputDialog) -from PyQt5.QtCore import QThread, pyqtSignal, QTimer, Qt, QObject, QMetaObject, Q_ARG, QMutex, QMutexLocker + QInputDialog, QRadioButton, QComboBox) from PyQt5.QtGui import QFont, QColor, QPalette, QTextCursor, QDoubleValidator, QValidator - +import multiprocessing + +# --- INJEÇÃO DE MINERAÇÃO REAL (GPU/CPU) --- +try: + import pyopencl as cl + import numpy as np + HAS_GPU = True + platforms = cl.get_platforms() + if not platforms: + raise Exception("Nenhuma plataforma OpenCL encontrada") + print(f"[SISTEMA] OpenCL Detectado: {platforms[0].name}") +except Exception as e: + HAS_GPU = False + print(f"[SISTEMA] Modo GPU Indisponível ({e}). Usando CPU.") # --- Configurações --- -DIFFICULTY = 1 # Dificuldade inicial para o bloco Gênese -MINING_REWARD = 50 # Recompensa padrão (será sobrescrita pela lógica de halving) +DIFFICULTY = 4 +MINING_REWARD = 50 DATABASE = 'chain.db' COIN_NAME = "Kert-One" COIN_SYMBOL = "KERT" PEERS_FILE = 'peers.json' -WALLET_FILE = "client_wallet.json" # Caminho para o arquivo da carteira do cliente +WALLET_FILE = "client_wallet.json" +NGROK_AUTH_FILE = "ngrok_auth.txt" + +# --- CHECKPOINT DE SEGURANÇA --- +LEGACY_CUTOFF_INDEX = 3330 # --- NÓS SEMENTES (SEED NODES) --- SEED_NODES = [ "https://seend.kert-one.com", - "https://seend2.kert-one.com", - "https://seend3.kert-one.com", + "http://seend3.kert-one.com:8001" ] +# --- Configurações de Intensidade da GPU --- +# AJUSTADO PARA EVITAR CRASH DO DRIVER (TDR) +INTENSITY_CONFIG = { + "LOW": {"batch": 500000, "loops": 1000}, # Uso leve + "MEDIUM": {"batch": 1000000, "loops": 5000}, # Uso normal + "HIGH": {"batch": 2000000, "loops": 10000}, # Uso intenso + "INSANE": {"batch": 1000000, "loops": 10} +} +mining_intensity_global = "MEDIUM" + +# --- CORREÇÃO CRÍTICA: CABEÇALHO PARA NGROK --- +# Isso impede o erro 403 Forbidden +NGROK_HEADERS = { + "ngrok-skip-browser-warning": "true", + "User-Agent": "KertOne-Miner/3.0" +} + +# Kernel Otimizado +OPENCL_KERNEL = """ +#define ROR(x, y) ((x >> y) | (x << (32 - y))) +#define Ch(x, y, z) (z ^ (x & (y ^ z))) +#define Maj(x, y, z) ((x & y) | (z & (x | y))) +#define S0(x) (ROR(x, 2) ^ ROR(x, 13) ^ ROR(x, 22)) +#define S1(x) (ROR(x, 6) ^ ROR(x, 11) ^ ROR(x, 25)) +#define s0(x) (ROR(x, 7) ^ ROR(x, 18) ^ (x >> 3)) +#define s1(x) (ROR(x, 17) ^ ROR(x, 19) ^ (x >> 10)) + +__constant unsigned int K[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 +}; + +void sha256_transform(unsigned int *state, const unsigned int *data) { + unsigned int a, b, c, d, e, f, g, h, t1, t2; + unsigned int W[64]; + for (int i = 0; i < 16; ++i) W[i] = data[i]; + for (int i = 16; i < 64; ++i) W[i] = s1(W[i - 2]) + W[i - 7] + s0(W[i - 15]) + W[i - 16]; + a = state[0]; b = state[1]; c = state[2]; d = state[3]; + e = state[4]; f = state[5]; g = state[6]; h = state[7]; + for (int i = 0; i < 64; ++i) { + t1 = h + S1(e) + Ch(e, f, g) + K[i] + W[i]; + t2 = S0(a) + Maj(a, b, c); + h = g; g = f; f = e; e = d + t1; + d = c; c = b; b = a; a = t1 + t2; + } + state[0] += a; state[1] += b; state[2] += c; state[3] += d; + state[4] += e; state[5] += f; state[6] += g; state[7] += h; +} + +__kernel void search_block(__global unsigned int *result, __global int *found, const unsigned int difficulty, const unsigned int start_nonce, const unsigned int loops) { + unsigned int gid = get_global_id(0); + for(unsigned int i=0; i < loops; i++) { + if(*found != 0) return; + unsigned int nonce = start_nonce + (gid * loops) + i; + unsigned int state[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; + unsigned int data[16] = {0}; + data[0] = nonce; + sha256_transform(state, data); + + if (state[0] <= (0xFFFFFFFF >> (difficulty * 4))) { + *result = nonce; + *found = 1; + return; + } + } +} +""" + app = Flask(__name__) node_id = str(uuid4()).replace('-', '') CORS(app) -# --- Funções de Persistência de Peers --- +known_nodes = set() +miner_lock = threading.Lock() +blockchain = None +miner_address = None +miner_address_global = None +meu_url = None +meu_ip = None +port = None +mining_active = False +mining_stop_flag = multiprocessing.Event() +mining_result = multiprocessing.Value('i', -1) +current_hashrate_global = 0.0 + +# --- Persistência --- def salvar_peers(peers): - """Salva a lista de peers conhecidos em um arquivo JSON.""" - with open(PEERS_FILE, 'w') as f: - json.dump(list(peers), f) + with open(PEERS_FILE, 'w') as f: json.dump(list(peers), f) def carregar_peers(): - """Carrega a lista de peers conhecidos de um arquivo JSON.""" - if not os.path.exists(PEERS_FILE): - return [] + if not os.path.exists(PEERS_FILE): return [] with open(PEERS_FILE, 'r') as f: - try: - return json.load(f) - except json.JSONDecodeError: - print(f"[ERRO] {PEERS_FILE} está corrompido ou vazio. Recriando.") - return [] + try: return json.load(f) + except: return [] known_nodes = set(carregar_peers()) -miner_lock = threading.Lock() - -blockchain = None -miner_address = None # Agora será definido por um endpoint ou configuração -meu_url = None # Definido no main -meu_ip = None # Definido no main -port = None # Definido no main - -# Global variable for mining control -is_mining = False # --- Classe Blockchain --- class Blockchain: - ADJUST_INTERVAL = 2016 # Blocos para recalcular dificuldade - TARGET_TIME = 600 # Tempo alvo entre blocos em segundos (10 minutos) + ADJUST_INTERVAL = 50 + TARGET_TIME = 20 + def _calculate_difficulty_for_index(self, target_block_index): + if target_block_index % self.ADJUST_INTERVAL != 0: + return self.chain[-1].get('difficulty', 4) + if len(self.chain) < self.ADJUST_INTERVAL: return 4 + last_block = self.chain[-1] + first_block = self.chain[-self.ADJUST_INTERVAL] + actual_time = last_block['timestamp'] - first_block['timestamp'] + expected_time = self.ADJUST_INTERVAL * self.TARGET_TIME + old_diff = last_block.get('difficulty', 4) + new_diff = int(old_diff * (expected_time / max(1, actual_time))) + return max(1, min(20, new_diff)) + def __init__(self, conn, node_id): self.conn = conn self.node_id = node_id self._init_db() self.chain = self._load_chain() - self.current_transactions = [] - + self.current_transactions = self._load_mempool() + if not self.chain: - print("[BOOT] Criando bloco Gênese...") - genesis_difficulty = DIFFICULTY - self.new_block(proof=100, previous_hash='1', miner=self.node_id, initial_difficulty=genesis_difficulty) - + print("[BOOT] 📡 Inserindo Gênese Base 500.0...") + genesis_block = {'index': 1, 'previous_hash': '1', 'proof': 100, 'timestamp': 1700000000.0, 'miner': 'genesis', 'transactions': [], 'difficulty': 1, 'protocol_value': 500.0} + self.chain.append(genesis_block) + self._save_block(genesis_block) self.difficulty = self._calculate_difficulty_for_index(len(self.chain)) - print(f"[BOOT] Dificuldade inicial da cadeia: {self.difficulty}") + print(f"[BOOT] Dificuldade inicial: {self.difficulty}") @staticmethod def hash(block): - """ - Cria um hash SHA-256 de um Bloco. - Ignora o campo 'transactions' e 'hash' (se presente) para hashing. - """ - block_string = json.dumps({k: v for k, v in block.items() if k not in ['transactions', 'hash']}, sort_keys=True) - return hashlib.sha256(block_string.encode()).hexdigest() + block_core = {"index": block["index"], "previous_hash": block["previous_hash"], "proof": block["proof"], "timestamp": block["timestamp"], "miner": block["miner"], "difficulty": block.get("difficulty", 1), "protocol_value": block.get("protocol_value", 0), "transactions": block["transactions"]} + block_string = json.dumps(block_core, sort_keys=True, separators=(',', ':')).encode() + return hashlib.sha256(block_string).hexdigest() def is_duplicate_transaction(self, new_tx): - """Verifica se uma transação já está na fila de transações pendentes.""" for tx in self.current_transactions: - if tx.get('id') == new_tx.get('id'): - return True - # Compara com uma pequena tolerância para floats, mas idealmente amount/fee são strings agora - if (tx.get('sender') == new_tx.get('sender') and - tx.get('recipient') == new_tx.get('recipient') and - tx.get('amount') == new_tx.get('amount') and # Agora compara strings - tx.get('fee') == new_tx.get('fee') and # Agora compara strings - tx.get('signature') == new_tx.get('signature')): - print(f"[DUPLICIDADE] Detectada transação pendente quase idêntica (sender={new_tx.get('sender')}, amount={new_tx.get('amount')}).") - return True + if tx.get('id') == new_tx.get('id'): return True return False @staticmethod def custom_asic_resistant_hash(data_bytes, nonce): - """Função de hash resistente a ASICs.""" raw = data_bytes + str(nonce).encode() - h1 = hashlib.sha256(raw).digest() - h2 = hashlib.sha512(h1).digest() - h3 = hashlib.blake2b(h2).digest() - return hashlib.sha256(h3).hexdigest() + return hashlib.sha256(hashlib.sha256(raw).digest()).hexdigest() def _init_db(self): - """Inicializa o esquema do banco de dados SQLite.""" c = self.conn.cursor() - c.execute(''' - CREATE TABLE IF NOT EXISTS blocks( - index_ INTEGER PRIMARY KEY, - previous_hash TEXT, - proof INTEGER, - timestamp REAL, - miner TEXT, - difficulty INTEGER - ) - ''') - # Armazenar amount e fee como TEXT para evitar problemas de float precision - c.execute(''' - CREATE TABLE IF NOT EXISTS txs( - id TEXT PRIMARY KEY, - sender TEXT, - recipient TEXT, - amount TEXT, -- Alterado para TEXT - fee TEXT, -- Alterado para TEXT - signature TEXT, - block_index INTEGER, - public_key TEXT - ) - ''') + c.execute('CREATE TABLE IF NOT EXISTS blocks(index_ INTEGER PRIMARY KEY, previous_hash TEXT, proof INTEGER, timestamp REAL, miner TEXT, difficulty INTEGER, protocol_value REAL)') + c.execute('CREATE TABLE IF NOT EXISTS txs(id TEXT PRIMARY KEY, sender TEXT, recipient TEXT, amount TEXT, fee TEXT, signature TEXT, block_index INTEGER, public_key TEXT)') + c.execute('CREATE TABLE IF NOT EXISTS mempool(id TEXT PRIMARY KEY, sender TEXT, recipient TEXT, amount TEXT, fee TEXT, signature TEXT, public_key TEXT, timestamp REAL)') self.conn.commit() def _load_chain(self): - """Carrega a cadeia de blocos do banco de dados.""" c = self.conn.cursor() - c.execute("SELECT index_, previous_hash, proof, timestamp, miner, difficulty FROM blocks ORDER BY index_") + c.execute("SELECT index_, previous_hash, proof, timestamp, miner, difficulty, protocol_value FROM blocks ORDER BY index_") chain = [] - for idx, prev, proof, ts, miner, difficulty in c.fetchall(): + for idx, prev, proof, ts, miner, difficulty, p_val in c.fetchall(): c.execute("SELECT id, sender, recipient, amount, fee, signature, public_key FROM txs WHERE block_index=?", (idx,)) txs = [] for r in c.fetchall(): - # amount e fee são armazenados como TEXT, então os usamos diretamente - txs.append(dict(id=r[0], sender=r[1], recipient=r[2], - amount=r[3], - fee=r[4], - signature=r[5], public_key=r[6])) - block = { - 'index': idx, - 'previous_hash': prev, - 'proof': proof, - 'timestamp': ts, - 'miner': miner, - 'transactions': txs, - 'difficulty': difficulty - } - chain.append(block) + txs.append(dict(id=r[0], sender=r[1], recipient=r[2], amount=r[3], fee=r[4], signature=r[5], public_key=r[6])) + chain.append({'index': idx, 'previous_hash': prev, 'proof': proof, 'timestamp': ts, 'miner': miner, 'transactions': txs, 'difficulty': difficulty, 'protocol_value': p_val}) return chain + def _load_mempool(self): + c = self.conn.cursor() + c.execute("SELECT id, sender, recipient, amount, fee, signature, public_key, timestamp FROM mempool") + txs = [] + for r in c.fetchall(): + txs.append({'id': r[0], 'sender': r[1], 'recipient': r[2], 'amount': r[3], 'fee': r[4], 'signature': r[5], 'public_key': r[6], 'timestamp': r[7]}) + return txs + + def _save_to_mempool(self, tx): + c = self.conn.cursor() + try: + c.execute("INSERT OR IGNORE INTO mempool VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + (tx['id'], tx['sender'], tx['recipient'], tx['amount'], tx['fee'], tx['signature'], tx['public_key'], tx.get('timestamp', time.time()))) + self.conn.commit() + except: pass + + def _remove_from_mempool(self, tx_ids): + if not tx_ids: return + c = self.conn.cursor() + for tid in tx_ids: + c.execute("DELETE FROM mempool WHERE id=?", (tid,)) + self.conn.commit() + def new_block(self, proof, previous_hash, miner, initial_difficulty=None): - """Cria um novo bloco e o adiciona à cadeia.""" block_index = len(self.chain) + 1 reward = self._get_mining_reward(block_index) - difficulty = self._calculate_difficulty_for_index(block_index) if initial_difficulty is None else initial_difficulty - - # Recompensa de mineração como string formatada - mining_reward_tx = { - 'id': str(uuid4()), 'sender': '0', 'recipient': miner, - 'amount': f"{reward:.8f}", 'fee': f"{0.0:.8f}", 'signature': '', 'public_key': '' - } + mining_reward_tx = {'id': str(uuid4()), 'sender': '0', 'recipient': miner, 'amount': f"{reward:.8f}", 'fee': f"{0.0:.8f}", 'signature': '', 'public_key': ''} + if not (proof == 100 and previous_hash == '1'): self.current_transactions.insert(0, mining_reward_tx) + + block = {'index': block_index, 'previous_hash': previous_hash, 'proof': proof, 'timestamp': time.time(), 'miner': miner, 'transactions': self.current_transactions, 'difficulty': difficulty} + + tx_ids_to_remove = [tx['id'] for tx in self.current_transactions if tx['sender'] != '0'] + self._remove_from_mempool(tx_ids_to_remove) - if not (proof == 100 and previous_hash == '1'): - self.current_transactions.insert(0, mining_reward_tx) - - block = { - 'index': block_index, - 'previous_hash': previous_hash, - 'proof': proof, - 'timestamp': time.time(), - 'miner': miner, - 'transactions': self.current_transactions, - 'difficulty': difficulty - } - self.current_transactions = [] self.chain.append(block) - + c = self.conn.cursor() c.execute("SELECT 1 FROM blocks WHERE index_=?", (block['index'],)) - if not c.fetchone(): - self._save_block(block) - else: - print(f"[AVISO] Bloco com índice {block['index']} já existe no DB. Ignorando salvamento duplicado.") + if not c.fetchone(): self._save_block(block) return block def _save_block(self, block): - """Salva um bloco e suas transações no banco de dados.""" c = self.conn.cursor() - c.execute("INSERT INTO blocks VALUES (?, ?, ?, ?, ?, ?)", - (block['index'], block['previous_hash'], block['proof'], - block['timestamp'], block['miner'], block['difficulty'])) + c.execute("INSERT INTO blocks VALUES (?, ?, ?, ?, ?, ?, ?)", (block['index'], block['previous_hash'], block['proof'], block['timestamp'], block['miner'], block['difficulty'], block.get('protocol_value', 500.0))) for t in block['transactions']: - # Salvar amount e fee como TEXT - c.execute("INSERT INTO txs VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - (t['id'], t['sender'], t['recipient'], t['amount'], # amount já é string - t['fee'], t['signature'], block['index'], t.get('public_key', ''))) + c.execute("INSERT INTO txs VALUES (?, ?, ?, ?, ?, ?, ?, ?)", (t['id'], t['sender'], t['recipient'], t['amount'], t['fee'], t['signature'], block['index'], t.get('public_key', ''))) self.conn.commit() - def new_tx(self, sender, recipient, amount_str, fee_str, signature, public_key): - """Adiciona uma nova transação à lista de transações pendentes. - amount_str e fee_str já devem ser strings formatadas.""" - tx = { - 'id': str(uuid4()), 'sender': sender, 'recipient': recipient, - 'amount': amount_str, 'fee': fee_str, 'signature': signature, 'public_key': public_key - } - if self.is_duplicate_transaction(tx): - print(f"[TX] Transação {tx.get('id', '')} já pendente. Ignorando.") - return -1 - - self.current_transactions.append(tx) - print(f"[TX] Nova transação adicionada: {tx['id']}") - return self.last_block()['index'] + 1 if self.chain else 1 - def _get_mining_reward(self, block_index): - """Calcula a recompensa de mineração com base no índice do bloco (halving).""" - if block_index <= 1200: - return 50.0 - elif block_index <= 2200: - return 25.0 - elif block_index <= 4000: - return 12.5 - elif block_index <= 5500: - return 6.5 - elif block_index <= 6200: - return 3.25 - elif block_index <= 20000: - return 1.25 - elif block_index <= 1000000: - return 0.03 + if block_index <= 1200: return 50.0 + elif block_index <= 2200: return 25.0 + elif block_index <= 4000: return 12.5 + elif block_index <= 5500: return 6.5 + elif block_index <= 6200: return 3.25 + elif block_index <= 20000: return 1.25 + elif block_index <= 1000000: return 0.03 else: halvings = (block_index - 1000000) // 2100000 - base_reward = 0.03 - reward = base_reward / (2 ** halvings) - return max(reward, 0.0) + return max(0.03 / (2 ** halvings), 0.0) - def last_block(self): - """Retorna o último bloco da cadeia.""" - return self.chain[-1] if self.chain else None + def last_block(self): return self.chain[-1] if self.chain else None def proof_of_work(self, last_proof): - """ - Encontra uma prova de trabalho que satisfaça os requisitos de dificuldade. - Retorna a prova (nonce) ou -1 se a mineração for abortada. - """ - difficulty_for_pow = self._calculate_difficulty_for_index(len(self.chain) + 1) + difficulty = self._calculate_difficulty_for_index(len(self.chain) + 1) proof = 0 - print(f"Iniciando mineração com dificuldade {difficulty_for_pow}...") - start_time = time.time() - - while not self.valid_proof(last_proof, proof, difficulty_for_pow): - global is_mining - if not is_mining: - print("[Miner] Sinal para parar recebido durante PoW. Abortando mineração.") - return -1 - - if self.last_block()['proof'] != last_proof: - print("[Miner] Outro bloco chegou na cadeia principal durante PoW. Abortando e reiniciando.") - return -1 - - if time.time() - start_time > 10 and proof % 100000 == 0: - print(f" Tentativa: {proof}") + print(f"⛏️ [MINER] Iniciando CPU. Dif: {difficulty}") + while not self.valid_proof(last_proof, proof, difficulty): + global mining_active + if not mining_active: return -1 + if proof % 1000 == 0: time.sleep(0.001) + if self.last_block()['proof'] != last_proof: return -1 proof += 1 - print(f"Mineração concluída: proof = {proof}") return proof @staticmethod def valid_proof(last_proof, proof, difficulty): - """ - Valida se um dado hash de prova satisfaz os requisitos de dificuldade. - """ guess = f"{last_proof}{proof}".encode() guess_hash = Blockchain.custom_asic_resistant_hash(guess, proof) return guess_hash[:difficulty] == "0" * difficulty + + @staticmethod + def _mine_gpu(last_proof, difficulty, stop_event, result_value): + global current_hashrate_global, mining_intensity_global + try: + import pyopencl as cl + import numpy as np + import time + except ImportError: return -1 + + try: + platforms = cl.get_platforms() + if not platforms: return -1 + target_platform = next((p for p in platforms if "nvidia" in p.name.lower()), platforms[0]) + device = target_platform.get_devices(device_type=cl.device_type.GPU)[0] + + ctx = cl.Context(devices=[device]) + queue = cl.CommandQueue(ctx) + prg = cl.Program(ctx, OPENCL_KERNEL).build() + kernel = cl.Kernel(prg, "search_block") + + result_nonce = np.zeros(1, dtype=np.uint32) + found = np.zeros(1, dtype=np.int32) + res_buf = cl.Buffer(ctx, cl.mem_flags.WRITE_ONLY, result_nonce.nbytes) + found_buf = cl.Buffer(ctx, cl.mem_flags.READ_WRITE | cl.mem_flags.COPY_HOST_PTR, hostbuf=found) + + # --- CARREGA CONFIGURAÇÃO DE INTENSIDADE --- + config = INTENSITY_CONFIG.get(mining_intensity_global, INTENSITY_CONFIG["MEDIUM"]) + batch_size = config["batch"] + loop_intern0 = config["loops"] + + print(f"🔥 [GPU] ENGINE: {device.name} | MODO: {mining_intensity_global} | BATCH: {batch_size}") + + current_nonce = 0 + start_time = time.time() + total_hashes = 0 + + while not stop_event.is_set(): + config = INTENSITY_CONFIG.get(mining_intensity_global, INTENSITY_CONFIG["MEDIUM"]) + batch_size = config["batch"] + loop_intern0 = config["loops"] + + iter_start = time.time() + kernel(queue, (batch_size,), None, res_buf, found_buf, np.uint32(difficulty), np.uint32(current_nonce), np.uint32(loop_intern0)) + queue.finish() + + if (time.time() - iter_start) < 0.001: + print("[GPU WATCHDOG] ⚠️ Driver rápido demais ou travou. Ajustando...") + + cl.enqueue_copy(queue, found, found_buf) + total_hashes += (batch_size * loop_intern0) + + now = time.time() + if now - start_time >= 3.0: + current_hashrate_global = (total_hashes / (now - start_time)) / 1e6 + print(f"⚡ [GPU] Speed: {current_hashrate_global:.2f} MH/s | Modo: {mining_intensity_global}") + start_time = now + total_hashes = 0 + + if found[0] == 1: + cl.enqueue_copy(queue, result_nonce, res_buf) + nonce = int(result_nonce[0]) + if Blockchain.valid_proof(last_proof, nonce, difficulty): + print(f"💎 [GPU] BLOCO ENCONTRADO: {nonce}") + result_value.value = nonce + stop_event.set() + return nonce + found[0] = 0 + cl.enqueue_copy(queue, found_buf, found) + + current_nonce += (batch_size * loop_intern0) + if current_nonce > 4000000000: current_nonce = 0 + + except Exception as e: + print(f"[GPU ERROR] {e}") + current_hashrate_global = 0.0 + return -1 + return -1 def tx_already_mined(self, tx_id): - """Verifica se uma transação com o dado ID já foi minerada em algum bloco.""" c = self.conn.cursor() c.execute("SELECT 1 FROM txs WHERE id=?", (tx_id,)) return c.fetchone() is not None - def valid_chain(self, chain): - """ - Determina se uma dada cadeia de blocos é válida. - Verifica hashes, provas de trabalho, transações e dificuldade. - """ - if not chain: - return False - - if chain[0]['index'] != 1 or chain[0]['previous_hash'] != '1' or chain[0]['proof'] != 100: - print("[VAL_CHAIN_ERRO] Bloco Gênese inválido.") - return False - + def valid_chain(self, chain, check_strict=True): + if not chain: return False + if chain[0]['index'] != 1 or chain[0]['previous_hash'] != '1': return False for idx in range(1, len(chain)): prev = chain[idx - 1] curr = chain[idx] - - prev_hash = self.hash(prev) - - if curr['previous_hash'] != prev_hash: - print(f"[VAL_CHAIN_ERRO] Hash anterior incorreto no bloco {curr['index']}. Esperado: {prev_hash}, Obtido: {curr['previous_hash']}.") - return False - - block_declared_difficulty = curr.get('difficulty', DIFFICULTY) - - if not self.valid_proof(prev['proof'], curr['proof'], block_declared_difficulty): - hash_check = self.custom_asic_resistant_hash(f"{prev['proof']}{curr['proof']}".encode(), curr['proof']) - print(f"[VAL_CHAIN_ERRO] Proof of Work inválido no bloco {curr['index']} com dificuldade {block_declared_difficulty}. Hash: {hash_check}") - return False - - for tx in curr.get('transactions', []): - if tx['sender'] == '0': - if tx['recipient'] != curr['miner']: - print(f"[VAL_CHAIN_ERRO] TX de recompensa inválida no bloco {curr['index']}: Recipiente incorreto.") - return False - expected_reward = self._get_mining_reward(curr['index']) - # Comparar recompensas como floats, mas tx['amount'] é string - if abs(float(tx['amount']) - expected_reward) > 0.000001: - print(f"[VAL_CHAIN_ERRO] TX de recompensa inválida no bloco {curr['index']}: Valor incorreto. Esperado: {expected_reward}, Obtido: {tx['amount']}") - return False - continue - - try: - pk_for_address_derivation = tx['public_key'] - if pk_for_address_derivation.startswith('04') and len(pk_for_address_derivation) == 130: - pk_for_address_derivation = pk_for_address_derivation[2:] - - derived_address = hashlib.sha256(bytes.fromhex(pk_for_address_derivation)).hexdigest()[:40] - if derived_address != tx['sender']: - print(f"[VAL_CHAIN_ERRO] Transação {tx['id']} no bloco {curr['index']}: Endereço ({tx['sender']}) não bate com o derivado da chave pública ({derived_address}).") - return False - - # CRÍTICO: Garantir que amount e fee são strings formatadas para a verificação - # Sempre converte para float primeiro, depois formata para string com .8f - amount_to_verify = f"{float(tx['amount']):.8f}" - fee_to_verify = f"{float(tx['fee']):.8f}" - - tx_copy_for_signature = { - 'amount': amount_to_verify, - 'fee': fee_to_verify, - 'recipient': tx['recipient'], - 'sender': tx['sender'] - } - message = json.dumps(tx_copy_for_signature, sort_keys=True, separators=(",", ":")).encode() - - vk = VerifyingKey.from_string(bytes.fromhex(tx['public_key']), curve=SECP256k1) - vk.verify_digest(bytes.fromhex(tx['signature']), hashlib.sha256(message).digest()) - - except BadSignatureError: - print(f"[VAL_CHAIN_ERRO] Transação {tx['id']} inválida no bloco {curr['index']}: Assinatura inválida.") - return False - except Exception as e: - print(f"[VAL_CHAIN_ERRO] Transação {tx['id']} inválida no bloco {curr['index']}: {e}") - return False + if check_strict: + if curr['previous_hash'] != self.hash(prev): return False + if curr['index'] >= LEGACY_CUTOFF_INDEX: + if not self.valid_proof(prev['proof'], curr['proof'], curr.get('difficulty', DIFFICULTY)): return False + else: + if curr['index'] != prev['index'] + 1: return False return True - def _calculate_difficulty_for_index(self, target_block_index): - """ - Calcula a dificuldade esperada para um dado índice de bloco. - Implementa o ajuste de dificuldade do Bitcoin. - """ - if target_block_index <= self.ADJUST_INTERVAL: - return DIFFICULTY - - if len(self.chain) < target_block_index - 1: - return self.chain[-1].get('difficulty', DIFFICULTY) if self.chain else DIFFICULTY - - start_block_index_in_chain = target_block_index - self.ADJUST_INTERVAL - 1 - end_block_index_in_chain = target_block_index - 2 - - if start_block_index_in_chain < 0 or end_block_index_in_chain < 0: - return DIFFICULTY - - start_block_for_calc = self.chain[start_block_index_in_chain] - end_block_for_calc = self.chain[end_block_index_in_chain] - - actual_window_time = end_block_for_calc['timestamp'] - start_block_for_calc['timestamp'] - expected_time = self.TARGET_TIME * self.ADJUST_INTERVAL - - current_calculated_difficulty = end_block_for_calc.get('difficulty', DIFFICULTY) - - new_difficulty = current_calculated_difficulty - if actual_window_time < expected_time / 4: - new_difficulty += 2 - elif actual_window_time < expected_time / 2: - new_difficulty += 1 - elif actual_window_time > expected_time * 4 and new_difficulty > 1: - new_difficulty -= 2 - elif actual_window_time > expected_time * 2 and new_difficulty > 1: - new_difficulty -= 1 - - return max(1, new_difficulty) - def get_total_difficulty(self, chain_to_check): - """Calcula a dificuldade acumulada de uma cadeia.""" - total_difficulty = 0 - for block in chain_to_check: - total_difficulty += block.get('difficulty', DIFFICULTY) - return total_difficulty + return sum([block.get('difficulty', DIFFICULTY) for block in chain_to_check]) def resolve_conflicts(self): - """ - Implementa o algoritmo de consenso para resolver conflitos na cadeia. - Substitui a cadeia local pela mais longa e válida da rede. - """ - neighbors = known_nodes.copy() + neighbors = list(known_nodes) new_chain = None - current_total_difficulty = self.get_total_difficulty(self.chain) - - print(f"[CONSENSO] Tentando resolver conflitos com {len(neighbors)} vizinhos... Cadeia local dificuldade: {current_total_difficulty}") + max_difficulty = self.get_total_difficulty(self.chain) + + is_fresh_install = (len(self.chain) <= 1) + if is_fresh_install: + print("[BOOT] 🐇 MODO CÓPIA CEGA: Baixando tudo (Blocos + Mempool) do Seed...") + print(f"[CONSENSO] A verificar {len(neighbors)} vizinhos...") for node_url in neighbors: - if node_url == meu_url: - continue + if node_url == meu_url: continue try: - response = requests.get(f"{node_url}/chain", timeout=10) + # HEADER NGROK ADICIONADO AQUI + response = requests.get(f"{node_url}/chain", timeout=60, headers=NGROK_HEADERS) if response.status_code == 200: data = response.json() peer_chain = data.get("chain") - - if not peer_chain: - print(f"[CONSENSO] Resposta malformada (sem 'chain') de {node_url}. Removendo peer.") - known_nodes.discard(node_url) - salvar_peers(known_nodes) - continue - - peer_total_difficulty = self.get_total_difficulty(peer_chain) + peer_mempool = data.get("pending_transactions", []) - print(f"[CONSENSO] Node {node_url}: Dificuldade Total={peer_total_difficulty}, Comprimento={len(peer_chain)}. Local Comprimento={len(self.chain)}") - - if peer_total_difficulty > current_total_difficulty and self.valid_chain(peer_chain): - current_total_difficulty = peer_total_difficulty - new_chain = peer_chain - print(f"[CONSENSO] ✔ Cadeia mais difícil e válida encontrada em {node_url} (Dificuldade: {peer_total_difficulty})") - else: - print(f"[CONSENSO] ✘ Cadeia de {node_url} (Dificuldade: {peer_total_difficulty}) não é mais difícil ou não é válida.") - else: - print(f"[CONSENSO] Resposta inválida de {node_url}: Status {response.status_code}. Removendo peer.") - known_nodes.discard(node_url) - salvar_peers(known_nodes) - except requests.exceptions.RequestException as e: - print(f"[CONSENSO] Erro ao buscar cadeia de {node_url}: {e}. Removendo peer.") - known_nodes.discard(node_url) - salvar_peers(known_nodes) - - if new_chain: - old_chain_tx_ids = set() - for block in self.chain: - for tx in block.get('transactions', []): - old_chain_tx_ids.add(tx['id']) - - new_chain_tx_ids = set() - for block in new_chain: - for tx in block.get('transactions', []): - new_chain_tx_ids.add(tx['id']) - - re_add_txs = [] - for block in self.chain: - for tx in block.get('transactions', []): - if tx['id'] not in new_chain_tx_ids and tx['sender'] != '0': - re_add_txs.append(tx) - - for tx in self.current_transactions: - if tx['id'] not in new_chain_tx_ids: - re_add_txs.append(tx) - - self.current_transactions = [] - for tx in re_add_txs: - temp_tx_for_duplicate_check = { - 'sender': tx['sender'], - 'recipient': tx['recipient'], - 'amount': tx['amount'], # Já é string - 'fee': tx['fee'], # Já é string - 'id': tx.get('id') - } - if not self.is_duplicate_transaction(temp_tx_for_duplicate_check): - self.current_transactions.append(tx) + if not peer_chain: continue + + peer_difficulty = self.get_total_difficulty(peer_chain) + + if peer_difficulty > max_difficulty: + if self.valid_chain(peer_chain, check_strict=not is_fresh_install): + max_difficulty = peer_difficulty + new_chain = peer_chain + print(f"[CONSENSO] 📥 Nova chain encontrada em: {node_url}") + + if is_fresh_install or peer_difficulty >= max_difficulty: + count_new_tx = 0 + for tx in peer_mempool: + if not self.is_duplicate_transaction(tx) and not self.tx_already_mined(tx['id']): + self.current_transactions.append(tx) + self._save_to_mempool(tx) + count_new_tx += 1 + if count_new_tx > 0: + print(f"[MEMPOOL] 📥 Baixadas {count_new_tx} transações pendentes do Seed.") + + except: pass + if new_chain: self.chain = new_chain self._rebuild_db_from_chain() - print(f"[CONSENSO] ✅ Cadeia substituída com sucesso pela mais difícil e válida (Dificuldade: {current_total_difficulty}). {len(re_add_txs)} transações re-adicionadas.") + print(f"[CONSENSO] ✅ Sincronizado Completamente. Blocos: {len(self.chain)}") return True - - print("[CONSENSO] 🔒 Cadeia local continua sendo a mais difícil ou nenhuma cadeia mais difícil/válida foi encontrada.") return False def _rebuild_db_from_chain(self): - """Reconstrói o banco de dados local a partir da cadeia atual (usado após consenso).""" - print("[REBUILD] Reconstruindo dados locais a partir da nova cadeia...") try: c = self.conn.cursor() - c.execute("DELETE FROM blocks") - c.execute("DELETE FROM txs") - + c.execute("DELETE FROM txs"); c.execute("DELETE FROM blocks") for block in self.chain: - difficulty_to_save = block.get('difficulty', DIFFICULTY) - c.execute("INSERT INTO blocks VALUES (?, ?, ?, ?, ?, ?)", - (block['index'], block['previous_hash'], block['proof'], - block['timestamp'], block['miner'], difficulty_to_save)) + c.execute("INSERT INTO blocks VALUES (?, ?, ?, ?, ?, ?, ?)", (block['index'], block['previous_hash'], block['proof'], block['timestamp'], block['miner'], block.get('difficulty', 1), block.get('protocol_value', 0.0))) for tx in block['transactions']: - c.execute("INSERT INTO txs VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - (tx['id'], tx['sender'], tx['recipient'], tx['amount'], # amount já é string - tx['fee'], tx['signature'], block['index'], tx.get('public_key', ''))) + c.execute("INSERT INTO txs VALUES (?, ?, ?, ?, ?, ?, ?, ?)", (tx['id'], tx['sender'], tx['recipient'], tx['amount'], tx['fee'], tx['signature'], block['index'], tx.get('public_key', ''))) self.conn.commit() - print("[REBUILD] Banco reconstruído com sucesso.") - except Exception as e: - print(f"[REBUILD] Erro ao reconstruir banco: {e}") - sys.exit(1) + except Exception as e: print(f"[REBUILD ERRO] {e}") def balance(self, address): - """Calcula o saldo de um endereço, incluindo transações pendentes.""" bal = 0.0 for block in self.chain: for t in block['transactions']: - if t['sender'] == address: - bal -= (float(t['amount']) + float(t['fee'])) # Converter para float para cálculo - if t['recipient'] == address: - bal += float(t['amount']) # Converter para float para cálculo - + if t['sender'] == address: bal -= (float(t['amount']) + float(t['fee'])) + if t['recipient'] == address: bal += float(t['amount']) for t in self.current_transactions: - if t['sender'] == address: - bal -= (float(t['amount']) + float(t['fee'])) # Converter para float para cálculo - if t['recipient'] == address: - bal += float(t['amount']) # Converter para float para cálculo + if t['sender'] == address: bal -= (float(t['amount']) + float(t['fee'])) return bal -# --- Funções de Criptografia e Carteira --- +# --- Funções de Carteira --- def gerar_endereco(public_key_hex): - """Gera um endereço de carteira a partir de uma chave pública hexadecimal.""" try: - # A chave pública pode vir com prefixo '04' - if public_key_hex.startswith("04"): - public_key_hex = public_key_hex[2:] - public_key_bytes = bytes.fromhex(public_key_hex) - return hashlib.sha256(public_key_bytes).hexdigest()[:40] - except ValueError: - return None + if public_key_hex.startswith("04"): public_key_hex = public_key_hex[2:] + return hashlib.sha256(bytes.fromhex(public_key_hex)).hexdigest()[:40] + except: return None def sign_transaction(private_key_hex, tx_data): - """ - Assina uma transação com a chave privada ECDSA (SECP256k1). - tx_data deve ter: 'sender', 'recipient', 'amount' (string), 'fee' (string). - Retorna a assinatura em hex. - """ sk = SigningKey.from_string(bytes.fromhex(private_key_hex), curve=SECP256k1) - - # Recria o dicionário na ordem que o servidor espera. - # amount e fee já devem ser strings formatadas aqui. - message_data = { - 'amount': tx_data['amount'], - 'fee': tx_data['fee'], - 'recipient': tx_data['recipient'], - 'sender': tx_data['sender'] - } - - # Serialização determinística sem espaços - message_json = json.dumps( - message_data, - sort_keys=True, - separators=(',',':') - ).encode('utf-8') - - print(f"DEBUG_SIGN: JSON da mensagem para assinatura (decodificado): {message_json.decode('utf-8')}") - print(f"DEBUG_SIGN: Bytes da mensagem para assinatura (raw): {message_json}") - print(f"DEBUG_SIGN: Hash da mensagem para assinatura (SHA256, HEX): {hashlib.sha256(message_json).hexdigest()}") - - # SHA256 + sign_digest - message_hash = hashlib.sha256(message_json).digest() - return sk.sign_digest(message_hash).hex() + message_json = json.dumps({'amount': tx_data['amount'], 'fee': tx_data['fee'], 'recipient': tx_data['recipient'], 'sender': tx_data['sender']}, sort_keys=True, separators=(',',':')).encode('utf-8') + return sk.sign_digest(hashlib.sha256(message_json).digest()).hex() def create_wallet(): - """Cria e retorna dados de uma nova carteira.""" - private_key_obj = SigningKey.generate(curve=SECP256k1) - public_key_obj = private_key_obj.get_verifying_key() - private_key_hex = private_key_obj.to_string().hex() - public_key_hex = "04" + public_key_obj.to_string().hex() # Adicionar prefixo '04' - address = gerar_endereco(public_key_hex) - - if address is None: # Corrigido de '===' para 'is' - return None - - return { - 'private_key': private_key_hex, - 'public_key': public_key_hex, - 'address': address - } + pk = SigningKey.generate(curve=SECP256k1) + pub = "04" + pk.get_verifying_key().to_string().hex() + return {'private_key': pk.to_string().hex(), 'public_key': pub, 'address': gerar_endereco(pub)} def load_wallet_file(filepath): - """Carrega dados da carteira de um arquivo JSON.""" if os.path.exists(filepath): try: - with open(filepath, 'r') as f: - wallet_data = json.load(f) - if 'public_key' in wallet_data: - derived_addr_check = gerar_endereco(wallet_data['public_key']) - if derived_addr_check and derived_addr_check != wallet_data.get('address'): - wallet_data['address'] = derived_addr_check - # Opcional: Salvar a carteira atualizada se o endereço foi corrigido - with open(filepath, "w") as fw: - json.dump(wallet_data, fw, indent=4) - return wallet_data - except (json.JSONDecodeError, FileNotFoundError): - return None + with open(filepath, 'r') as f: return json.load(f) + except: return None return None def save_wallet_file(wallet_data, filepath): - """Salva dados da carteira em um arquivo JSON.""" - with open(filepath, 'w') as f: - json.dump(wallet_data, f, indent=4) + with open(filepath, 'w') as f: json.dump(wallet_data, f, indent=4) -# --- Flask Endpoints (do nó) --- -@app.route('/', methods=['GET']) -def index_web(): - return "Kert-One Blockchain Node is running!" +# --- Funções de Token Visual --- +def load_ngrok_token(): + if os.path.exists(NGROK_AUTH_FILE): + try: + with open(NGROK_AUTH_FILE, 'r') as f: return f.read().strip() + except: return None + return None -@app.route('/miner') -def miner_web(): - return render_template('miner.html') +def save_ngrok_token(token): + try: + with open(NGROK_AUTH_FILE, 'w') as f: f.write(token.strip()) + except: pass +# --- Flask Endpoints --- @app.route('/chain', methods=['GET']) def chain_api(): - response = { - 'chain': blockchain.chain, - 'length': len(blockchain.chain), - 'pending_transactions': blockchain.current_transactions, - 'coin_name': COIN_NAME, - 'coin_symbol': COIN_SYMBOL, - 'node_id': node_id - } - return jsonify(response), 200 + return jsonify({'chain': blockchain.chain, 'length': len(blockchain.chain), 'pending_transactions': blockchain.current_transactions, 'node_id': node_id}), 200 @app.route('/nodes/register', methods=['POST']) def register_nodes_api(): - data = request.get_json() - new_node_ip = data.get('ip') - new_node_port = data.get('port') - - if not new_node_ip or not new_node_port: - return jsonify({"message": "IP ou porta inválidos/ausentes."}), 400 - - new_node_url = f"http://{new_node_ip}:{new_node_port}" - - if new_node_url != meu_url: - if new_node_url not in known_nodes: - known_nodes.add(new_node_url) - salvar_peers(known_nodes) - print(f"[INFO] Peer {new_node_url} registrado.") - else: - print(f"[INFO] Peer {new_node_url} já estava registrado. Atualizando, se necessário.") - else: - print(f"[INFO] Recebi meu próprio registro: {new_node_url}. Ignorando.") - - return jsonify({ - "message": f"Peer {new_node_url} registrado ou atualizado.", - "known_peers": list(known_nodes) - }), 200 + data = request.get_json(silent=True) or {} + new_node_url = data.get("url") + if not new_node_url and data.get("ip"): new_node_url = f"http://{data['ip']}:{data.get('port')}" + if not new_node_url: return jsonify({"message": "Invalido"}), 400 + new_node_url = new_node_url.strip().rstrip("/") + if not new_node_url.startswith("http"): new_node_url = "http://" + new_node_url + if new_node_url == meu_url: return jsonify({"message": "Self ignored"}), 200 + if new_node_url not in known_nodes: + known_nodes.add(new_node_url); salvar_peers(known_nodes) + try: + # HEADER NGROK ADICIONADO AQUI + requests.post(f"{new_node_url}/nodes/register", json={"url": meu_url}, timeout=5, headers=NGROK_HEADERS) + except: pass + return jsonify({"message": "Registrado", "known_peers": list(known_nodes)}), 200 @app.route('/nodes', methods=['GET']) -def get_nodes_api(): - return jsonify({'nodes': list(known_nodes)}), 200 +def get_nodes_api(): return jsonify({'nodes': list(known_nodes)}), 200 @app.route('/nodes/resolve', methods=['GET']) def resolve_api(): replaced = blockchain.resolve_conflicts() - if replaced: - response = {'message': 'Nossa cadeia foi substituída.'} - else: - response = {'message': 'Nossa cadeia é a mais longa.'} - return jsonify(response), 200 + return jsonify({'message': 'Cadeia substituida' if replaced else 'Cadeia mantida'}), 200 @app.route('/balance/', methods=['GET']) def balance_api(addr): - return jsonify({ - 'address': addr, - 'balance': blockchain.balance(addr), - 'coin_name': COIN_NAME, - 'coin_symbol': COIN_SYMBOL - }), 200 + return jsonify({'address': addr, 'balance': blockchain.balance(addr), 'symbol': COIN_SYMBOL}), 200 @app.route('/tx/new', methods=['POST']) def new_transaction_api(): - """Recebe uma nova transação do cliente e a adiciona à fila pendente.""" - print(f"DEBUG_SERVER: Requisição recebida para /tx/new") - print(f"DEBUG_SERVER: Headers da requisição: {request.headers}") - print(f"DEBUG_SERVER: Mimetype da requisição: {request.mimetype}") - print(f"DEBUG_SERVER: Content-Type da requisição: {request.content_type}") - print(f"DEBUG_SERVER: Dados da requisição (raw): {request.data}") - - raw_values = None - try: - raw_values = request.get_json(silent=True) - print(f"DEBUG_SERVER: Payload JSON parseado (request.get_json()): {raw_values}") - except Exception as e: - print(f"DEBUG_SERVER: ERRO - Exceção durante o parsing JSON: {e}") - - if raw_values is None: - print(f"DEBUG_SERVER: ERRO - request.get_json() retornou None. Verifique o Content-Type ou a validade do JSON.") - return jsonify({'message': 'Erro: Não foi possível parsear o JSON da requisição. Verifique o Content-Type ou a validade do JSON.'}), 400 - - values = raw_values - + values = request.get_json(silent=True) required = ['id', 'sender', 'recipient', 'amount', 'fee', 'public_key', 'signature'] - if not all(k in values for k in required): - missing = [k for k in required if k not in values] - print(f"[ERRO 400] Valores ausentes na transação: {missing}") - return jsonify({'message': f'Valores ausentes na requisição: {", ".join(missing)}'}), 400 - + if not values or not all(k in values for k in required): return jsonify({'message': 'Dados incompletos'}), 400 try: - # amount e fee vêm como strings do cliente, mas podem precisar de formatação - amount_float = float(values['amount']) - fee_float = float(values['fee']) - amount_str_formatted = f"{amount_float:.8f}" - fee_str_formatted = f"{fee_float:.8f}" - - transaction = { - 'id': values['id'], - 'sender': values['sender'], - 'recipient': values['recipient'], - 'amount': amount_str_formatted, # Armazenar como string formatada - 'fee': fee_str_formatted, # Armazenar como string formatada - 'public_key': values['public_key'], - 'signature': values['signature'], - 'timestamp': values.get('timestamp', time.time()) - } - except Exception as e: - print(f"[ERRO 400] Erro ao construir transação: {e}") - return jsonify({'message': f'Erro ao processar dados da transação: {e}'}), 400 - - temp_tx_for_duplicate_check = { - 'sender': transaction['sender'], - 'recipient': transaction['recipient'], - 'amount': transaction['amount'], # Já é string - 'fee': transaction['fee'], # Já é string - 'id': transaction.get('id') - } - if blockchain.is_duplicate_transaction(temp_tx_for_duplicate_check): - print(f"[AVISO] Transação duplicada detectada para {transaction['sender']} -> {transaction['recipient']}. Ignorando.") - return jsonify({'message': 'Transação duplicada detectada. Não adicionada novamente.'}), 200 - - try: - pk_for_address_derivation = transaction['public_key'] - if pk_for_address_derivation.startswith('04') and len(pk_for_address_derivation) == 130: - pk_for_address_derivation = pk_for_address_derivation[2:] + tx = {'id': values['id'], 'sender': values['sender'], 'recipient': values['recipient'], 'amount': f"{float(values['amount']):.8f}", 'fee': f"{float(values['fee']):.8f}", 'public_key': values['public_key'], 'signature': values['signature'], 'timestamp': values.get('timestamp', time.time())} + if blockchain.is_duplicate_transaction(tx): return jsonify({'message': 'Duplicada'}), 200 - derived_address = hashlib.sha256(bytes.fromhex(pk_for_address_derivation)).hexdigest()[:40] - if derived_address != transaction['sender']: - print(f"[ERRO 400] Assinatura inválida: Endereço do remetente ({transaction['sender']}) não corresponde à chave pública fornecida ({derived_address}).") - return jsonify({'message': 'Assinatura inválida: Endereço do remetente não corresponde à chave pública fornecida'}), 400 - - if not verify_signature(transaction['public_key'], transaction['signature'], transaction): - print(f"[ERRO 400] Assinatura inválida ou chave pública malformada para TX ID: {transaction.get('id')}") - return jsonify({'message': 'Assinatura inválida ou chave pública malformada: Falha na verificação da assinatura'}), 400 - - except Exception as e: - print(f"[ERRO 400] Erro inesperado na validação da assinatura: {e}. TX ID: {transaction.get('id')}") - return jsonify({'message': f'Erro inesperado na validação da transação: {e}'}), 400 - - # Usar float() para cálculo de saldo, pois balance() espera floats para isso - current_balance = blockchain.balance(transaction['sender']) - required_amount = float(transaction['amount']) + float(transaction['fee']) - if current_balance < required_amount: - print(f"[ERRO 400] Saldo insuficiente para {transaction['sender']}: Necessário {required_amount}, Disponível {current_balance}. TX ID: {transaction.get('id')}") - return jsonify({'message': f'Saldo insuficiente para a transação. Saldo atual: {current_balance}, Necessário: {required_amount}'}), 400 - - # Adicionar à fila de transações pendentes (amount e fee já são strings formatadas) - blockchain.current_transactions.append(transaction) - - broadcast_tx_to_peers(transaction) - - response = {'message': f'Transação adicionada à fila de transações pendentes.', - 'coin_name': COIN_NAME, - 'coin_symbol': COIN_SYMBOL, - 'transaction_id': transaction['id']} - return jsonify(response), 201 + vk = VerifyingKey.from_string(bytes.fromhex(values['public_key']), curve=SECP256k1) + msg = json.dumps({'amount': tx['amount'], 'fee': tx['fee'], 'recipient': tx['recipient'], 'sender': tx['sender']}, sort_keys=True, separators=(',', ':')).encode('utf-8') + vk.verify_digest(bytes.fromhex(tx['signature']), hashlib.sha256(msg).digest()) + + if blockchain.balance(tx['sender']) < (float(tx['amount']) + float(tx['fee'])): return jsonify({'message': 'Saldo insuficiente'}), 400 + + blockchain.current_transactions.append(tx) + blockchain._save_to_mempool(tx) + + broadcast_tx_to_peers(tx) + return jsonify({'message': 'TX Adicionada e Salva'}), 201 + except Exception as e: return jsonify({'message': f'Erro: {e}'}), 400 def broadcast_tx_to_peers(tx): - """Envia uma transação para todos os peers conhecidos.""" - print(f"[Broadcast TX] Enviando transação {tx.get('id')} para peers.") - peers_to_remove = set() - for peer in known_nodes.copy(): + all_targets = known_nodes | set(SEED_NODES) + print(f"[REDE] 📡 Propagando transação para {len(all_targets)} nós.") + for peer in all_targets: if peer == meu_url: continue - try: - requests.post(f"{peer}/tx/receive", json=tx, timeout=3) - except requests.exceptions.RequestException as e: - print(f"[Broadcast TX] Erro ao enviar TX para {peer}: {e}. Removendo peer (se não for seed).") - if peer not in SEED_NODES: - peers_to_remove.add(peer) - - if peers_to_remove: - known_nodes.difference_update(peers_to_remove) - salvar_peers(known_nodes) - print(f"[Broadcast TX] Removidos {len(peers_to_remove)} peers problemáticos.") + try: + # HEADER NGROK ADICIONADO AQUI + requests.post(f"{peer}/tx/receive", json=tx, timeout=3, headers=NGROK_HEADERS) + except: pass @app.route('/tx/receive', methods=['POST']) def receive_transaction_api(): - """Recebe uma transação de outro nó e a adiciona à fila pendente após validação.""" - tx_data = request.get_json() - if not tx_data: - return jsonify({"message": "Nenhum dado de transação recebido."}), 400 - - required = ['id', 'sender', 'recipient', 'amount', 'fee', 'public_key', 'signature'] - if not all(k in tx_data for k in required): - return jsonify({'message': 'Dados de transação incompletos.'}), 400 - - try: - # amount e fee vêm como strings (esperado), garantir formatação - amount_float = float(tx_data['amount']) - fee_float = float(tx_data['fee']) - amount_str_formatted = f"{amount_float:.8f}" - fee_str_formatted = f"{fee_float:.8f}" - - temp_tx_for_duplicate_check = { - 'sender': tx_data['sender'], - 'recipient': tx_data['recipient'], - 'amount': amount_str_formatted, # Usar string formatada - 'fee': fee_str_formatted, # Usar string formatada - 'id': tx_data.get('id') - } - if blockchain.is_duplicate_transaction(temp_tx_for_duplicate_check): - print(f"[RECEIVE TX] Transação {tx_data.get('id')} já existe na fila pendente. Ignorando.") - return jsonify({'message': 'Transação já conhecida.'}), 200 - - # Passar a transação com amount/fee como strings formatadas para verify_signature - tx_for_verification = { - 'id': tx_data['id'], - 'sender': tx_data['sender'], - 'recipient': tx_data['recipient'], - 'amount': amount_str_formatted, - 'fee': fee_str_formatted, - 'public_key': tx_data['public_key'], - 'signature': tx_data['signature'], - 'timestamp': tx_data.get('timestamp', time.time()) - } - - if not verify_signature(tx_for_verification['public_key'], tx_for_verification['signature'], tx_for_verification): - print(f"[RECEIVE TX ERROR] TX {tx_data.get('id')}: Assinatura inválida ou chave pública malformada.") - return jsonify({'message': 'Transação inválida: Assinatura inválida ou chave pública malformada.'}), 400 - - # Usar float() para cálculo de saldo, pois balance() espera floats para isso - current_balance = blockchain.balance(tx_data['sender']) - required_amount = float(tx_data['amount']) + float(tx_data['fee']) - if current_balance < required_amount: - print(f"[RECEIVE TX ERROR] TX {tx_data.get('id')}: Saldo insuficiente para {tx_data['sender']}.") - return jsonify({'message': 'Transação inválida: Saldo insuficiente.'}), 400 - - # Adicionar à fila de transações pendentes (amount e fee já são strings formatadas) - blockchain.current_transactions.append(tx_for_verification) - print(f"[RECEIVE TX] Transação {tx_data.get('id')} recebida e adicionada à fila pendente.") - return jsonify({"message": "Transação recebida e adicionada com sucesso."}), 200 + tx = request.get_json() + if not tx: return jsonify({"message": "No data"}), 400 + if not blockchain.is_duplicate_transaction(tx): + blockchain.current_transactions.append(tx) + blockchain._save_to_mempool(tx) + return jsonify({"message": "OK"}), 200 - except Exception as e: - print(f"[RECEIVE TX ERROR] Erro inesperado ao processar TX {tx_data.get('id')}: {e}") - return jsonify({'message': f'Erro interno ao processar transação: {e}'}), 500 - -def verify_signature(public_key_hex, signature_hex, tx_data): - """ - Verifica a assinatura de uma transação. - tx_data deve conter 'sender', 'recipient', 'amount', 'fee'. - 'amount' e 'fee' podem ser strings ou floats ao entrar nesta função, - mas serão convertidos para string formatada para a verificação. - """ - try: - vk = VerifyingKey.from_string(bytes.fromhex(public_key_hex), curve=SECP256k1) - - # CRÍTICO: Garantir que amount e fee são strings formatadas para a verificação - # Sempre converte para float primeiro, depois formata para string com .8f - amount_to_verify = f"{float(tx_data['amount']):.8f}" - fee_to_verify = f"{float(tx_data['fee']):.8f}" - - prepared_message_data = { - 'amount': amount_to_verify, - 'fee': fee_to_verify, - 'recipient': tx_data['recipient'], - 'sender': tx_data['sender'] - } - - message = json.dumps(prepared_message_data, sort_keys=True, separators=(',', ':')).encode('utf-8') - - message_hash_bytes = hashlib.sha256(message).digest() - signature_bytes = bytes.fromhex(signature_hex) - - print(f"DEBUG_VERIFY: Chave Pública recebida (hex): {public_key_hex}") - print(f"DEBUG_VERIFY: Assinatura recebida (hex): {signature_hex}") - print(f"DEBUG_VERIFY: Dados da mensagem para verificação (antes de json.dumps): {prepared_message_data}") - print(f"DEBUG_VERIFY: JSON da mensagem para verificação (decodificado): {message.decode('utf-8')}") - print(f"DEBUG_VERIFY: Bytes da mensagem para verificação (raw): {message}") - print(f"DEBUG_VERIFY: Hash da mensagem para verificação (SHA256, HEX): {hashlib.sha256(message).hexdigest()}") - - vk.verify_digest(signature_bytes, message_hash_bytes) - return True - except BadSignatureError: - print("Falha na verificação da assinatura: BadSignatureError!") - return False - except ValueError as ve: - print(f"Falha na verificação da assinatura: ValueError (e.g., bad hex string or malformed key): {ve}") - return False - except Exception as e: - print(f"Erro durante a verificação da assinatura: {e}") - return False - @app.route('/blocks/receive', methods=['POST']) def receive_block_api(): - """Recebe um bloco de outro nó e tenta adicioná-lo à cadeia local.""" - block_data = request.get_json() - if not block_data: - print("[RECEIVE_BLOCK ERROR] Nenhum dado de bloco recebido.") - return jsonify({"message": "Nenhum dado de bloco recebido."}), 400 - - required_keys = ['index', 'previous_hash', 'proof', 'timestamp', 'miner', 'transactions', 'difficulty'] - if not all(k in block_data for k in required_keys): - print(f"[RECEIVE_BLOCK ERROR] Bloco recebido com chaves ausentes: {block_data}") - return jsonify({"message": "Dados de bloco incompletos ou malformados."}), 400 - - if not blockchain.chain: - print("[RECEIVE_BLOCK INFO] Cadeia local vazia. Iniciando resolução de conflitos para sincronização inicial.") - threading.Thread(target=blockchain.resolve_conflicts, daemon=True).start() - return jsonify({'message': 'Cadeia local vazia. Tentando sincronizar com a rede.'}), 202 - - last_local_block = blockchain.last_block() - - if block_data['index'] <= last_local_block['index']: - if block_data['index'] == last_local_block['index'] and \ - block_data['previous_hash'] == last_local_block['previous_hash'] and \ - block_data['proof'] == last_local_block['proof'] and \ - block_data['miner'] == last_local_block['miner'] and \ - block_data['difficulty'] == last_local_block['difficulty']: - print(f"[RECEIVE_BLOCK INFO] Bloco {block_data['index']} já recebido e processado (duplicado).") - return jsonify({'message': 'Bloco já recebido e processado'}), 200 - else: - print(f"[RECEIVE_BLOCK INFO] Bloco {block_data['index']} é antigo ou de um fork mais curto/inválido (Local: {last_local_block['index']}). Ignorando.") - return jsonify({'message': 'Bloco antigo ou de um fork irrelevante.'}), 200 - - if block_data['index'] == last_local_block['index'] + 1: - expected_previous_hash = blockchain.hash(last_local_block) - if block_data['previous_hash'] != expected_previous_hash: - print(f"[RECEIVE_BLOCK ERROR] Bloco {block_data['index']}: Hash anterior incorreto. Esperado: {expected_previous_hash}, Recebido: {block_data['previous_hash']}. Iniciando sincronização.") - threading.Thread(target=blockchain.resolve_conflicts, daemon=True).start() - return jsonify({'message': 'Hash anterior incorreto, resolução de conflitos iniciada'}), 400 - - if not blockchain.valid_proof(last_local_block['proof'], block_data['proof'], block_data['difficulty']): - print(f"[RECEIVE_BLOCK ERROR] Bloco {block_data['index']}: Prova de Trabalho inválida. Iniciando sincronização.") - threading.Thread(target=blockchain.resolve_conflicts, daemon=True).start() - return jsonify({'message': 'Prova inválida, resolução de conflitos iniciada'}), 400 - - for tx in block_data.get('transactions', []): - if tx['sender'] == '0': - continue - - try: - # Certificar que amount e fee são strings formatadas para verificação - tx_for_verification = { - 'id': tx['id'], - 'sender': tx['sender'], - 'recipient': tx['recipient'], - 'amount': f"{float(tx['amount']):.8f}", # Garante formato string .8f - 'fee': f"{float(tx['fee']):.8f}", # Garante formato string .8f - 'public_key': tx['public_key'], - 'signature': tx['signature'], - 'timestamp': tx.get('timestamp', time.time()) - } - if not verify_signature(tx_for_verification['public_key'], tx_for_verification['signature'], tx_for_verification): - raise ValueError(f"Assinatura inválida para transação {tx.get('id', 'N/A')}") - - except Exception as e: - print(f"[RECEIVE_BLOCK ERROR] Transação inválida {tx.get('id', 'N/A')} no bloco {block_data['index']}: {e}. Iniciando sincronização.") - threading.Thread(target=blockchain.resolve_conflicts, daemon=True).start() - return jsonify({'message': f'Transação inválida no bloco: {e}'}), 400 - - print(f"[RECEIVE_BLOCK SUCCESS] Bloco {block_data['index']} aceito e adicionado localmente.") - blockchain.chain.append(block_data) - blockchain._save_block(block_data) - - mined_tx_ids = {t.get('id') for t in block_data.get('transactions', []) if t.get('id')} - blockchain.current_transactions = [ - tx for tx in blockchain.current_transactions if tx.get('id') not in mined_tx_ids - ] - print(f"[RECEIVE_BLOCK] Removidas {len(mined_tx_ids)} transações da fila pendente.") - - return jsonify({'message': 'Bloco aceito e adicionado'}), 200 - - elif block_data['index'] > last_local_block['index'] + 1: - print(f"[RECEIVE_BLOCK INFO] Bloco {block_data['index']} está à frente da cadeia local ({last_local_block['index']}). Iniciando resolução de conflitos.") + block = request.get_json() + last = blockchain.last_block() + if block['index'] > last['index'] + 1: threading.Thread(target=blockchain.resolve_conflicts, daemon=True).start() - return jsonify({'message': 'Bloco está à frente. Iniciando sincronização.'}), 202 - - print(f"[RECEIVE_BLOCK WARNING] Condição inesperada para o bloco {block_data['index']}. Iniciando resolução de conflitos.") - threading.Thread(target=blockchain.resolve_conflicts, daemon=True).start() - return jsonify({'message': 'Bloco com status inesperado, resolução de conflitos iniciada'}), 400 + return jsonify({'message': 'Sync started'}), 202 + if blockchain.valid_proof(last['proof'], block['proof'], block['difficulty']): + blockchain.chain.append(block); blockchain._save_block(block) + mined_ids = {t['id'] for t in block['transactions']} + blockchain.current_transactions = [t for t in blockchain.current_transactions if t['id'] not in mined_ids] + blockchain._remove_from_mempool(list(mined_ids)) + return jsonify({'message': 'Bloco aceito'}), 200 + return jsonify({'message': 'Invalido'}), 400 @app.route('/sync/check', methods=['GET']) def check_sync_api(): last = blockchain.last_block() - local_hash = blockchain.hash(last) - return jsonify({ - 'index': last['index'], - 'hash': local_hash, - 'timestamp': last['timestamp'], - 'miner': last['miner'], - 'num_txs': len(last['transactions']) - }) + return jsonify({'index': last['index'], 'hash': blockchain.hash(last)}) @app.route('/miner/set_address', methods=['POST']) def set_miner_address_api(): - """Define o endereço de mineração para o nó.""" - global miner_address + global miner_address_global + data = request.get_json() or {} + addr = data.get("address") + if addr: miner_address_global = addr; return jsonify({"message": "OK"}), 200 + return jsonify({"message": "Error"}), 400 + +@app.route('/miner/set_mode', methods=['POST']) +def set_miner_mode_api(): + data = request.get_json() + if data.get('mode') == 'GPU' and HAS_GPU: blockchain.use_gpu = True + else: blockchain.use_gpu = False + return jsonify({"message": "OK"}), 200 + +@app.route('/miner/set_intensity', methods=['POST']) +def set_miner_intensity_api(): + global mining_intensity_global data = request.get_json() - address = data.get('address') - if not address: - return jsonify({"message": "Endereço do minerador ausente."}), 400 - miner_address = address - return jsonify({"message": f"Endereço do minerador definido para {miner_address}"}), 200 + level = data.get('level', 'MEDIUM') + if level in INTENSITY_CONFIG: + mining_intensity_global = level + print(f"[API] Intensidade ajustada para: {level}") + return jsonify({"message": f"Intensidade setada para {level}"}), 200 + return jsonify({"message": "Nivel invalido"}), 400 + +@app.route('/miner/stop', methods=['POST']) +def stop_mining_api(): + global mining_active, mining_stop_flag + if mining_active: + mining_stop_flag.set() + with miner_lock: mining_active = False + return jsonify({"message": "Parado"}), 200 @app.route('/mine', methods=['GET']) def mine_api(): - """Inicia o processo de mineração de um novo bloco.""" - global is_mining, miner_address - if not miner_address: - return jsonify({"message": "Endereço do minerador não definido. Por favor, defina um endereço primeiro."}), 400 - - with miner_lock: # Garante que apenas um processo de mineração seja executado por vez - if is_mining: - return jsonify({"message": "Mineração já está em andamento."}), 409 - is_mining = True - - last_block = blockchain.last_block() - if not last_block: - return jsonify({"message": "Blockchain não inicializada. Não é possível minerar."}), 500 - - last_proof = last_block['proof'] - - # Executa a Prova de Trabalho de forma que possa ser interrompida - proof = blockchain.proof_of_work(last_proof) - + global mining_active, miner_address_global, mining_stop_flag, mining_result + if not miner_address_global: return jsonify({"message": "Sem endereço"}), 400 with miner_lock: - is_mining = False # Redefine o flag de mineração após a tentativa de PoW - - if proof == -1: # Mineração foi abortada - return jsonify({"message": "Mineração abortada ou interrompida."}), 200 - - previous_hash = blockchain.hash(last_block) - new_block = blockchain.new_block(proof, previous_hash, miner_address) + if mining_active: return jsonify({"message": "Ocupado"}), 409 + mining_active = True + + try: + last_block = blockchain.last_block() + proof = -1 + mining_stop_flag.clear(); mining_result.value = -1 + + if getattr(blockchain, 'use_gpu', False) and HAS_GPU: + proof = Blockchain._mine_gpu(last_block['proof'], blockchain._calculate_difficulty_for_index(len(blockchain.chain)+1), mining_stop_flag, mining_result) + else: + proof = blockchain.proof_of_work(last_block['proof']) + + if proof != -1: + new_block = blockchain.new_block(proof, blockchain.hash(last_block), miner_address_global) + broadcast_block(new_block) + return jsonify({"message": "Minerado!", "index": new_block['index']}), 200 + return jsonify({"message": "Parado"}), 200 + finally: + with miner_lock: mining_active = False - # Transmite o bloco recém-minerado para a rede - broadcast_block(new_block) +def broadcast_block(block): + for peer in known_nodes | set(SEED_NODES): + if peer == meu_url: continue + try: + # HEADER NGROK ADICIONADO AQUI + requests.post(f"{peer}/blocks/receive", json=block, timeout=5, headers=NGROK_HEADERS) + except: pass - response = { - 'message': "Novo bloco forjado!", - 'index': new_block['index'], - 'transactions': new_block['transactions'], - 'proof': new_block['proof'], - 'previous_hash': new_block['previous_hash'], - 'difficulty': new_block['difficulty'] - } - return jsonify(response), 200 +@app.route('/dashboard') +def dashboard_visual(): return render_template('dashboard.html') +@app.route('/miner') +def miner_web(): return render_template('miner.html') -# --- Funções de Peer-to-Peer (do nó) --- -def broadcast_block(block): - """Envia um bloco recém-minerado para todos os peers conhecidos.""" - print(f"[BROADCAST] Enviando bloco #{block['index']} para {len(known_nodes)} peers...") - peers_to_remove = set() - for peer in known_nodes.copy(): - if peer == meu_url: continue - try: - requests.post(f"{peer}/blocks/receive", json=block, timeout=5) - except requests.exceptions.RequestException as e: - print(f"[BROADCAST] Erro ao enviar bloco para {peer}: {e}. Removendo peer (se não for seed).") - if peer not in SEED_NODES: - peers_to_remove.add(peer) - except Exception as e: - print(f"[BROADCAST] Erro inesperado ao enviar bloco para {peer}: {e}") - - if peers_to_remove: - known_nodes.difference_update(peers_to_remove) - salvar_peers(known_nodes) - print(f"[BROADCAST] Removidos {len(peers_to_remove)} peers problemáticos.") - -def discover_peers(): - """ - Descobre e registra peers na rede. - Prioriza a conexão com os nós semente (SEED_NODES) para iniciar a descoberta. - """ - global known_nodes, meu_url - - # 1. Adiciona os nós semente à lista de peers conhecidos. - for seed in SEED_NODES: - if seed not in known_nodes and seed != meu_url: - known_nodes.add(seed) - print(f"[DISCOVERY] Adicionando nó semente: {seed}") - - salvar_peers(known_nodes) # Salva a lista atualizada de peers - - # 2. Itera sobre a lista de peers conhecidos (incluindo os nós semente) - # para descobrir novos peers e registrar o nó local. - initial_peers = list(known_nodes) # Cria uma cópia para iterar - for peer in initial_peers: - if peer == meu_url: - continue # Não tentar conectar a si mesmo - try: - # Tenta obter a lista de nós conhecidos pelo peer - r = requests.get(f"{peer}/nodes", timeout=3) - if r.status_code == 200: - raw_new_peers = r.json().get('nodes', []) - new_peers = [] - for item in raw_new_peers: - if isinstance(item, dict) and 'url' in item: - new_peers.append(item['url']) - elif isinstance(item, str): - new_peers.append(item) - - for np in new_peers: - if np not in known_nodes and np != meu_url: - known_nodes.add(np) - print(f"[DISCOVERY] Descoberto novo peer {np} via {peer}") - salvar_peers(known_nodes) # Salva a lista após cada nova descoberta - - # Tenta registrar o nó local com o novo peer descoberto - try: - parsed_url = urlparse(meu_url) - my_ip = parsed_url.hostname - my_port = parsed_url.port - requests.post(f"{np}/nodes/register", json={'ip': my_ip, 'port': my_port}, timeout=2) - except Exception as e: - print(f"[DISCOVERY ERROR] Falha ao registrar em {np}: {e}") - - # Tenta registrar o nó local com o peer atual (seja ele semente ou descoberto) - parsed_url = urlparse(meu_url) - my_ip = parsed_url.hostname - my_port = parsed_url.port - requests.post(f"{peer}/nodes/register", json={'ip': my_ip, 'port': my_port}, timeout=2) - - except requests.exceptions.RequestException as e: - print(f"[DISCOVERY ERROR] Falha ao conectar/descobrir peer {peer}: {e}. Removendo.") - # Remove o peer se não for um nó semente e falhar na conexão - if peer not in SEED_NODES: - known_nodes.discard(peer) - salvar_peers(known_nodes) +@app.route('/api/stats', methods=['GET']) +def get_stats(): + last_block = blockchain.last_block() + return jsonify({ + "hashrate": f"{current_hashrate_global:.2f}", + "index": last_block['index'], + "difficulty": last_block.get('difficulty', 1), + "status": "Mining" if mining_active else "Idle", + "gpu": f"GPU Mode ({mining_intensity_global})" if HAS_GPU else "CPU Mode" + }), 200 def get_my_ip(): - """Tenta obter o IP local do nó e avisa se for privado.""" try: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(("8.8.8.8", 80)) # Conecta a um IP externo para obter o IP local de saída - ip = s.getsockname()[0] - s.close() - try: - # Verifica se o IP é privado - if ipaddress.ip_address(ip).is_private: - print(f"[AVISO IP] Seu IP ({ip}) é privado. Para comunicação completa com peers públicos, configure o redirecionamento de portas (port forwarding) para a porta {port} no seu roteador.") - except ValueError: - # Não é um endereço IP válido, apenas continua - pass - return ip - except Exception: - print("[AVISO IP] Não foi possível determinar o IP local. Usando 127.0.0.1 como fallback. A comunicação com peers externos pode ser limitada.") - return "127.0.0.1" # Retorna localhost como fallback - -def load_or_create_node_id(filename="node_id.txt"): - """Carrega ou cria um ID de nó único.""" - if os.path.exists(filename): - with open(filename, "r") as f: - return f.read().strip() - else: - new_id = str(uuid4()).replace("-", "")[:16] - with open(filename, "w") as f: - f.write(new_id) - return new_id - -# Funções auxiliares para auto_sync_checker (movidas para antes do main) + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.connect(("8.8.8.8", 80)); ip = s.getsockname()[0]; s.close(); return ip + except: return "127.0.0.1" + +def load_or_create_node_id(): + if os.path.exists("node_id.txt"): return open("node_id.txt").read().strip() + node_id = str(uuid4()).replace("-", "")[:16] + with open("node_id.txt", "w") as f: f.write(node_id) + return node_id + def auto_sync_checker(blockchain_instance): while True: - comparar_ultimos_blocos(blockchain_instance) + try: blockchain_instance.resolve_conflicts() + except: pass time.sleep(60) -def comparar_ultimos_blocos(blockchain_instance): - if blockchain_instance is None or blockchain_instance.last_block() is None: - print("[SYNC] Blockchain ainda não inicializada. Aguardando...") - return - - print("\n🔍 Verificando sincronização com os peers...") - local_block = blockchain_instance.last_block() - local_hash = blockchain_instance.hash(local_block) - - for peer in known_nodes.copy(): - try: - r = requests.get(f"{peer}/sync/check", timeout=5) - data = r.json() - peer_index = data['index'] - peer_hash = data['hash'] - - if peer_index == local_block['index'] and peer_hash == local_hash: - print(f"[SYNC ✅] {peer} está sincronizado com índice {peer_index}.") - else: - print(f"[SYNC ⚠️] {peer} DIFERENTE! Local: {local_block['index']} | Peer: {peer_index}") - threading.Thread(target=blockchain_instance.resolve_conflicts, daemon=True).start() - except Exception as e: - print(f"[SYNC ❌] Falha ao verificar {peer}: {e}") - if peer not in SEED_NODES: - known_nodes.discard(peer) - salvar_peers(known_nodes) +class APIClient: + def __init__(self, base_url): self.base_url = base_url + def set_base_url(self, new_url): self.base_url = new_url -# --- Cliente Kert-One Core GUI (QMainWindow) --- +# --- Cliente GUI --- class KertOneCoreClient(QMainWindow): - start_mining_timer_signal = pyqtSignal() + start_mining_signal = pyqtSignal() log_signal = pyqtSignal(str, str) chain_viewer_signal = pyqtSignal(str) @@ -1252,33 +735,76 @@ def __init__(self): self.mining_active = False self.miner_address = None self.wallet_data = None + self.is_mining_busy = False self.apply_dark_theme() - self.api_client = APIClient(f"http://{meu_ip}:{port}") # Usar meu_ip e port globais + self.api_client = APIClient(f"http://127.0.0.1:5001") self.setup_ui() self.load_wallet() - self.chain_viewer_signal.connect(self.chain_viewer.setPlainText) self.log_signal.connect(self.update_log_viewer) - self.start_mining_timer_signal.connect(self.start_mining_timer_safe) + self.start_mining_signal.connect(self.mine_block_via_api) + self._on_flask_url_ready(f"http://127.0.0.1:5001") # Força localhost - self.mining_timer = QTimer(self) - self.mining_timer.setInterval(6000) - self.mining_timer.timeout.connect(self.mine_block_via_api) + def start_continuous_mining(self): + if self.mining_active: return + addr = self.get_miner_address() + if not addr: return + self.miner_address = addr + self.mining_active = True + self.is_mining_busy = False + self.mine_single_btn.setEnabled(False) + self.start_mining_btn.setEnabled(False) + self.stop_mining_btn.setEnabled(True) + self.radio_cpu.setEnabled(False) + self.radio_gpu.setEnabled(False) + self.status_bar.showMessage(f"Mineração ativa: {self.miner_address}", 0) + self.log_signal.emit("🚀 Mineração Sequencial Iniciada.", "success") + self.mine_block_via_api() - self._on_flask_url_ready("https://seend.kert-one.com") + def stop_continuous_mining(self): + self.mining_active = False + try: requests.post(f"{meu_url}/miner/stop", timeout=2, headers=NGROK_HEADERS) + except: pass + self.mine_single_btn.setEnabled(True) + self.start_mining_btn.setEnabled(True) + self.stop_mining_btn.setEnabled(False) + self.radio_cpu.setEnabled(True) + if HAS_GPU: self.radio_gpu.setEnabled(True) + self.status_bar.showMessage("Parado.", 5000) + self.log_signal.emit("Mineração interrompida.", "warning") - def update_ui_info(self): - self.update_log_viewer("Interface atualizada.", "info") + def mine_block_via_api(self): + if not self.mining_active: return + if self.is_mining_busy: return + self.is_mining_busy = True + threading.Thread(target=self._mine_async, args=(self.miner_address,)).start() - @pyqtSlot() - def start_mining_timer_safe(self): - if not self.mining_active: - self.mining_active = True - self.mining_timer.start() - self.log_signal.emit("Mineração iniciada com segurança.", "success") + def _mine_async(self, miner_address): + try: + requests.post(f"{meu_url}/miner/set_address", json={"address": miner_address}, timeout=5, headers=NGROK_HEADERS) + response = requests.get(f"{meu_url}/mine", timeout=None, headers=NGROK_HEADERS) + if response.status_code == 200: + data = response.json() + if "Minerado" in data.get("message", ""): + self.log_signal.emit(f"💎 BLOCO ENCONTRADO!", "success") + self.check_wallet_balance() + elif response.status_code == 409: pass + except Exception as e: + self.log_signal.emit(f"Erro no ciclo: {e}", "info") + time.sleep(1) + finally: + self.is_mining_busy = False + if self.mining_active: + time.sleep(0.1) + self.start_mining_signal.emit() + + def update_log_viewer(self, message, message_type="info"): + color_map = {"info": "#a0a0ff", "success": "#66ff66", "error": "#ff6666", "warning": "#ffff66", "default": "#f0f0f0"} + color = color_map.get(message_type, color_map["default"]) + timestamp = datetime.now().strftime('%H:%M:%S') + self.log_viewer.append(f'[{timestamp}] {message}') def apply_dark_theme(self): - """Aplica um tema escuro (Dark Mode).""" dark_palette = QPalette() dark_palette.setColor(QPalette.ColorRole.Window, QColor(45, 45, 45)) dark_palette.setColor(QPalette.ColorRole.WindowText, QColor(200, 200, 200)) @@ -1288,626 +814,285 @@ def apply_dark_theme(self): dark_palette.setColor(QPalette.ColorRole.ButtonText, QColor(200, 200, 200)) dark_palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218)) QApplication.instance().setPalette(dark_palette) - - self.setStyleSheet(""" - QWidget { background-color: rgb(45, 45, 45); color: rgb(200, 200, 200); } - QPushButton { background-color: rgb(60, 60, 60); border: 1px solid rgb(80, 80, 80); padding: 8px; border-radius: 5px; } - QPushButton:hover { background-color: rgb(80, 80, 80); } - QPushButton:pressed { background-color: rgb(100, 100, 100); } - QLineEdit, QTextEdit, QPlainTextEdit { background-color: rgb(30, 30, 30); border: 1px solid rgb(60, 60, 60); padding: 5px; border-radius: 3px; } - QGroupBox { border: 1px solid rgb(80, 80, 80); margin-top: 10px; padding-top: 15px; } - QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 5px; color: rgb(150, 150, 255); } - QTabWidget::pane { border: 1px solid rgb(60, 60, 60); } - QTabBar::tab { background: rgb(55, 55, 55); border: 1px solid rgb(60, 60, 60); padding: 8px; border-bottom: none; } - QTabBar::tab:selected { background: rgb(75, 75, 75); border-bottom: none; } - #LogViewer { background-color: #202020; color: #f0f0f0; border: none; } - """) + self.setStyleSheet("QWidget { background-color: rgb(45, 45, 45); color: rgb(200, 200, 200); } QPushButton { background-color: rgb(60, 60, 60); border: 1px solid rgb(80, 80, 80); padding: 8px; border-radius: 5px; } QPushButton:hover { background-color: rgb(80, 80, 80); } QPushButton:pressed { background-color: rgb(100, 100, 100); } QLineEdit, QTextEdit, QPlainTextEdit, QComboBox { background-color: rgb(30, 30, 30); border: 1px solid rgb(60, 60, 60); padding: 5px; border-radius: 3px; } QGroupBox { border: 1px solid rgb(80, 80, 80); margin-top: 10px; padding-top: 15px; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; padding: 0 5px; color: rgb(150, 150, 255); } QTabWidget::pane { border: 1px solid rgb(60, 60, 60); } QTabBar::tab { background: rgb(55, 55, 55); border: 1px solid rgb(60, 60, 60); padding: 8px; border-bottom: none; } QTabBar::tab:selected { background: rgb(75, 75, 75); border-bottom: none; } #LogViewer { background-color: #202020; color: #f0f0f0; border: none; }") def setup_ui(self): - """Configura a interface principal.""" - self.central_widget = QWidget() - self.setCentralWidget(self.central_widget) - self.main_layout = QVBoxLayout(self.central_widget) - - self.tabs = QTabWidget() - self.tab_wallet = QWidget() - self.tab_send = QWidget() - self.tab_mine = QWidget() - self.tab_network = QWidget() - - self.tabs.addTab(self.tab_wallet, "Carteira") - self.tabs.addTab(self.tab_send, "Enviar") - self.tabs.addTab(self.tab_mine, "Mineração") - self.tabs.addTab(self.tab_network, "Rede/Blockchain") - + self.central_widget = QWidget(); self.setCentralWidget(self.central_widget); self.main_layout = QVBoxLayout(self.central_widget) + self.tabs = QTabWidget(); self.tab_wallet = QWidget(); self.tab_send = QWidget(); self.tab_mine = QWidget(); self.tab_network = QWidget() + self.tabs.addTab(self.tab_wallet, "Carteira"); self.tabs.addTab(self.tab_send, "Enviar"); self.tabs.addTab(self.tab_mine, "Mineração"); self.tabs.addTab(self.tab_network, "Rede/Blockchain") self.main_layout.addWidget(self.tabs) - - self.log_viewer = QTextEdit() - self.log_viewer.setObjectName("LogViewer") - self.log_viewer.setReadOnly(True) - self.main_layout.addWidget(QLabel("Log de Atividade:")) - self.main_layout.addWidget(self.log_viewer, 3) - - self.status_bar = QStatusBar(self) - self.setStatusBar(self.status_bar) - self.status_bar.showMessage(f"Cliente Kert-One conectado ao nó: {meu_url}", 5000) - - self.setup_wallet_tab() - self.setup_send_tab() - self.setup_mine_tab() - self.setup_network_tab() - - node_info_group = QGroupBox("Informações do Nó") - node_info_layout = QFormLayout(node_info_group) - - self.node_id_label = QLabel(f"{node_id[:8]}...") - self.node_url_label = QLabel("Aguardando...") - - node_info_layout.addRow("ID do Nó:", self.node_id_label) - node_info_layout.addRow("URL do Nó:", self.node_url_label) - + self.log_viewer = QTextEdit(); self.log_viewer.setObjectName("LogViewer"); self.log_viewer.setReadOnly(True) + self.main_layout.addWidget(QLabel("Log de Atividade:")); self.main_layout.addWidget(self.log_viewer, 3) + self.status_bar = QStatusBar(self); self.setStatusBar(self.status_bar); self.status_bar.showMessage(f"Cliente Kert-One conectado ao nó: {meu_url}", 5000) + self.setup_wallet_tab(); self.setup_send_tab(); self.setup_mine_tab(); self.setup_network_tab() + node_info_group = QGroupBox("Informações do Nó"); node_info_layout = QFormLayout(node_info_group) + self.node_id_label = QLabel(f"{node_id[:16]}..."); self.node_url_label = QLabel(f"{meu_url}") + node_info_layout.addRow("ID do Nó:", self.node_id_label); node_info_layout.addRow("URL do Nó:", self.node_url_label) self.main_layout.insertWidget(0, node_info_group) - @pyqtSlot(str) def _on_flask_url_ready(self, url): - global meu_url - meu_url = url - self.api_client.set_base_url(meu_url) # Atualiza a URL base do cliente API - - self.update_log_viewer(f"Servidor Flask pronto em: {meu_url}", "success") - self.node_url_label.setText(f"{meu_url}") - self.status_bar.showMessage(f"Cliente Kert-One conectado ao nó: {meu_url}", 5000) - - self.update_ui_info() - - - def update_log_viewer(self, message, message_type="info"): - """Adiciona mensagens ao visualizador de log com cores.""" - color_map = { - "info": "#a0a0ff", - "success": "#66ff66", - "error": "#ff6666", - "warning": "#ffff66", - "default": "#f0f0f0" - } - color = color_map.get(message_type, color_map["default"]) - - timestamp = datetime.now().strftime('%H:%M:%S') - formatted_message = f"[{timestamp}] {message}" - - self.log_viewer.append(f'{formatted_message}') + global meu_url; meu_url = url; self.api_client.set_base_url(meu_url); self.update_log_viewer(f"Servidor Flask pronto em: {meu_url}", "success"); self.node_url_label.setText(f"{meu_url}"); self.status_bar.showMessage(f"Cliente Kert-One conectado ao nó: {meu_url}", 5000) - # --- Aba Carteira (Opções 1 e 2 do CLI) --- - def setup_wallet_tab(self): - layout = QVBoxLayout(self.tab_wallet) - - wallet_group = QGroupBox("Carteira Atual") - wallet_layout = QFormLayout(wallet_group) - - self.balance_label = QLabel(f"0.0 {COIN_SYMBOL}") - self.balance_label.setFont(QFont("Arial", 28, QFont.Weight.Bold)) - - self.address_label = QLineEdit("N/A") - self.address_label.setReadOnly(True) - self.public_key_label = QTextEdit("N/A") - self.public_key_label.setReadOnly(True) - self.public_key_label.setFixedHeight(80) - - wallet_layout.addRow("Saldo Atual:", self.balance_label) - wallet_layout.addRow("Endereço:", self.address_label) - wallet_layout.addRow("Chave Pública:", self.public_key_label) - - layout.addWidget(wallet_group) - - button_layout = QHBoxLayout() - new_wallet_btn = QPushButton("Criar Nova Carteira") - new_wallet_btn.clicked.connect(self.create_new_wallet) - load_wallet_btn = QPushButton("Carregar Carteira (client_wallet.json)") - load_wallet_btn.clicked.connect(self.load_wallet) - check_balance_btn = QPushButton("Atualizar Saldo") - check_balance_btn.clicked.connect(self.check_wallet_balance) - - button_layout.addWidget(new_wallet_btn) - button_layout.addWidget(load_wallet_btn) - button_layout.addWidget(check_balance_btn) - layout.addLayout(button_layout) - - layout.addStretch(1) + layout = QVBoxLayout(self.tab_wallet); wallet_group = QGroupBox("Carteira Atual"); wallet_layout = QFormLayout(wallet_group) + self.balance_label = QLabel(f"0.0 {COIN_SYMBOL}"); self.balance_label.setFont(QFont("Arial", 28, QFont.Weight.Bold)) + self.address_label = QLineEdit("N/A"); self.address_label.setReadOnly(True) + self.public_key_label = QTextEdit("N/A"); self.public_key_label.setReadOnly(True); self.public_key_label.setFixedHeight(80) + wallet_layout.addRow("Saldo Atual:", self.balance_label); wallet_layout.addRow("Endereço:", self.address_label); wallet_layout.addRow("Chave Pública:", self.public_key_label) + layout.addWidget(wallet_group); button_layout = QHBoxLayout() + new_wallet_btn = QPushButton("Criar Nova Carteira"); new_wallet_btn.clicked.connect(self.create_new_wallet) + load_wallet_btn = QPushButton("Carregar Carteira (client_wallet.json)"); load_wallet_btn.clicked.connect(self.load_wallet) + check_balance_btn = QPushButton("Atualizar Saldo"); check_balance_btn.clicked.connect(self.check_wallet_balance) + button_layout.addWidget(new_wallet_btn); button_layout.addWidget(load_wallet_btn); button_layout.addWidget(check_balance_btn); layout.addLayout(button_layout); layout.addStretch(1) def create_new_wallet(self): - """Cria uma nova carteira, salva e carrega na UI.""" wallet_data = create_wallet() if wallet_data: - save_wallet_file(wallet_data, WALLET_FILE) - self.wallet_data = wallet_data - self.update_wallet_status() - self.log_signal.emit(f"Nova carteira criada e salva em {WALLET_FILE}.", "success") - QMessageBox.information(self, "Carteira Criada", f"Nova carteira salva com sucesso. Endereço: {wallet_data['address']}") - self.check_wallet_balance() - else: - self.log_signal.emit("Falha ao criar nova carteira.", "error") + save_wallet_file(wallet_data, WALLET_FILE); self.wallet_data = wallet_data; self.update_wallet_status(); self.log_signal.emit(f"Nova carteira criada e salva em {WALLET_FILE}.", "success"); QMessageBox.information(self, "Carteira Criada", f"Nova carteira salva com sucesso. Endereço: {wallet_data['address']}"); self.check_wallet_balance() + else: self.log_signal.emit("Falha ao criar nova carteira.", "error") def load_wallet(self): - """Carrega a carteira do arquivo e atualiza a UI.""" self.wallet_data = load_wallet_file(WALLET_FILE) - if self.wallet_data: - self.update_wallet_status() - self.log_signal.emit(f"Carteira carregada com sucesso.", "info") - self.check_wallet_balance() - else: - self.update_wallet_status() - self.log_signal.emit("Arquivo de carteira não encontrado ou corrompido.", "warning") - + if self.wallet_data: self.update_wallet_status(); self.log_signal.emit(f"Carteira carregada com sucesso.", "info"); self.check_wallet_balance() + else: self.update_wallet_status(); self.log_signal.emit("Arquivo de carteira não encontrado ou corrompido.", "warning") + def update_wallet_status(self): - """Atualiza a UI com os dados da carteira carregada.""" if self.wallet_data: - self.address_label.setText(self.wallet_data.get('address', 'N/A')) - self.public_key_label.setText(self.wallet_data.get('public_key', 'N/A')) - self.status_bar.showMessage(f"Carteira carregada: {self.wallet_data['address']}", 5000) + self.address_label.setText(self.wallet_data.get('address', 'N/A')); self.public_key_label.setText(self.wallet_data.get('public_key', 'N/A')); self.status_bar.showMessage(f"Carteira carregada: {self.wallet_data['address']}", 5000) else: - self.address_label.setText("N/A") - self.public_key_label.setText("N/A") - self.balance_label.setText("0.0 KRT") - self.status_bar.showMessage("Nenhuma carteira carregada.", 5000) + self.address_label.setText("N/A"); self.public_key_label.setText("N/A"); self.balance_label.setText("0.0 KRT"); self.status_bar.showMessage("Nenhuma carteira carregada.", 5000) def check_wallet_balance(self): - """Consulta o saldo da carteira carregada no nó da blockchain via API.""" - if not self.wallet_data: - self.log_signal.emit("Nenhuma carteira carregada.", "warning") - return - - address = self.wallet_data['address'] - - threading.Thread(target=self._fetch_balance_async, args=(address,)).start() + if not self.wallet_data: self.log_signal.emit("Nenhuma carteira carregada.", "warning"); return + address = self.wallet_data['address']; threading.Thread(target=self._fetch_balance_async, args=(address,)).start() def _fetch_balance_async(self, address): - """Função para buscar o saldo em segundo plano.""" try: - response = requests.get(f"{meu_url}/balance/{address}", timeout=5) # Usar meu_url - response.raise_for_status() - balance_data = response.json() - balance = balance_data.get('balance', 0) - - self.balance_label.setText(f"{balance} {COIN_SYMBOL}") - self.log_signal.emit(f"Saldo atualizado: {balance} {COIN_SYMBOL}", "info") - + # ADICIONADO HEADER + response = requests.get(f"{meu_url}/balance/{address}", timeout=5, headers=NGROK_HEADERS); response.raise_for_status(); balance_data = response.json(); balance = balance_data.get('balance', 0) + self.balance_label.setText(f"{balance} {COIN_SYMBOL}"); self.log_signal.emit(f"Saldo atualizado: {balance} {COIN_SYMBOL}", "info") except requests.exceptions.RequestException as e: - self.log_signal.emit(f"Erro ao conectar ao nó ({meu_url}) ou buscar saldo: {e}", "error") - self.balance_label.setText("Erro de Conexão") - - # --- Aba Enviar (Opção 3 do CLI) --- + self.log_signal.emit(f"Erro ao conectar ao nó ({meu_url}) ou buscar saldo: {e}", "error"); self.balance_label.setText("Erro de Conexão") def setup_send_tab(self): - layout = QVBoxLayout(self.tab_send) - - transaction_group = QGroupBox("Nova Transação") - form_layout = QFormLayout(transaction_group) - - self.recipient_input = QLineEdit() - self.amount_input = QLineEdit() - self.fee_input = QLineEdit() - - validator = QDoubleValidator(0.0, 100000000.0, 8, self) - validator.setNotation(QDoubleValidator.StandardNotation) - - self.amount_input.setValidator(validator) - self.fee_input.setValidator(validator) - - form_layout.addRow("Destinatário (Endereço):", self.recipient_input) - form_layout.addRow(f"Valor ({COIN_SYMBOL}):", self.amount_input) - form_layout.addRow("Taxa (Fee):", self.fee_input) - - send_btn = QPushButton("Assinar e Enviar Transação") - send_btn.clicked.connect(self.enviar_transacao) - - layout.addWidget(transaction_group) - layout.addWidget(send_btn) - layout.addStretch(1) + layout = QVBoxLayout(self.tab_send); transaction_group = QGroupBox("Nova Transação"); form_layout = QFormLayout(transaction_group) + self.recipient_input = QLineEdit(); self.amount_input = QLineEdit(); self.fee_input = QLineEdit() + validator = QDoubleValidator(0.0, 100000000.0, 8, self); validator.setNotation(QDoubleValidator.StandardNotation) + self.amount_input.setValidator(validator); self.fee_input.setValidator(validator) + form_layout.addRow("Destinatário (Endereço):", self.recipient_input); form_layout.addRow(f"Valor ({COIN_SYMBOL}):", self.amount_input); form_layout.addRow("Taxa (Fee):", self.fee_input) + send_btn = QPushButton("Assinar e Enviar Transação"); send_btn.clicked.connect(self.enviar_transacao) + layout.addWidget(transaction_group); layout.addWidget(send_btn); layout.addStretch(1) def enviar_transacao(self): - """ - Cria, assina e envia uma nova transação para o nó via interface gráfica. - """ - if not self.wallet_data: - QMessageBox.warning(self, "Aviso", "Nenhuma carteira carregada.") - return - - recipient_addr = self.recipient_input.text().strip() - amount_str = self.amount_input.text().strip().replace(',', '.') - fee_str = self.fee_input.text().strip().replace(',', '.') - - if not recipient_addr or not amount_str or not fee_str: - QMessageBox.warning(self, "Erro", "Todos os campos são obrigatórios.") - return - + if not self.wallet_data: QMessageBox.warning(self, "Aviso", "Nenhuma carteira carregada."); return + recipient_addr = self.recipient_input.text().strip(); amount_str = self.amount_input.text().strip().replace(',', '.'); fee_str = self.fee_input.text().strip().replace(',', '.') + if not recipient_addr or not amount_str or not fee_str: QMessageBox.warning(self, "Erro", "Todos os campos são obrigatórios."); return try: - amount = float(amount_str) - fee = float(fee_str) - if amount <= 0 or fee < 0: - raise ValueError("Valor ou taxa inválidos.") - - transaction_id = str(uuid4()) - - amount_fmt = f"{amount:.8f}" - fee_fmt = f"{fee:.8f}" - - # Passar amount e fee como strings formatadas para sign_transaction - tx_data_for_signing = { - 'sender': self.wallet_data['address'], - 'recipient': recipient_addr, - 'amount': amount_fmt, - 'fee': fee_fmt - } + amount = float(amount_str); fee = float(fee_str) + if amount <= 0 or fee < 0: raise ValueError("Valor ou taxa inválidos.") + transaction_id = str(uuid4()); amount_fmt = f"{amount:.8f}"; fee_fmt = f"{fee:.8f}" + tx_data_for_signing = {'sender': self.wallet_data['address'], 'recipient': recipient_addr, 'amount': amount_fmt, 'fee': fee_fmt} signature = sign_transaction(self.wallet_data['private_key'], tx_data_for_signing) - if signature is None: - raise Exception("Falha ao assinar a transação.") - - tx_full_data = { - 'id': transaction_id, - 'sender': self.wallet_data['address'], - 'recipient': recipient_addr, - 'amount': amount_fmt, # Armazenar como string formatada - 'fee': fee_fmt, # Armazenar como string formatada - 'signature': signature, - 'public_key': self.wallet_data['public_key'], - 'timestamp': time.time() - } - + if signature is None: raise Exception("Falha ao assinar a transação.") + tx_full_data = {'id': transaction_id, 'sender': self.wallet_data['address'], 'recipient': recipient_addr, 'amount': amount_fmt, 'fee': fee_fmt, 'signature': signature, 'public_key': self.wallet_data['public_key'], 'timestamp': time.time()} self.log_signal.emit("Enviando transação para o nó...", "info") - threading.Thread( - target=self._send_transaction_async, - args=(tx_full_data,), - daemon=True - ).start() - - except ValueError as e: - QMessageBox.critical(self, "Erro de Entrada", f"Valor inválido: {e}") - except Exception as e: - self.log_signal.emit(f"Ocorreu um erro inesperado: {e}", "error") + threading.Thread(target=self._send_transaction_async, args=(tx_full_data,), daemon=True).start() + except ValueError as e: QMessageBox.critical(self, "Erro de Entrada", f"Valor inválido: {e}") + except Exception as e: self.log_signal.emit(f"Ocorreu um erro inesperado: {e}", "error") def _send_transaction_async(self, tx_full_data): - """Função para enviar a transação via HTTP em segundo plano.""" try: - response = requests.post(f"{meu_url}/tx/new", json=tx_full_data, timeout=10) # Usar meu_url - response.raise_for_status() - - if response.status_code in [200, 201]: - self.log_signal.emit(f"Transação enviada com sucesso: {response.json().get('message')}", "success") - self._clear_transaction_fields() - self.check_wallet_balance() - else: - self.log_signal.emit(f"Erro ao enviar transação: {response.json().get('error', response.text)}", "error") - - except requests.exceptions.RequestException as e: - self.log_signal.emit(f"Taxa é obrigatória ou erro de conexão com o nó ({meu_url}) ao enviar transação: {e}", "error") - + # ADICIONADO HEADER + response = requests.post(f"{meu_url}/tx/new", json=tx_full_data, timeout=10, headers=NGROK_HEADERS); response.raise_for_status() + if response.status_code in [200, 201]: self.log_signal.emit(f"Transação enviada com sucesso: {response.json().get('message')}", "success"); self._clear_transaction_fields(); self.check_wallet_balance() + else: self.log_signal.emit(f"Erro ao enviar transação: {response.json().get('error', response.text)}", "error") + except requests.exceptions.RequestException as e: self.log_signal.emit(f"Taxa é obrigatória ou erro de conexão com o nó ({meu_url}) ao enviar transação: {e}", "error") def _clear_transaction_fields(self): - """Limpa os campos de input de transação.""" - self.recipient_input.clear() - self.amount_input.clear() - self.fee_input.clear() - - # --- Aba Mineração (Opções 4, 8, 9 do CLI) --- + self.recipient_input.clear(); self.amount_input.clear(); self.fee_input.clear() def setup_mine_tab(self): - layout = QVBoxLayout(self.tab_mine) - - mine_addr_group = QGroupBox("Configuração de Mineração") - mine_addr_layout = QHBoxLayout(mine_addr_group) - - self.miner_addr_input = QLineEdit() - self.miner_addr_input.setPlaceholderText("Endereço para recompensa (Opcional, usa a carteira carregada)") + layout = QVBoxLayout(self.tab_mine); mine_addr_group = QGroupBox("Carteira de Recompensa"); mine_addr_layout = QHBoxLayout(mine_addr_group) + self.miner_addr_input = QLineEdit(); self.miner_addr_input.setPlaceholderText("Endereço para receber KERT minerados"); mine_addr_layout.addWidget(self.miner_addr_input); layout.addWidget(mine_addr_group) + hw_group = QGroupBox("Modo de Mineração (Hardware)"); hw_layout = QHBoxLayout(hw_group) + self.radio_cpu = QRadioButton("CPU (Multicore)"); self.radio_gpu = QRadioButton("GPU (OpenCL Real)") + self.radio_cpu.toggled.connect(lambda: self.update_mining_mode("CPU")); self.radio_gpu.toggled.connect(lambda: self.update_mining_mode("GPU")) + if HAS_GPU: self.radio_gpu.setEnabled(True); self.radio_gpu.setText("GPU (OpenCL Real - DETECTADA)"); self.radio_gpu.setChecked(True) + else: self.radio_cpu.setChecked(True); self.radio_gpu.setEnabled(False); self.radio_gpu.setText("GPU (Drivers não encontrados)") + hw_layout.addWidget(self.radio_cpu); hw_layout.addWidget(self.radio_gpu); layout.addWidget(hw_group) - mine_addr_layout.addWidget(self.miner_addr_input) - layout.addWidget(mine_addr_group) - - mining_control_group = QGroupBox("Controle de Mineração") - mining_control_layout = QHBoxLayout(mining_control_group) - - self.mine_single_btn = QPushButton("Minerar Bloco Único") - self.start_mining_btn = QPushButton("Iniciar Mineração Contínua") - self.stop_mining_btn = QPushButton("Parar Mineração Contínua") - self.stop_mining_btn.setEnabled(False) - - self.mine_single_btn.clicked.connect(self.mine_single_block) - self.start_mining_btn.clicked.connect(self.start_continuous_mining) - self.stop_mining_btn.clicked.connect(self.stop_continuous_mining) + # --- SELETOR DE INTENSIDADE --- + intensity_group = QGroupBox("Nível de Poder da GPU"); intensity_layout = QHBoxLayout(intensity_group) + self.intensity_combo = QComboBox() + self.intensity_combo.addItem("Baixo (Silencioso)", "LOW") + self.intensity_combo.addItem("Médio (Equilibrado)", "MEDIUM") + self.intensity_combo.addItem("Alto (Forte)", "HIGH") + self.intensity_combo.addItem("INSANO (MAXIMO - FAZ BARULHO)", "INSANE") + self.intensity_combo.setCurrentIndex(1) + self.intensity_combo.currentIndexChanged.connect(self.update_intensity) + intensity_layout.addWidget(QLabel("Força de Mineração:")); intensity_layout.addWidget(self.intensity_combo) + layout.addWidget(intensity_group) + + mining_control_group = QGroupBox("Controle"); mining_control_layout = QHBoxLayout(mining_control_group) + self.mine_single_btn = QPushButton("Minerar 1 Bloco"); self.start_mining_btn = QPushButton("Iniciar Mineração Contínua"); self.stop_mining_btn = QPushButton("Parar"); self.stop_mining_btn.setEnabled(False) + self.mine_single_btn.clicked.connect(self.mine_single_block); self.start_mining_btn.clicked.connect(self.start_continuous_mining); self.stop_mining_btn.clicked.connect(self.stop_continuous_mining) + mining_control_layout.addWidget(self.mine_single_btn); mining_control_layout.addWidget(self.start_mining_btn); mining_control_layout.addWidget(self.stop_mining_btn); layout.addWidget(mining_control_group); layout.addStretch(1) + + def update_mining_mode(self, mode): + sender = self.sender() + if sender.isChecked(): + try: requests.post(f"{meu_url}/miner/set_mode", json={'mode': mode}, headers=NGROK_HEADERS); self.log_signal.emit(f"Modo de mineração alterado para: {mode}", "info") + except: self.log_signal.emit("Erro ao alterar modo de mineração.", "error") + + def update_intensity(self): + level = self.intensity_combo.currentData() + threading.Thread(target=self._update_intensity_async, args=(level,)).start() - mining_control_layout.addWidget(self.mine_single_btn) - mining_control_layout.addWidget(self.start_mining_btn) - mining_control_layout.addWidget(self.stop_mining_btn) - - layout.addWidget(mining_control_group) - layout.addStretch(1) + def _update_intensity_async(self, level): + try: + requests.post(f"{meu_url}/miner/set_intensity", json={'level': level}, timeout=2, headers=NGROK_HEADERS) + self.log_signal.emit(f"Poder da GPU ajustado para: {level}", "warning") + except: self.log_signal.emit("Erro ao ajustar intensidade.", "error") def get_miner_address(self): addr = self.miner_addr_input.text().strip() - if addr: - return addr - if self.wallet_data and 'address' in self.wallet_data: - return self.wallet_data['address'] - QMessageBox.warning(self, "Aviso", "Nenhum endereço de mineração fornecido e nenhuma carteira carregada.") - return None + if addr: return addr + if self.wallet_data and 'address' in self.wallet_data: return self.wallet_data['address'] + QMessageBox.warning(self, "Aviso", "Nenhum endereço de mineração fornecido e nenhuma carteira carregada."); return None def mine_single_block(self): - """Inicia uma mineração de bloco único via API em thread separada.""" miner_addr = self.get_miner_address() - if miner_addr: - self.log_signal.emit("Iniciando mineração de bloco único...", "info") - threading.Thread(target=self._mine_async, args=(miner_addr,)).start() - - def start_continuous_mining(self): - if self.mining_active: - self.log_signal.emit("Mineração já está ativa.", "warning") - return - - addr = self.get_miner_address() - if not addr: - return - - self.miner_address = addr - self.mining_active = True - self.mine_single_btn.setEnabled(False) - self.start_mining_btn.setEnabled(False) - self.stop_mining_btn.setEnabled(True) - self.status_bar.showMessage(f"Mineração contínua ativa para {self.miner_address}...", 0) - self.mining_timer.start(5000) # 5 segundos - self.log_signal.emit("Mineração contínua iniciada.", "success") - - def stop_continuous_mining(self): - if not self.mining_active: - return - self.mining_active = False - self.mining_timer.stop() - self.mine_single_btn.setEnabled(True) - self.start_mining_btn.setEnabled(True) - self.stop_mining_btn.setEnabled(False) - self.status_bar.showMessage("Mineração contínua parada.", 5000) - self.log_signal.emit("Mineração contínua parada.", "info") - - def _mine_async(self, miner_address): - """Método que define o endereço do minerador e executa a mineração em thread separada.""" - try: - self.log_signal.emit(f"Definindo endereço do minerador no nó...", "info") - set_addr_response = requests.post(f"{meu_url}/miner/set_address", json={"address": miner_address}, timeout=10) - set_addr_response.raise_for_status() - - self.log_signal.emit(f"Endereço definido: {miner_address}. Iniciando mineração...", "info") - - response = requests.get(f"{meu_url}/mine", timeout=30) - response.raise_for_status() - - result = response.json() - self.log_signal.emit(f"✅ Bloco minerado com sucesso: {result.get('message', '')}", "success") - self.check_wallet_balance() - - except requests.exceptions.RequestException as e: - self.log_signal.emit(f"Dificuldade alta: {e}. Minerando o próximo bloco...", "error") - - - def mine_block_via_api(self): - if not self.mining_active: - return - - if not self.miner_address: - self.log_signal.emit("Endereço do minerador não definido. Abortando mineração.", "error") - return - - threading.Thread(target=self._mine_async, args=(self.miner_address,)).start() - - # --- Aba Rede/Blockchain (Opções 5, 6, 7 e 10 do CLI) --- + if miner_addr: self.log_signal.emit("Iniciando mineração de bloco único...", "info"); threading.Thread(target=self._mine_async, args=(miner_addr,)).start() def setup_network_tab(self): - layout = QVBoxLayout(self.tab_network) - - chain_group = QGroupBox("Blockchain View") - chain_layout = QVBoxLayout(chain_group) - - self.chain_viewer = QPlainTextEdit() - self.chain_viewer.setReadOnly(True) - self.chain_viewer.setPlaceholderText("Clique em 'Ver Blockchain Completa' para carregar os dados do nó.") - - self.view_chain_btn = QPushButton("Ver Blockchain Completa") - self.sync_chain_btn = QPushButton("Sincronizar Blockchain (Consenso)") - - chain_layout.addWidget(self.chain_viewer) - chain_layout.addWidget(self.view_chain_btn) - chain_layout.addWidget(self.sync_chain_btn) - - self.view_chain_btn.clicked.connect(self.view_blockchain) - self.sync_chain_btn.clicked.connect(self.sync_blockchain) - - layout.addWidget(chain_group) - - network_options_group = QGroupBox("Opções de Rede") - network_options_layout = QHBoxLayout(network_options_group) - - self.register_peer_btn = QPushButton("Registrar Novo Peer") - self.consult_contract_btn = QPushButton("Consultar Contrato Inteligente") - - self.register_peer_btn.clicked.connect(self.register_peer_dialog) - self.consult_contract_btn.clicked.connect(self.consult_contract_dialog) - - network_options_layout.addWidget(self.register_peer_btn) - network_options_layout.addWidget(self.consult_contract_btn) - - layout.addWidget(network_options_group) - - self.open_urls_button = QPushButton("Abrir Portais") - self.open_urls_button.clicked.connect(self.abrir_portais) - layout.addWidget(self.open_urls_button) - - layout.addStretch(1) - + layout = QVBoxLayout(self.tab_network); chain_group = QGroupBox("Blockchain View"); chain_layout = QVBoxLayout(chain_group) + self.chain_viewer = QPlainTextEdit(); self.chain_viewer.setReadOnly(True); self.chain_viewer.setPlaceholderText("Clique em 'Ver Blockchain Completa' para carregar os dados do nó.") + self.view_chain_btn = QPushButton("Ver Blockchain Completa"); self.sync_chain_btn = QPushButton("Sincronizar Blockchain (Consenso)") + chain_layout.addWidget(self.chain_viewer); chain_layout.addWidget(self.view_chain_btn); chain_layout.addWidget(self.sync_chain_btn) + self.view_chain_btn.clicked.connect(self.view_blockchain); self.sync_chain_btn.clicked.connect(self.sync_blockchain) + layout.addWidget(chain_group); network_options_group = QGroupBox("Opções de Rede"); network_options_layout = QHBoxLayout(network_options_group) + self.register_peer_btn = QPushButton("Registrar Novo Peer"); self.consult_contract_btn = QPushButton("Consultar Contrato Inteligente") + self.change_token_btn = QPushButton("Ativar Nó Público (Ngrok)") + self.register_peer_btn.clicked.connect(self.register_peer_dialog); self.consult_contract_btn.clicked.connect(self.consult_contract_dialog); self.change_token_btn.clicked.connect(self.change_ngrok_token) + network_options_layout.addWidget(self.register_peer_btn); network_options_layout.addWidget(self.consult_contract_btn); network_options_layout.addWidget(self.change_token_btn); layout.addWidget(network_options_group) + self.open_urls_button = QPushButton("Abrir Portais"); self.open_urls_button.clicked.connect(self.abrir_portais); layout.addWidget(self.open_urls_button); layout.addStretch(1) def abrir_portais(self): - import webbrowser # Importar aqui para evitar problemas de dependência - webbrowser.open(f"http://{meu_ip}:{port}/") # Usar meu_ip e port - webbrowser.open(f"http://{meu_ip}:{port}/miner") # Usar meu_ip e port - webbrowser.open("https://kert-one.com/") - self.log_signal.emit("Abrindo portais do Kert-One...", "info") - + import webbrowser; webbrowser.open(f"http://{meu_ip}:{port}/"); webbrowser.open(f"http://{meu_ip}:{port}/miner"); webbrowser.open("https://kert-one.com/"); self.log_signal.emit("Abrindo portais do Kert-One...", "info") def view_blockchain(self): - """Busca e exibe a blockchain completa do nó.""" - self.log_signal.emit("Buscando blockchain completa...", "info") - threading.Thread(target=self._fetch_blockchain_async).start() + self.log_signal.emit("Buscando blockchain completa...", "info"); threading.Thread(target=self._fetch_blockchain_async).start() def _fetch_blockchain_async(self): - """Função para buscar a blockchain em segundo plano.""" try: - response = requests.get(f"{meu_url}/chain", timeout=10) # Usar meu_url - response.raise_for_status() - chain_data = response.json() - - formatted_chain = json.dumps(chain_data, indent=2) - - self.chain_viewer_signal.emit(formatted_chain) - self.log_signal.emit(f"Blockchain carregada. Comprimento: {len(chain_data['chain'])} blocos.", "success") - - except requests.exceptions.RequestException as e: - self.log_signal.emit(f"Erro ao buscar blockchain: {e}", "error") - self.chain_viewer_signal.emit("Erro ao carregar a blockchain.") + # ADICIONADO HEADER + response = requests.get(f"{meu_url}/chain", timeout=10, headers=NGROK_HEADERS); response.raise_for_status(); chain_data = response.json() + formatted_chain = json.dumps(chain_data, indent=2); self.chain_viewer_signal.emit(formatted_chain); self.log_signal.emit(f"Blockchain carregada. Comprimento: {len(chain_data['chain'])} blocos.", "success") + except requests.exceptions.RequestException as e: self.log_signal.emit(f"Erro ao buscar blockchain: {e}", "error"); self.chain_viewer_signal.emit("Erro ao carregar a blockchain.") def sync_blockchain(self): - """Inicia a sincronização da blockchain numa thread separada.""" threading.Thread(target=self._sync_blockchain_async, daemon=True).start() def _sync_blockchain_async(self): while True: try: - self.log_signal.emit("Iniciando sincronização (consenso)...", "info") - response = requests.get(f"{meu_url}/nodes/resolve", timeout=30) # Usar meu_url - response.raise_for_status() - data = response.json() - - if data.get("message") == "Nossa cadeia foi substituída.": - self.log_signal.emit("Blockchain sincronizada com sucesso. Cadeia atualizada para a mais longa.", "success") - self.view_blockchain() - else: - self.log_signal.emit("Blockchain já sincronizada ou não houve alteração.", "info") - - except requests.exceptions.RequestException as e: - self.log_signal.emit(f"Erro ao sincronizar com o nó: {e}", "error") - + # ADICIONADO HEADER + self.log_signal.emit("Iniciando sincronização (consenso)...", "info"); response = requests.get(f"{meu_url}/nodes/resolve", timeout=30, headers=NGROK_HEADERS); response.raise_for_status(); data = response.json() + if data.get("message") == "Nossa cadeia foi substituída.": self.log_signal.emit("Blockchain sincronizada com sucesso. Cadeia atualizada para a mais longa.", "success"); self.view_blockchain() + else: self.log_signal.emit("Blockchain já sincronizada ou não houve alteração.", "info") + except requests.exceptions.RequestException as e: self.log_signal.emit(f"Erro ao sincronizar com o nó: {e}", "error") time.sleep(10) def register_peer_dialog(self): - """Diálogo para registrar um novo peer.""" text, ok = QInputDialog.getText(self, 'Registrar Peer', 'Digite a URL completa do novo peer (ex: http://IP:PORTA):') - if ok and text: - self.log_signal.emit(f"Tentando registrar peer: {text}", "info") - threading.Thread(target=self._register_peer_async, args=(text,)).start() + if ok and text: self.log_signal.emit(f"Tentando registrar peer: {text}", "info"); threading.Thread(target=self._register_peer_async, args=(text,)).start() def _register_peer_async(self, node_url): - """Função para registrar peer em segundo plano.""" try: - parsed_url = urlparse(node_url) - peer_ip = parsed_url.hostname - peer_port = parsed_url.port or 5000 - - if not peer_ip: - self.log_signal.emit(f"URL do peer inválida: {node_url}", "error") - return - - payload = {'ip': peer_ip, 'port': peer_port} - response = requests.post(f"{meu_url}/nodes/register", json=payload, timeout=10) # Usar meu_url - response.raise_for_status() - + parsed_url = urlparse(node_url); peer_ip = parsed_url.hostname; peer_port = parsed_url.port or 5000 + if not peer_ip: self.log_signal.emit(f"URL do peer inválida: {node_url}", "error"); return + payload = {'ip': peer_ip, 'port': peer_port}; + # ADICIONADO HEADER + response = requests.post(f"{meu_url}/nodes/register", json=payload, timeout=10, headers=NGROK_HEADERS); response.raise_for_status() self.log_signal.emit(f"Peer '{node_url}' registrado com sucesso! Resposta: {response.json()}", "success") - - except requests.exceptions.RequestException as e: - self.log_signal.emit(f"Erro ao registrar peer: {e}", "error") + except requests.exceptions.RequestException as e: self.log_signal.emit(f"Erro ao registrar peer: {e}", "error") def consult_contract_dialog(self): - """Diálogo para consultar um contrato inteligente.""" text, ok = QInputDialog.getText(self, 'Consultar Contrato', 'Digite o endereço do contrato inteligente:') - if ok and text: - self.log_signal.emit(f"Consultando contrato: {text}", "info") - threading.Thread(target=self._consult_contract_async, args=(text,)).start() + if ok and text: self.log_signal.emit(f"Consultando contrato: {text}", "info"); threading.Thread(target=self._consult_contract_async, args=(text,)).start() def _consult_contract_async(self, contract_address): - """Função para consultar contrato em segundo plano.""" try: - response = requests.get(f"{meu_url}/contract/{contract_address}/transactions", timeout=10) # Usar meu_url - response.raise_for_status() - - contract_data = response.json() - formatted_data = json.dumps(contract_data, indent=2) - - self.log_signal.emit(f"Detalhes do Contrato ({contract_address}):\n{formatted_data}", "info") - + # ADICIONADO HEADER + response = requests.get(f"{meu_url}/contract/{contract_address}/transactions", timeout=10, headers=NGROK_HEADERS); response.raise_for_status(); contract_data = response.json(); formatted_data = json.dumps(contract_data, indent=2); self.log_signal.emit(f"Detalhes do Contrato ({contract_address}):\n{formatted_data}", "info") except requests.exceptions.HTTPError as e: - if e.response.status_code == 404: - self.log_signal.emit("Contrato não encontrado na blockchain.", "warning") - else: - self.log_signal.emit(f"Erro HTTP ao consultar contrato: {e}", "error") - except requests.exceptions.RequestException as e: - self.log_signal.emit(f"Erro de conexão ao consultar contrato: {e}", "error") - -# --- APIClient para a GUI --- -class APIClient: - def __init__(self, base_url): - self.base_url = base_url - - def set_base_url(self, new_url): - self.base_url = new_url + if e.response.status_code == 404: self.log_signal.emit("Contrato não encontrado na blockchain.", "warning") + else: self.log_signal.emit(f"Erro HTTP ao consultar contrato: {e}", "error") + except requests.exceptions.RequestException as e: self.log_signal.emit(f"Erro de conexão ao consultar contrato: {e}", "error") - def get_node_info(self): - try: - response = requests.get(f"{self.base_url}/chain", timeout=5) - response.raise_for_status() - data = response.json() - return { - "node_id": data.get("node_id", "N/A"), - "url": self.base_url, - "chain_length": data.get("length", 0), - "pending_transactions": len(data.get("pending_transactions", [])) - } - except requests.exceptions.RequestException as e: - print(f"Erro ao buscar informações do nó: {e}") - return { - "node_id": "Erro", - "url": self.base_url, - "chain_length": "Erro", - "pending_transactions": "Erro" - } + def change_ngrok_token(self): + text, ok = QInputDialog.getText(self, "Configurar Ngrok", "Insira seu novo Ngrok Authtoken:\n(Reinicie o programa após salvar)", QLineEdit.Normal, load_ngrok_token() or "") + if ok and text: + save_ngrok_token(text) + self.log_signal.emit("Token Ngrok salvo! Reinicie o programa para aplicar.", "warning") + QMessageBox.information(self, "Token Salvo", "O novo token foi salvo. Por favor, feche e abra o programa novamente para conectar.") # --- Execução Principal --- def run_server(): - port = int(os.environ.get('PORT', 5000)) - app.run(host='0.0.0.0', port=port) + app.run(host='0.0.0.0', port=5001, threaded=True) if __name__ == "__main__": - conn = sqlite3.connect(DATABASE, check_same_thread=False) - node_id = load_or_create_node_id() - blockchain = Blockchain(conn, node_id) - - port = int(os.environ.get('PORT', 5000)) - meu_ip = get_my_ip() - meu_url = f"http://{meu_ip}:{port}" - print(f"[INFO] Node URL: {meu_url}") + multiprocessing.freeze_support() + token = load_ngrok_token() + port = int(os.environ.get('PORT', 5001)) - threading.Thread(target=discover_peers, daemon=True).start() + try: + from pyngrok import ngrok, conf + if token: + conf.get_default().auth_token = token + public_url = ngrok.connect(port).public_url + meu_url = public_url + print(f"[REDE] 🌍 Seu nó está público em: {meu_url}") + else: + raise Exception("Sem token") + except Exception as e: + print(f"[NGROK] Modo Local Ativo (Sem Túnel Público). Motivo: {e}") + meu_ip = get_my_ip() + meu_url = f"http://{meu_ip}:{port}" + print(f"[REDE] 🏠 Rodando localmente em: {meu_url}") - if len(known_nodes) > 0: - print("[BOOT] Tentando resolver conflitos na inicialização...") - blockchain.resolve_conflicts() - else: - print("[BOOT] Nenhum peer conhecido. Operando de forma isolada inicialmente.") + conn = sqlite3.connect(DATABASE, check_same_thread=False) + node_id_val = load_or_create_node_id() + blockchain = Blockchain(conn, node_id_val) - # Iniciar servidor Flask em thread separada server_thread = threading.Thread(target=run_server, daemon=True) server_thread.start() - time.sleep(1) # Pequeno atraso para garantir que o Flask esteja totalmente pronto + time.sleep(2) + + print("\n[BOOT] 📡 Conectando aos Seeds...") + for seed in SEED_NODES: known_nodes.add(seed) + salvar_peers(known_nodes) + + # --- NOVO: Sincronização Forçada e Profunda --- + print("[BOOT] 🔄 Iniciando Sincronização Profunda (Blocos + Mempool)...") + sync_success = False + for _ in range(3): # Tenta 3 vezes para garantir + if blockchain.resolve_conflicts(): + sync_success = True + break + time.sleep(1) + + if sync_success: + print("[BOOT] ✅ Sincronizado com sucesso (Dados recuperados)!") + else: + print("[BOOT] ⚠️ Rodando com dados locais ou chain local é a mestre.") - # Iniciar verificação de sincronização automática threading.Thread(target=auto_sync_checker, args=(blockchain,), daemon=True).start() + print("[GUI] 🚀 Iniciando Interface...") qt_app = QApplication(sys.argv) window = KertOneCoreClient() + window._on_flask_url_ready(f"http://127.0.0.1:{port}") window.show() sys.exit(qt_app.exec_()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0326274 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +flask +flask-cors +requests +ecdsa +qrcode +pillow diff --git a/seend.kert.py b/seend.kert.py new file mode 100644 index 0000000..d877d48 --- /dev/null +++ b/seend.kert.py @@ -0,0 +1,689 @@ +import hashlib +import json +import time +import threading +import sqlite3 +import os +from uuid import uuid4 +from flask import Flask, jsonify, request, render_template, send_from_directory +import requests +from urllib.parse import urlparse +import socket +import ipaddress +import sys +from ecdsa import SigningKey, VerifyingKey, SECP256k1, BadSignatureError +import multiprocessing +import urllib3 + +# Desativa avisos de SSL +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# --- DEFINIÇÃO DE DIRETÓRIO BASE --- +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +# --- INJEÇÃO DE MINERAÇÃO REAL (GPU/CPU) --- +try: + import pyopencl as cl + import numpy as np + HAS_GPU = True +except ImportError: + HAS_GPU = False + print("[SISTEMA] PyOpenCL ou Numpy não instalados. Mineração GPU desativada (Usando CPU).") + +# --- Configurações --- +DIFFICULTY = 4 +MINING_REWARD = 50 +DATABASE = 'chain.db' +COIN_NAME = "Kert-One" +COIN_SYMBOL = "KERT" +PEERS_FILE = 'peers.json' +WALLET_FILE = "server_wallet.json" + +# --- NÓS SEMENTES (SEED NODES) --- +SEED_NODES = [ + "https://seend.kert-one.com", + "http://seend3.kert-one.com:8001" +] + +# --- KERNEL TURBO ALINHADO --- +OPENCL_KERNEL = """ +typedef unsigned int uint; +#define ROR(x, y) ((x >> y) | (x << (32 - y))) +#define Ch(x, y, z) (z ^ (x & (y ^ z))) +#define Maj(x, y, z) ((x & y) | (z & (x | y))) +#define S0(x) (ROR(x, 2) ^ ROR(x, 13) ^ ROR(x, 22)) +#define S1(x) (ROR(x, 6) ^ ROR(x, 11) ^ ROR(x, 25)) +#define s0(x) (ROR(x, 7) ^ ROR(x, 18) ^ (x >> 3)) +#define s1(x) (ROR(x, 17) ^ ROR(x, 19) ^ (x >> 10)) + +__constant uint K[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 +}; + +void sha256_transform(uint *state, const uint *data) { + uint a, b, c, d, e, f, g, h, t1, t2; + uint W[64]; + for (int i = 0; i < 16; ++i) W[i] = data[i]; + for (int i = 16; i < 64; ++i) W[i] = s1(W[i - 2]) + W[i - 7] + s0(W[i - 15]) + W[i - 16]; + a = state[0]; b = state[1]; c = state[2]; d = state[3]; e = state[4]; f = state[5]; g = state[6]; h = state[7]; + for (int i = 0; i < 64; ++i) { + t1 = h + S1(e) + Ch(e, f, g) + K[i] + W[i]; t2 = S0(a) + Maj(a, b, c); + h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2; + } + state[0] += a; state[1] += b; state[2] += c; state[3] += d; state[4] += e; state[5] += f; state[6] += g; state[7] += h; +} + +__kernel void search_block(__global unsigned int *result, __global int *found, const unsigned int difficulty, const unsigned int start_nonce) { + unsigned int gid = get_global_id(0); + unsigned int loop_count = 2000; + for(unsigned int i=0; i < loop_count; i++) { + if(*found != 0) return; + unsigned int nonce = start_nonce + (gid * loop_count) + i; + unsigned int state[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; + unsigned int data[16] = {0}; + data[0] = nonce; + sha256_transform(state, data); + + if (state[0] <= (0xFFFFFFFF >> (difficulty * 4))) { + *result = nonce; + *found = 1; + return; + } + } +} +""" + +app = Flask(__name__) +node_id = str(uuid4()).replace('-', '') + +# --- Funções de Persistência de Peers --- +def salvar_peers(peers): + try: + with open(PEERS_FILE, 'w') as f: + json.dump(list(peers), f) + print(f"[P2P] Arquivo {PEERS_FILE} salvo.") + except Exception as e: + print(f"[ERRO] Falha ao salvar peers: {e}") + +def carregar_peers(): + if not os.path.exists(PEERS_FILE): + return [] + with open(PEERS_FILE, 'r') as f: + try: + return json.load(f) + except json.JSONDecodeError: + return [] + +known_nodes = set(carregar_peers()) +miner_lock = threading.Lock() + +blockchain = None +miner_address = None +miner_address_global = None +meu_url = None +port = None + +# Global variable for mining control +mining_active = False +mining_stop_flag = multiprocessing.Event() +mining_result = multiprocessing.Value('i', -1) + +@app.route('/coin/value', methods=['GET']) +def coin_value_api(): + if not blockchain.chain: + price = 500.0 + else: + last_block = blockchain.last_block() + price = float(last_block.get('protocol_value', 0.0)) + + if price < 500.0: + display_price = 500.0 + price + else: + display_price = price + + return jsonify({ + "coin": COIN_SYMBOL, + "protocol_value": price, + "protocol_value_display": f"{display_price:.2f}", + "unit": "USD" + }), 200 + +# --- Classe Blockchain --- +class Blockchain: + ADJUST_INTERVAL = 50 + TARGET_TIME = 20 + + def _calculate_difficulty_for_index(self, target_block_index): + if target_block_index % self.ADJUST_INTERVAL != 0: + return self.chain[-1].get('difficulty', 4) + + if len(self.chain) < self.ADJUST_INTERVAL: + return 4 + + last_block = self.chain[-1] + first_block = self.chain[-self.ADJUST_INTERVAL] + actual_time = last_block['timestamp'] - first_block['timestamp'] + + expected_time = self.ADJUST_INTERVAL * self.TARGET_TIME + old_diff = last_block['difficulty'] + + if actual_time <= 0: actual_time = 1 + new_diff = int(old_diff * (expected_time / actual_time)) + return max(1, min(20, new_diff)) + + def __init__(self, conn, node_id): + self.conn = conn + self.node_id = node_id + self._init_db() + self.chain = self._load_chain() + self.current_transactions = [] + if not self.chain: + print("[BOOT] 📡 Inserindo Gênese Base...") + genesis_block = { + 'index': 1, 'previous_hash': '1', 'proof': 100, + 'timestamp': 1700000000.0, 'miner': 'genesis', + 'transactions': [], 'difficulty': 1, 'protocol_value': 500.0 + } + self.chain.append(genesis_block) + self._save_block(genesis_block) + self.difficulty = self._calculate_difficulty_for_index(len(self.chain)) + print(f"[BOOT] Dificuldade inicial: {self.difficulty}") + + @staticmethod + def hash(block): + block_core = { + "index": block["index"], "previous_hash": block["previous_hash"], + "proof": block["proof"], "timestamp": block["timestamp"], + "miner": block["miner"], "difficulty": block.get("difficulty", 1), + "protocol_value": block.get("protocol_value", 0), + "transactions": block["transactions"] + } + # Garante ordenação consistente + block_string = json.dumps(block_core, sort_keys=True, separators=(',', ':')).encode() + return hashlib.sha256(block_string).hexdigest() + + def is_duplicate_transaction(self, new_tx): + for tx in self.current_transactions: + if tx.get('id') == new_tx.get('id'): + return True + return False + + @staticmethod + def custom_asic_resistant_hash(data_bytes, nonce): + raw = data_bytes + str(nonce).encode() + return hashlib.sha256(hashlib.sha256(raw).digest()).hexdigest() + + def _init_db(self): + c = self.conn.cursor() + c.execute('''CREATE TABLE IF NOT EXISTS blocks(index_ INTEGER PRIMARY KEY, previous_hash TEXT, proof INTEGER, timestamp REAL, miner TEXT, difficulty INTEGER, protocol_value REAL)''') + c.execute('''CREATE TABLE IF NOT EXISTS txs(id TEXT PRIMARY KEY, sender TEXT, recipient TEXT, amount TEXT, fee TEXT, signature TEXT, block_index INTEGER, public_key TEXT)''') + self.conn.commit() + + def _load_chain(self): + c = self.conn.cursor() + c.execute("SELECT index_, previous_hash, proof, timestamp, miner, difficulty, protocol_value FROM blocks ORDER BY index_") + chain = [] + for idx, prev, proof, ts, miner, difficulty, p_val in c.fetchall(): + c.execute("SELECT id, sender, recipient, amount, fee, signature, public_key FROM txs WHERE block_index=?", (idx,)) + txs = [] + for r in c.fetchall(): + txs.append(dict(id=r[0], sender=r[1], recipient=r[2], amount=r[3], fee=r[4], signature=r[5], public_key=r[6])) + block = { + 'index': idx, 'previous_hash': prev, 'proof': proof, + 'timestamp': ts, 'miner': miner, 'transactions': txs, + 'difficulty': difficulty, 'protocol_value': p_val + } + chain.append(block) + return chain + + def new_block(self, proof, previous_hash, miner, initial_difficulty=None): + block_index = len(self.chain) + 1 + reward = self._get_mining_reward(block_index) + difficulty = self._calculate_difficulty_for_index(block_index) if initial_difficulty is None else initial_difficulty + mining_reward_tx = { + 'id': str(uuid4()), 'sender': '0', 'recipient': miner, + 'amount': f"{reward:.8f}", 'fee': f"{0.0:.8f}", 'signature': '', 'public_key': '' + } + if not (proof == 100 and previous_hash == '1'): + self.current_transactions.insert(0, mining_reward_tx) + block = { + 'index': block_index, 'previous_hash': previous_hash, 'proof': proof, + 'timestamp': time.time(), 'miner': miner, + 'transactions': self.current_transactions, 'difficulty': difficulty + } + self.current_transactions = [] + self.chain.append(block) + c = self.conn.cursor() + c.execute("SELECT 1 FROM blocks WHERE index_=?", (block['index'],)) + if not c.fetchone(): + self._save_block(block) + return block + + def _save_block(self, block): + c = self.conn.cursor() + c.execute("INSERT INTO blocks VALUES (?, ?, ?, ?, ?, ?, ?)", + (block['index'], block['previous_hash'], block['proof'], + block['timestamp'], block['miner'], block['difficulty'], + block.get('protocol_value', 500.0))) + for t in block['transactions']: + c.execute("INSERT INTO txs VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + (t['id'], t['sender'], t['recipient'], t['amount'], + t['fee'], t['signature'], block['index'], t.get('public_key', ''))) + self.conn.commit() + + def _get_mining_reward(self, block_index): + if block_index <= 1200: return 50.0 + elif block_index <= 2200: return 25.0 + elif block_index <= 4000: return 12.5 + elif block_index <= 5500: return 6.5 + elif block_index <= 6200: return 3.25 + elif block_index <= 20000: return 1.25 + elif block_index <= 1000000: return 0.03 + else: + halvings = (block_index - 1000000) // 2100000 + return max(0.03 / (2 ** halvings), 0.0) + + def last_block(self): + return self.chain[-1] if self.chain else None + + def proof_of_work(self, last_proof): + difficulty_for_pow = self._calculate_difficulty_for_index(len(self.chain) + 1) + proof = 0 + print(f"⛏️ [MINER] CPU Mining. Dif: {difficulty_for_pow}") + while not self.valid_proof(last_proof, proof, difficulty_for_pow): + global mining_active + if not mining_active: return -1 + if proof % 1000 == 0: time.sleep(0.001) + if self.last_block()['proof'] != last_proof: return -1 + proof += 1 + return proof + + @staticmethod + def valid_proof(last_proof, proof, difficulty): + guess = f"{last_proof}{proof}".encode() + guess_hash = Blockchain.custom_asic_resistant_hash(guess, proof) + return guess_hash[:difficulty] == "0" * difficulty + + @staticmethod + def _mine_gpu(last_proof, difficulty, stop_event, result_value): + try: + import pyopencl as cl + import numpy as np + import time + except ImportError: return -1 + + try: + platforms = cl.get_platforms() + if not platforms: return -1 + target_device = platforms[0].get_devices(device_type=cl.device_type.GPU)[0] + + ctx = cl.Context(devices=[target_device]) + queue = cl.CommandQueue(ctx) + prg = cl.Program(ctx, OPENCL_KERNEL).build() + kernel = cl.Kernel(prg, "search_block") + + result_nonce = np.zeros(1, dtype=np.uint32) + found = np.zeros(1, dtype=np.int32) + res_buf = cl.Buffer(ctx, cl.mem_flags.WRITE_ONLY, result_nonce.nbytes) + found_buf = cl.Buffer(ctx, cl.mem_flags.READ_WRITE | cl.mem_flags.COPY_HOST_PTR, hostbuf=found) + + batch_size = 500000 + loop_intern0 = 2000 + current_nonce = 0 + + print(f"🔥 [GPU] SEED ATIVO: {target_device.name}") + + while not stop_event.is_set(): + iter_start = time.time() + kernel(queue, (batch_size,), None, res_buf, found_buf, np.uint32(difficulty), np.uint32(current_nonce)) + queue.finish() + + if (time.time() - iter_start) < 0.001: + print("[GPU] Erro de driver detectado. Reiniciando worker.") + return -1 + + cl.enqueue_copy(queue, found, found_buf) + + if found[0] == 1: + cl.enqueue_copy(queue, result_nonce, res_buf) + nonce = int(result_nonce[0]) + if Blockchain.valid_proof(last_proof, nonce, difficulty): + print(f"💎 [GPU-SEED] BLOCO ENCONTRADO: {nonce}") + result_value.value = nonce + stop_event.set() + return nonce + found[0] = 0 + cl.enqueue_copy(queue, found_buf, found) + + current_nonce += (batch_size * loop_intern0) + if current_nonce > 4000000000: current_nonce = 0 + + except Exception as e: + print(f"[GPU ERROR] {e}") + return -1 + return -1 + + def tx_already_mined(self, tx_id): + c = self.conn.cursor() + c.execute("SELECT 1 FROM txs WHERE id=?", (tx_id,)) + return c.fetchone() is not None + + # --- CORREÇÃO CRÍTICA PARA ACEITAR BLOCOS ANTIGOS (MODO BLIND SYNC) --- + def valid_chain(self, chain, check_strict=True): + if not chain: return False + if chain[0]['index'] != 1 or chain[0]['previous_hash'] != '1': return False + for idx in range(1, len(chain)): + prev = chain[idx - 1] + curr = chain[idx] + + # Se for checagem estrita (padrão), valida hash e PoW + if check_strict: + if curr['previous_hash'] != self.hash(prev): + print(f"[SYNC ERROR] Hash inválido no bloco {curr['index']}.") + return False + if not self.valid_proof(prev['proof'], curr['proof'], curr.get('difficulty', DIFFICULTY)): + print(f"[SYNC ERROR] Prova inválida no bloco {curr['index']}.") + return False + else: + # Se for MODO CÓPIA (check_strict=False), confia e só verifica a sequência + if curr['index'] != prev['index'] + 1: + return False + + return True + + def get_total_difficulty(self, chain_to_check): + return sum([block.get('difficulty', DIFFICULTY) for block in chain_to_check]) + + def resolve_conflicts(self): + # GARANTE que estamos lendo os peers do arquivo + memória + current_peers = known_nodes.union(set(carregar_peers())) + neighbors = list(current_peers) + new_chain = None + max_difficulty = self.get_total_difficulty(self.chain) + + # --- DETECÇÃO DE BOOTSTRAP (Se eu sou novo, ativo o modo 'Confia no Pai') --- + # Se eu só tenho o Genesis, eu NÃO valido hashes antigos, eu só baixo. + is_fresh_install = (len(self.chain) <= 1) + if is_fresh_install: + print("[BOOT] 🐇 MODO CÓPIA CEGA ATIVADO: Baixando chain sem validar hashes antigos...") + + print(f"[SYNC] Verificando {len(neighbors)} peers...") + + for node_url in neighbors: + if node_url == meu_url: continue + try: + print(f" -> Conectando a {node_url}...") + response = requests.get(f"{node_url}/chain", timeout=5, verify=False) + if response.status_code == 200: + data = response.json() + peer_chain = data.get("chain") + if not peer_chain: continue + + peer_difficulty = self.get_total_difficulty(peer_chain) + print(f" [INFO] Peer Dif: {peer_difficulty} | Local Dif: {max_difficulty}") + + if peer_difficulty > max_difficulty: + # O PULO DO GATO: Se for instalação nova, passa check_strict=False + if self.valid_chain(peer_chain, check_strict=not is_fresh_install): + max_difficulty = peer_difficulty + new_chain = peer_chain + print(" [UPGRADE] Nova chain aceita e baixada!") + else: + print(" [REJECT] Chain rejeitada (Inválida).") + except Exception as e: + print(f" [OFFLINE] {node_url}: {e}") + + if new_chain: + self.chain = new_chain + self._rebuild_db_from_chain() + print(f"[CONSENSO] ✅ Sincronizado com sucesso! Total Blocos: {len(self.chain)}") + return True + print("[CONSENSO] Mantendo cadeia local.") + return False + + def _rebuild_db_from_chain(self): + try: + c = self.conn.cursor() + c.execute("DELETE FROM txs"); c.execute("DELETE FROM blocks") + for block in self.chain: + c.execute("INSERT INTO blocks VALUES (?, ?, ?, ?, ?, ?, ?)", (block['index'], block['previous_hash'], block['proof'], block['timestamp'], block['miner'], block.get('difficulty', 1), block.get('protocol_value', 0.0))) + for tx in block['transactions']: + c.execute("INSERT INTO txs VALUES (?, ?, ?, ?, ?, ?, ?, ?)", (tx['id'], tx['sender'], tx['recipient'], tx['amount'], tx['fee'], tx['signature'], block['index'], tx.get('public_key', ''))) + self.conn.commit() + except Exception as e: print(f"[DB] Erro rebuild: {e}") + + def balance(self, address): + bal = 0.0 + for block in self.chain: + for t in block['transactions']: + if t['sender'] == address: bal -= (float(t['amount']) + float(t['fee'])) + if t['recipient'] == address: bal += float(t['amount']) + for t in self.current_transactions: + if t['sender'] == address: bal -= (float(t['amount']) + float(t['fee'])) + if t['recipient'] == address: bal += float(t['amount']) + return bal + +# --- Endpoints Flask --- +@app.route('/', methods=['GET']) +def index_web(): return "Kert-One Seed Node Running (Linux/Ubuntu)" + +@app.route('/chain', methods=['GET']) +def chain_api(): + response = {'chain': blockchain.chain, 'length': len(blockchain.chain), 'pending_transactions': blockchain.current_transactions, 'node_id': node_id} + return jsonify(response), 200 + +@app.route('/nodes/register', methods=['POST']) +def register_nodes_api(): + data = request.get_json(silent=True) or {} + new_node_url = data.get("url") + if not new_node_url: + new_node_ip = data.get("ip"); new_node_port = data.get("port") + if new_node_ip and new_node_port: new_node_url = f"http://{new_node_ip}:{new_node_port}" + if not new_node_url: return jsonify({"message": "Invalido"}), 400 + + new_node_url = new_node_url.strip().rstrip("/") + if not new_node_url.startswith("http"): new_node_url = "http://" + new_node_url + if new_node_url == meu_url: return jsonify({"message": "Self ignored"}), 200 + + if new_node_url not in known_nodes: + known_nodes.add(new_node_url) + salvar_peers(known_nodes) + print(f"[P2P] Novo peer registrado: {new_node_url}") + try: requests.post(f"{new_node_url}/nodes/register", json={"url": meu_url}, timeout=5, verify=False) + except: pass + return jsonify({"message": "Registrado", "known_peers": list(known_nodes)}), 200 + +@app.route('/nodes', methods=['GET']) +def get_nodes_api(): return jsonify({'nodes': list(known_nodes)}), 200 + +@app.route('/nodes/resolve', methods=['GET']) +def resolve_api(): + replaced = blockchain.resolve_conflicts() + return jsonify({'message': 'Cadeia substituida' if replaced else 'Cadeia autoritativa'}), 200 + +@app.route('/balance/', methods=['GET']) +def balance_api(addr): + return jsonify({'address': addr, 'balance': blockchain.balance(addr), 'symbol': COIN_SYMBOL}), 200 + +@app.route('/tx/new', methods=['POST']) +def new_transaction_api(): + values = request.get_json(silent=True) + required = ['id', 'sender', 'recipient', 'amount', 'fee', 'public_key', 'signature'] + if not values or not all(k in values for k in required): return jsonify({'message': 'Faltando dados'}), 400 + + try: + amount_fmt = f"{float(values['amount']):.8f}" + fee_fmt = f"{float(values['fee']):.8f}" + transaction = { + 'id': values['id'], 'sender': values['sender'], 'recipient': values['recipient'], + 'amount': amount_fmt, 'fee': fee_fmt, + 'public_key': values['public_key'], 'signature': values['signature'], + 'timestamp': values.get('timestamp', time.time()) + } + + vk = VerifyingKey.from_string(bytes.fromhex(values['public_key']), curve=SECP256k1) + msg_data = {'amount': amount_fmt, 'fee': fee_fmt, 'recipient': values['recipient'], 'sender': values['sender']} + message = json.dumps(msg_data, sort_keys=True, separators=(',', ':')).encode('utf-8') + vk.verify_digest(bytes.fromhex(values['signature']), hashlib.sha256(message).digest()) + + if blockchain.balance(values['sender']) < (float(amount_fmt) + float(fee_fmt)): + return jsonify({'message': 'Saldo insuficiente'}), 400 + + blockchain.current_transactions.append(transaction) + broadcast_tx_to_peers(transaction) + return jsonify({'message': 'TX Adicionada'}), 201 + except Exception as e: + return jsonify({'message': f'Erro: {e}'}), 400 + +def broadcast_tx_to_peers(tx): + for peer in known_nodes.copy(): + if peer == meu_url: continue + try: requests.post(f"{peer}/tx/receive", json=tx, timeout=3, verify=False) + except: pass + +@app.route('/tx/receive', methods=['POST']) +def receive_transaction_api(): + tx_data = request.get_json() + if not tx_data: return jsonify({'message': 'No data'}), 400 + blockchain.current_transactions.append(tx_data) + return jsonify({'message': 'TX Recebida'}), 200 + +@app.route('/blocks/receive', methods=['POST']) +def receive_block_api(): + block_data = request.get_json() + last_block = blockchain.last_block() + if block_data['index'] > last_block['index'] + 1: + threading.Thread(target=blockchain.resolve_conflicts, daemon=True).start() + return jsonify({'message': 'Sync started'}), 202 + if blockchain.valid_proof(last_block['proof'], block_data['proof'], block_data['difficulty']): + blockchain.chain.append(block_data) + blockchain._save_block(block_data) + mined_ids = {t['id'] for t in block_data['transactions']} + blockchain.current_transactions = [tx for tx in blockchain.current_transactions if tx['id'] not in mined_ids] + return jsonify({'message': 'Bloco aceito'}), 200 + return jsonify({'message': 'Bloco invalido'}), 400 + +@app.route('/sync/check', methods=['GET']) +def check_sync_api(): + last = blockchain.last_block() + return jsonify({'index': last['index'], 'hash': blockchain.hash(last)}) + +@app.route('/miner/set_address', methods=['POST']) +def set_miner_address_api(): + global miner_address_global + data = request.get_json() or {} + addr = data.get("address") + if addr: miner_address_global = addr; return jsonify({"message": "OK"}), 200 + return jsonify({"message": "Missing address"}), 400 + +@app.route('/mine', methods=['GET']) +def mine_api(): + global mining_active, miner_address_global, mining_stop_flag, mining_result + if not miner_address_global: return jsonify({"message": "Endereço minerador nao definido"}), 400 + with miner_lock: + if mining_active: return jsonify({"message": "Ja minerando"}), 409 + mining_active = True + try: + last_block = blockchain.last_block() + proof = -1 + mining_stop_flag.clear(); mining_result.value = -1 + + if HAS_GPU: + proof = Blockchain._mine_gpu(last_block['proof'], blockchain._calculate_difficulty_for_index(len(blockchain.chain)+1), mining_stop_flag, mining_result) + if proof == -1: + proof = blockchain.proof_of_work(last_block['proof']) + + if proof != -1: + new_block = blockchain.new_block(proof, blockchain.hash(last_block), miner_address_global) + broadcast_block(new_block) + return jsonify({"message": "Bloco Minerado!", "index": new_block['index']}), 200 + return jsonify({"message": "Parado"}), 200 + finally: + with miner_lock: mining_active = False + +def broadcast_block(block): + for peer in known_nodes | set(SEED_NODES): + if peer == meu_url: continue + try: requests.post(f"{peer}/blocks/receive", json=block, timeout=5, verify=False) + except: pass + +# --- ROTAS PWA E FRONTEND --- +@app.route('/card') +def card_web(): + try: return render_template('card.html') + except Exception as e: return f"Erro: {e}", 500 + +@app.route('/manifest.json') +def manifest(): + try: return send_from_directory(os.path.join(BASE_DIR, 'static'), 'manifest.json', mimetype='application/json') + except Exception as e: return f"Erro: {e}", 500 + +@app.route('/sw.js') +def service_worker(): + try: return send_from_directory(os.path.join(BASE_DIR, 'static'), 'sw.js', mimetype='application/javascript') + except Exception as e: return f"Erro: {e}", 500 + +@app.route('/static/') +def serve_static_files(filename): + return send_from_directory(os.path.join(BASE_DIR, 'static'), filename) + +def get_my_ip(): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0]; s.close() + return ip + except: return "127.0.0.1" + +def auto_sync_checker(blockchain_instance): + while True: + try: + blockchain_instance.resolve_conflicts() + except Exception as e: print(f"[SYNC] Erro: {e}") + time.sleep(60) + +# --- EXECUÇÃO PRINCIPAL --- +if __name__ == "__main__": + conn = sqlite3.connect(DATABASE, check_same_thread=False) + node_id_val = str(uuid4()).replace("-", "")[:16] + blockchain = Blockchain(conn, node_id_val) + + port = int(os.environ.get('PORT',5001)) + meu_ip = get_my_ip() + meu_url = f"http://{meu_ip}:{port}" + print(f"[LINUX SEED] 🐧 Rodando em: {meu_url}") + + # --- INICIALIZAÇÃO AGRESSIVA --- + print("[BOOT] 📡 Carregando Seeds e Peers...") + for seed in SEED_NODES: + known_nodes.add(seed) + + salvar_peers(known_nodes) + print(f"[SISTEMA] Arquivo peers.json criado/atualizado com {len(known_nodes)} nós.") + + print("[BOOT] ⏳ Iniciando Sync Inicial...") + if blockchain.resolve_conflicts(): + print("[BOOT] ✅ Sync Concluído!") + else: + print("[BOOT] ⚠️ Sync terminou sem mudanças (ou chain local é a maior).") + + threading.Thread(target=auto_sync_checker, args=(blockchain,), daemon=True).start() + + kwargs = {'host': '0.0.0.0', 'port': 5001, 'threaded': True, 'use_reloader': False} + flask_thread = threading.Thread(target=app.run, kwargs=kwargs, daemon=True) + flask_thread.start() + + print("[SISTEMA] 🚀 Servidor ONLINE.") + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print("Desligando nó...") diff --git a/templates.zip b/templates.zip new file mode 100644 index 0000000..a79d4a7 Binary files /dev/null and b/templates.zip differ