Capítulo 04 de 10 · 13 min de lectura

Transacciones: billetes, no saldos

Bitcoin no sabe cuánto dinero tienes. No hay ninguna línea que diga «Ana: 75». Solo hay monedas sueltas, cada una con su candado, que se gastan enteras. Como los billetes.

No hay saldos: hay monedas

Lo natural sería que una blockchain guardase una tabla de saldos, como la del banco del capítulo 1, y que un pago restase de una fila y sumase en otra. Bitcoin no lo hace así, y xavicoin tampoco.

Lo que guarda es un montón de monedas sueltas. Cada una tiene dos datos, y nada más:

  • un importe: 50, 12.5, 0.0003
  • un candado: el hash de la clave pública de su dueño, que vimos en el capítulo anterior.

A cada una de estas monedas se la llama UTXO, las siglas en inglés de salida de transacción no gastada; enseguida se entiende por qué. Tu “saldo” no está escrito en ningún sitio: es la suma de las monedas cuyo candado abren tus claves. Lo calcula tu wallet; la cadena ni lo sabe ni le importa.

Una transacción destruye monedas y crea otras

Una transacción tiene dos listas:

  • Entradas: cada una señala una moneda que ya existe y aporta la prueba de que quien gasta es su dueño (su clave pública y una firma). Esas monedas dejan de existir.
  • Salidas: cada una es una moneda nueva, con su importe y su candado.

Y una regla de hierro: una moneda solo se puede gastar entera. Igual que no puedes arrancar un trozo de un billete de 50 €, no puedes gastar “12,5 de una moneda de 50”. La gastas toda y te devuelves el cambio a ti misma, en una segunda salida.

Si Ana tiene una moneda de 50 y quiere pagar 12,5 a Luis, su transacción es esta:

Entrada la moneda de 50 de Ana con su clave pública y su firma
Salida 0 12,5 con el candado de Luis
Salida 1 37,4999791 con el candado de Ana: el cambio
no aparece en ningún sitio 0,0000209 la comisión

Fíjate en la última fila. La comisión no se escribe: es lo que entra y no sale. Se la quedará el minero que incluya la transacción en un bloque (capítulo 7).

Una moneda no tiene número de serie propio. Se identifica por dónde nació: el ID de la transacción que la creó y su posición en la lista de salidas, por ejemplo 36a60931…:0. De ahí el nombre: una UTXO es una salida de una transacción anterior que todavía nadie ha usado como entrada.

Constrúyela tú

Esta es la wallet de Ana, con tres monedas. Lo que sale de aquí son transacciones reales de xavicoin: mismo formato binario, misma firma y mismas reglas que aplica un nodo de verdad. Paga a Luis, mira cómo cambia el panel 4… y luego intenta hacer trampa de las cuatro formas posibles.

1 · La wallet de Ana

