Blockchatting: A Peer-to-Peer Messaging dApp
Tags: webdev, showdev, blockchain, crypto
Published: 2022-12-01 09:50:57

Greetings! In this post, I will describe how I have built a decentralized chatting application that runs on the blockchain.

This application is smart-contract based, rather than a more lower-level protocol such as [Ethereum Whisper](https://eips.ethereum.org/EIPS/eip-627), [Waku](https://waku.org/), [TransferChain](https://transferchain.io/), or [Status](https://status.im/). As such, it is a much non-optimal application and is mostly done for educational purposes.

Messages are end-to-end encrypted with a secret key unique to each peer. However, each message is a transaction on the blockchain; which is funny enough that I used to call this app "Chat for Rich People" since for each message you must pay a transaction fee. Not an issue for the testnets though.

We will have three sections in this post:
- Methodology
- Solidity Contract
- NextJS Frontend

Let's begin!

## Methodology
We want a chatting application where users login with their wallets, talk to each other with end-to-end encryption, with no central backend server whatsoever. Furthermore, we want no further ID than the user address.

For the end-to-end encryption, we will have two phases:
- User Initialization
- Chat Initialization

### User Initialization
When a user comes to the application for the first time, we ask the user for an initialization. The user will create a random 32-byte seed phrase, which will be used to generate a private & public key-pair. We denote this as `sk_chat` and `pk_chat` respectively.

`pk_chat` can be derived from the private key. So, if we could store the `sk_chat` with encryption, then we could retrieve it later to derive our keys again. However, what other key would we use to encrypt that `sk_chat`?

We have two deprecated MetaMask RPC calls for this purpose: 
- [`eth_decrypt`](https://docs.metamask.io/guide/rpc-api.html#eth-decrypt-deprecated) that can decrypt a message with the EOA private key.
- [`eth_getEncryptionPublicKey`](https://docs.metamask.io/guide/rpc-api.html#eth-getencryptionpublickey-deprecated) that retrieves the EOA public key to be used for encryption.

It is important to note again that these are deprecated, and might be removed in future. Furthermore, making these RPC calls require user interaction via the wallet, which is not good for UX. Anyways, we can encrypt `sk_chat` with our own EOA public key, store it in the contract, and then decrypt it later with our EOA private key! This needs to be done only once during the application, so it is an acceptable UX cost.

![user initialization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b4l3nchjxt269y8450lv.PNG)

Upon completion, the user stores their encrypted `sk_chat`, and plain `pk_chat` in the contract! Oh, and we also take a small bit of entry fee for all this 💸💸.

### Chat Initialization
Now suppose that two users (say Alice and Bob) have completed the user initialization steps, and they would like to talk to each other. Note that to do this, one needs to know another's address. Assuming they do know their addresses, they want to ensure that only they can read their messages! 

Symmetric encryption suits perfectly here: it is a lot more efficient than using asymmetric encryption (with `sk_chat` and `pk_chat` from before). Here is how: the first time either Alice or Bob comes to the chat screen, they must initialize the chatting session; suppose Alice was there the first. She generates a random 32-byte symmetric key, and encrypt this key with both her and Bob's public key that was stored from the user initialization phase. This way, there is a secret key stored in the contract that only Alice and Bob can read, and this key is unique to these two only!

![chat initialization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wqotyuleq3agyrow10ga.PNG)

### End-to-End Encryption
Now that two users have agreed upon a secret symmetric key, which only they can access via their private keys, they can start using this key to encrypt & decrypt their messages. 

![end-to-end encryption](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j2aa13jcz0loyemofctu.PNG)

## Solidity Smart-Contract
The key idea is that each message is stored as an event log:

```solidity
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);
}
```

and a user retrieves their messages by querying these events. There are also events emitted from user initializations and chat initializations:

```solidity
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);
}
```

```solidity
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);
}
```

You might also notice the usage of custom error codes. These are much more optimized than using `require` with custom messages stored as strings; instead, we do as follows:

```solidity
uint8 constant ErrUserAlreadyInitialized = 1;
uint8 constant ErrChatNotInitialized = 2;
uint8 constant ErrUserNotInitialized = 3;
uint8 constant ErrPeerNotInitialized = 4; 
uint8 constant ErrIncorrectEntryFee = 5; 
error BlockchattingError(uint8 code);
```

The client can read error code meanings from here, which are named following Go language error variable naming conventions.

The contract itself is quite simple as you can see, it is on the client-side that much of the heavy lifting goes on. You can check the source code of the contract [here](https://github.com/erhant/blockchatting/blob/main/contracts/contracts/Chat.sol). It also has tests implemented with Hardhat + TypeScript.

## NextJS Frontend
Admittedly, this application is a single-page app and NextJS could be a bit overkill for this. I used it regardless, may be due to change in the future!

For the wallet connection, we use [WAGMI](https://wagmi.sh/). The chat contract is connected within a React context, so that all components can interact with it. Contract interactions are also type-checked via [TypeChain](https://github.com/dethcrypto/TypeChain). These types are created on the contract development phase, but you can easily copy & paste them to your `types` directory at frontend, or wherever you store them.

For UI/UX purposes, we also map each address to some randomly generated nickname and avatar, making things a lot more readable! A similar approach is used in [Status](https://status.im/) messenger. I have also used [MantineUI](https://mantine.dev/) as my component library, which I highly recommend.

The application basically has 3 phases:
- `UserInitialization` is checked, and is requested if it is not present.
- The user then sees previous chat session by querying `ChatInitialization` events with their address as a parameter. They can also create a new session by entering the address of who they would like to talk to.
- When they begin a chatting session with some peer, they can query their previous messages via `MessageSent` events.

You can see a live demo at <https://blockchatting.vercel.app/> which uses [Göerli testnet](https://goerli.etherscan.io/address/0x01eA11fE95123946FC22979BAF73Bb417C5e078F), and feel free to check out the code at <https://github.com/erhant/blockchatting>!

Happy coding :)