Refleja el cambio de modelo del peritaje forense: de "automático
acotado con seguir más saltos" a "a demanda, salto a salto, con
control manual del usuario", más el tope de tx_count como cinturón
de seguridad adicional. El mecanismo antiguo "seguir más saltos (+4)"
ya no existe en el código — se documenta el que lo reemplaza.
PeritajeForense pasa de "un clic, rastreo automático completo" a "un
clic por salto": el usuario ve el progreso (salto actual, tx
exploradas, ramas pendientes, ramas terminales) y decide cuándo
avanzar, igual que ya funciona el rastro de procedencia hacia atrás
(RastroProcedencia). El rastreo se guarda en un ref mutable
(traceRef) entre clics — initForensicTrace + advanceForensicHop del
commit anterior encajan directamente, sin cambios.
"Iniciar rastreo forense" ahora también ejecuta el primer salto (un
clic para ver algo). "Generar informe" está disponible desde el
primer salto, no solo al terminar, y puede volver a pulsarse en
cualquier momento — incluida la declaración del afectado, editable
mientras el rastreo sigue en marcha. Los campos txid/vout/importe se
deshabilitan una vez iniciado (no tienen efecto a mitad de rastreo);
el tope de saltos y la declaración siguen editables.
Quita el mecanismo antiguo "seguir más saltos (+4)": ya no tiene
sentido con control manual salto a salto, y el texto de ramas
truncadas por el tope se reescribe para no prometer una reanudación
que el motor no soporta (una rama ya truncada no se puede retomar
suelta).
Verificado en navegador con un servidor HTTP local que sirve la
cadena de peeling de 3 saltos ya usada en pruebas anteriores,
pulsando "seguir el rastro" cuatro veces manualmente: el progreso
avanza correctamente salto a salto (1→2→3→4), termina con "rastro
completo", y el informe generado es idéntico en contenido al que
produce el modo automático (misma cadena de peeling CERTEZA, mismas
direcciones atribuidas, mismos fondos sin gastar). Export JSON/MD y
"Limpiar" verificados sin errores de consola.
LARGE_ADDR_TX_COUNT=5000: si el perfil de una dirección de salida
tiene chain_stats.tx_count por encima del umbral, se trata como
custodio presunto (stopReason="exchange", byVolume:true) aunque el
heurístico de perfil no la clasifique "hot_wallet" (p.ej. residual
momentáneamente alto en el momento de la consulta). No cuesta
peticiones extra — chain_stats ya se pide para el perfil de toda
dirección de salida nueva.
Cierra el hueco identificado en la auditoría previa: sin este tope,
una dirección enorme no detectada por el heurístico se encolaría para
el siguiente salto, y findSpendingTx intentaría paginar hasta 200 de
sus transacciones (8 páginas × 8s de timeout, ~64s en el peor caso)
buscando una entre decenas de miles, sin ninguna posibilidad realista
de encontrarla.
El texto del informe distingue este caso: es HECHO (medida de
protección, con el tx_count real citado), no INFERENCIA sobre quién
controla la dirección — a diferencia de una parada por ENTITY_INDEX o
por el heurístico de hot wallet, aquí no se afirma nada sobre la
naturaleza de la dirección.
Verificado en navegador con un caso sintético diseñado para que el
heurístico de perfil NO dispare (residual del 100%, fuera del umbral
<0.05) pero con tx_count=79.828 (el caso real que se va a probar): el
tope se activa correctamente y la dirección enorme recibe EXACTAMENTE
1 petición (el perfil barato) — nunca se llega a paginar su historial.
Separa el motor en tres piezas para poder pausar entre saltos: crear
el estado del rastreo (initForensicTrace), avanzar UN salto completo
(advanceForensicHop, con el mismo throttling por lotes BATCH=5/
PAUSE=120ms de siempre dentro de ese salto) y ensamblar el
ForensicGraph desde el estado en cualquier momento, completo o
parcial (finalizeForensicGraph). buildForensicGraph se mantiene como
caso trivial que llama advanceForensicHop en bucle — modo automático
de una sola pasada, sin cambio de comportamiento.
Prepara el terreno para que la UI (pestaña Peritaje) deje que el
usuario decida cuándo seguir al siguiente salto, en vez de que el
motor drene todas las ramas sin vigilancia. Es la primera de las
protecciones acordadas contra el estrés al nodo con direcciones de
volumen enorme (hot wallets de exchange).
Verificado: balance de sintaxis + node --check sobre el fragmento
puro. En navegador, los dos casos sintéticos ya usados (salto simple,
cadena de peeling de 3 saltos) dan resultado IDÉNTICO byte a byte
entre el modo automático (buildForensicGraph) y el modo por saltos
manual (initForensicTrace + advanceForensicHop en bucle) — grafo,
clusters, cadenas de peeling y conclusiones del informe coinciden.
También verificado que finalizeForensicGraph + buildForensicReport
funcionan correctamente sobre estado PARCIAL (tras un solo salto, sin
terminar el rastreo), que es justo lo que necesitará la UI a demanda.
CHANGELOG (1.10.0) y README reflejan que el peritaje forense ya está
implementado, no solo especificado: qué hace, qué heurísticas reutiliza
y cuáles son nuevas, el marco HECHO/INFERENCIA/DECLARACION, y la
limitación conocida de depender de /api/address/{addr}/txs en vez de
/outspend (el backend Mempool self-hosted no lo expone).
Nueva pestaña "PERITAJE" junto a las existentes. Formulario de entrada
(txid:vout, importe estimado robado opcional, declaración en texto
libre), botón "Iniciar rastreo forense" con progreso por salto, y
render completo del informe (resumen, declaración, cronología,
direcciones atribuidas, fondos sin gastar, aviso de ramas truncadas
con botón "seguir más saltos", conclusiones, recomendaciones,
metodología, anexo de verificación) con export JSON/MD inline, mismo
patrón que el informe de wallet.
Verificado en navegador (servidor estático local):
- Balance de sintaxis del bloque Babel correcto, sin errores de
transpilación JSX en consola.
- Validación del formulario (txid vacío/inválido, nodo no conectado)
funciona.
- Motor completo probado con datos sintéticos vía consola: caso simple
(1 salto, cambio detectado correctamente, 2 ramas terminan en UTXO
sin gastar) y caso de cadena de peeling de 3 saltos (detectada con
certeza CERTEZA, direcciones de cambio atribuidas con fundamento
correcto, 4 fondos sin gastar localizados en las hojas).
Cierra la implementación del módulo Peritaje descrito en TRASPASO.md.
Ensambla las secciones de la plantilla (spec en TRASPASO.md) sobre el
grafo de buildForensicGraph: resumen, declaración del afectado,
cronología, direcciones atribuidas (CIOH + cambio detectado + huella
estable), fondos sin gastar, conclusiones numeradas, recomendaciones
(con el aviso anti-estafa de "recuperación" fijo cuando hay
declaración), metodología y anexo de verificación.
Marco HECHO/INFERENCIA/DECLARACION aplicado en cada afirmación; el
export a JSON/MD queda para la UI (mismo patrón inline que el informe
de wallet, sin función compartida hoy).
También: buildForensicGraph ahora guarda origin.analysis y
node.changeAddress (dirección resuelta, no el índice) para que el
informe no tenga que reindexar en addresses.out, que al deduplicar
podría desalinearse con el orden real de vout.
Rastreo hacia adelante desde {txid,vout} siguiendo TODOS los outputs de
cada salto (no solo el presunto cambio) porque los fondos pueden
repartirse en varias ramas; cada rama se detiene de forma
independiente. Condiciones de parada: CoinJoin real, dilución (3+
direcciones de entrada no relacionadas), entidad conocida o perfil de
hot wallet no indexado, UTXO sin gastar, límite de saltos.
findSpendingTx implementa el fallback ya verificado en la sesión
anterior (TRASPASO.md): este backend Mempool no expone /outspend, así
que se recorre /api/address/{addr}/txs buscando la tx cuyo vin
referencia el txid:vout de origen — mismo patrón de paginación y
throttling por lotes que scanWallet.
detectPeelingChains cierra el hueco que el check `peeling` de
analyzeTx ya señalaba (confirmar una cadena requiere mirar hacia
adelante): reconstruye tramos de nodos 1-in/2-out conectados por la
señal de cambio combinada, sube a CERTEZA con 3+ saltos y huella de
wallet estable.
MAX_NODES=80 como red de seguridad aparte de maxHops, para no hammer
el nodo del usuario si un salto desemboca en una tx con muchos
outputs. Verificado: balance de sintaxis del bloque Babel y
node --check sobre el fragmento JS puro del motor.
behavioralChangeGuess: para cada output, si se gasta rápido (≤6
bloques) o queda quieto (no gastado todavía). combineChangeSignals
cruza esto con la señal estructural de guessChangeOutput — sube la
certeza cuando coinciden, reporta la discrepancia en vez de forzar
una conclusión cuando no.
compareFingerprints compara la huella de detectWallets entre dos
saltos consecutivos del rastro: un cambio de huella es
INFERENCIA/POSIBLE de cambio de actor o entrada en infraestructura
de un servicio, nunca CERTEZA.
addressProfile(addr, addrInfo, addrTxs) clasifica personal vs hot
wallet usando chain_stats (residual/tx_count) y detección de barrido
automático (recibe y reenvía 1-in/1-out sin cambio, pocos bloques
después). No identifica al custodio, solo el patrón de comportamiento;
la atribución a un exchange concreto sigue viniendo de ENTITY_INDEX.
Primera pieza nueva del módulo Peritaje (spec en TRASPASO.md), sobre
la base de los refactors de unionFindCluster y guessChangeOutput.
Autocontenida para poder llamarse por salto desde el futuro motor de
rastreo forense, que necesita saber qué output concreto seguir (no solo
si el cambio es identificable). analyzeTx delega en ella sin cambio de
comportamiento — mismas señales, mismos umbrales, mismo texto.
Prepara la reutilización del union-find CIOH en el módulo de peritaje
forense (semilla = direcciones del actor rastreado en vez de mis
direcciones). Sin cambio de comportamiento en el informe de wallet.