Artículo del Blog

Experimentando con Multi-Firma

Autor

George Kudrayvtsev

Fecha de publicación

En el primero de una posible serie de artículos que juegan con las características únicas de la red de Stellar, discutiremos "multisig". Abreviatura de "multi-firma", esta característica, cuando se combina junto con el soporte de Stellar para una variedad de tipos de firma, habilita de forma nativa un poderoso conjunto de características. Los usuarios pueden preparar transacciones que:

  • son válidas después de resolver mecanismos avanzados de consenso multi-partido,
  • son válidas solo condicionalmente en el futuro,
  • establecen un compromiso con alguna información secreta,
  • crean estructuras de incentivo al estilo prueba de trabajo,
  • ¡y mucho más!

En este artículo, nos adentraremos en los detalles sobre cómo es posible esto y crearemos algunas demos exploratorias pequeñas para que fluyan las ideas creativas.

Ahora, vale la pena mencionar que este tema se cubre en la documentación para desarrolladores, pero la visión general allí es bastante superficial. Aunque definitivamente es un recurso útil y de referencia, entraremos en mucho más detalle en este post sobre cómo todo encaja.

Antecedentes: Firmas

En el pasado, las familias poseían "anillos de sello" y los usaban para estampar el sello de cera en una carta para "autenticar" a su autor1. Estos anillos eran únicos por ser un objeto físico, hecho a mano y relacionado directamente con el escudo de la familia1. Todos estamos familiarizados con el garabato que ahora llamamos firmas en el mundo real; firmamos recibos al salir a comer, documentos legales que vienen con comprar una casa o conseguir un trabajo, y tarjetas de cumpleaños para nuestros seres queridos. En general, están diseñadas como una forma de prestar autenticidad a una acción.

Sin embargo, como sabe cualquiera que tuviera una racha rebelde de niño, las firmas son bastante fáciles de falsificar. Hoy en día, no ofrecen muchas garantías (y tampoco lo harían los anillos de sello, con la impresión 3D y todo). A veces pongo caras sonrientes en mis cuentas de bar, y ni el bar ni el banco parecen importarles.

Firmas Digitales

Entra digital las firmas, el equivalente del mundo virtual. Y no estoy hablando de algo como HelloSign, sino de una construcción matemática rigurosa que proporciona garantías de autenticidad de manera eficiente y es imposible de falsificar.

Un esquema de firma digital tiene tres partes:

  • un par de claves, pública (pk, compartida con el mundo) y privada (sk, mantenida en secreto)
  • un algoritmo de firma, que básicamente usa una clave privada como pluma para firmar cualquier cosa arbitraria y crear una firma
  • un algoritmo de verificación, que usa la clave pública, la misma cosa arbitraria y la firma para asegurar que la clave privada correspondiente realmente firmó esa cosa para crear esa firma

Más formalmente,

KeyGen() → (pk, sk)

Sign(sk, x) → s

Verify(pk, x, s) → sí / no

Supongamos que tengo un documento (llamémoslo X) que quiero firmar para que el mundo lo vea. Usaría mi clave secreta (llamémosla sk) y lo firmaría:

sig = Sign(sk, X)

Luego, transmitiría X, sig, y pk (mi clave pública) al mundo de alguna manera. Cualquiera puede usar los tres para asegurarse de que Verify(pk, X, sig) tenga éxito: esta es prueba indiscutible de que yo (y por "yo", queremos decir "la identidad que posee el par de claves (pk, sk)") de hecho firmó ese documento.

Esquemas de Multi-firma

Las multi-firmas llevan la utilidad de las firmas digitales y la elevan a otro nivel. Uno de los esquemas de multi-firma más flexibles y poderosos se llama "m-de-n multisig". Esto significa: de n participantes, exactamente m de ellos tienen que firmar el valor para que la firma sea "válida".

Esta construcción es particularmente útil en un entorno multi-partido, donde los participantes independientes no se fían completamente entre sí (o no están completamente de acuerdo) pero necesitan coordinar una acción. Por ejemplo, podrías dar a cada uno de los Jueces del Tribunal Supremo una clave de firma y usar un esquema de multi-firma 4-de-7 para significar sus fallos.

