591 lines
19 KiB
Python
Executable File
591 lines
19 KiB
Python
Executable File
#!/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")
|