refactor: extraer unionFindCluster de buildWalletReport a utilidad compartida
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.
This commit is contained in:
+38
-29
@@ -1288,6 +1288,42 @@
|
|||||||
// ── Informe de wallet completo ────────────────────────────────────────
|
// ── Informe de wallet completo ────────────────────────────────────────
|
||||||
// Analiza el conjunto de txs del wallet: vinculación (CIOH), reutilización
|
// 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.
|
// 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) {
|
function buildWalletReport(activeAddrs, txs, myAddrSet) {
|
||||||
// 1) Reutilización: direcciones propias que aparecen en más de una tx
|
// 1) Reutilización: direcciones propias que aparecen en más de una tx
|
||||||
const addrUseCount = new Map();
|
const addrUseCount = new Map();
|
||||||
@@ -1307,35 +1343,8 @@
|
|||||||
// Nota: umbral 2 porque una dirección normal aparece en 2 txs como mínimo
|
// 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.
|
// (la que recibe y la que gasta). Más de 2 = reutilización real.
|
||||||
|
|
||||||
// 2) Clusters de vinculación por CIOH (common-input-ownership):
|
// 2) Clusters de vinculación por CIOH — ver unionFindCluster
|
||||||
// si una tx gasta varias direcciones propias como inputs, un observador
|
const { clusters, linkReasons } = unionFindCluster(txs, myAddrSet);
|
||||||
// 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);
|
|
||||||
|
|
||||||
// 3) Historial: cada tx con su banda
|
// 3) Historial: cada tx con su banda
|
||||||
const history = txs.map(tx => {
|
const history = txs.map(tx => {
|
||||||
|
|||||||
Reference in New Issue
Block a user