-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathLinux.server.Kert-One.py
More file actions
1445 lines (1234 loc) · 67.8 KB
/
Linux.server.Kert-One.py
File metadata and controls
1445 lines (1234 loc) · 67.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
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/<addr>', 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