fix: no atribuir al actor direcciones del otro lado de un CoinJoin

El informe decía 'el rastro se rompe en un CoinJoin, no se puede atribuir con
honestidad más allá de este punto' y acto seguido listaba 10 direcciones
atribuidas al actor, cinco de ellas salidas de esa misma mezcla — es decir,
de otros participantes, señalados en un documento para una denuncia.

CIOH no aplica dentro de un CoinJoin (es su excepción clásica: la mezcla
existe para romper la suposición de dueño común) y la huella de software sale
estable por construcción, porque todos usan el mismo programa.

finalizeForensicGraph excluye las tx marcadas como mixer del conjunto del
actor y del union-find; la atribución por huella salta esos nodos.

Verificado contra el nodo con un Whirlpool real: de 10 atribuciones a 0.
This commit is contained in:
Aitor
2026-07-27 13:29:08 +02:00
parent 7c12f63f7a
commit d0cd9ef587
2 changed files with 37 additions and 1 deletions
+18
View File
@@ -42,6 +42,24 @@ y el versionado sigue [Versionado Semántico](https://semver.org/lang/es/):
`system-metrics.js` en el servidor.
- Medido: repetir un rastreo forense ya explorado cuesta **cero peticiones**.
### Corregido
- **El informe ya no atribuye al actor direcciones del otro lado de un
CoinJoin.** Detectado probando un rastro que atraviesa un Whirlpool real: el
informe declaraba *"el rastro se rompe en un CoinJoin — no se puede atribuir
con honestidad más allá de este punto"* y, tres líneas más abajo, listaba
**10 direcciones atribuidas al actor**, cinco de ellas salidas de esa misma
mezcla. Eran direcciones de otros participantes, señaladas en un documento
pensado para acompañar una denuncia.
- Los dos fundamentos que las colaban fallan precisamente ahí: **CIOH** asume
que quien gasta varios inputs juntos los controla, y un CoinJoin existe para
romper esa suposición (es la excepción clásica a la heurística); y la
**huella de software** sale estable por construcción, porque todos los
participantes usan el mismo programa.
- `finalizeForensicGraph` excluye ahora las transacciones marcadas como
`mixer` del conjunto de direcciones del actor y del union-find, y la
atribución por huella salta esos nodos.
- Verificado con el mismo caso: de 10 direcciones atribuidas a 0.
---
## [1.10.1] — 2026-07-27
### Añadido
+19 -1
View File
@@ -4201,12 +4201,24 @@
// pertenecen al servicio, no al mismo actor rastreado). Exclusión por
// dirección, no por nodo entero — un pago normal junto a un depósito
// de exchange en la misma tx sí cuenta como del actor.
// Los CoinJoin quedan FUERA por completo, entradas y salidas. CIOH
// asume que quien gasta varios inputs juntos los controla, y un CoinJoin
// existe precisamente para romper esa suposición: es la excepción
// clásica a la heurística. Sus entradas y salidas son de participantes
// distintos, así que meterlas aquí produce un falso positivo garantizado
// y contradice la propia conclusión del informe ("no se puede atribuir
// con honestidad más allá de este punto").
const actorAddrSet = new Set([originOut.scriptpubkey_address].filter(Boolean));
const mixerTxids = new Set();
for (const n of nodes.values()) {
if (n.stopReason === "mixer") { mixerTxids.add(n.txid); continue; }
for (const a of n.addresses.in) actorAddrSet.add(a);
for (const a of n.addresses.out) { if (!n.custodyAddrs.includes(a)) actorAddrSet.add(a); }
}
const { clusters, linkReasons } = unionFindCluster([...txCache.values()], actorAddrSet);
// El union-find tampoco debe ver las transacciones de mezcla: agruparía
// por inputs comunes a gente que no tiene nada que ver entre sí.
const txsForCluster = [...txCache.values()].filter(t => !mixerTxids.has(t.txid));
const { clusters, linkReasons } = unionFindCluster(txsForCluster, actorAddrSet);
// marcasDeTx necesita vin/vout crudos (coinbase, OFAC, minería,
// exchange, CoinJoin) — se calcula aquí, sobre originTx de verdad, y se
@@ -4294,8 +4306,14 @@
agreement: n.changeGuess?.combined?.agreement || null,
});
}
// Igual que con CIOH, la huella de software no vale nada dentro de una
// mezcla: en un CoinJoin todos los participantes usan el mismo programa,
// así que la huella sale "estable" por construcción, no porque sea el
// mismo actor. Atribuir salidas de un CoinJoin por esa señal sería
// señalar a terceros que solo coincidieron en la misma transacción.
const fingerprintStableAddrs = new Set();
for (const n of allNodes) {
if (n.stopReason === "mixer") continue;
if (n.fingerprintComparison?.changed === false) n.addresses.out.forEach(a=>fingerprintStableAddrs.add(a));
}