diff --git a/dashboard.html b/dashboard.html index e799dfa..2445af0 100644 --- a/dashboard.html +++ b/dashboard.html @@ -3893,6 +3893,18 @@ const maxHops = (opts && opts.maxHops) || 8; const onProgress = opts && opts.onProgress; const BATCH = 5, PAUSE = 120, MAX_NODES = 80; + // Cinturón de seguridad independiente del heurístico de perfil: una + // dirección con un volumen de transacciones así de alto es, con + // altísima probabilidad, infraestructura de un servicio (exchange, + // custodio, procesador de pagos) aunque el perfil no llegue a + // clasificarla "hot_wallet" (residual momentáneamente alto, etc.). Sin + // este tope, seguir esa dirección dispararía en findSpendingTx una + // paginación de hasta 200 tx (8 páginas × 8s de timeout) buscando una + // aguja en un pajar de decenas de miles — coste real para el nodo del + // usuario sin ninguna posibilidad realista de encontrar el gasto. No + // cuesta peticiones extra: chain_stats.tx_count ya viene en el perfil + // que se pide de todos modos para cada dirección de salida nueva. + const LARGE_ADDR_TX_COUNT = 5000; const { get, originTxid, originFingerprint, nodes, edges, unspentTerminals, txCache, enqueuedKeys } = trace; if (trace.frontier.length === 0) { trace.done = true; return trace; } @@ -3978,17 +3990,26 @@ const tracedShare = totalInHop > 0 ? amount / totalInHop : 1; const diluted = inAddrs.length >= 3 && tracedShare < 0.5; - // Parada por custodio: entidad conocida (ENTITY_INDEX) o perfil de - // hot wallet no indexado ("posible custodio no identificado"). + // Parada por custodio: entidad conocida (ENTITY_INDEX), perfil de + // hot wallet no indexado ("posible custodio no identificado"), o + // — cinturón de seguridad — volumen de tx tan alto que seguir esa + // dirección sería, con seguridad práctica, hammer al nodo sin + // esperanza real de encontrar el gasto (ver LARGE_ADDR_TX_COUNT). let custodyStop = null; for (const a of outAddrs) { const hit = ENTITY_INDEX.get(a); - if (hit?.cat === "exchange") { custodyStop = { addr:a, name:hit.name, known:true }; break; } + if (hit?.cat === "exchange") { custodyStop = { addr:a, name:hit.name, known:true, byVolume:false }; break; } } if (!custodyStop) { for (const a of outAddrs) { const prof = addressProfiles[a]; - if (prof && prof.kind === "hot_wallet") { custodyStop = { addr:a, name:null, known:false }; break; } + if (prof && prof.kind === "hot_wallet") { custodyStop = { addr:a, name:null, known:false, byVolume:false }; break; } + } + } + if (!custodyStop) { + for (const a of outAddrs) { + const prof = addressProfiles[a]; + if (prof && prof.txCount >= LARGE_ADDR_TX_COUNT) { custodyStop = { addr:a, name:null, known:false, byVolume:true, txCount:prof.txCount }; break; } } } @@ -4169,7 +4190,11 @@ } } for (const n of allNodes) { - if (n.stopReason === "exchange" && n.custodyStop) { + if (n.stopReason === "exchange" && n.custodyStop && n.custodyStop.byVolume) { + conclusions.push({ level:"HECHO", + text:`El rastro se detiene por precaución en la transacción ${n.txid}: la dirección de destino tiene ${fmt.num(n.custodyStop.txCount)} transacciones registradas, un volumen que hace inviable buscar el gasto concreto sin sobrecargar el nodo. No es una conclusión sobre quién controla esa dirección — solo una medida de protección. Puedes intentar continuar manualmente si lo necesitas.`, + refs:[n.txid, n.custodyStop.addr] }); + } else if (n.stopReason === "exchange" && n.custodyStop) { const label = n.custodyStop.known ? `${n.custodyStop.name} (fuente: índice de entidades)` : "hot wallet no identificada — posible custodio"; conclusions.push({ level:"INFERENCIA", certainty: n.custodyStop.known ? "PROBABLE" : "POSIBLE", text:`El rastro se detiene en ${label} en la transacción ${n.txid}.`, refs:[n.txid, n.custodyStop.addr] });