Commit iniziale
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Virtual Environment
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
466
main.py
Normal file
466
main.py
Normal file
@@ -0,0 +1,466 @@
|
||||
import requests
|
||||
import sqlite3
|
||||
import time
|
||||
import json
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
class P2PKBlockchainScanner:
|
||||
def __init__(self, db_path: str = "bitcoin_p2pk.db"):
|
||||
"""Inizializza lo scanner con connessione al database"""
|
||||
self.db_path = db_path
|
||||
self.api_base = "https://mempool.space/api"
|
||||
self.init_database()
|
||||
|
||||
def init_database(self):
|
||||
"""Crea il database SQLite per memorizzare i P2PK"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS p2pk_addresses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
block_height INTEGER NOT NULL,
|
||||
txid TEXT NOT NULL,
|
||||
output_index INTEGER NOT NULL,
|
||||
scriptpubkey TEXT NOT NULL,
|
||||
value_satoshi INTEGER,
|
||||
timestamp INTEGER,
|
||||
is_unspent INTEGER DEFAULT 0,
|
||||
last_checked INTEGER DEFAULT 0,
|
||||
UNIQUE(txid, output_index)
|
||||
)
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS scan_progress (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
last_scanned_block INTEGER DEFAULT 0,
|
||||
total_p2pk_found INTEGER DEFAULT 0
|
||||
)
|
||||
''')
|
||||
|
||||
# Inizializza il progresso se non esiste
|
||||
cursor.execute('INSERT OR IGNORE INTO scan_progress (id, last_scanned_block) VALUES (1, 0)')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_block_hash(self, height: int) -> Optional[str]:
|
||||
"""Ottieni l'hash del blocco data l'altezza"""
|
||||
try:
|
||||
response = requests.get(f"{self.api_base}/block-height/{height}", timeout=10)
|
||||
if response.status_code == 200:
|
||||
return response.text.strip()
|
||||
except Exception as e:
|
||||
print(f"Errore ottenendo hash per blocco {height}: {e}")
|
||||
return None
|
||||
|
||||
def get_block_transactions(self, block_hash: str) -> Optional[List[Dict]]:
|
||||
"""Ottieni le transazioni di un blocco con paginazione"""
|
||||
try:
|
||||
all_transactions = []
|
||||
start_index = 0
|
||||
|
||||
# L'API restituisce massimo 25 transazioni per volta
|
||||
while True:
|
||||
url = f"{self.api_base}/block/{block_hash}/txs/{start_index}"
|
||||
response = requests.get(url, timeout=30)
|
||||
|
||||
if response.status_code != 200:
|
||||
break
|
||||
|
||||
txs = response.json()
|
||||
|
||||
if not txs or not isinstance(txs, list) or len(txs) == 0:
|
||||
break
|
||||
|
||||
all_transactions.extend(txs)
|
||||
|
||||
# Se riceviamo meno di 25 transazioni, siamo alla fine
|
||||
if len(txs) < 25:
|
||||
break
|
||||
|
||||
start_index += 25
|
||||
time.sleep(0.1) # Piccola pausa tra le richieste paginate
|
||||
|
||||
return all_transactions if all_transactions else None
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Errore ottenendo transazioni per blocco {block_hash}: {e}")
|
||||
return None
|
||||
|
||||
def extract_p2pk_from_transaction(self, tx: Dict) -> List[Dict]:
|
||||
"""Estrai output P2PK da una transazione"""
|
||||
p2pk_outputs = []
|
||||
|
||||
for idx, output in enumerate(tx.get('vout', [])):
|
||||
scriptpubkey_type = output.get('scriptpubkey_type', '')
|
||||
scriptpubkey = output.get('scriptpubkey', '')
|
||||
scriptpubkey_asm = output.get('scriptpubkey_asm', '')
|
||||
|
||||
# Metodo 1: Controlla il tipo esplicito
|
||||
is_p2pk_type = scriptpubkey_type in ['pubkey', 'p2pk']
|
||||
|
||||
# Metodo 2: Controlla la lunghezza dello script (P2PK = 67 o 35 byte)
|
||||
# 67 byte = chiave pubblica non compressa (04 + 64 hex chars) + OP_CHECKSIG
|
||||
# 35 byte = chiave pubblica compressa (02/03 + 32 hex chars) + OP_CHECKSIG
|
||||
script_len = len(scriptpubkey) // 2 if scriptpubkey else 0
|
||||
is_p2pk_length = script_len in [67, 35]
|
||||
|
||||
# Metodo 3: Controlla il pattern ASM (PUBLIC_KEY OP_CHECKSIG)
|
||||
is_p2pk_asm = False
|
||||
if scriptpubkey_asm:
|
||||
parts = scriptpubkey_asm.split()
|
||||
# P2PK ha esattamente 2 elementi: chiave pubblica + OP_CHECKSIG
|
||||
if len(parts) == 2 and parts[1] in ['OP_CHECKSIG', 'CHECKSIG']:
|
||||
# Verifica che il primo elemento sia una chiave pubblica valida
|
||||
pubkey = parts[0]
|
||||
# Chiave non compressa: 130 caratteri (65 byte)
|
||||
# Chiave compressa: 66 caratteri (33 byte)
|
||||
if len(pubkey) in [130, 66]:
|
||||
is_p2pk_asm = True
|
||||
|
||||
# Metodo 4: Controlla il pattern hex dello scriptPubKey
|
||||
is_p2pk_hex = False
|
||||
if scriptpubkey:
|
||||
# P2PK non compresso: 41 (65 byte pubkey) + pubkey + ac (OP_CHECKSIG)
|
||||
# P2PK compresso: 21 (33 byte pubkey) + pubkey + ac (OP_CHECKSIG)
|
||||
if scriptpubkey.endswith('ac'): # OP_CHECKSIG
|
||||
if scriptpubkey.startswith('41') and len(scriptpubkey) == 134: # 67 byte * 2
|
||||
is_p2pk_hex = True
|
||||
elif scriptpubkey.startswith('21') and len(scriptpubkey) == 70: # 35 byte * 2
|
||||
is_p2pk_hex = True
|
||||
|
||||
# Se uno dei metodi identifica P2PK
|
||||
if is_p2pk_type or is_p2pk_length or is_p2pk_asm or is_p2pk_hex:
|
||||
p2pk_data = {
|
||||
'txid': tx['txid'],
|
||||
'output_index': idx, # Usa l'indice dell'array
|
||||
'scriptpubkey': scriptpubkey,
|
||||
'value': output.get('value', 0),
|
||||
'asm': scriptpubkey_asm
|
||||
}
|
||||
p2pk_outputs.append(p2pk_data)
|
||||
|
||||
return p2pk_outputs
|
||||
|
||||
def save_p2pk_data(self, block_height: int, p2pk_data: List[Dict], block_timestamp: int):
|
||||
"""Salva i dati P2PK nel database e verifica lo stato UTXO"""
|
||||
if not p2pk_data:
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
for data in p2pk_data:
|
||||
try:
|
||||
# Verifica se l'UTXO è ancora non speso
|
||||
utxo_status = self.check_utxo_status(data['txid'], data['output_index'])
|
||||
is_unspent = 1 if utxo_status['is_unspent'] else 0
|
||||
current_time = int(time.time())
|
||||
|
||||
cursor.execute('''
|
||||
INSERT OR REPLACE INTO p2pk_addresses
|
||||
(block_height, txid, output_index, scriptpubkey, value_satoshi, timestamp, is_unspent, last_checked)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
block_height,
|
||||
data['txid'],
|
||||
data['output_index'],
|
||||
data['scriptpubkey'],
|
||||
data['value'], # Il valore è già in satoshi dall'API
|
||||
block_timestamp,
|
||||
is_unspent,
|
||||
current_time
|
||||
))
|
||||
|
||||
# Log dello stato
|
||||
if is_unspent:
|
||||
print(f" 💰 UTXO NON SPESO! Valore: {data['value']} sat")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Errore salvataggio dato: {e}")
|
||||
|
||||
# Aggiorna il progresso
|
||||
cursor.execute('''
|
||||
UPDATE scan_progress
|
||||
SET last_scanned_block = ?,
|
||||
total_p2pk_found = total_p2pk_found + ?
|
||||
WHERE id = 1
|
||||
''', (block_height, len(p2pk_data)))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_block_timestamp(self, block_hash: str) -> Optional[int]:
|
||||
"""Ottieni timestamp del blocco"""
|
||||
try:
|
||||
response = requests.get(f"{self.api_base}/block/{block_hash}", timeout=10)
|
||||
if response.status_code == 200:
|
||||
block_data = response.json()
|
||||
return block_data.get('timestamp', 0)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def check_utxo_status(self, txid: str, vout: int) -> Dict:
|
||||
"""Verifica se l'UTXO è ancora non speso (ha saldo)"""
|
||||
try:
|
||||
response = requests.get(f"{self.api_base}/tx/{txid}/outspend/{vout}", timeout=10)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
# Se 'spent' è False, l'UTXO non è stato speso
|
||||
is_unspent = not data.get('spent', True)
|
||||
return {
|
||||
'is_unspent': is_unspent,
|
||||
'spent_txid': data.get('txid') if data.get('spent') else None,
|
||||
'spent_vin': data.get('vin') if data.get('spent') else None
|
||||
}
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Errore verifica UTXO {txid}:{vout} - {e}")
|
||||
|
||||
return {'is_unspent': False, 'spent_txid': None, 'spent_vin': None}
|
||||
|
||||
def get_last_scanned_block(self) -> int:
|
||||
"""Ottieni l'ultimo blocco scannerizzato"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT last_scanned_block FROM scan_progress WHERE id = 1')
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
return result[0] if result else 0
|
||||
|
||||
def scan_blocks(self, start_block: int, end_block: int, delay: float = 0.5):
|
||||
"""Scannerizza blocchi in un range"""
|
||||
print(f"🚀 Inizio scansione blocchi {start_block} a {end_block}")
|
||||
print(f"Database: {self.db_path}")
|
||||
|
||||
for height in range(start_block, end_block + 1):
|
||||
try:
|
||||
print(f"\n📦 Analisi blocco {height}...")
|
||||
|
||||
# Ottieni hash del blocco
|
||||
block_hash = self.get_block_hash(height)
|
||||
if not block_hash:
|
||||
print(f" ⚠️ Hash non trovato per blocco {height}")
|
||||
continue
|
||||
|
||||
# Ottieni timestamp
|
||||
timestamp = self.get_block_timestamp(block_hash) or 0
|
||||
|
||||
# Ottieni transazioni
|
||||
transactions = self.get_block_transactions(block_hash)
|
||||
if transactions is None:
|
||||
print(f" ⚠️ Transazioni non trovate per blocco {height}")
|
||||
continue
|
||||
|
||||
# Processa transazioni
|
||||
total_p2pk_in_block = 0
|
||||
all_p2pk_in_block = []
|
||||
|
||||
for tx in transactions:
|
||||
p2pk_outputs = self.extract_p2pk_from_transaction(tx)
|
||||
if p2pk_outputs:
|
||||
total_p2pk_in_block += len(p2pk_outputs)
|
||||
all_p2pk_in_block.extend(p2pk_outputs)
|
||||
for p2pk in p2pk_outputs:
|
||||
print(f" ✅ P2PK in TX {tx['txid'][:16]}... | Valore: {p2pk['value']} sat | Script: {p2pk['scriptpubkey'][:20]}...")
|
||||
|
||||
# Salva tutti i P2PK trovati nel blocco
|
||||
if all_p2pk_in_block:
|
||||
self.save_p2pk_data(height, all_p2pk_in_block, timestamp)
|
||||
print(f" 📊 Blocco {height}: {total_p2pk_in_block} P2PK trovati e salvati")
|
||||
else:
|
||||
# Aggiorna comunque il progresso anche se non ci sono P2PK
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('UPDATE scan_progress SET last_scanned_block = ? WHERE id = 1', (height,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f" 🔍 Blocco {height}: Nessun P2PK trovato")
|
||||
|
||||
# Aggiorna progresso ogni 10 blocchi
|
||||
if height % 10 == 0:
|
||||
self.print_progress()
|
||||
|
||||
# Rispetta l'API (rate limiting)
|
||||
time.sleep(delay)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n⏸️ Scansione interrotta dall'utente")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ Errore durante scansione blocco {height}: {e}")
|
||||
time.sleep(2) # Pausa più lunga in caso di errore
|
||||
|
||||
self.print_final_report()
|
||||
|
||||
def print_progress(self):
|
||||
"""Stampa il progresso attuale"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT last_scanned_block, total_p2pk_found FROM scan_progress WHERE id = 1')
|
||||
progress = cursor.fetchone()
|
||||
|
||||
cursor.execute('SELECT COUNT(DISTINCT block_height) FROM p2pk_addresses')
|
||||
blocks_with_p2pk = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute('SELECT COUNT(*) FROM p2pk_addresses')
|
||||
total_p2pk = cursor.fetchone()[0]
|
||||
|
||||
conn.close()
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"📈 PROGRESSO SCANSIONE")
|
||||
print(f"📦 Ultimo blocco: {progress[0]}")
|
||||
print(f"🔑 P2PK totali: {progress[1]}")
|
||||
print(f"📊 Blocchi con P2PK: {blocks_with_p2pk}")
|
||||
print(f"📝 Indirizzi unici: {total_p2pk}")
|
||||
print(f"{'='*50}\n")
|
||||
|
||||
def print_final_report(self):
|
||||
"""Stampa report finale"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Statistiche finali
|
||||
cursor.execute('SELECT MAX(block_height), MIN(block_height) FROM p2pk_addresses')
|
||||
max_block, min_block = cursor.fetchone()
|
||||
|
||||
cursor.execute('SELECT SUM(value_satoshi) FROM p2pk_addresses')
|
||||
total_value = cursor.fetchone()[0] or 0
|
||||
|
||||
cursor.execute('SELECT COUNT(*) FROM p2pk_addresses')
|
||||
total_addresses = cursor.fetchone()[0]
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"🎉 SCANSIONE COMPLETATA")
|
||||
print(f"{'='*60}")
|
||||
print(f"📊 Totale indirizzi P2PK: {total_addresses}")
|
||||
print(f"📦 Primo blocco con P2PK: {min_block or 'Nessuno'}")
|
||||
print(f"📦 Ultimo blocco con P2PK: {max_block or 'Nessuno'}")
|
||||
print(f"💰 Valore totale: {total_value / 100000000:.8f} BTC")
|
||||
print(f"💾 Database: {self.db_path}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Esempio query per estrarre chiavi pubbliche
|
||||
if total_addresses > 0:
|
||||
print(f"\n📋 Esempio dati raccolti:")
|
||||
cursor.execute('SELECT scriptpubkey FROM p2pk_addresses LIMIT 3')
|
||||
samples = cursor.fetchall()
|
||||
for i, sample in enumerate(samples):
|
||||
print(f" {i+1}. {sample[0][:50]}...")
|
||||
|
||||
conn.close()
|
||||
|
||||
def export_to_csv(self, output_file: str = "p2pk_data.csv"):
|
||||
"""Esporta dati in CSV per analisi"""
|
||||
import csv
|
||||
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT block_height, txid, output_index,
|
||||
scriptpubkey, value_satoshi, timestamp
|
||||
FROM p2pk_addresses
|
||||
ORDER BY block_height
|
||||
''')
|
||||
|
||||
with open(output_file, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(['Block', 'TXID', 'Output Index',
|
||||
'ScriptPubKey', 'Value (satoshi)', 'Timestamp'])
|
||||
|
||||
for row in cursor.fetchall():
|
||||
writer.writerow(row)
|
||||
|
||||
conn.close()
|
||||
print(f"📁 Dati esportati in: {output_file}")
|
||||
|
||||
# USO DIDATTICO
|
||||
if __name__ == "__main__":
|
||||
# AVVERTENZA IMPORTANTE
|
||||
print("⚠️ AVVISO: Questo script è SOLO per SCOPI EDUCATIVI")
|
||||
print("⛔ NON usare per attività illegali")
|
||||
print("📚 Scopo: Studio della struttura blockchain Bitcoin\n")
|
||||
|
||||
# Crea scanner
|
||||
scanner = P2PKBlockchainScanner("bitcoin_p2pk_study.db")
|
||||
|
||||
# Recupera l'ultimo blocco scannerizzato
|
||||
last_scanned = scanner.get_last_scanned_block()
|
||||
|
||||
print(f"📊 Ultimo blocco scannerizzato: {last_scanned}")
|
||||
print(f"💡 I primi blocchi di Bitcoin (1-10000) contengono molti P2PK")
|
||||
|
||||
# Chiedi all'utente il blocco di inizio
|
||||
start_input = input(f"\n📍 Blocco di inizio scansione (default: {last_scanned + 1 if last_scanned > 0 else 1}): ").strip()
|
||||
|
||||
if start_input:
|
||||
try:
|
||||
START_BLOCK = int(start_input)
|
||||
if START_BLOCK < 1:
|
||||
print("⚠️ Il blocco deve essere >= 1, uso blocco 1")
|
||||
START_BLOCK = 1
|
||||
except ValueError:
|
||||
print(f"⚠️ Valore non valido, uso blocco {last_scanned + 1 if last_scanned > 0 else 1}")
|
||||
START_BLOCK = last_scanned + 1 if last_scanned > 0 else 1
|
||||
else:
|
||||
START_BLOCK = last_scanned + 1 if last_scanned > 0 else 1
|
||||
|
||||
# Chiedi all'utente il blocco finale
|
||||
end_input = input(f"📍 Blocco finale scansione (default: {START_BLOCK + 999}): ").strip()
|
||||
|
||||
if end_input:
|
||||
try:
|
||||
END_BLOCK = int(end_input)
|
||||
if END_BLOCK < START_BLOCK:
|
||||
print(f"⚠️ Il blocco finale deve essere >= {START_BLOCK}, uso {START_BLOCK + 999}")
|
||||
END_BLOCK = START_BLOCK + 999
|
||||
except ValueError:
|
||||
print(f"⚠️ Valore non valido, uso blocco {START_BLOCK + 999}")
|
||||
END_BLOCK = START_BLOCK + 999
|
||||
else:
|
||||
END_BLOCK = START_BLOCK + 999
|
||||
|
||||
# Chiedi il delay tra le richieste
|
||||
delay_input = input("⏱️ Delay tra richieste in secondi (default: 1.0): ").strip()
|
||||
|
||||
if delay_input:
|
||||
try:
|
||||
REQUEST_DELAY = float(delay_input)
|
||||
if REQUEST_DELAY < 0.1:
|
||||
print("⚠️ Il delay minimo è 0.1s per rispettare l'API")
|
||||
REQUEST_DELAY = 0.1
|
||||
except ValueError:
|
||||
print("⚠️ Valore non valido, uso 1.0s")
|
||||
REQUEST_DELAY = 1.0
|
||||
else:
|
||||
REQUEST_DELAY = 1.0
|
||||
|
||||
print(f"\n🔧 Configurazione:")
|
||||
print(f" Primo blocco: {START_BLOCK}")
|
||||
print(f" Ultimo blocco: {END_BLOCK}")
|
||||
print(f" Totale blocchi: {END_BLOCK - START_BLOCK + 1}")
|
||||
print(f" Delay richieste: {REQUEST_DELAY}s")
|
||||
print(f" Tempo stimato: ~{((END_BLOCK - START_BLOCK + 1) * REQUEST_DELAY) / 60:.1f} minuti")
|
||||
|
||||
# Conferma
|
||||
response = input("\n▶️ Avviare la scansione? (s/n): ").strip().lower()
|
||||
|
||||
if response == 's':
|
||||
scanner.scan_blocks(START_BLOCK, END_BLOCK, REQUEST_DELAY)
|
||||
|
||||
# Esporta in CSV
|
||||
export = input("\n📤 Esportare dati in CSV? (s/n): ").strip().lower()
|
||||
if export == 's':
|
||||
scanner.export_to_csv()
|
||||
else:
|
||||
print("⏹️ Scansione annullata")
|
||||
|
||||
print("\n💡 Per analisi ulteriori:")
|
||||
print(" 1. Usa SQLite per query: SELECT * FROM p2pk_addresses LIMIT 10")
|
||||
print(" 2. Analizza gli script P2PK con librerie Bitcoin")
|
||||
print(" 3. Studia la struttura delle transazioni early Bitcoin")
|
||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
requests>=2.31.0
|
||||
590
view_db.py
Executable file
590
view_db.py
Executable file
@@ -0,0 +1,590 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script per visualizzare i dati P2PK dal database
|
||||
Genera un report HTML interattivo
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
class P2PKDatabaseViewer:
|
||||
def __init__(self, db_path: str = "bitcoin_p2pk_study.db"):
|
||||
self.db_path = db_path
|
||||
|
||||
def check_database_exists(self) -> bool:
|
||||
"""Verifica se il database esiste"""
|
||||
return Path(self.db_path).exists()
|
||||
|
||||
def get_statistics(self) -> dict:
|
||||
"""Ottieni statistiche dal database"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
stats = {}
|
||||
|
||||
try:
|
||||
# Progresso scansione
|
||||
cursor.execute('SELECT last_scanned_block, total_p2pk_found FROM scan_progress WHERE id = 1')
|
||||
progress = cursor.fetchone()
|
||||
stats['last_block'] = progress[0] if progress else 0
|
||||
stats['total_found'] = progress[1] if progress else 0
|
||||
|
||||
# Totale P2PK nel database
|
||||
cursor.execute('SELECT COUNT(*) FROM p2pk_addresses')
|
||||
stats['total_in_db'] = cursor.fetchone()[0]
|
||||
|
||||
# Range blocchi
|
||||
cursor.execute('SELECT MIN(block_height), MAX(block_height) FROM p2pk_addresses')
|
||||
min_block, max_block = cursor.fetchone()
|
||||
stats['min_block'] = min_block if min_block is not None else 0
|
||||
stats['max_block'] = max_block if max_block is not None else 0
|
||||
|
||||
# Blocchi unici con P2PK
|
||||
cursor.execute('SELECT COUNT(DISTINCT block_height) FROM p2pk_addresses')
|
||||
stats['unique_blocks'] = cursor.fetchone()[0]
|
||||
|
||||
# Transazioni unique
|
||||
cursor.execute('SELECT COUNT(DISTINCT txid) FROM p2pk_addresses')
|
||||
stats['unique_txs'] = cursor.fetchone()[0]
|
||||
|
||||
# P2PK non spesi (con saldo attuale)
|
||||
cursor.execute('SELECT COUNT(*) FROM p2pk_addresses WHERE is_unspent = 1')
|
||||
stats['unspent_count'] = cursor.fetchone()[0]
|
||||
|
||||
# Valore totale - calcolo manuale per evitare overflow
|
||||
cursor.execute('SELECT value_satoshi FROM p2pk_addresses')
|
||||
all_values = cursor.fetchall()
|
||||
total_sat = 0.0
|
||||
for (val,) in all_values:
|
||||
if val is not None:
|
||||
total_sat += float(val)
|
||||
|
||||
stats['total_value_btc'] = total_sat / 100000000.0
|
||||
stats['total_value_sat'] = int(total_sat)
|
||||
|
||||
# Valore non speso
|
||||
cursor.execute('SELECT value_satoshi FROM p2pk_addresses WHERE is_unspent = 1')
|
||||
unspent_values = cursor.fetchall()
|
||||
unspent_sat = 0.0
|
||||
for (val,) in unspent_values:
|
||||
if val is not None:
|
||||
unspent_sat += float(val)
|
||||
|
||||
stats['unspent_value_btc'] = unspent_sat / 100000000.0
|
||||
stats['unspent_value_sat'] = int(unspent_sat)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Errore nel calcolo statistiche: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
stats = {
|
||||
'last_block': 0,
|
||||
'total_found': 0,
|
||||
'total_in_db': 0,
|
||||
'min_block': 0,
|
||||
'max_block': 0,
|
||||
'unique_blocks': 0,
|
||||
'total_value_btc': 0.0,
|
||||
'total_value_sat': 0,
|
||||
'unique_txs': 0,
|
||||
'unspent_count': 0,
|
||||
'unspent_value_btc': 0.0,
|
||||
'unspent_value_sat': 0
|
||||
}
|
||||
|
||||
conn.close()
|
||||
return stats
|
||||
|
||||
def get_all_p2pk(self, limit: int = None) -> list:
|
||||
"""Ottieni tutti i P2PK dal database"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
query = '''
|
||||
SELECT id, block_height, txid, output_index,
|
||||
scriptpubkey, value_satoshi, timestamp, is_unspent, last_checked
|
||||
FROM p2pk_addresses
|
||||
ORDER BY block_height ASC, id ASC
|
||||
'''
|
||||
|
||||
if limit:
|
||||
query += f' LIMIT {limit}'
|
||||
|
||||
cursor.execute(query)
|
||||
results = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return results
|
||||
|
||||
def extract_pubkey_from_script(self, scriptpubkey: str) -> str:
|
||||
"""Estrae la chiave pubblica dallo scriptPubKey"""
|
||||
if not scriptpubkey:
|
||||
return ""
|
||||
|
||||
# P2PK non compresso: 41 + pubkey (130 hex chars) + ac
|
||||
if scriptpubkey.startswith('41') and len(scriptpubkey) == 134:
|
||||
return scriptpubkey[2:132] # Rimuovi 41 all'inizio e ac alla fine
|
||||
|
||||
# P2PK compresso: 21 + pubkey (66 hex chars) + ac
|
||||
elif scriptpubkey.startswith('21') and len(scriptpubkey) == 70:
|
||||
return scriptpubkey[2:68] # Rimuovi 21 all'inizio e ac alla fine
|
||||
|
||||
return scriptpubkey
|
||||
|
||||
def _generate_table_html(self, p2pk_data: list) -> str:
|
||||
"""Genera l'HTML della tabella"""
|
||||
if not p2pk_data:
|
||||
return '<div class="no-data">📭 Nessun dato P2PK trovato nel database</div>'
|
||||
|
||||
rows_html = []
|
||||
for row in p2pk_data:
|
||||
pubkey = self.extract_pubkey_from_script(row[4])
|
||||
txid_short = row[2][:16] if len(row[2]) > 16 else row[2]
|
||||
timestamp_str = datetime.fromtimestamp(row[6]).strftime('%Y-%m-%d %H:%M') if row[6] else 'N/A'
|
||||
# row[5] è già in satoshi, lo convertiamo in BTC dividendo per 100000000
|
||||
value_satoshi = row[5]
|
||||
value_btc = value_satoshi / 100000000.0
|
||||
|
||||
# Stato UTXO (row[7] = is_unspent)
|
||||
is_unspent = row[7] if len(row) > 7 else 0
|
||||
utxo_status = '🟢 NON SPESO' if is_unspent else '🔴 SPESO'
|
||||
utxo_class = 'unspent' if is_unspent else 'spent'
|
||||
|
||||
row_html = f'''
|
||||
<tr class="{utxo_class}">
|
||||
<td>{row[0]}</td>
|
||||
<td><span class="block">{row[1]}</span></td>
|
||||
<td>
|
||||
<span class="txid">{txid_short}...</span>
|
||||
<button class="copy-btn" onclick="copyToClipboard('{row[2]}')">📋</button>
|
||||
</td>
|
||||
<td>{row[3]}</td>
|
||||
<td>
|
||||
<div class="pubkey">
|
||||
{pubkey}
|
||||
<button class="copy-btn" onclick="copyToClipboard('{pubkey}')">📋</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="value">{value_btc:.8f} BTC<br><small>({value_satoshi:,} sat)</small></td>
|
||||
<td><span class="status-badge {utxo_class}">{utxo_status}</span></td>
|
||||
<td>{timestamp_str}</td>
|
||||
</tr>'''
|
||||
rows_html.append(row_html)
|
||||
|
||||
return f'''
|
||||
<table id="dataTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Blocco</th>
|
||||
<th>TXID</th>
|
||||
<th>Output</th>
|
||||
<th>Chiave Pubblica</th>
|
||||
<th>Valore (BTC)</th>
|
||||
<th>Stato UTXO</th>
|
||||
<th>Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{''.join(rows_html)}
|
||||
</tbody>
|
||||
</table>'''
|
||||
|
||||
def generate_html_report(self, output_file: str = "p2pk_report.html"):
|
||||
"""Genera un report HTML interattivo"""
|
||||
if not self.check_database_exists():
|
||||
print(f"❌ Database non trovato: {self.db_path}")
|
||||
return
|
||||
|
||||
stats = self.get_statistics()
|
||||
p2pk_data = self.get_all_p2pk()
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>P2PK Database Report</title>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
|
||||
body {{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
}}
|
||||
|
||||
.container {{
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
overflow: hidden;
|
||||
}}
|
||||
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}}
|
||||
|
||||
.header h1 {{
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
|
||||
.header p {{
|
||||
opacity: 0.9;
|
||||
font-size: 1.1em;
|
||||
}}
|
||||
|
||||
.stats {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 30px;
|
||||
background: #f8f9fa;
|
||||
}}
|
||||
|
||||
.stat-card {{
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
transition: transform 0.3s;
|
||||
}}
|
||||
|
||||
.stat-card:hover {{
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
|
||||
}}
|
||||
|
||||
.stat-card .label {{
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
|
||||
.stat-card .value {{
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}}
|
||||
|
||||
.content {{
|
||||
padding: 30px;
|
||||
}}
|
||||
|
||||
.search-box {{
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
}}
|
||||
|
||||
.search-box input {{
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-size: 1em;
|
||||
transition: border-color 0.3s;
|
||||
}}
|
||||
|
||||
.search-box input:focus {{
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}}
|
||||
|
||||
table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}}
|
||||
|
||||
thead {{
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}}
|
||||
|
||||
th {{
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.9em;
|
||||
letter-spacing: 1px;
|
||||
}}
|
||||
|
||||
td {{
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}}
|
||||
|
||||
tr:hover {{
|
||||
background: #f8f9fa;
|
||||
}}
|
||||
|
||||
.pubkey {{
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85em;
|
||||
color: #e74c3c;
|
||||
word-break: break-all;
|
||||
background: #fff3cd;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
|
||||
.txid {{
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85em;
|
||||
color: #3498db;
|
||||
}}
|
||||
|
||||
.block {{
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
.value {{
|
||||
color: #27ae60;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
.footer {{
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
|
||||
.copy-btn {{
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8em;
|
||||
margin-left: 5px;
|
||||
}}
|
||||
|
||||
.copy-btn:hover {{
|
||||
background: #764ba2;
|
||||
}}
|
||||
|
||||
.no-data {{
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
color: #999;
|
||||
font-size: 1.2em;
|
||||
}}
|
||||
|
||||
.status-badge {{
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
}}
|
||||
|
||||
.status-badge.unspent {{
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}}
|
||||
|
||||
.status-badge.spent {{
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}}
|
||||
|
||||
tr.unspent {{
|
||||
background: #f0fff4;
|
||||
}}
|
||||
|
||||
tr.spent {{
|
||||
opacity: 0.7;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔐 P2PK Database Report</h1>
|
||||
<p>Bitcoin Pay-to-Public-Key Transaction Scanner</p>
|
||||
<p style="font-size: 0.9em; margin-top: 10px;">Database: {self.db_path}</p>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="label">Ultimo Blocco</div>
|
||||
<div class="value">{stats['last_block']:,}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">P2PK Trovati</div>
|
||||
<div class="value">{stats['total_in_db']:,}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">Blocchi Unici</div>
|
||||
<div class="value">{stats['unique_blocks']:,}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">Valore Totale</div>
|
||||
<div class="value">{stats['total_value_btc']:.8f} BTC</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">Range Blocchi</div>
|
||||
<div class="value">{stats['min_block']:,} - {stats['max_block']:,}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">Transazioni Uniche</div>
|
||||
<div class="value">{stats['unique_txs']:,}</div>
|
||||
</div>
|
||||
<div class="stat-card" style="background: #d4edda;">
|
||||
<div class="label" style="color: #155724;">💰 P2PK Non Spesi</div>
|
||||
<div class="value" style="color: #155724;">{stats['unspent_count']:,}</div>
|
||||
</div>
|
||||
<div class="stat-card" style="background: #d1ecf1;">
|
||||
<div class="label" style="color: #0c5460;">💎 Valore Non Speso</div>
|
||||
<div class="value" style="color: #0c5460;">{stats['unspent_value_btc']:.8f} BTC</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="search-box">
|
||||
<input type="text" id="searchInput" placeholder="🔍 Cerca per blocco, txid, o chiave pubblica..." onkeyup="filterTable()">
|
||||
</div>
|
||||
|
||||
{self._generate_table_html(p2pk_data)}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Report generato il {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
<p style="margin-top: 10px;">⚠️ Questo report è solo per scopi educativi e di ricerca</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function filterTable() {{
|
||||
const input = document.getElementById('searchInput');
|
||||
const filter = input.value.toUpperCase();
|
||||
const table = document.getElementById('dataTable');
|
||||
|
||||
if (!table) return;
|
||||
|
||||
const tr = table.getElementsByTagName('tr');
|
||||
|
||||
for (let i = 1; i < tr.length; i++) {{
|
||||
const row = tr[i];
|
||||
const text = row.textContent || row.innerText;
|
||||
|
||||
if (text.toUpperCase().indexOf(filter) > -1) {{
|
||||
row.style.display = '';
|
||||
}} else {{
|
||||
row.style.display = 'none';
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
function copyToClipboard(text) {{
|
||||
navigator.clipboard.writeText(text).then(() => {{
|
||||
alert('✅ Copiato negli appunti!');
|
||||
}}).catch(err => {{
|
||||
console.error('Errore durante la copia:', err);
|
||||
}});
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(html)
|
||||
|
||||
print(f"✅ Report HTML generato: {output_file}")
|
||||
print(f"📊 Statistiche:")
|
||||
print(f" - P2PK totali: {stats['total_in_db']}")
|
||||
print(f" - Blocchi scansionati: {stats['last_block']}")
|
||||
print(f" - Valore totale: {stats['total_value_btc']:.8f} BTC")
|
||||
|
||||
def print_console_report(self):
|
||||
"""Stampa un report nel terminale"""
|
||||
if not self.check_database_exists():
|
||||
print(f"❌ Database non trovato: {self.db_path}")
|
||||
return
|
||||
|
||||
stats = self.get_statistics()
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("📊 REPORT DATABASE P2PK")
|
||||
print("="*60)
|
||||
print(f"📁 Database: {self.db_path}")
|
||||
print(f"📦 Ultimo blocco scansionato: {stats['last_block']:,}")
|
||||
print(f"🔑 P2PK totali trovati: {stats['total_in_db']:,}")
|
||||
print(f"📊 Blocchi unici con P2PK: {stats['unique_blocks']:,}")
|
||||
print(f"📈 Range blocchi: {stats['min_block']:,} - {stats['max_block']:,}")
|
||||
print(f"💰 Valore totale: {stats['total_value_btc']:.8f} BTC ({stats['total_value_sat']:,} sat)")
|
||||
print(f"📝 Transazioni uniche: {stats['unique_txs']:,}")
|
||||
print("-"*60)
|
||||
print(f"💎 P2PK NON SPESI: {stats['unspent_count']:,}")
|
||||
print(f"💵 Valore non speso: {stats['unspent_value_btc']:.8f} BTC ({stats['unspent_value_sat']:,} sat)")
|
||||
print("="*60)
|
||||
|
||||
# Mostra alcuni esempi
|
||||
p2pk_data = self.get_all_p2pk(limit=5)
|
||||
if p2pk_data:
|
||||
print("\n🔍 Primi 5 P2PK trovati:")
|
||||
for row in p2pk_data:
|
||||
pubkey = self.extract_pubkey_from_script(row[4])
|
||||
print(f"\n Blocco {row[1]} | TX: {row[2][:16]}...")
|
||||
print(f" Pubkey: {pubkey[:40]}...")
|
||||
print(f" Valore: {row[5] / 100000000:.8f} BTC")
|
||||
|
||||
if __name__ == "__main__":
|
||||
db_file = "bitcoin_p2pk_study.db"
|
||||
|
||||
# Se viene passato un argomento, usalo come path del database
|
||||
if len(sys.argv) > 1:
|
||||
db_file = sys.argv[1]
|
||||
|
||||
viewer = P2PKDatabaseViewer(db_file)
|
||||
|
||||
print("🔐 P2PK Database Viewer")
|
||||
print("="*60)
|
||||
print("Scegli un'opzione:")
|
||||
print("1. Genera report HTML")
|
||||
print("2. Mostra report nel terminale")
|
||||
print("3. Entrambi")
|
||||
print("="*60)
|
||||
|
||||
choice = input("Scelta (1/2/3): ").strip()
|
||||
|
||||
if choice == "1":
|
||||
viewer.generate_html_report()
|
||||
elif choice == "2":
|
||||
viewer.print_console_report()
|
||||
elif choice == "3":
|
||||
viewer.generate_html_report()
|
||||
viewer.print_console_report()
|
||||
else:
|
||||
print("❌ Scelta non valida")
|
||||
Reference in New Issue
Block a user