De los pagos firmados a los bloques
Una firma es solo una de las condiciones de un pago válido. Construimos un pequeño registro UTXO, cuadramos entradas, salidas y comisiones, y distinguimos la aceptación local de una transacción de su confirmación. Los enlaces hash y los árboles de Merkle nos permiten comprometer de forma compacta el historial y su contenido.
Alice quiere pagarle a Bob 20 Ð por un libro. En una aplicación bancaria escribiría 20 y pulsaría Enviar. Detrás de ese gesto, el sistema tiene que resolver varias cuestiones: de dónde sale el dinero, quién puede gastarlo, qué ocurre con el resto y qué registro impide que Alice vuelva a gastarlo.
En la parte anterior dotamos a DCoin de reglas públicas de validación, comunicación entre pares, hashes y firmas digitales. Ahora necesitamos objetos sobre los que aplicar esas reglas. Usaremos un modelo inspirado en Bitcoin: las transacciones consumen salidas existentes y crean otras nuevas.
No es la única opción. Un registro basado en cuentas podría actualizar saldos. El modelo de salidas nos resulta útil porque hace explícitos la conservación y los gastos incompatibles, y nos prepara para leer transacciones reales de Bitcoin.
2.1 Gastar salidas, no un campo de saldo
Una salida de transacción no gastada, o UTXO por sus siglas en inglés, registra una cantidad y la condición necesaria para gastarla. Se identifica mediante la transacción que la creó y su posición dentro de ella. Esa pareja se denomina outpoint.
En nuestras reglas simplificadas de DCoin, la condición de gasto puede ser una clave pública: hay que aportar una firma válida para esa clave. Las salidas reales de Bitcoin utilizan scripts y programas de testigo; el caso de la clave pública es una introducción, no una descripción de todo el sistema.
El saldo que muestra una cartera se calcula a partir de las salidas que puede gastar. Si Alice controla salidas de 200 Ð y 300 Ð, su saldo es 500 Ð, pero no existe una fila universal Alice.balance que todos los nodos actualicen. Hay dos registros gastables, y cada uno puede consumirse una sola vez.
Supongamos, en cambio, que Alice controla una única salida de 500 Ð. Pagar 20 Ð no consume una parte de ella. La transacción consume los 500 Ð y crea una salida nueva que devuelve el cambio a Alice.
La regla de conservación de una transacción que no emite moneda es
El valor de una entrada se obtiene de la salida a la que hace referencia en el registro existente. Un remitente no puede darle un valor de 500 con solo escribir amount: 500 en un mensaje. Los validadores consultan por sí mismos el registro referenciado.
Estos cálculos deben usar unidades enteras. La aritmética de coma flotante es una mala base para una regla que todos los validadores han de reproducir exactamente. Bitcoin expresa los valores en satoshis; DCoin puede elegir su propia unidad mínima indivisible y limitar el rango para evitar desbordamientos o cantidades sin sentido.
El cambio también es una salida real con una condición de gasto. No debe ser una instrucción informal de «quedarse con el resto». Si Alice olvida crear la salida de cambio, aumenta la diferencia entre entradas y salidas; una transacción válida podría acabar pagando una comisión inesperadamente alta.
2.2 Qué comprueba un validador
Una transacción de DCoin contiene referencias a entradas, una lista de salidas nuevas y el material de autorización que exigen esas entradas. Su codificación en bytes debe ser determinista: el orden de los campos, los tamaños de los enteros, las longitudes y aquello que cubre cada firma forman parte de la especificación.
Dado un estado acordado del registro, un validador puede comprobar mecánicamente una transacción candidata. Analiza la estructura, rechaza referencias de entrada duplicadas, localiza cada salida referenciada, comprueba que siga sin gastarse, verifica las condiciones de gasto y cuadra las cantidades. También aplica las reglas de tamaño y tiempo que defina DCoin.
El orden de las comprobaciones importa en la práctica. Un nodo no debería dedicar CPU ilimitada a verificar firmas de un objeto cuyos campos de longitud ya son imposibles. El analizador debe rechazar truncamientos y cantidades desmesuradas, en lugar de reservar memoria a ciegas. Las reglas de consenso definen la validez; un programa robusto también necesita tratar con cuidado la frontera entre bytes no fiables y esas reglas.
Esta podría ser una transición de estado de nuestro modelo:
def apply_payment(tx, unspent):
# Pseudocódigo: el análisis y las reglas de firma se especifican aparte.
inputs = [item.outpoint for item in tx.inputs]
assert inputs and len(inputs) == len(set(inputs))
previous = [unspent[outpoint] for outpoint in inputs]
assert all(authorizes(tx, i, old) for i, old in enumerate(previous))
assert all(0 <= output.value <= MAX_MONEY for output in tx.outputs)
fee = sum(old.value for old in previous) - sum(o.value for o in tx.outputs)
assert fee >= 0
updated = dict(unspent)
for outpoint in inputs:
del updated[outpoint]
for index, output in enumerate(tx.outputs):
updated[(tx.id, index)] = output
return updated, feeNo es una cartera ni una implementación completa de consenso. Omite la emisión, las reglas de unicidad de los identificadores de transacción, las comprobaciones del rango monetario de las sumas y muchos otros detalles. Su propósito es mostrar el cambio de estado: eliminar las salidas consumidas e insertar las nuevas. El estado original permanece intacto hasta que todas las comprobaciones terminan correctamente.
Fijémonos en «dado un estado acordado del registro». Si dos nodos discrepan sobre qué pago ocurrió primero, uno puede conservar una salida que el otro ya ha consumido. La validación es determinista respecto a un estado; no elige por sí sola ese estado.
2.3 Pendiente no significa confirmada
Cuando Alice difunde su pago, los nodos pueden guardarlo en un conjunto de transacciones pendientes, la mempool. No existe una mempool global única. Cada nodo puede enterarse en un momento distinto, conservar candidatos diferentes o aplicar políticas de retransmisión diferentes.
Supongamos que Alice firma otra transacción que gasta los mismos 500 Ð para pagar a Carol. Cualquiera de los dos pagos puede ser válido respecto al estado anterior al gasto. No pueden aplicarse ambos. Rechazar un conflicto en la mempool de un nodo no demuestra que otro nodo no haya recibido antes la versión incompatible.
Un bloque agrupa un lote ordenado de transacciones. Cuando pasa a formar parte del historial seleccionado, sus transacciones quedan confirmadas respecto a ese historial. Un bloque puede incluir una transacción que gaste una salida creada antes dentro del mismo bloque, por lo que el orden del lote también puede importar.
La guía de transacciones de Bitcoin desarrolla su modelo concreto de salidas y gasto. El ejemplo de DCoin conserva solo los mecanismos necesarios para entender el registro.
2.4 Enlazar bloques mediante sus cabeceras
Cada bloque tiene una cabecera y un cuerpo. El cuerpo contiene transacciones. La cabecera incluye metadatos, un compromiso criptográfico con las transacciones y el hash de la cabecera anterior. Podemos definir esta estructura de DCoin sin decidir todavía quién puede proponer el siguiente bloque.
Si alguien cambia una transacción, cambia su compromiso criptográfico. Eso modifica el hash de la cabecera que la contiene y el campo de hash anterior del siguiente bloque deja de coincidir. Corregir esa discrepancia cambia también la siguiente cabecera, y el efecto se propaga por sus descendientes.
Es una cadena de dependencias, no una prohibición mágica de editar. Cualquiera puede construir una secuencia alternativa con hashes internamente coherentes. Aún necesitamos una regla que dificulte sustituir el historial aceptado y permita elegir entre secuencias válidas. Ese es el problema de consenso que abordaremos en la siguiente parte.
El primer bloque, o bloque génesis, no tiene un predecesor ordinario. Su identidad y sus reglas iniciales deben quedar fijadas por el protocolo u otro acuerdo explícito. Una cadena no puede justificar sus condiciones iniciales limitándose a apuntar indefinidamente hacia atrás.
2.5 Comprometer las transacciones con un árbol de Merkle
Incluir todas las transacciones directamente en la cabecera haría que esta fuera tan grande como el bloque. Un árbol de Merkle permite condensar el compromiso en un solo hash.
Partimos de los identificadores de transacción . Calculamos el hash de cada pareja y después el de la pareja resultante. El resumen final es la raíz de Merkle. Aquí representa la construcción hash elegida para el árbol y indica concatenación de bytes, no de sus representaciones hexadecimales.
Quien conoce la raíz puede verificar esta prueba de inclusión sin recibir las otras dos transacciones. En un árbol equilibrado de hojas, una rama necesita aproximadamente hashes hermanos en lugar de identificadores. El verificador reconstruye los hashes del camino y compara el resultado final con la raíz comprometida.
La inclusión es una afirmación limitada. No demuestra que la firma de la transacción sea válida, que sus entradas estuvieran sin gastar ni que el bloque pertenezca al historial aceptado. Eso requiere otras pruebas y comprobaciones.
El árbol de Bitcoin usa SHA-256 doble y duplica la última entrada de cada nivel impar antes de formar parejas. Sus reglas exactas importan para reproducir una raíz a partir de bytes: un esquema abstracto de árbol binario no basta como especificación del formato. Consultaremos la referencia de bloques cuando lleguemos a esa frontera de implementación.
Ya tenemos dos relaciones hash diferentes. El árbol de Merkle compromete las transacciones dentro de un bloque. El hash de la cabecera anterior conecta cada bloque con su predecesor. Distinguirlas ayuda a entender qué demuestra una cabecera y qué no.
2.6 Una estructura completa, una elección pendiente
Podemos analizar un pago, verificar su autorización, actualizar un conjunto de salidas y comprometer un lote de pagos en una cabecera. También podemos detectar si un bloque propuesto encaja con un padre concreto. Nada de eso impide que dos bloques válidos extiendan el mismo padre casi a la vez.
El resultado es un árbol de historiales posibles. Un nodo necesita tanto reglas de validez para sus ramas como una regla para seleccionar la rama activa. A continuación dotaremos a DCoin de una prueba de trabajo y examinaremos qué aportan realmente su seguridad y sus incentivos.