WEB3DEV Español

Hector
Hector

Posted on

Blockchatting: una dAPP de mensajerias Punto a Punto

Este artículo es una traducción de Erhan Tezcan, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.

¡Saludos! En esta publicación describiré cómo he construido una aplicación de chat descentralizada que se ejecuta en el blockchain.

Esta aplicación está basada en contratos inteligentes en vez de un protocolo de niveles inferiores como Ethereum Whisper, Waku, TransferChain o Status. Por lo tanto, es una aplicación menos óptima y que se hace, principalmente, para propósitos educacionales.

Los mensajes son encriptados extremo a extremo con una llave secreta, única para cada para. Sin embargo, cada mensaje es una transacción en la blockchain, lo cual es gracioso porque solía llamar a esta aplicación “Chatea para la gente Rica” ya que, por cada mensaje, debes pagar una tarifa. Esto no es un problema con los testnets.

Tendremos tres secciones en esta publicación:

  • Metodología
  • Contrato de Solidity
  • NextJS Frontend

¡Comenzemos!

Metodología

Queremos una aplicación para chatear donde los usuarios inicien la sesión con sus billeteras, hablen los unos con los otros con cifrado extremo a extremo, sin que haya un servidor central backend. Además, no queremos más ID si no la dirección del usuario.

Para la encriptación extremo a extremo, tendremos dos fases:

  • Inicialización del Usuario
  • Inicialización del Chat

Inicialización del Usuario

Cuando un usuario accede a la aplicación por primera vez, le preguntaremos al usuario por una inicialización. El usuario creará, aleatoriamente, una frase semilla aleatoria de 32 bytes, la cual será usada para generar un par de llaves públicas y privadas. Denotaremos esto como sk_chat y pk_chat respectivamente.

pk_chat puede ser derivado de una llave privada. Así que, si pudiéramos almacenar sk_chat de forma encriptada, entonces podríamos retirarla luego para, otra vez, derivar nuestras llaves. Sin embargo, ¿qué otra llave usaríamos para encriptar sk_chat?

Tenemos dos invocaciones MetaMask RPC en desuso para este propósito:

  • eth_decrypt que puede decodificar un mensaje con la llave privada EOA
  • eth_getEncryptionPublicKey que retira la llave pública EOA para que sea usada para la codificación.

Es importante volver a tomar en cuenta que ambas están en desuso, y puede que sean removidas en el futuro. Además, hacer estas invocaciones RPC requieren interacción del usuario a través de la cartera, lo cual no es bueno para el UX. De todos modos, podemos encriptar sk_chat con nuestra propia llave pública EOA, almacenada en el contrato, y ¡podremos decodificarla luego con nuestra llave privada EOA! Esto sólo debe hacerse una vez durante la aplicación, así que es un costo aceptable del UX.

Image description

Una vez completado, ¡el usuario almacena su sk_chat codificado y pk_chat en el contrato! Aprovecharemos para cobrar una pequeña tarifa de entrada para todo esto 💸💸.

Inicialización del chat

Ahora, supongamos que dos usuarios (digamos que Alice y Bob) hayan completado los pasos de la inicialización del usuario y les gustaría hablar entre sí. Observa que, para poder hacer esto, uno debe saber la dirección del otro. Asumiendo que saben sus direcciones, ¡se quieren asegurar que solo ellos puedan leer sus mensajes!

La encriptación simétrica encaja perfectamente aquí: es mucho más eficiente que usar la encriptación asimétrica (con sk_chat y pk_chatde antes). Aquí está la explicación: la primera vez que Alicia o Bob van a la pantalla del chat, deben inicializar la sesión del chat; supón que Alicia fue la primera. Ella genera una llave simétrica aleatoria de 32 bytes y encripta esta llave con ambas, su llave y la llave pública de Bob, que fue almacenada en la fase de la inicialización De esta forma, hay una llave secreta almacenada en el contrato que sólo Alicia y Bob pueden leer, ¡y esta llave es única para ellos!

Image description

Encriptación extremo a extremo

Ahora que los dos usuarios han acordado una llave simétrica secreta, la cual sólo ellos pueden acceder a través de sus llaves privadas. Ellos pueden comenzar a usar esta llave para codificar y decodificar sus mensajes.

Image description

Contrato Inteligente Solidity

La idea más importante es que cada mensaje sea almacenado en un registro de eventos:

event MessageSent(
  address indexed _from, // sender address
  address indexed _to,   // recipient address
  string _message,       // encrypted message
  uint256 _time          // UNIX timestamp
);

