From e89fea499c8fde9eabacca88899c915185c2060f Mon Sep 17 00:00:00 2001 From: Aitor Date: Thu, 16 Jul 2026 12:06:25 +0200 Subject: [PATCH] refactor: extraer unionFindCluster de buildWalletReport a utilidad compartida MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- dashboard.html | 67 ++++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/dashboard.html b/dashboard.html index 2a8681b..4b9468e 100644 --- a/dashboard.html +++ b/dashboard.html @@ -1288,6 +1288,42 @@ // ── Informe de wallet completo ──────────────────────────────────────── // Analiza el conjunto de txs del wallet: vinculación (CIOH), reutilización // y un resumen de salud. Funciona sobre datos ya traídos, en memoria. + // Clusters de vinculación por CIOH (common-input-ownership): si una tx gasta + // varias direcciones semilla como inputs, un observador asume que pertenecen + // al mismo dueño. Union-Find sobre direcciones. Compartida entre el informe + // de wallet (semilla = mis direcciones) y el peritaje forense (semilla = + // direcciones atribuidas al actor rastreado). + function unionFindCluster(txs, seedAddrSet) { + const parent = new Map(); + const find = (x) => { while (parent.get(x) !== x) { parent.set(x, parent.get(parent.get(x))); x = parent.get(x); } return x; }; + const union = (a, b) => { const ra=find(a), rb=find(b); if (ra!==rb) parent.set(ra, rb); }; + for (const a of seedAddrSet) parent.set(a, a); + + const linkReasons = []; + for (const tx of txs) { + const ownInputs = (tx.vin||[]) + .map(v => v.prevout?.scriptpubkey_address) + .filter(a => a && seedAddrSet.has(a)); + const uniq = [...new Set(ownInputs)]; + if (uniq.length > 1) { + for (let i = 1; i < uniq.length; i++) union(uniq[0], uniq[i]); + linkReasons.push({ txid: tx.txid, addrs: uniq, reason: "compartieron inputs en una misma transacción (CIOH)" }); + } + } + + const clusterMap = new Map(); + const linked = new Set(linkReasons.flatMap(l => l.addrs)); + for (const a of linked) { + const r = find(a); + if (!clusterMap.has(r)) clusterMap.set(r, []); + clusterMap.get(r).push(a); + } + const clusters = [...clusterMap.values()].filter(c => c.length > 1) + .sort((a,b) => b.length - a.length); + + return { clusters, linkReasons }; + } + function buildWalletReport(activeAddrs, txs, myAddrSet) { // 1) Reutilización: direcciones propias que aparecen en más de una tx const addrUseCount = new Map(); @@ -1307,35 +1343,8 @@ // Nota: umbral 2 porque una dirección normal aparece en 2 txs como mínimo // (la que recibe y la que gasta). Más de 2 = reutilización real. - // 2) Clusters de vinculación por CIOH (common-input-ownership): - // si una tx gasta varias direcciones propias como inputs, un observador - // asume que pertenecen al mismo dueño. Union-Find sobre direcciones. - const parent = new Map(); - const find = (x) => { while (parent.get(x) !== x) { parent.set(x, parent.get(parent.get(x))); x = parent.get(x); } return x; }; - const union = (a, b) => { const ra=find(a), rb=find(b); if (ra!==rb) parent.set(ra, rb); }; - for (const a of myAddrSet) parent.set(a, a); - - const linkReasons = []; - for (const tx of txs) { - const ownInputs = (tx.vin||[]) - .map(v => v.prevout?.scriptpubkey_address) - .filter(a => a && myAddrSet.has(a)); - const uniq = [...new Set(ownInputs)]; - if (uniq.length > 1) { - for (let i = 1; i < uniq.length; i++) union(uniq[0], uniq[i]); - linkReasons.push({ txid: tx.txid, addrs: uniq, reason: "compartieron inputs en una misma transacción (CIOH)" }); - } - } - - const clusterMap = new Map(); - const linked = new Set(linkReasons.flatMap(l => l.addrs)); - for (const a of linked) { - const r = find(a); - if (!clusterMap.has(r)) clusterMap.set(r, []); - clusterMap.get(r).push(a); - } - const clusters = [...clusterMap.values()].filter(c => c.length > 1) - .sort((a,b) => b.length - a.length); + // 2) Clusters de vinculación por CIOH — ver unionFindCluster + const { clusters, linkReasons } = unionFindCluster(txs, myAddrSet); // 3) Historial: cada tx con su banda const history = txs.map(tx => {