También puedes usarlo en un entorno de un solo partido para hacer algo como la recuperación de cuenta. Genera un puñado de claves de respaldo y guárdalas en un lugar seguro, y entonces cualquier 1-de-n te permitiría usar tu cuenta incluso si pierdes tu "clave principal" secreta. Incluso podrías requerir más de una clave de respaldo para "reemplazar" la clave principal.

Multi-Firma en Otros Lugares

Hay otras blockchains que ofrecen multi-firmas de forma nativa, como Bitcoin, pero esto a menudo implica una configuración complicada y podría ser inmutable después de la configuración inicial. Algunas plataformas como Ethereum ofrecen multi-firma como una capa adicional a través de contratos inteligentes (como Gnosis), pero estas soluciones a medida pueden llevar a fallos de seguridad catastróficos como el encontrado encontrado en el contrato multi-firma de Parity.

Tener soporte nativo para multi-firmas es poderoso: significa seguridad, interoperabilidad y extensibilidad. Dado que las cuentas son ciudadanos de primera clase en Stellar y la multi-firma ha estado allí desde el Día 1, todo es fácil de usar y completamente personalizable.

Veamos los detalles de cómo funcionan las firmas en Stellar.

Multi-Sig en Stellar

En Stellar, las multi-firmas se logran combinando firmantes, sus pesos, y los umbrales de operación. Deberías comenzar con esta sección de los documentos para entender los umbrales, pero en resumen, puedes pensar en una transacción como válida si la combinación lineal de firmantes y sus respectivos pesos supera el umbral necesario de la operación:

signer_1 * weight_1 + signer_2 * weight_2 + ... >= umbral

El poder proviene del hecho de que todos estos parámetros pueden ser modificados por los propios firmantes de la cuenta, y esa capacidad de modificación misma puede ser controlada por umbrales.

Una cuenta, muchos firmantes

Cuando creas una cuenta Stellar, solo tiene un firmante—este es la llave maestra—y sin umbrales. Puedes añadir firmantes a una cuenta que tengan diferentes pesos, y los pesos de las firmas en una transacción deben superar el umbral para sus operaciones. Por ejemplo, si una cuenta tiene un umbral medio de 200 y tiene 5 firmantes con 50 de peso cada uno, necesitarías exactamente cuatro firmas para ejecutar una operación de umbral medio como un Pago.

Con soporte nativo para multisig, es realmente sencillo añadir otro firmante a una cuenta. Aprovechamos la operación SetOptions, por ejemplo.2

const sdk = require("stellar-sdk");
const newSigner = sdk.Keypair.random();
const setOptOp = sdk.Operation.setOptions({
 masterWeight: 255,
 signer: {
 ed25519PublicKey: newSigner.publicKey(),
 weight: 255,
 },
});

Nota que establecemos el peso del nuevo firmante en 255 (el máximo). Esto significa que el firmante tiene "plena fuerza", por lo que tiene los mismos privilegios que la llave maestra. Podemos reducir este peso hasta 1.

Puedes ir aquí para leer más sobre eso: en resumen, de manera similar usas SetOptions. Por ejemplo, vamos a establecer los umbrales de la cuenta en 100, 200 y 255 para operaciones de umbral bajo, medio y alto, respectivamente:

const threshOp = sdk.Operation.setOptions({
 lowThreshold: 100,
 mediumThreshold: 200,
 highThreshold: 255
});

Cada operación debe cumplir un umbral particular, y puedes usar estos para controlar qué operaciones son posibles por qué firmantes, incluyendo cuando los firmantes colaboran. Esto desbloqueará el avanzado comportamiento de consenso de múltiples partes que discutiremos más adelante.

Tipos de firma alternativos

