¿Cómo funciona un mensajero descentralizado en la cadena de bloques

A principios de 2017, comenzamos a crear un mensajero en la cadena de bloques [el nombre y el enlace están en el perfil] discutiendo las ventajas sobre los mensajeros P2P clásicos.

Pasaron 2.5 años y pudimos confirmar nuestro concepto: las aplicaciones de mensajería instantánea para iOS, Web PWA, Windows, GNU / Linux, Mac OS y Android ya están disponibles.

Hoy le diremos cómo se organiza el messenger en la cadena de bloques y cómo las aplicaciones cliente pueden funcionar con su API.


Queríamos que la cadena de bloques resolviera los problemas de seguridad y privacidad de los mensajeros P2P clásicos:

  • Un clic para crear una cuenta: sin teléfonos ni correos electrónicos, sin acceso a libretas de direcciones y geolocalizaciones.
  • Los interlocutores nunca establecen conexiones directas, toda comunicación pasa a través de un sistema distribuido de nodos. Las direcciones IP de los usuarios no son accesibles entre sí.
  • Todos los mensajes están encriptados End-to-End curve25519xsalsa20poly1305. Parece que no sorprenderá a nadie, pero tenemos código fuente abierto.
  • El ataque MITM está excluido: cada mensaje es una transacción y está firmado por Ed25519 EdDSA.
  • El mensaje cae en su bloque. La secuencia y la timestamp bloques no se pueden corregir y, por lo tanto, el orden de los mensajes.
  • "No dije esto" no funcionará con los mensajes en la cadena de bloques.
  • No existe una estructura central que verifique la "autenticidad" de un mensaje. Esto se realiza mediante un sistema de nodo distribuido basado en consenso y pertenece a los usuarios.
  • Imposibilidad de censura: las cuentas no se pueden bloquear y los mensajes se eliminan.
  • La cadena de bloques 2FA es una alternativa a la infernal 2FA por SMS, que ha roto mucha salud.
  • La capacidad de obtener todos sus cuadros de diálogo desde cualquier dispositivo en cualquier momento es la capacidad de no almacenar cuadros de diálogo localmente.
  • Mensaje de confirmación de entrega. No al dispositivo del usuario, sino a la red. De hecho, esto es una confirmación de la capacidad del destinatario para leer su mensaje. Esta es una característica útil para enviar notificaciones críticas.

De los bollos blockchain, también hay una estrecha integración con las criptomonedas Ethereum, Dogecoin, Lisk, Dash, Bitcoin (esto todavía está en proceso) y la capacidad de enviar tokens en los chats. Incluso hicimos un intercambiador de cifrado incorporado.

Y luego, cómo funciona todo.

El mensaje es una transacción.


Todos ya están acostumbrados al hecho de que las transacciones en la cadena de bloques transfieren tokens (monedas) de un usuario a otro. Como bitcoin Hemos creado un tipo especial de transacción para enviar mensajes.

Para enviar un mensaje en el messenger en la cadena de bloques, debe pasar por varias etapas:

  1. Cifrar texto del mensaje
  2. Poner texto cifrado en la transacción
  3. Firmar transacción
  4. Enviar una transacción a cualquier host
  5. Un sistema distribuido de nodos determina la "confiabilidad" de un mensaje
  6. Si todo está bien, la transacción con el mensaje se incluye en el siguiente bloque.
  7. El destinatario recupera la transacción del mensaje y descifra

Las etapas 1-3 y 7 se realizan localmente en el cliente y 5-6 en los nodos de la red.

Cifrado de mensajes


El mensaje se cifra con la clave privada del remitente y la clave pública del destinatario. Tomaremos la clave pública de la red, pero para esto la cuenta del destinatario debe ser inicializada, es decir, tener al menos una transacción. Puede usar la solicitud REST GET /api/accounts/getPublicKey?address={ADAMANT address} , y cuando descargue chats, las claves públicas de los interlocutores ya estarán disponibles.



El mensajero cifra los mensajes con el algoritmo curve25519xsalsa20poly1305 ( NaCl Box ). Dado que la cuenta contiene claves Ed25519, para formar un cuadro, las claves primero deben convertirse a Curve25519 Diffie-Hellman.

Aquí hay un ejemplo en JavaScript:

 /** * Encodes a text message for sending to ADM * @param {string} msg message to encode * @param {*} recipientPublicKey recipient's public key * @param {*} privateKey our private key * @returns {{message: string, nonce: string}} */ adamant.encodeMessage = function (msg, recipientPublicKey, privateKey) { const nonce = Buffer.allocUnsafe(24) sodium.randombytes(nonce) if (typeof recipientPublicKey === 'string') { recipientPublicKey = hexToBytes(recipientPublicKey) } const plainText = Buffer.from(msg) const DHPublicKey = ed2curve.convertPublicKey(recipientPublicKey) const DHSecretKey = ed2curve.convertSecretKey(privateKey) const encrypted = nacl.box(plainText, nonce, DHPublicKey, DHSecretKey) return { message: bytesToHex(encrypted), nonce: bytesToHex(nonce) } } 

