feat: peritaje forense — cambio conductual y comparación de huella
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.
This commit is contained in:
@@ -3717,6 +3717,68 @@
|
|||||||
return { kind, certainty, signals, funded, spent, residual, txCount, sweepCount };
|
return { kind, certainty, signals, funded, spent, residual, txCount, sweepCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cambio conductual: para cada output de una tx, ¿se gasta rápido (pocos
|
||||||
|
// bloques después) o queda quieto (no gastado todavía)? spendInfo ya viene
|
||||||
|
// resuelto por el motor de rastreo desde /api/address/{addr}/txs — esta
|
||||||
|
// función no hace peticiones, solo interpreta.
|
||||||
|
// Umbral de "rápido" = 6 bloques, igual que el barrido automático de
|
||||||
|
// addressProfile, por coherencia dentro del módulo.
|
||||||
|
function behavioralChangeGuess(tx, spendInfo) {
|
||||||
|
const outputs = (tx.vout||[]).map((v,i) => ({ index:i, ...(spendInfo.get(i)||{spent:false,blocksLater:null}) }));
|
||||||
|
const quiet = outputs.filter(o => !o.spent).map(o=>o.index);
|
||||||
|
const fast = outputs.filter(o => o.spent && o.blocksLater != null && o.blocksLater <= 6).map(o=>o.index);
|
||||||
|
return { outputs, quiet, fast };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cruza la señal estructural (guessChangeOutput) con la conductual: cuando
|
||||||
|
// coinciden —el output señalado como cambio queda quieto mientras el otro
|
||||||
|
// se mueve rápido— la certeza sube un nivel. Cuando discrepan, se reporta
|
||||||
|
// la discrepancia en vez de forzar una conclusión. "Queda quieto" solo
|
||||||
|
// significa "no gastado todavía", nunca una prueba por sí sola.
|
||||||
|
function combineChangeSignals(structural, behavioral) {
|
||||||
|
if (!structural.identifiable) {
|
||||||
|
return { index: structural.index, certainty: null, agreement: null,
|
||||||
|
note: "Sin señal estructural suficiente para cruzar con el comportamiento." };
|
||||||
|
}
|
||||||
|
const idx = structural.index;
|
||||||
|
const otherIdx = idx === 0 ? 1 : 0;
|
||||||
|
const idxQuiet = behavioral.quiet.includes(idx);
|
||||||
|
const idxFast = behavioral.fast.includes(idx);
|
||||||
|
const otherQuiet = behavioral.quiet.includes(otherIdx);
|
||||||
|
const otherFast = behavioral.fast.includes(otherIdx);
|
||||||
|
|
||||||
|
if (idxQuiet && otherFast) {
|
||||||
|
return { index: idx, certainty: "PROBABLE", agreement: "coincide",
|
||||||
|
note: `El output señalado como cambio (#${idx}) sigue sin gastar mientras el otro (#${otherIdx}) se movió pocos bloques después de esta tx — coincide con lo esperado de un cambio.` };
|
||||||
|
}
|
||||||
|
if (idxFast && otherQuiet) {
|
||||||
|
return { index: idx, certainty: structural.certainty, agreement: "discrepa",
|
||||||
|
note: `El output señalado estructuralmente como cambio (#${idx}) se movió rápido mientras el otro (#${otherIdx}) sigue quieto — discrepa con la señal estructural. No se fuerza una conclusión: se deja constancia de la discrepancia.` };
|
||||||
|
}
|
||||||
|
return { index: idx, certainty: structural.certainty, agreement: "sin_dato",
|
||||||
|
note: "Sin datos de gasto suficientes (ambos gastados o ambos quietos) para cruzar con la señal estructural." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compara la huella de software (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, porque una misma persona puede cambiar de wallet sin cambiar
|
||||||
|
// de dueño de los fondos.
|
||||||
|
function compareFingerprints(prevFp, nextFp) {
|
||||||
|
const prevTop = prevFp?.[0]?.name || null;
|
||||||
|
const nextTop = nextFp?.[0]?.name || null;
|
||||||
|
if (!prevTop || !nextTop) {
|
||||||
|
return { changed: null, certainty: null,
|
||||||
|
note: "Huella insuficiente en uno de los dos saltos para comparar." };
|
||||||
|
}
|
||||||
|
if (prevTop === nextTop) {
|
||||||
|
return { changed: false, certainty: null,
|
||||||
|
note: `Misma huella de software en ambos saltos (${prevTop}) — compatible con el mismo actor.` };
|
||||||
|
}
|
||||||
|
return { changed: true, certainty: "POSIBLE",
|
||||||
|
note: `La huella cambia de ${prevTop} a ${nextTop} entre saltos — posible cambio de actor o entrada en infraestructura de un servicio (custodio, exchange). No es una prueba por sí sola.` };
|
||||||
|
}
|
||||||
|
|
||||||
// Eslabón recursivo: muestra una tx de origen y permite seguir SUS inputs.
|
// Eslabón recursivo: muestra una tx de origen y permite seguir SUS inputs.
|
||||||
// depth limita la profundidad visual; cache y getTx se comparten desde arriba.
|
// depth limita la profundidad visual; cache y getTx se comparten desde arriba.
|
||||||
function Eslabon({txData, fluyo, vout, depth, getTx, base}) {
|
function Eslabon({txData, fluyo, vout, depth, getTx, base}) {
|
||||||
|
|||||||
Reference in New Issue
Block a user