Puede que hayas notado que especificamos un ed25519PublicKey al añadir el nuevo firmante. Si adivinaste que esto implica que hay otros tipos de firmantes, ¡adivinaste bien! Y ahí es donde entra el verdadero poder del soporte nativo de multi-firma. Los otros tipos de firmantes se describen en la entrada del glosario de Multifirma mencionado anteriormente, pero aquí hay un rápido resumen:

  • Transacciones pre-autorizadas: Puedes firmar una transacción "de antemano," tal que podría ser válida en el futuro. Para lograr esto, añades el hash de la transacción como un firmante en la cuenta. Entonces, cuando esa transacción se envía realmente a la red, actúa como si ya hubiera sido firmada por la cuenta!
  • Firmas de hash: Puedes añadir el hash de cualquier valor como el firmante a una cuenta. Entonces, el valor mismo puede ser usado como una firma. Por supuesto, una vez que se usa, se revela al mundo, por lo que puede ser usado por cualquiera.
  • Cargas firmadas: Puedes requerir que un firmante incluya la firma para una carga arbitraria. En cierto modo, esto es lo opuesto a una firma de hash: la carga es pública y necesitas la llave secreta para crear una firma, en lugar de que la carga sea secreta y el hash (comparable a la firma) sea público.

Las cargas firmadas son los firmantes más complicados del grupo, así que vamos a elaborar más sobre ellos. Comienzas con un par de claves, (pk, sk) y una carga P. Luego, configuras un firmante de "carga firmada" con (pk, P). Esto significa que Firma(sk, P) es una firma de transacción válida.

¡Podemos combinar multi-firmas y los diversos tipos de firmantes para hacer algunas cosas divertidas!

Estudios de Caso

Los siguientes estudios de caso serán bastante irreales, pero harán el trabajo al demostrar el comportamiento interesante que puede surgir de estos bloques de construcción primitivos.

Déjame enfatizar algo aquí: Estos "estudios de caso" son solo demostraciones de funcionalidad. Por favor, no los uses como bases para nada: ni la utilidad ni la seguridad se consideraron profundamente al crear estos ejemplos de juguete.

Consenso de Múltiples Partes, Sin DAObt

Demostramos cómo se puede configurar multi-sig para una sola cuenta anteriormente, pero hagamos algo incluso mejor que eso. Dado que una sola cuenta puede tener múltiples firmantes, y una sola transacción puede tener múltiples cuentas firmando, podemos tener mecanismos complicados como votación por mayoría usando grupos de gobernanza independientes.

Ahora, Stellar no tiene contratos inteligentes (¡aún!), pero aún podemos simular los mecanismos de votación de un DAO usando consenso de multi-firma y multi-parte.

Nuestro DAO será un poco limitado en su funcionalidad, restringido a hacer cosas posibles con la funcionalidad nativa de Stellar: votar para expulsar miembros, hacer pagos, etc. pero será un DAO de todos modos.

Tierra de DAO

Supongamos que nuestro DAO comienza con tres miembros, todos de acuerdo en financiar el DAO con 1000 lumens cada uno.

Deciden estructurar el DAO de tal manera que cada miembro (es decir, firmante) tenga peso proporcional a su contribución al DAO. Para empezar, los tres miembros tienen firmantes con peso de 85. Los fundadores también deciden que solo un voto de super-mayoría (2/3) puede ejecutar operaciones de alto umbral, mientras que todo lo demás se puede hacer con una mayoría simple:

// Supongamos que A, B, y C son los pares de claves de los miembros inaugurales.
const initDAO = [A, B, C].map((kp) => {
 sdk.Operation.setOptions({
 signer: {
 ed25519PublicKey: kp.publicKey(),
 weight: 85,
 }
});
}).concat([
 sdk.Operation.setOptions({
 lowThreshold: 128, // mayoría simple
 medThreshold: 128, // mayoría simple
 highThreshold: 170, // super mayoría
 masterWeight: 0,
 }),
]); 

Si todos (incluyendo la cuenta DAO) firman una transacción con este conjunto de operaciones, el DAO se establecerá con su estructura de votación inicial.

Partidos Políticos

Stellar tiene algunas barreras de seguridad fundamentales alrededor de multi-sig:

  1. Las cuentas están limitadas a tener 1000 sub-entradas.
  2. Una transacción solo puede tener 20 firmas.