function sendMessage(
  string calldata ciphertext,
  address to,
  uint256 time
) external {
  if (!isChatInitialized(msg.sender, to)) {
    revert BlockchattingError(ErrChatNotInitialized);
  } 
  emit MessageSent(msg.sender, to, ciphertext, time);
}
Enter fullscreen mode Exit fullscreen mode

Un usuario retira sus mensajes, consultando esos eventos. También hay eventos emitidos de la inicialización del usuario y de la inicialización del chat:

struct UserInitialization {
  bytes encryptedUserSecret;
  bool publicKeyPrefix;
  bytes32 publicKeyX;
}

event UserInitialized(address indexed user);

function initializeUser(
  bytes calldata encryptedUserSecret,
  bool publicKeyPrefix,
  bytes32 publicKeyX
) external payable {
  if (isUserInitialized(msg.sender)) {
    revert BlockchattingError(ErrUserAlreadyInitialized);
  }
  if (msg.value != entryFee) {
    revert BlockchattingError(ErrIncorrectEntryFee);
  } 
  userInitializations[msg.sender] = UserInitialization(encryptedUserSecret, publicKeyPrefix, publicKeyX);
  emit UserInitialized(msg.sender);
}
Enter fullscreen mode Exit fullscreen mode
event ChatInitialized(address indexed initializer, address indexed peer);

function initializeChat(
  bytes calldata yourEncryptedChatSecret,
  bytes calldata peerEncryptedChatSecret,
  address peer
) external {
  if (!isUserInitialized(msg.sender)) {
    revert BlockchattingError(ErrUserNotInitialized);
  }
  if (!isUserInitialized(peer)) {
    revert BlockchattingError(ErrPeerNotInitialized);
  } 
  chatInitializations[msg.sender][peer] = yourEncryptedChatSecret;
  chatInitializations[peer][msg.sender] = peerEncryptedChatSecret;
  emit ChatInitialized(msg.sender, peer);
}
Enter fullscreen mode Exit fullscreen mode

También te percatarás del uso de errores de códigos personalizados. Estos son mucho más optimizados que usar require con mensajes personalizados, almacenados como hilos; en vez de esto, haremos lo siguiente:

uint8 constant ErrUserAlreadyInitialized = 1;
uint8 constant ErrChatNotInitialized = 2;
uint8 constant ErrUserNotInitialized = 3;
uint8 constant ErrPeerNotInitialized = 4; 
uint8 constant ErrIncorrectEntryFee = 5; 
error BlockchattingError(uint8 code);
Enter fullscreen mode Exit fullscreen mode

El cliente puede leer los significados de los errores del código aquí, los cuales son nombrados siguiendo las convenciones de nombres de los errores de variables Go.

El contrato en sí mismo es muy simple ya que, como puedes ver, es el lado del cliente el que tiene gran parte de la carga pesada. Puedes revisar el código madre del contrato aquí. También tiene pruebas implementadas con Hardhat + TypeScript.

NextJS Frontend

Sin duda, esta aplicación es una aplicación de una sola página y NextJS puede ser excesivo para esto. Lo usé, de todas formas, ¡pero esto puede que cambie en el futuro!

Para la conexión con la billetera, usaremos WAGMI. El contrato del chat está conectado con el contexto de React, para que todos los componentes puedan interactuar. Las interacciones del contrato también son revisadas a través de TypeChain. Estos tipos son creados en la fase del desarrollo del contrato, pero puedes copiarlos y pegarlos fácilmente en tu directorio types en el frontend, o donde sea que los almacenes.

Para los propósitos UI/UX, también asignamos cada dirección a un avatar y apodo generado al azar, ¡haciendo que las cosas sean más fáciles de leer! Un acercamiento similar es usado en el messenger de Status. También he usado MantineUI como el componente de mi librería, el cual recomiendo.

La aplicación, básicamente, tiene 3 fases:

  • UserInitialization es revisado y, es solicitado, si no está presente.
  • El usuario entonces ve sesiones antiguas del chat, consultando los eventos ChatInitalization con sus direcciones como parámetros. El usuario también puede crear una nueva sesión usando la dirección de la persona con la que quiera hablar.
  • Cuando el usuario comience una sesión con algún colega, puede consultar sus mensajes previos a través de los eventos MessageSent.

Puedes ver una demostración en vivo en https://blockchatting.vercel.app/ el cual usa Göerli testnet y, ¡siéntete libre de revisar nuestro código en https://github.com/erhant/blockchatting!

Feliz programación :)

Discussion (0)