WEB3DEV

Cover image for Blockchatting: Uma dApp de Mensagem Peer-to-Peer
Fatima Lima
Fatima Lima

Posted on • Atualizado em

Blockchatting: Uma dApp de Mensagem Peer-to-Peer

Saudações! Neste post, descreverei como construí um aplicativo de bate-papo descentralizado que funciona na blockchain.

Este aplicativo é baseado em contratos inteligentes, ao invés de um protocolo de nível mais baixo, como Ethereum Whisper, Waku, TransferChain, or Status. Como tal, é um aplicativo não otimizado, feito principalmente para fins educacionais.

As mensagens são criptografadas de ponta a ponta com uma chave secreta única para cada par. Entretanto, cada mensagem é uma transação na blockchain; o que é engraçado o suficiente para que eu chamasse este aplicativo de "Chat for Rich People" (Chat para Pessoas Ricas), já que para cada mensagem você deve pagar uma taxa de transação. Não é um problema para as testnets, no entanto.

Teremos três seções neste post:

Metodologia
Contrato Solidity
Frontend NextJS

Vamos começar!


Sumário

1 . Metodologia
..... . Inicialização do usuário
..... . Inicialização do Chat
..... . Criptografia de ponta a ponta

2 . Contrato Inteligente Solidity

3 . Frontend NextJS


Metodologia

Queremos um aplicativo de bate-papo onde os usuários façam login com suas carteiras, conversem uns com os outros com criptografia de ponta a ponta, sem nenhum servidor backend central. Além disso, não queremos nenhuma outra identificação além do endereço do usuário.

Para a criptografia de ponta a ponta, teremos duas fases:

Inicialização do usuário
Inicialização do Chat

Inicialização do usuário

Quando um usuário entra no aplicativo pela primeira vez, pedimos ao usuário uma inicialização. O usuário criará uma frase-semente aleatória de 32 bytes, que será usada para gerar um par de chaves privadas e públicas. Denotamos isto como sk_chat e pk_chat, respectivamente.

pk_chat pode ser derivada da chave privada. Assim, se pudéssemos armazenar a sk_chat com criptografia, então poderíamos recuperá-la mais tarde para derivar nossas chaves novamente. Entretanto, que outra chave usaríamos para criptografar a sk_chat?

Temos duas chamadas obsoletas da MetaMask RPC para este fim:

eth_decrypt que pode decodificar uma mensagem com a chave privada EOA.
eth_getEncryptionPublicKey que recupera a chave pública EOA a ser usada para criptografia.

É importante observar novamente que estas são obsoletas, e podem ser removidas no futuro. Além disso, fazer estas chamadas RPC exige a interação do usuário através da carteira, o que não é bom para a UX (User Experience). De qualquer forma, podemos criptografar sk_chat com nossa própria chave pública EOA, armazená-la no contrato, e depois decodificá-la mais tarde com nossa chave privada EOA! Isto precisa ser feito apenas uma vez durante a aplicação, por isso é um custo de UX aceitável.

Image description

Após a conclusão, o usuário armazena sua sk_chat criptografada, e a pk_chat simples no contrato! Ah, e também cobramos uma pequena taxa de entrada por tudo isso 💸💸.

Inicialização do Chat

Agora suponha que dois usuários (digamos Alice e Bob) tenham completado as etapas de inicialização do usuário e que gostariam de conversar um com o outro. Note que para fazer isso, um precisa conhecer o endereço do outro. Supondo que eles saibam seus endereços, eles querem garantir que somente eles possam ler suas mensagens!

A criptografia simétrica se encaixa perfeitamente aqui: é muito mais eficiente do que usar a criptografia assimétrica (com a sk_chat e a pk_chat de antes). Eis como: a primeira vez que ou Alice ou Bob chega à tela de bate-papo, eles devem inicializar a sessão de bate-papo; suponha que Alice tenha sido a primeira a chegar lá. Ela gera uma chave aleatória simétrica de 32 bytes e criptografa esta chave com chave pública dela e do Bob que foi armazenada a partir da fase de inicialização do usuário. Desta forma, há uma chave secreta armazenada no contrato que somente Alice e Bob podem ler, e esta chave é única apenas para eles dois!

Image description

Criptografia de ponta a ponta

Agora que dois usuários concordaram com uma chave simétrica secreta, que só eles podem acessar através de suas chaves privadas, eles podem começar a usar esta chave para criptografar e decodificar suas mensagens.

Image description

Contrato Inteligente Solidity

A idéia chave é que cada mensagem seja armazenada como um registro de eventos:

event MessageSent(
  address indexed _from, // endereço do remetente
  address indexed _to,   // endereço do destinatário
  string _message,       // mensagem criptografada
  uint256 _time          // timestamp UNIX 
);

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

e um usuário recupera suas mensagens consultando esses eventos. Há também eventos emitidos por inicializações de usuários e inicializações de 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);
}

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

Você também pode observar o uso de códigos de erro personalizados. Estes são muito mais otimizados do que o uso de mensagens personalizadas armazenadas como strings; em vez disso, fazemos o seguinte:

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

O cliente pode ler os significados dos códigos de erro daqui, que são nomeados de acordo com as convenções de nomes de variáveis de erro da linguagem Go.

O contrato em si é bastante simples, como você pode ver, está no lado do cliente onde grande parte do trabalho pesado prossegue. Você pode verificar o código fonte do contrato aqui. Também tem testes implementados com Hardhat + TypeScript.

Frontend NextJS

É verdade que este aplicativo é de uma única página e o NextJS poderia ser um pouco exagerado para isto. Eu o usei mesmo assim, por causa de mudanças no futuro!

Para a conexão da carteira, utilizamos WAGMI. O contrato de chat é conectado dentro de um contexto React, de modo que todos os componentes possam interagir com ele. As interações do contrato também são verificadas por meio de TypeChain. Estes tipos são criados na fase de desenvolvimento do contrato, mas você pode facilmente copiá-los e colá-los em seu diretório types no frontend ou onde quer que você os armazene.

Para fins de UI/UX, também mapeamos cada endereço para algum apelido e avatar gerado aleatoriamente, tornando as coisas muito mais legíveis! Uma abordagem similar é usada no mensageiro Status. Eu também usei a MantineUI como minha biblioteca de componentes, o que eu recomendo fortemente.

O aplicativo tem basicamente 3 fases:

A UserInitialization é verificada, e é solicitada caso não esteja presente.

O usuário então vê a sessão de bate-papo anterior consultando os eventos de ChatInitialization com seu endereço como um parâmetro. Também pode ser criada uma nova sessão inserindo o endereço da pessoa com quem ele gostaria de falar.
Ao iniciar uma sessão de bate-papo com algum colega, ele pode consultar suas mensagens anteriores através de eventos MessageSent.

Você pode ver uma demonstração ao vivo em https://blockchatting.vercel.app/ que utiliza a testnet Göerli e sinta-se à vontade para verificar o código em https://github.com/erhant/blockchatting!

Feliz codificação :)

Esse artigo foi escrito por Erhan Tezcan e traduzido por Fátima Lima. O original pode ser lido aqui.

Top comments (0)