Si tenemos un DAO "controlando" una cuenta, entonces, las “acciones” están limitadas a solo 20 votos. Ahora, George Washington puede habernos aconsejado en contra de la formación de partidos políticos, pero son inevitables en este caso: Debemos introducirlos para crear jerarquías de membresía en cascada que voten juntas para sortear estas limitaciones.

No entremos en eso aquí, sin embargo. Debería ser suficiente decir que podemos evitar la limitación teniendo miembros que sean firmantes en cuentas que son firmantes en cuentas que son... etc. con mayor autoridad en el DAO. Un poco como una democracia representativa, pero con más influencia directa en el “electorado”.

Membresía

La membresía en el DAO es controlada por los miembros existentes, y todos ajustan sus pesos para los nuevos miembros apropiadamente.

Para unirse al DAO, las personas deben ofrecer una transacción especial para firmar:

  • Primero, deben recalcular los nuevos pesos de cada miembro después de tener en cuenta su contribución a las reservas. Si el primer nuevo miembro ofreció 100 lumens, entonces, poseerían floor(100 / (3000 + 100)) = 3% de los derechos de voto, lo que significa que los pesos ahora son (82, 82, 82, 7).
  • Segundo, deben agregar una SetOptions(source: DAO, signer: {address, weight}) operación para cada miembro del DAO con el peso "reequilibrado" mencionado, incluidos ellos mismos.
  • Finalmente, añaden una operación de pago financiando el DAO para indicar su contribución propuesta.

Naturalmente, deben firmar esta transacción. Si suficientes miembros firman la transacción (es decir, una super mayoría, ya que SetOptions es una operación de alto umbral), entonces la transacción se valida y el nuevo miembro pasa a formar parte del DAO.

Tomando Acción

En cualquier momento dado, ahora, el DAO puede votar colectivamente con sus firmas para permitir que la cuenta gobernante tome acción. Pueden enviar todos sus fondos a un único destino (por ejemplo, hacer una donación), votar para expulsar miembros (es decir, eliminando su firmante), emitir activos (por ejemplo, poseer un activo podría representar alguna legislación), y más.

¡Este es el poder del consenso multi-partidario en acción!

Transacciones Pre-Autorizadas: Validez Futura

Los firmantes de transacciones pre-autorizadas permiten a las cuentas planificar comportamientos futuros.

Un ejemplo simple y útil es resolución de escrow: La cuenta de escrow podría pre-autorizar dos transacciones con el mismo número de secuencia: una que envía dinero a un destino, mientras que la otra lo envía a otro lugar. Dado que los números de secuencia deben ser únicos, se garantiza que como máximo una de las transacciones se ejecutará.

Esto también puede ser útil en descongelar suministros de activos, por ejemplo, al bloquear una cuenta que emite NFTs. Dado que bloquear cuentas (es decir, eliminar todos los firmantes) es permanente, puedes usar una transacción pre-autorizada que te permita "desbloquear" la cuenta en algún punto en el futuro. Esto podría usarse para expandir tu suministro de NFT después de una cierta fecha.

Como un ejemplo más divertido, podemos agregar algún comportamiento siniestro al "DAO" descrito anteriormente. Los tres fundadores iniciales decidieron que quieren agregar un plan de contingencia a su organización, dándoles el derecho de restablecer completamente los derechos de voto si quieren:

const builder = sdk.TransactionBuilder(daoAccount, {
 networkPassphrase: sdk.Network.TESTNET,
 fee: sdk.BASE_FEE
}).setTimeout(sdk.TIMEOUT_INFINITE);
 
initDAO.forEach(builder.addOperation);

Luego, establecen este hash de transacción como un firmante pre-autorizado en la cuenta del DAO antes de abrir la membresía:

const resetDAO = sdk.Operation.setOptions({
 signer: {
 preAuthTx: builder.build().hash(),
 weight: 255
 }
}); 

Por supuesto, un ciudadano responsable de DAO-Land cuestionaría por qué hay un firmante de transacción pre-autorizado misterioso en la cuenta gobernante antes de unirse, pero eso quitaría la diversión de este caso de estudio.

Firmas Hash: Compromisos

La idea criptográfica de un "esquema de compromiso" tiene algunas aplicaciones interesantes, y se pueden expandir a cualquier escenario humano donde necesites comprometerte de antemano sin revelar el compromiso en sí.