Formación de una transacción con un mensaje.


Una transacción tiene la siguiente estructura general:

 { "id": "15161295239237781653", "height": 7585271, "blockId": "16391508373936326027", "type": 8, "block_timestamp": 45182260, "timestamp": 45182254, "senderPublicKey": "bd39cc708499ae91b937083463fce5e0668c2b37e78df28f69d132fce51d49ed", "senderId": "U16023712506749300952", "recipientId": "U17653312780572073341", "recipientPublicKey": "23d27f616e304ef2046a60b762683b8dabebe0d8fc26e5ecdb1d5f3d291dbe21", "amount": 204921300000000, "fee": 50000000, "signature": "3c8e551f60fedb81e52835c69e8b158eb1b8b3c89a04d3df5adc0d99017ffbcb06a7b16ad76d519f80df019c930960317a67e8d18ab1e85e575c9470000cf607", "signatures": [], "confirmations": 3660548, "asset": {} } 

Para el mensaje de transacción, el asset es lo más importante: debe colocar el mensaje en él en el objeto de chat con la estructura:

  • message : guarda el mensaje cifrado
  • own_message - nonce
  • type - tipo de mensaje

Los mensajes también se dividen en tipos. Esencialmente, el parámetro type le dice cómo entender el message . Puede enviar solo texto, o puede enviar un objeto con intereses interesantes en su interior; por ejemplo, así es como el mensajero realiza transferencias de criptomonedas en las salas de chat.

Como resultado, formamos la transacción:

 { "transaction": { "type": 8, "amount": 0, "senderId": "U12499126640447739963", "senderPublicKey": "e9cafb1e7b403c4cf247c94f73ee4cada367fcc130cb3888219a0ba0633230b6", "asset": { "chat": { "message": "cb682accceef92d7cddaaddb787d1184ab5428", "own_message": "e7d8f90ddf7d70efe359c3e4ecfb5ed3802297b248eacbd6", "type": 1 } }, "recipientId": "U15677078342684640219", "timestamp": 63228087, "signature": "  " } } 

Firma de transacción


Para que todos puedan estar seguros de la autenticidad del remitente y del destinatario, en el momento del envío y del contenido del mensaje, se firma la transacción. Una firma digital le permite verificar la autenticidad de una transacción utilizando una clave pública; no se necesita una clave privada para esto.

Pero la firma en sí solo es realizada por la clave privada:



Se puede ver en el diagrama que primero hacemos el hash de la transacción con SHA-256, y luego firmamos Ed25519 EdDSA y obtenemos la firma de la signature , y el identificador de la transacción es parte del hash SHA-256.

Ejemplo de implementación:


1 - Formamos un bloque de datos, que incluye un mensaje

 /** * Calls `getBytes` based on transaction type * @see privateTypes * @implements {ByteBuffer} * @param {transaction} trs * @param {boolean} skipSignature * @param {boolean} skipSecondSignature * @return {!Array} Contents as an ArrayBuffer. * @throws {error} If buffer fails. */ adamant.getBytes = function (transaction) { ... switch (transaction.type) { case constants.Transactions.SEND: break case constants.Transactions.CHAT_MESSAGE: assetBytes = this.chatGetBytes(transaction) assetSize = assetBytes.length breakdefault: alert('Not supported yet') } var bb = new ByteBuffer(1 + 4 + 32 + 8 + 8 + 64 + 64 + assetSize, true) bb.writeByte(transaction.type) bb.writeInt(transaction.timestamp) ... bb.flip() var arrayBuffer = new Uint8Array(bb.toArrayBuffer()) var buffer = [] for (var i = 0; i < arrayBuffer.length; i++) { buffer[i] = arrayBuffer[i] } return Buffer.from(buffer) } 

2 - Consideramos SHA-256 del bloque de datos

 /** * Creates hash based on transaction bytes. * @implements {getBytes} * @implements {crypto.createHash} * @param {transaction} trs * @return {hash} sha256 crypto hash */ adamant.getHash = function (trs) { return crypto.createHash('sha256').update(this.getBytes(trs)).digest() } 

3 - Firmamos la transacción

 adamant.transactionSign = function (trs, keypair) { var hash = this.getHash(trs) return this.sign(hash, keypair).toString('hex') } /** * Creates a signature based on a hash and a keypair. * @implements {sodium} * @param {hash} hash * @param {keypair} keypair * @return {signature} signature */ adamant.sign = function (hash, keypair) { return sodium.crypto_sign_detached(hash, Buffer.from(keypair.privateKey, 'hex')) } 

Enviar una transacción con un mensaje a un host


