Spaces:
Sleeping
Sleeping
File size: 15,943 Bytes
2619a21 b4e6c50 c62d2fd 50c343a b4e6c50 2619a21 5947ee0 b7c0e2d 7c96fc8 5947ee0 577c6bb 2619a21 66f49d1 2619a21 5947ee0 577c6bb 5947ee0 2619a21 5947ee0 577c6bb 5947ee0 577c6bb 5947ee0 577c6bb 5947ee0 2619a21 975142a 7c96fc8 b7c0e2d 975142a 2619a21 5947ee0 577c6bb b4e6c50 66f49d1 b4e6c50 b7c0e2d 577c6bb b7c0e2d 5947ee0 b4e6c50 2619a21 5947ee0 2619a21 443319a b1acf01 975142a 443319a 975142a d2d8373 975142a d2d8373 975142a d2d8373 975142a d2d8373 975142a b4e6c50 b1acf01 b4e6c50 b1acf01 b4e6c50 2619a21 5947ee0 2619a21 577c6bb 5947ee0 577c6bb 5947ee0 2619a21 5947ee0 577c6bb 5947ee0 2619a21 577c6bb 2619a21 5947ee0 2619a21 b4e6c50 2909f2b b4e6c50 2909f2b b4e6c50 66f49d1 b4e6c50 d2d8373 2619a21 14b03b3 577c6bb b1acf01 5947ee0 577c6bb b1acf01 2909f2b d2d8373 975142a 14b03b3 b4e6c50 443319a 2619a21 b4e6c50 577c6bb 2619a21 b4e6c50 7c96fc8 b4e6c50 d2d8373 b4e6c50 b7c0e2d 7c96fc8 b7c0e2d d2d8373 b7c0e2d d2d8373 b7c0e2d d2d8373 b7c0e2d d2d8373 b7c0e2d b4e6c50 2619a21 7062884 b4e6c50 7062884 7c96fc8 b1acf01 adb8f1d 6b55438 2619a21 d2d8373 577c6bb 6b55438 d2d8373 577c6bb 580d36a 577c6bb b4e6c50 7c0d33f 6b55438 975142a 6b55438 975142a 7c96fc8 7c0d33f 975142a c62d2fd 6b55438 b4e6c50 6b55438 b4e6c50 b7c0e2d 7c96fc8 b7c0e2d b4e6c50 c62d2fd b4e6c50 c62d2fd d2d8373 b4e6c50 d2d8373 b7c0e2d 6b55438 d2d8373 |
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 |
import requests
import gradio as gr
import networkx as nx
import plotly.graph_objects as go
from datetime import datetime
import re
import os
import tempfile
import base64
# ---------------- Funciones de análisis y grafo -------------------
def get_transaction(tx_id):
"""
Obtiene los datos de una transacción Bitcoin usando endpoints públicos.
Se prueba primero con Blockstream.info y, de fallar, con mempool.space.
"""
urls = [
f"https://blockstream.info/api/tx/{tx_id}",
f"https://mempool.space/api/tx/{tx_id}"
]
for url in urls:
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
return response.json()
else:
print(f"Error en {url}: Código de estado {response.status_code}")
except Exception as e:
print(f"Excepción al consultar {url}: {e}")
return None
def get_btc_price():
"""
Obtiene el precio actual de Bitcoin en USD utilizando la API de CoinGecko.
"""
try:
response = requests.get("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", timeout=10)
if response.status_code == 200:
data = response.json()
price = data.get("bitcoin", {}).get("usd")
return price
except Exception as e:
print("Error fetching BTC price:", e)
return None
def generar_grafo(tx_data):
"""
Genera un grafo dirigido a partir de los datos de la transacción.
Nodo central: la transacción.
Nodos secundarios: direcciones de inputs y outputs.
"""
G = nx.DiGraph()
txid = tx_data.get("txid", "desconocido")
G.add_node(txid, color="#FF6B6B", size=20, tipo="Transacción")
for inp in tx_data.get("vin", []):
prevout = inp.get("prevout", {})
addr = prevout.get("scriptpubkey_address")
if addr:
G.add_node(addr, color="#4ECDC4", size=15, tipo="Input")
G.add_edge(addr, txid)
for out in tx_data.get("vout", []):
addr = out.get("scriptpubkey_address")
if addr:
G.add_node(addr, color="#45B7D1", size=15, tipo="Output")
G.add_edge(txid, addr)
return G
def check_blockchain_tags(tx_id):
"""
Consulta la API pública de blockchain.com para ver si el TXID (o sus metadatos)
indica que la transacción ha sido etiquetada como MIXER.
(Esta función es un ejemplo y podría necesitar ajustes según la respuesta real.)
"""
url = f"https://blockchain.info/rawtx/{tx_id}?format=json"
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
data = response.json()
tags = data.get("tags", [])
if "MIXER" in tags:
return True
if data.get("category", "").upper() == "MIXER":
return True
else:
print(f"Error consultando blockchain.com: {response.status_code}")
except Exception as e:
print(f"Excepción en check_blockchain_tags: {e}")
return False
def analizar_transaccion(tx_id):
"""
Analiza una transacción Bitcoin:
- Obtiene datos desde Blockstream.info o mempool.space.
- Calcula totales en BTC y fee, mostrando también su equivalente en USD.
- Muestra información adicional (versión, tamaño, peso, fee rate).
- Aplica heurística para detectar posibles CoinJoin/mixers.
- Incorpora información adicional de blockchain.com (etiqueta MIXER).
- Genera un grafo interactivo.
- Retorna un informe en HTML, la figura del grafo y una tupla (reporte, fig) para su uso.
"""
tx_data = get_transaction(tx_id)
if not tx_data:
return ("❌ Transacción no encontrada o error al obtener datos. Asegúrate de ingresar un TXID válido."), None, None
try:
num_inputs = len(tx_data.get("vin", []))
num_outputs = len(tx_data.get("vout", []))
montos = [out.get("value", 0) / 1e8 for out in tx_data.get("vout", [])]
montos_unicos = len(set(montos))
total_input_value = sum(inp.get("prevout", {}).get("value", 0) for inp in tx_data.get("vin", [])) / 1e8
total_output_value = sum(out.get("value", 0) for out in tx_data.get("vout", [])) / 1e8
fee = total_input_value - total_output_value
heuristic_mixer = ((num_inputs > 5 and num_outputs > 5 and montos_unicos < 3) or
(num_outputs > 2 and montos_unicos <= 2))
es_mixer_blockchain = check_blockchain_tags(tx_id)
es_mixer = heuristic_mixer or es_mixer_blockchain
if es_mixer:
if heuristic_mixer and es_mixer_blockchain:
mixer_message = (
"<p style='color: #FF5252;'><strong>Alerta de Mixer / CoinJoin:</strong> La transacción cumple criterios heurísticos y la API de blockchain.com la etiqueta como MIXER.</p>"
)
elif heuristic_mixer:
mixer_message = (
"<p style='color: #FF5252;'><strong>Alerta de Mixer / CoinJoin:</strong> La transacción cumple criterios heurísticos compatibles con un mixer.</p>"
)
elif es_mixer_blockchain:
mixer_message = (
"<p style='color: #FF5252;'><strong>Alerta de Mixer / CoinJoin:</strong> La API de blockchain.com etiqueta esta transacción como MIXER.</p>"
)
else:
mixer_message = (
"<p style='color: #4CAF50;'><strong>Sin indicios de mezcla:</strong> La transacción no presenta patrones de mixer.</p>"
)
btc_price = get_btc_price() or 0
version = tx_data.get("version", "N/A")
size = tx_data.get("size", None)
weight = tx_data.get("weight", "N/A")
fee_rate_str = f"{(fee * 1e8) / size:.2f} sat/byte" if size else "N/A"
status = tx_data.get("status", {})
block_time = status.get("block_time")
fecha_hora_str = datetime.fromtimestamp(block_time).strftime('%Y-%m-%d %H:%M:%S') if block_time else "Desconocida"
# Generar grafo
G = generar_grafo(tx_data)
pos = nx.spring_layout(G, seed=42)
edge_x, edge_y = [], []
for edge in G.edges():
x0, y0 = pos[edge[0]]
x1, y1 = pos[edge[1]]
edge_x.extend([x0, x1, None])
edge_y.extend([y0, y1, None])
node_x, node_y, hover_texts, node_colors, node_sizes = [], [], [], [], []
for node in G.nodes():
x, y = pos[node]
node_x.append(x)
node_y.append(y)
tipo = G.nodes[node].get("tipo", "desconocido")
hover_texts.append(f"Tipo: {tipo}<br>ID: {node[:8]}...")
node_colors.append(G.nodes[node].get("color", "#FFFFFF"))
node_sizes.append(G.nodes[node].get("size", 10))
fig = go.Figure(
data=[
go.Scatter(
x=edge_x, y=edge_y,
line=dict(width=0.5, color="#888"),
hoverinfo="none",
mode="lines"
),
go.Scatter(
x=node_x, y=node_y,
mode="markers+text",
marker=dict(color=node_colors, size=node_sizes, line_width=1),
text=[node[:8] for node in G.nodes()],
hovertext=hover_texts,
hoverinfo="text"
)
],
layout=go.Layout(
title="Visualización de Conexiones de la Transacción",
showlegend=False,
margin=dict(b=0, l=0, r=0, t=40),
xaxis=dict(showgrid=False, zeroline=False, visible=False),
yaxis=dict(showgrid=False, zeroline=False, visible=False),
template="plotly_dark"
)
)
if es_mixer:
unique_outputs_dict = {}
for out in tx_data.get("vout", []):
amount = out.get("value", 0) / 1e8
addr = out.get("scriptpubkey_address")
if addr:
unique_outputs_dict.setdefault(amount, []).append(addr)
unique_outputs_details = "<ul>"
for amt, addrs in unique_outputs_dict.items():
unique_outputs_details += f"<li>{amt:.8f} BTC: {', '.join(addrs)}</li>"
unique_outputs_details += "</ul>"
mixer_metrics = (
f"<p>📤 <strong>Outputs:</strong> {num_outputs}</p>"
f"<p>💰 <strong>Montos únicos en outputs:</strong> {montos_unicos}</p>"
f"<p><strong>Detalles de outputs únicos:</strong></p>{unique_outputs_details}"
)
else:
mixer_metrics = ""
total_input_str = f"{total_input_value:.8f} BTC"
total_input_str += f" (${total_input_value * btc_price:,.2f})" if btc_price else ""
total_output_str = f"{total_output_value:.8f} BTC"
total_output_str += f" (${total_output_value * btc_price:,.2f})" if btc_price else ""
fee_str = f"{fee:.8f} BTC"
fee_str += f" (${fee * btc_price:,.2f})" if btc_price else ""
# Se ha eliminado el <hr> para que la información aparezca de forma continua.
reporte = f"""
<div style="padding: 20px; border-radius: 10px;">
<h3 style="color: {'#FF5252' if es_mixer else '#4CAF50'};">
{'🔮 POSIBLE MIXER / COINJOIN' if es_mixer else '✅ Transacción Normal'}
</h3>
<p><strong>TXID:</strong> {tx_id}</p>
<p><strong>Fecha y Hora:</strong> {fecha_hora_str}</p>
<p><strong>Versión:</strong> {version}</p>
<p><strong>Tamaño:</strong> {size if size else 'N/A'} bytes</p>
<p><strong>Peso:</strong> {weight}</p>
<p><strong>Fee rate:</strong> {fee_rate_str}</p>
<p>📥 <strong>Inputs:</strong> {num_inputs}</p>
{mixer_metrics}
<p><strong>Total Entradas:</strong> {total_input_str}</p>
<p><strong>Total Salidas:</strong> {total_output_str}</p>
<p><strong>Fee:</strong> {fee_str}</p>
<p><strong>Estado de Confirmación:</strong> {"Confirmada" if status.get("confirmed", False) else "No confirmada"}</p>
{mixer_message}
<p style="font-size: 0.9em;">
Nota: La detección se basa en heurísticas y en información adicional de blockchain.com. Se recomienda usar herramientas especializadas para un análisis forense completo.
</p>
</div>
"""
return reporte, fig, (reporte, fig)
except Exception as e:
return f"⚠️ Error durante el análisis: {str(e)}", None, None
def mostrar_modal(analysis_tuple):
"""
A partir de la tupla de análisis, devuelve un HTML que simula un modal emergente con el informe completo.
"""
if not analysis_tuple:
return "<p>No hay análisis generado.</p>"
report, _ = analysis_tuple
html_modal = f'''
<div id="modalOverlay" style="
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.8); z-index: 9999; overflow: auto;">
<div style="
position: relative; margin: 5% auto; padding: 20px;
background: inherit; max-width: 800px; border-radius: 10px;">
{report}
<br><br>
<button onclick="document.getElementById('modalOverlay').remove();"
style='padding: 5px 10px; font-size: 12px;'>Cerrar</button>
</div>
</div>
'''
return html_modal
# ---------------- INTERFAZ GRÁFICA CON GRADIO -------------------
with gr.Blocks(
theme=gr.themes.Soft(),
title="🔍 Detector de Mixers / CoinJoin en Transacciones Bitcoin",
css=".custom-btn { background-color: #ADD8E6 !important; color: black !important; padding: 5px 10px; font-size: 12px; }"
) as demo:
# Encabezado
gr.Markdown("# 🔍 Detector de Mixers / CoinJoin en Transacciones Bitcoin")
gr.Markdown("Desarrollado por José R. Leonett para la comunidad de Peritos Forenses Digitales de Guatemala [www.forensedigital.gt](http://www.forensedigital.gt)")
gr.Markdown("**Nota:** Este analizador funciona únicamente con transacciones de Bitcoin. No se pueden analizar transacciones de Ethereum.")
with gr.Row():
# Columna izquierda: Campo de TXID, ejemplos y explicación de campos
with gr.Column(scale=1):
tx_input = gr.Textbox(
label="TXID de la Transacción",
placeholder="Ej: 9dd51e2d45f4f7bddcc3f0f7a05c3fd60543a11cfc9fbd0e1ca4434668cfa3e1"
)
analyze_btn = gr.Button("Analizar Transacción", elem_classes=["custom-btn"])
gr.Markdown("### Ejemplos de TXIDs válidos:")
gr.Examples(
examples=[
["0042c32659ef432276a0feb59e0418dee97e80ecd0ffa8a3f6228744388e65ac"],
["cdacd625a332f46913209aa3480002fc5beb67d031e5b99a0bfe3fddbaef402b"]
],
inputs=tx_input
)
explanation_box = gr.HTML(value="""
<div style="overflow-y: auto; height: 350px; border: 1px solid #cccccc; padding: 10px;">
<h4>Explicación de los campos:</h4>
<ul>
<li><strong>TXID:</strong> Identificador único de la transacción.</li>
<li><strong>Fecha y Hora:</strong> Momento en que la transacción fue confirmada (según la blockchain).</li>
<li><strong>Versión:</strong> Versión del protocolo de la transacción.</li>
<li><strong>Tamaño:</strong> Tamaño en bytes de la transacción.</li>
<li><strong>Peso:</strong> Peso de la transacción en unidades de peso.</li>
<li><strong>Fee rate:</strong> Tarifa pagada por byte (sat/byte).</li>
<li><strong>Inputs:</strong> Número de entradas de la transacción.</li>
<li><strong>Outputs:</strong> Se muestran solo si se detecta posible mixer.</li>
<li><strong>Montos únicos en outputs:</strong> Se muestran solo si es mixer.</li>
<li><strong>Detalles de outputs únicos:</strong> Lista de cada monto (en BTC) y las direcciones asociadas (solo si es mixer).</li>
<li><strong>Total Entradas:</strong> Suma total de los valores de entrada (en BTC y USD).</li>
<li><strong>Total Salidas:</strong> Suma total de los valores de salida (en BTC y USD).</li>
<li><strong>Fee:</strong> Diferencia entre entradas y salidas (en BTC y USD).</li>
<li><strong>Estado de Confirmación:</strong> Indica si la transacción está confirmada o no.</li>
<li><strong>Alerta de Mixer / CoinJoin:</strong> Muestra si la transacción presenta patrones de mezcla.</li>
</ul>
</div>
""")
# Columna derecha: Grafo, Reporte y botón para mostrar modal con análisis completo
with gr.Column(scale=1):
plot_output = gr.Plot()
reporte_html = gr.HTML()
modal_btn = gr.Button("GENERAR ANALISIS", elem_classes=["custom-btn"])
modal_output = gr.HTML()
# Estado para almacenar el análisis (tupla: (reporte, fig))
analysis_state = gr.State()
# Al pulsar "Analizar Transacción", se generan reporte, grafo y se guarda el análisis.
analyze_btn.click(fn=analizar_transaccion, inputs=tx_input, outputs=[reporte_html, plot_output, analysis_state])
# Al pulsar "GENERAR ANALISIS", se muestra un modal emergente con el informe completo.
modal_btn.click(fn=mostrar_modal, inputs=analysis_state, outputs=modal_output)
demo.launch()
|