El esquema de compromiso más simple es uno donde revelas tu compromiso como c = H(x), donde x es "la cosa a la que te comprometiste" y H es una función hash criptográfica como SHA2, lo que hace c un "compromiso unidireccional". Esto significa que es imposible derivar x cuando solo tienes c. Por ejemplo, me dices que lanzaste una moneda y te comprometiste a un “resultado”. Entonces, puedo hacer una suposición sobre el resultado del lanzamiento, y puedes revelar a qué te comprometiste previamente sin poder engañarme sobre el resultado.

¡Eso es exactamente como las firmas hash en Stellar! Entonces, ¿qué podemos hacer con eso?

Pongámonos raros: usemos la red para hacer una plataforma de apuestas. No será completamente sin confianza, desafortunadamente, ya que los participantes aún necesitarán confiar en "La Casa", pero aún podemos divertirnos aprovechando algunos de los primitivos multi-firma de Stellar.

Elige un número, cualquier número

El juego de apuestas será un simple juego de "adivina el número", donde el número es de 4 bits de largo, permitiendo 24 = 16 valores diferentes. La cuenta de "La Casa" o "casino" C primero se compromete a un valor dándose a sí misma un firmante hash con el valor H(r || x) y un peso de 1.

Aquí, r es un valor generado aleatoriamente de 252 bits (un "nonce") y x es el valor real de 0-15 que obtendrá recompensas. La || sintaxis indica concatenación a nivel de bits, lo que simplemente significa unir r y x juntos en un solo valor de 256 bits.

Usamos un nonce aquí porque si solo usáramos x, alguien podría simplemente probar todos los valores posibles para x y aprender el valor (por ejemplo, "¿H(0) = x? No, entonces ¿H(1) = x?" y así sucesivamente). El nonce hace que el valor de entrada sea demasiado grande para forzarlo bruscamente de esa manera.[@portabletext/react] Unknown block type "span", specify a component for it in the `components.types` prop[@portabletext/react] Unknown block type "span", specify a component for it in the `components.types` prop

El firmante tiene un peso de 1 porque su único propósito es ser un compromiso público a x.

Haciendo apuestas

Ahora que el casino se ha comprometido a un valor, los participantes pueden hacer sus apuestas. Para hacerlo, los apostadores siguen un "apretón de manos" con el casino para establecer una cuenta de escrow. Involucra tres fases: dos fases de compromiso y una fase de ejecución.

Primero, el apostador B crea una cuenta de escrow E con su apuesta como saldo inicial. Luego, se compromete a una suposición particular creando un firmante hash H(s || y), donde s es un valor aleatorio diferente y y es la suposición (similar a lo que hizo el casino). Luego, crea una transacción con los siguientes efectos:

  • estableciendo todos los umbrales en 255
  • aumentando el peso de la clave maestra a 255
  • añadiendo su compromiso, el firmante hash H(s || y), con peso 1
  • añadiendo el compromiso del casino, H(r || x), como un firmante con peso 100
  • añadiendo al propio casino como un firmante con peso 100

En este punto, la cuenta está lista para el fideicomiso pero sigue estando bajo el control total del participante. Esto permite que el participante se retire y conserve sus fondos hasta que el casino haga su parte en el proceso.

El participante ahora pre-firma una transacción que cederá el control al casino si el casino coloca su contraapuesta en fideicomiso. La transacción hará:

  • que el casino ponga 16x la apuesta (ya que las probabilidades son de 16:1) en fideicomiso
  • eliminar la cuenta maestra de fideicomiso como firmante
  • establecer todos los umbrales de la cuenta de fideicomiso a 201

Para "pasar la antorcha" al casino, podría almacenar el hash de la transacción y su firma como entradas de datos en la cuenta de fideicomiso, pero una plataforma real simplemente usaría un canal de comunicación externo para esto.

Si el casino acepta los términos y el fideicomiso “se verifica,” añade su firma a la transacción y la envía a la red. Ahora, la cuenta de fideicomiso puede ser controlada por el casino si y solo si revela su elección comprometida, x, y si el apostador revela su suposición, y.

