feat: cierra las cinco heurísticas ausentes del repaso
La comisión como huella: una comisión absoluta redonda o una tarifa entera no salen de un estimador, salen de alguien que las escribió a mano. Informativo y sin penalizar, igual que RBF — castigarlo empujaría a elegir comisiones peores para disimular. Cambio con más de dos salidas, solo el caso inequívoco: si exactamente una salida comparte tipo con las entradas no hay nada que adivinar, y con más salidas la coincidencia por azar es todavía más improbable. POSIBLE, no PROBABLE, porque es una sola señal. Autoenvíos en el informe de wallet: solo cabe ahí, porque saber que todas las salidas son tuyas exige conocer el wallet entero. Avisa solo cuando ataron más de una dirección — mover una a otra no vincula nada nuevo.
This commit is contained in:
@@ -87,6 +87,35 @@ T("CoinJoin: la guardia desactiva la heurística aunque haya señales",
|
||||
vout:[OUT(100000,"v1_p2tr","bc1pX"), OUT(99895000,"v0_p2wpkh","bc1qY")] },
|
||||
null, true);
|
||||
|
||||
// ── Más de dos salidas: solo el caso inequívoco ─────────────────────────
|
||||
// Antes la detección se plantaba en seco con más de 2 salidas. Ahora se
|
||||
// pronuncia solo cuando no hay nada que adivinar.
|
||||
console.log("\n=== Cambio con más de dos salidas ===\n");
|
||||
|
||||
T("3 salidas, solo una comparte tipo con las entradas: esa es el cambio",
|
||||
{ vin:[IN(10000000,"v0_p2wpkh","bc1qA")],
|
||||
vout:[OUT(3000000,"v1_p2tr","bc1pX"), OUT(2000000,"p2pkh","1Y"),
|
||||
OUT(4990000,"v0_p2wpkh","bc1qCAMBIO")] },
|
||||
"bc1qCAMBIO");
|
||||
|
||||
T("3 salidas, dos comparten tipo con las entradas: ambiguo, no se afirma",
|
||||
{ vin:[IN(10000000,"v0_p2wpkh","bc1qA")],
|
||||
vout:[OUT(3000000,"v0_p2wpkh","bc1qX"), OUT(2000000,"p2pkh","1Y"),
|
||||
OUT(4990000,"v0_p2wpkh","bc1qZ")] },
|
||||
null);
|
||||
|
||||
T("4 salidas, una vuelve a una dirección de las entradas: señal más fuerte",
|
||||
{ vin:[IN(10000000,"v0_p2wpkh","bc1qA")],
|
||||
vout:[OUT(3000000,"v0_p2wpkh","bc1qX"), OUT(2000000,"v0_p2wpkh","bc1qY"),
|
||||
OUT(1000000,"v0_p2wpkh","bc1qZ"), OUT(3990000,"v0_p2wpkh","bc1qA")] },
|
||||
"bc1qA");
|
||||
|
||||
T("CoinJoin con muchas salidas: la guardia sigue mandando",
|
||||
{ vin:[IN(10000000,"v0_p2wpkh","bc1qA")],
|
||||
vout:[OUT(3000000,"v1_p2tr","bc1pX"), OUT(2000000,"p2pkh","1Y"),
|
||||
OUT(4990000,"v0_p2wpkh","bc1qCAMBIO")] },
|
||||
null, true);
|
||||
|
||||
// ── Redondez ────────────────────────────────────────────────────────────
|
||||
// La versión anterior era `v%1000000===0 || v%100000===0 || v%10000000===0`,
|
||||
// donde la primera y la tercera condición sobran (todo múltiplo de un millón
|
||||
|
||||
@@ -160,6 +160,44 @@ ok("se etiqueta como consolidación, con certeza y no como probable",
|
||||
ok("no se cobra dos veces: la penalización la lleva input_linkage",
|
||||
cu?.penalty === 0 && check(consol, "input_linkage")?.pass === false);
|
||||
|
||||
console.log("\n=== La comisión como huella (check nuevo) ===\n");
|
||||
|
||||
// Comisión absoluta redonda: nadie llega a 10.000 sats clavados con un
|
||||
// estimador, que calcula tarifa por tamaño y devuelve números feos.
|
||||
const feeRedonda = analyzeTx(TX(
|
||||
[IN(5000000, "v1_p2tr", "bc1pA")],
|
||||
[OUT(4990000, "v1_p2tr", "bc1pB")],
|
||||
{ fee: 10000, weight: 600 }
|
||||
));
|
||||
ok("detecta una comisión absoluta redonda",
|
||||
check(feeRedonda, "fee_fingerprint")?.pass === true &&
|
||||
/10000 sats exactos/.test(check(feeRedonda, "fee_fingerprint")?.detail || ""),
|
||||
check(feeRedonda, "fee_fingerprint")?.detail?.slice(0, 90));
|
||||
|
||||
// Tarifa entera: 20,00 sat/vB sale de teclear "20" en la casilla.
|
||||
const feeEntera = analyzeTx(TX(
|
||||
[IN(5000000, "v1_p2tr", "bc1pA")],
|
||||
[OUT(4996000, "v1_p2tr", "bc1pB")],
|
||||
{ fee: 3000, weight: 600 } // vsize 150 -> 20,00 sat/vB
|
||||
));
|
||||
ok("detecta una tarifa prácticamente entera",
|
||||
/prácticamente un número entero|sats exactos/.test(check(feeEntera, "fee_fingerprint")?.detail || ""),
|
||||
check(feeEntera, "fee_fingerprint")?.detail?.slice(0, 90));
|
||||
|
||||
// Un estimador automático deja números feos.
|
||||
const feeEstimada = analyzeTx(TX(
|
||||
[IN(5000000, "v1_p2tr", "bc1pA")],
|
||||
[OUT(4996873, "v1_p2tr", "bc1pB")],
|
||||
{ fee: 3127, weight: 601 }
|
||||
));
|
||||
ok("no ve huella donde hay un número feo de estimador",
|
||||
/sin forma de cifra elegida a mano/.test(check(feeEstimada, "fee_fingerprint")?.detail || ""),
|
||||
check(feeEstimada, "fee_fingerprint")?.detail?.slice(0, 90));
|
||||
|
||||
ok("la huella de comisión no penaliza (misma decisión que con RBF)",
|
||||
check(feeRedonda, "fee_fingerprint")?.penalty === 0 &&
|
||||
check(feeRedonda, "fee_fingerprint")?.informational === true);
|
||||
|
||||
console.log("\n=== Guardia de CoinJoin ===\n");
|
||||
|
||||
// Whirlpool: 5 entradas, 5 salidas de la misma denominación.
|
||||
|
||||
Reference in New Issue
Block a user