Elige qué monedas gastar. Cada una se gasta entera.

    2 · La transacción

    Entradas · monedas que se destruyen

      Salidas · monedas que se crean

        O haz trampa

        3 · El nodo decide

        Aún no ha recibido ninguna transacción.

          4 · Todas las monedas que existen · el «conjunto UTXO»

          Cosas que merece la pena probar:

          • Paga 12,5 con la moneda de 50 y mira el panel 4: la moneda de 50 ya no existe. Hay una de 12,5 (de Luis) y una de 37,49… (de Ana). Nadie ha “restado” nada.
          • Intenta pagar 60 con solo la moneda de 50: faltan fondos. Marca también la de 20: ahora la transacción tiene dos entradas, cada una con su firma, y ocupa más bytes.
          • Pon la comisión a 0: todas las reglas de consenso se cumplen, pero el nodo no la quiere. El espacio en un bloque es escaso, y los nodos no propagan transacciones que no pagan nada.
          • Haz un pago y luego pulsa Reenviar el último pago. Esta es la defensa contra el doble gasto del capítulo 1, y es de una sencillez aplastante: las monedas que gastaba ya no existen.

          Las reglas de gasto

          Todo lo que ha hecho “el nodo” de la demo son cuatro comprobaciones. Son las mismas en Bitcoin, y en xavicoin están en una sola función:

          // internal/chain/validate.go — CheckTxInputs (resumido)
          var totalIn uint64
          for i, in := range tx.Inputs {
          	utxo, err := view.GetUTXO(in.PrevOut)
          	if utxo == nil {                                              // 1. la moneda existe y está sin gastar
          		return 0, &MissingInputError{OutPoint: in.PrevOut}
          	}
          	if crypto.PubKeyHash(crypto.Hash160(in.PubKey)) != utxo.Out.PubKeyHash {
          		return 0, ruleErrorf("la clave pública de la entrada %d no es la del dueño de la salida", i)
          	}                                                             // 2. quien gasta tiene la llave del candado
          	if !crypto.Verify(in.PubKey, tx.SigHash(i, utxo.Out), in.Signature) {
          		return 0, ruleErrorf("firma inválida en la entrada %d", i)
          	}                                                             // 3. y ha firmado ESTA transacción
          	totalIn += utxo.Out.Value
          }
          var totalOut uint64
          for _, out := range tx.Outputs {
          	totalOut += out.Value
          }
          if totalOut > totalIn {                                           // 4. no se crea dinero
          	return 0, ruleErrorf("las salidas (%d) superan a las entradas (%d)", totalOut, totalIn)
          }
          return totalIn - totalOut, nil                                    // lo que sobra es la comisión

          Cuando una transacción entra en un bloque, el conjunto UTXO cambia de la forma más simple posible: se borran las monedas que gasta y se añaden las que crea.

          Qué cubre la firma, exactamente

          En la demo, desviar el pago a Marta después de firmar no funciona. La razón está en qué se firma. No es “12,5 para Luis”, sino un hash de toda la transacción:

          // internal/core/tx.go — SigHash (comentarios añadidos aquí)
          func (tx *Tx) SigHash(idx int, spent TxOut) crypto.Hash {
          	var w writer
          	tx.serialize(&w, false) // todas las entradas y todas las salidas, sin las firmas
          	w.u32(uint32(idx))      // qué entrada se está firmando
          	w.u64(spent.Value)      // y qué moneda gasta: importe…
          	w.raw(spent.PubKeyHash[:]) // …y candado
          	return crypto.Hash256(w.buf)
          }

          Cambiar un destinatario, un importe o una entrada cambia ese hash, y la firma deja de valer. Una transacción firmada es un bloque de granito: se acepta tal cual o se rechaza, pero no se puede retocar.

          Las firmas, lógicamente, no pueden formar parte de lo que firman. En xavicoin tampoco forman parte del ID de la transacción, que se calcula sobre esos mismos datos sin firmas. En el Bitcoin original sí entraban, y eso permitía a un tercero alterar ligeramente una firma ajena (sin invalidarla) y cambiarle el ID a una transacción en tránsito. Se llamó maleabilidad, dio problemas muy reales —la casa de cambio Mt. Gox le echó la culpa al suspender retiradas en 2014— y no se arregló del todo hasta 2017, con SegWit. xavicoin nace con esa lección aprendida.

          Por qué monedas y no saldos

          Parece más complicado que una tabla de saldos (que es, por cierto, lo que usa Ethereum). Tiene varias ventajas que para Bitcoin pesan más:

          • El doble gasto es trivial de detectar. Una moneda existe o no existe. No hay que razonar sobre el orden de varias operaciones contra un mismo saldo.
          • Un pago no se puede repetir. Con saldos, un “paga 10 a Luis” firmado podría reenviarse una y otra vez mientras haya fondos, y hay que inventar contadores para impedirlo. Aquí el pago nombra las monedas concretas que gasta, y solo puede ocurrir una vez.
          • Se valida en paralelo. Dos transacciones que gastan monedas distintas no se afectan: se pueden comprobar a la vez, en cualquier orden.
          • Algo más de privacidad. Cada moneda puede ir a una dirección distinta. Una wallet seria genera una dirección nueva para cada cobro y para cada cambio, de modo que desde fuera no es evidente qué monedas son de la misma persona.

          El precio es que la wallet trabaja más: tiene que elegir qué monedas gastar. La de xavicoin hace lo más sencillo: ordena de mayor a menor y va cogiendo hasta cubrir el importe y la comisión.

          // internal/wallet/wallet.go — BuildPayment (fragmento)
          // Primero las salidas grandes: menos entradas, transacción más pequeña.
          sort.Slice(available, func(i, j int) bool { return available[i].Out.Value > available[j].Out.Value })
          
          for _, s := range available {
          	selected = append(selected, s)
          	total += s.Out.Value
          	fee = feeRate * uint64(txOverhead+len(selected)*inputSize+2*outputSize)
          	if total >= amount+fee {
          		break
          	}
          }
          // …
          if change := total - amount - fee; change >= dustLimit {
          	tx.Outputs = append(tx.Outputs, core.TxOut{Value: change, PubKeyHash: selected[0].Out.PubKeyHash})
          }

          La última condición tiene nombre: polvo (dust). Un cambio de unas pocas unidades costaría más en comisiones gastarlo de lo que vale, así que no se crea esa moneda y el pico se deja de propina.

          Dinero en enteros

          Todos los importes de la demo llevan decimales, pero por dentro no existe ningún decimal. Una moneda son 100.000.000 unidades indivisibles (en Bitcoin se llaman satoshis), y todas las cuentas se hacen con números enteros:

          // internal/core/tx.go
          const Coin = 100_000_000

          No es una manía. Los números con coma de los ordenadores no pueden representar exactamente valores como 0,1. En casi cualquier lenguaje, 0.1 + 0.2 da 0.30000000000000004. En un sistema donde miles de nodos tienen que llegar exactamente al mismo resultado, y donde la regla 4 compara sumas, un error en el decimal diecisiete sería una catástrofe: dinero creado o destruido por redondeo.

          Lo que viene

          Hay una excepción a “no se crea dinero”, y tiene que haberla, o nunca habría existido la primera moneda: la transacción especial con la que cada bloque paga a su minero. Pero antes hay que ver el bloque. Tenemos pagos válidos sueltos por la red; falta lo que resolvía el problema del capítulo 1: ponerlos en un orden que todos acepten y que nadie pueda reescribir.