diff --git a/dashboard.html b/dashboard.html
index f8075c8..ffa30b9 100644
--- a/dashboard.html
+++ b/dashboard.html
@@ -1024,7 +1024,7 @@
input_reuse: 25, input_type_mixing: 14, output_type_mismatch: 8,
round_numbers: 10, rbf: 5, peeling: 8,
unnecessary_input: 12, dust: 8, change_detection: 10,
- wallet_fingerprint: 6, batch_payment: 45, entity_ofac: 20,
+ wallet_fingerprint: 6, batch_payment: 45, entity_ofac: 20, op_return: 30,
};
let deductions = 0;
@@ -1459,7 +1459,29 @@
});
if (isBatch) deductions += weights.batch_payment;
- // ── 15. Timing analysis — informativo ────────────────────────────
+ // ── 15. OP_RETURN ─────────────────────────────────────────────────
+ // Detecta outputs OP_RETURN (nulldata): presencia y tamaño total en bytes.
+ // No muestra el contenido — lo relevante para privacidad es que hay datos
+ // arbitrarios, no qué dicen. Ver v1.2.1 del changelog.
+ const opReturnOuts = tx.vout.filter(v => isOpReturn(v));
+ const hasOpReturn = opReturnOuts.length > 0;
+ const opReturnBytes = opReturnOuts.reduce((s, v) => {
+ // scriptpubkey: "6a" + longitud (1 byte) + datos. Datos = (hex.length/2) - 2
+ const hex = v.scriptpubkey || "";
+ return s + Math.max(0, hex.length / 2 - 2);
+ }, 0);
+ checks.push({
+ id:"op_return", label:"Datos OP_RETURN", certainty:"CERTEZA", pass:!hasOpReturn,
+ actionability: "no_corregible",
+ detail: hasOpReturn
+ ? `${opReturnOuts.length} output${opReturnOuts.length > 1 ? "s" : ""} OP_RETURN con ${opReturnBytes} bytes de datos arbitrarios. Revela que esta transacción usa un protocolo o servicio que escribe datos en la cadena (Stamps, Ordinals, OpenTimestamps, Omni, etc.). Se informa la presencia y el tamaño, no el contenido.`
+ : "No se detectan outputs OP_RETURN.",
+ didactic: "Un output OP_RETURN contiene datos arbitrarios que cualquiera puede leer. Su presencia revela el uso de un protocolo concreto y puede vincular esta transacción con actividad on-chain identificable. El contenido no se muestra — para la privacidad lo relevante es que existe, no qué dice.",
+ penalty: hasOpReturn ? weights.op_return : 0,
+ });
+ if (hasOpReturn) deductions += weights.op_return;
+
+ // ── 16. Timing analysis — informativo ────────────────────────────
let timingDetail = "No hay timestamp disponible (transacción pendiente o sin confirmar).";
if (tx.status?.block_time) {
const date = new Date(tx.status.block_time * 1000);
@@ -1547,7 +1569,9 @@
// ── Score compuesto ───────────────────────────────────────────────
const maxPossible = Object.values(weights).reduce((a,b)=>a+b,0);
- const score = Math.max(0, Math.min(100, Math.round(100 - (deductions / maxPossible) * 100)));
+ const rawScore = Math.max(0, Math.min(100, Math.round(100 - (deductions / maxPossible) * 100)));
+ // OP_RETURN es incompatible con privacidad aceptable — fuerza banda BAJA como máximo.
+ const score = hasOpReturn ? Math.min(rawScore, 44) : rawScore;
return {
score, checks,