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 = ( "

Alerta de Mixer / CoinJoin: La transacción cumple criterios heurísticos y la API de blockchain.com la etiqueta como MIXER.

" ) elif heuristic_mixer: mixer_message = ( "

Alerta de Mixer / CoinJoin: La transacción cumple criterios heurísticos compatibles con un mixer.

" ) elif es_mixer_blockchain: mixer_message = ( "

Alerta de Mixer / CoinJoin: La API de blockchain.com etiqueta esta transacción como MIXER.

" ) else: mixer_message = ( "

Sin indicios de mezcla: La transacción no presenta patrones de mixer.

" ) 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}
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 = "" mixer_metrics = ( f"

📤 Outputs: {num_outputs}

" f"

💰 Montos únicos en outputs: {montos_unicos}

" f"

Detalles de outputs únicos:

{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
para que la información aparezca de forma continua. reporte = f"""

{'🔮 POSIBLE MIXER / COINJOIN' if es_mixer else '✅ Transacción Normal'}

TXID: {tx_id}

Fecha y Hora: {fecha_hora_str}

Versión: {version}

Tamaño: {size if size else 'N/A'} bytes

Peso: {weight}

Fee rate: {fee_rate_str}

📥 Inputs: {num_inputs}

{mixer_metrics}

Total Entradas: {total_input_str}

Total Salidas: {total_output_str}

Fee: {fee_str}

Estado de Confirmación: {"Confirmada" if status.get("confirmed", False) else "No confirmada"}

{mixer_message}

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.

""" 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 "

No hay análisis generado.

" report, _ = analysis_tuple html_modal = f'''
{report}

''' 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="""

Explicación de los campos:

""") # 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()