Dado que la red está descentralizada, cualquiera de los nodos con una API abierta funcionará. Hacemos una solicitud POST para el punto final de la api/transactions :

 curl 'api/transactions' -X POST \ -d 'TX_DATA' 

En respuesta, obtenemos un ID de transacción de tipo

 { "success": true, "nodeTimestamp": 63228852, "transactionId": "6146865104403680934" } 

Validación de transacciones


Un sistema distribuido de nodos basado en consenso determina la "confiabilidad" de una transacción de mensaje. De quién y a quién, cuándo, si el mensaje fue reemplazado por otro y si la hora de envío se indicó correctamente. Esta es una ventaja muy importante de la cadena de bloques: no hay una estructura central responsable de las verificaciones, y la secuencia de mensajes y sus contenidos no pueden ser falsificados.

Primero, un nodo verifica la confiabilidad y luego lo envía a otros; si la mayoría dice que todo está en orden, la transacción se incluirá en el siguiente bloque de la cadena, esto es un consenso.



La parte del código de host responsable de la validación se puede ver en GitHub: validator.js y verificar.js . Sí, el nodo se ejecuta en Node.js.

Incluir transacción con mensaje en bloque


Si se llega a un consenso, la transacción con nuestro mensaje caerá en el siguiente bloque, junto con otras transacciones confiables.

Los bloques tienen una secuencia estricta, y cada bloque posterior se forma sobre la base de hashes de bloques anteriores.



La conclusión es que nuestro mensaje también se incluye en esta secuencia y no se puede "reorganizar". Si varios mensajes caen en el bloque, su orden estará determinado por la timestamp mensajes.

Leer mensajes


La aplicación de mensajería recupera las transacciones de la cadena de bloques que se envían al destinatario. Para hacer esto, hicimos el punto final api/chatrooms .

Todas las transacciones están disponibles para todos: puede recibir mensajes cifrados. Pero solo el destinatario puede descifrar con su clave privada y la clave pública del remitente:

 ** * Decodes the incoming message * @param {any} msg encoded message * @param {string} senderPublicKey sender public key * @param {string} privateKey our private key * @param {any} nonce nonce * @returns {string} */ adamant.decodeMessage = function (msg, senderPublicKey, privateKey, nonce) { if (typeof msg === 'string') { msg = hexToBytes(msg) } if (typeof nonce === 'string') { nonce = hexToBytes(nonce) } if (typeof senderPublicKey === 'string') { senderPublicKey = hexToBytes(senderPublicKey) } if (typeof privateKey === 'string') { privateKey = hexToBytes(privateKey) } const DHPublicKey = ed2curve.convertPublicKey(senderPublicKey) const DHSecretKey = ed2curve.convertSecretKey(privateKey) const decrypted = nacl.box.open(msg, nonce, DHPublicKey, DHSecretKey) return decrypted ? decode(decrypted) : '' } 

Que mas


Dado que los mensajes se entregan de esta manera durante aproximadamente 5 segundos, es decir, cuando apareció un nuevo bloque de red, se nos ocurrió una conexión de socket de nodo a cliente y de nodo a nodo. Cuando un nodo recibe una nueva transacción, verifica su validez y la transfiere a otros nodos. La transacción está disponible para los clientes de mensajería incluso antes del inicio del consenso y la inclusión en el bloque. Entonces entregaremos mensajes al instante, así como los mensajeros habituales.

Para almacenar la libreta de direcciones, creamos KVS: el almacenamiento de valores clave es otro tipo de transacción en la que un asset se cifra no con NaCl-box, sino con NaCl-secretbox . Entonces el mensajero almacena otros datos.

La transferencia de archivos / imágenes y los chats grupales aún requieren mucho trabajo. Por supuesto, en el formato tyap-bloop, esto se puede ajustar rápidamente, pero queremos mantener el mismo nivel de privacidad.

Sí, aún queda trabajo por hacer; idealmente, la privacidad real implica que los usuarios no se conectarán a los nodos de la red pública, sino que crearán los suyos propios. ¿Qué piensas, cuánto porcentaje de usuarios hace esto? Así es, 0. Parcialmente, logramos resolver este problema con la versión Tor del messenger.

Hemos demostrado que puede existir un mensajero en la cadena de bloques. Anteriormente, solo había un intento en 2012: el mensaje de bits , que falló debido al largo tiempo de entrega del mensaje, la carga de la CPU y la falta de aplicaciones móviles.

Pero el escepticismo está relacionado con el hecho de que los mensajeros en la cadena de bloques están adelantados: las personas no están listas para asumir la responsabilidad de su cuenta, la propiedad de la información personal aún no es una tendencia y la tecnología no permite garantizar altas velocidades en la cadena de bloques. A continuación aparecerán más análogos tecnológicos de nuestro proyecto. Ya veras.

Source: https://habr.com/ru/post/467751/


All Articles