From d0cd9ef5876e66ce7c3df873cc9d014cb9f686cc Mon Sep 17 00:00:00 2001 From: Aitor Date: Mon, 27 Jul 2026 13:29:08 +0200 Subject: [PATCH] fix: no atribuir al actor direcciones del otro lado de un CoinJoin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 18 ++++++++++++++++++ dashboard.html | 20 +++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d403883..98c56b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/dashboard.html b/dashboard.html index b380945..e4d0ba3 100644 --- a/dashboard.html +++ b/dashboard.html @@ -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)); }