fix: CoinJoin estructural (WabiSabi), checks neutros en mezcla
This commit is contained in:
+49
-15
@@ -1085,8 +1085,22 @@
|
||||
const isGenericCJ = tx.vin.length >= 5 && spendableOuts.length >= 5
|
||||
&& maxEqual >= Math.floor(spendableOuts.length * 0.5)
|
||||
&& equalValue >= CJ_MIN_DENOM;
|
||||
const likelyCJ = isWhirlpool || isGenericCJ;
|
||||
const cjVariant = isWhirlpool ? "Whirlpool" : "CoinJoin genérico";
|
||||
// CoinJoin por estructura de mezcla (WabiSabi / JoinMarket outputs variables).
|
||||
// WabiSabi no usa denominaciones fijas — sus outputs son de valor variable,
|
||||
// así que isGenericCJ no lo captura. Pero sí tiene firma estructural clara:
|
||||
// muchos inputs Y muchos outputs, ratio cercano a 1 (no es batch ni consolidación),
|
||||
// y sin un output dominante que concentre el valor (no hay "cambio" obvio).
|
||||
const CJ_STRUCT_MIN = 10;
|
||||
const inputOutputRatio = spendableOuts.length > 0 ? tx.vin.length / spendableOuts.length : 999;
|
||||
const maxOutValue = spendableOuts.length > 0 ? Math.max(...spendableValues) : 0;
|
||||
const totalOutValue = spendableValues.reduce((s,v) => s+v, 0);
|
||||
const dominantOutputShare = totalOutValue > 0 ? maxOutValue / totalOutValue : 1;
|
||||
const isStructuralCJ = !isWhirlpool && !isGenericCJ
|
||||
&& tx.vin.length >= CJ_STRUCT_MIN && spendableOuts.length >= CJ_STRUCT_MIN
|
||||
&& inputOutputRatio >= 0.5 && inputOutputRatio <= 2.0
|
||||
&& dominantOutputShare < 0.3;
|
||||
const likelyCJ = isWhirlpool || isGenericCJ || isStructuralCJ;
|
||||
const cjVariant = isWhirlpool ? "Whirlpool" : isStructuralCJ ? "CoinJoin (mezcla estructural)" : "CoinJoin genérico";
|
||||
|
||||
// ── PayJoin — señal débil informativa ────────────────────────────
|
||||
// Si hay exactamente 1 input "extra" que no era necesario y no es CoinJoin
|
||||
@@ -1114,15 +1128,20 @@
|
||||
const outTypes = [...new Set(tx.vout.map(v=>v.scriptpubkey_type).filter(Boolean))];
|
||||
const hasInputTypeMixing = inTypes.length > 1;
|
||||
checks.push({
|
||||
id:"input_type_mixing", label:"Mezcla de tipos de script en inputs", certainty:"CERTEZA", pass:!hasInputTypeMixing,
|
||||
actionability: "evitable",
|
||||
id:"input_type_mixing", label:"Mezcla de tipos de script en inputs", certainty:"CERTEZA",
|
||||
pass: likelyCJ ? true : !hasInputTypeMixing,
|
||||
informational: likelyCJ && hasInputTypeMixing,
|
||||
actionability: likelyCJ ? null : "evitable",
|
||||
detail: hasInputTypeMixing
|
||||
? `Hecho (certeza): los inputs son de tipos distintos (${inTypes.join(", ")}). Interpretación (probable): suele indicar consolidación de UTXOs de fuentes o épocas distintas — aunque un mismo wallet moderno también puede tener UTXOs de varios tipos.`
|
||||
? (likelyCJ
|
||||
? `Los inputs mezclan tipos (${inTypes.join(", ")}). En un CoinJoin es normal: cada participante aporta sus propios UTXOs, que pueden ser de tipos distintos.`
|
||||
: `Hecho (certeza): los inputs son de tipos distintos (${inTypes.join(", ")}). Interpretación (probable): suele indicar consolidación de UTXOs de fuentes o épocas distintas — aunque un mismo wallet moderno también puede tener UTXOs de varios tipos.`)
|
||||
: `Inputs uniformes: ${inTypes[0] || "—"}.`,
|
||||
didactic: "Que los inputs mezclen tipos de script es un hecho observable. Lo que se infiere de ello —que el emisor consolidaba UTXOs de wallets o épocas distintas— es probable pero no seguro: un wallet moderno puede acumular UTXOs de varios tipos con el tiempo. La señal es reveladora para el análisis, pero conviene leerla como indicio, no como prueba.",
|
||||
penalty: weights.input_type_mixing,
|
||||
penalty: likelyCJ ? 0 : weights.input_type_mixing,
|
||||
});
|
||||
if (hasInputTypeMixing) deductions += weights.input_type_mixing;
|
||||
// En CoinJoin la mezcla de tipos de script es esperada (participantes distintos) — no penaliza.
|
||||
if (hasInputTypeMixing && !likelyCJ) deductions += weights.input_type_mixing;
|
||||
|
||||
// ── 3. Mismatch tipo input/output ────────────────────────────────
|
||||
const dominantInType = inTypes[0];
|
||||
@@ -1210,13 +1229,18 @@
|
||||
}
|
||||
}
|
||||
checks.push({
|
||||
id:"unnecessary_input", label:"Inputs innecesarios (consolidación revelada)", certainty:"PROBABLE", pass:!hasUnnecessaryInput,
|
||||
actionability: "evitable",
|
||||
detail: hasUnnecessaryInput ? unnecessaryDetail : "No se detectan inputs claramente innecesarios para cubrir el pago.",
|
||||
id:"unnecessary_input", label:"Inputs innecesarios (consolidación revelada)", certainty:"PROBABLE",
|
||||
pass: likelyCJ ? true : !hasUnnecessaryInput,
|
||||
informational: likelyCJ,
|
||||
actionability: likelyCJ ? null : "evitable",
|
||||
detail: likelyCJ
|
||||
? `En un CoinJoin todos los inputs son necesarios por diseño del protocolo — cada participante aporta los suyos. Esta heurística no aplica.`
|
||||
: (hasUnnecessaryInput ? unnecessaryDetail : "No se detectan inputs claramente innecesarios para cubrir el pago."),
|
||||
didactic: "Si una transacción tiene más inputs de los necesarios para cubrir el pago, el emisor estaba consolidando UTXOs. Esto revela que esos UTXOs pertenecen al mismo propietario y es una de las heurísticas más usadas en chain analysis real.",
|
||||
penalty: weights.unnecessary_input,
|
||||
penalty: likelyCJ ? 0 : weights.unnecessary_input,
|
||||
});
|
||||
if (hasUnnecessaryInput) deductions += weights.unnecessary_input;
|
||||
// En CoinJoin múltiples inputs son la estructura del protocolo — no penaliza.
|
||||
if (hasUnnecessaryInput && !likelyCJ) deductions += weights.unnecessary_input;
|
||||
|
||||
// ── 8. Patrón de pago simple (posible eslabón de peeling chain) ────
|
||||
// OJO: una sola tx 1-in/2-out NO es una peeling chain — es el pago más
|
||||
@@ -1382,12 +1406,22 @@
|
||||
name: "Wasabi o JoinMarket (CoinJoin)", confidence: "POSIBLE",
|
||||
signals: ["estructura CoinJoin genérica — outputs de igual valor"]
|
||||
});
|
||||
} else if (isStructuralCJ) {
|
||||
detectedWallets.unshift({
|
||||
name: "Wasabi o JoinMarket (CoinJoin)", confidence: "POSIBLE",
|
||||
signals: ["estructura de mezcla — muchos inputs/outputs, sin output dominante"]
|
||||
});
|
||||
}
|
||||
checks.push({
|
||||
id:"wallet_fingerprint", label:"Fingerprinting de wallet", certainty: detectedWallets[0]?.confidence||"POSIBLE", pass:detectedWallets.length===0,
|
||||
actionability: "wallet",
|
||||
id:"wallet_fingerprint", label:"Fingerprinting de wallet",
|
||||
certainty: detectedWallets[0]?.confidence||"POSIBLE",
|
||||
pass: likelyCJ ? true : detectedWallets.length===0,
|
||||
informational: likelyCJ && detectedWallets.length > 0,
|
||||
actionability: likelyCJ ? null : "wallet",
|
||||
detail: detectedWallets.length > 0
|
||||
? detectedWallets.map(w=>`${w.name} (${w.confidence}): ${w.signals.join(", ")}`).join(" · ")
|
||||
? (likelyCJ
|
||||
? `En un CoinJoin identificar el software es informativo, no un problema. ${detectedWallets.map(w=>`${w.name} (${w.confidence}): ${w.signals.join(", ")}`).join(" · ")}`
|
||||
: detectedWallets.map(w=>`${w.name} (${w.confidence}): ${w.signals.join(", ")}`).join(" · "))
|
||||
: "No se detecta un patrón de wallet específico con suficiente certeza.",
|
||||
didactic: "El objetivo no es ser invisible sino ser indistinguible de millones de usuarios del mismo wallet. Un fingerprint de Bitcoin Core lo comparten millones de transacciones — no revela nada útil. Un fingerprint de Exodus o un wallet minoritario pertenece a un conjunto mucho menor y es más revelador. En un CoinJoin, el tipo de mezcla ya sugiere el software usado.",
|
||||
penalty: likelyCJ ? 0 : weights.wallet_fingerprint,
|
||||
|
||||
Reference in New Issue
Block a user