Aquí es donde entra la confianza: una vez que el apostador revela y, el casino tendría control total sobre la cuenta, por lo que podría barrer sus fondos.

Sin embargo, al hacerlo, hace público su propio juego sucio: no solo admite que conoce la suposición del apostador, y, también revela su propio valor oculto, x. Todos verán que si x=y, que el casino robó de la cuenta de fideicomiso. Esto reduce los incentivos para hacer trampa, ya que el casino solo puede hacer trampa una vez antes de que nadie quiera jugar más.

Distribuyendo ganancias

La fase final es donde el apostador revela su suposición. Al igual que un candidato electoral en la noche de elecciones, el apostador prepara dos transacciones: una para ganar, otra para perder.

Ambas contienen una operación de fusión de cuentas; en un caso, la fusión es hacia la cuenta del apostador, mientras que en el otro caso, la fusión es hacia la cuenta del casino. Estas transacciones se firman con s || y, revelando el nonce y la suposición del apostador. Una vez firmadas parcialmente, el casino puede elegir qué transacción firmar y enviar basándose en si la suposición del apostador es correcta o no.

¿Prueba de trabajo en Stellar?

El título es un poco de clickbait: no vamos a implementar un consenso de Nakamoto completo en la red. En cambio, podemos usar firmas hash para simular "recompensas de minería". Obviamente, no deberíamos incentivar a las personas a desperdiciar ciclos de CPU sin razón (por eso Stellar es una de las blockchains más ecológicas34), pero aún así es una demostración divertida.

Así es como funciona: Creamos una cuenta, la financiamos con algunos "premios" lumens, y le añadimos un firmante hash. Excepto que aquí está la parte divertida: la entrada x al firmante hash H(x) no es solo algún valor aleatorio, sino que en cambio se revela parcialmente al mundo. Por ejemplo, si revelamos los primeros 24 bytes de x al mundo (a través de un medio centralizado como Twitter, o incluso como una entrada de datos en la cuenta), quienquiera que pueda determinar los últimos 8 bytes puede obtener acceso parcial a la cuenta.

Le damos al firmante hash un peso de 200 y a la clave maestra un peso de 55. También establecemos todos los umbrales a 255, así que la única manera de que alguien pueda hacer algo con la cuenta es con ambos firmantes. Luego, revelamos una transacción T que modifica todos los umbrales a 200 que está solo firmada por el maestro. Si se envía, permitirá que el "ganador" tenga libre control sobre la cuenta y retire el premio.

Una vez que alguien ha "descifrado" los últimos 8 bytes del hash, pueden firmar la transacción anterior T, enviarla a la red, ¡y luego retirar su premio!

Conclusión

Como puedes ver, tener multi-firmas como una característica nativa de la red Stellar es poderoso. Es más seguro, flexible y eficiente en comparación con otras plataformas blockchain que dependen de soluciones de terceros para multi-firma. También pueden crecer en capacidades a medida que la red madura; por ejemplo, cargas de firma (que realmente no cubrimos aquí, pero son una parte importante de canales de pago) solo se lanzaron en Protocolo 19, pero obtienen todas las características multi-firma existentes del protocolo “de forma gratuita” como resultado de ser nativas.

Tener multi-firmas y una variedad de tipos de firmantes nos permite construir algunas cosas interesantes sin necesidad de contratos inteligentes completos. Por supuesto, las cosas que diseñamos aquí eran proyectos de juguete destinados a demostrar conceptos, pero eso solo resalta aún más el poder y la flexibilidad de la multi-firma en Stellar.

-----

1https://www.thehistorypress.co.uk/articles/a-brief-history-of-signet-rings/

2Nota: Por razones de brevedad, no incluyo código para verificar errores, preparar/firmar/enviar transacciones, etc. a menos que se desvíen de la norma. De lo contrario, son partes implícitas de la demostración que deberían ser autoexplicativas para cualquiera que construya en Stellar.

https://stellar.org/blog/developers/diving-into-energy-use-on-stellar-blockchain-payment-efficiency-examined

https://stellar.org/resources/sustainability-report