At the beginning of 2017, we started creating a blockchain messenger [name and link are in the profile] discussing the advantages over classic P2P messengers.
Years have passed, 2.5 and we have managed to validate our concept: messenger applications are now available for iOS, Web PWA, Windows, GNU/Linux, Mac OS, and Android.
Today we will explain how the blockchain messenger works and how client applications interact with its API.

We wanted blockchain to address the security and privacy issues of classic P2P messengers:
- One click to create an account — no phones or emails, no access to address books or geolocations.
- Participants never establish direct connections; all communication occurs through a distributed node system. Users' IP addresses are not accessible to each other.
- All messages are encrypted End-to-End using curve25519xsalsa20poly1305. While this might not be surprising, our source code is open.
- MITM attacks are excluded — each message is a transaction and is signed with Ed25519 EdDSA.
- A message lands in its block. The sequence of
timestampblocks cannot be altered, therefore neither can the order of messages. - You can’t say, 'I didn’t say that' about messages in the blockchain.
- There is no central structure to verify the 'authenticity' of messages. This is managed by the distributed node system based on consensus, which belongs to the users.
- Censorship is impossible — accounts cannot be blocked, and messages cannot be deleted.
- Blockchain 2FA is an alternative to the hellish 2FA via SMS,
- The ability to access all your dialogues from any device at any time means you don’t have to store dialogues locally at all.
- Message delivery confirmation. Not to the user's device but to the network. Essentially, this confirms the recipient’s ability to read your message. It's a useful feature for sending critical notifications.
Among the benefits of blockchain are its close integration with cryptocurrencies like Ethereum, Dogecoin, Lisk, Dash, and Bitcoin (this is still in progress), as well as the ability to send tokens in chats. We even created a built-in crypto exchange.
And now — how all this works.
A message is a transaction.
Everyone is already accustomed to the fact that transactions on the blockchain transfer tokens (coins) from one user to another, like with Bitcoin. We have created a special type of transaction for sending messages.
To send a message in a blockchain messenger, several steps must be followed:
- Encrypt the message text
- Embed the encrypted text in the transaction
- Sign the transaction
- Send the transaction to any node in the network
- The distributed system of nodes determines the 'validity' of the message
- If everything is OK, the transaction with the message is included in the next block
- The recipient extracts the transaction with the message and decrypts it
Steps 1–3 and 7 are performed locally on the client, while steps 5–6 are performed on the network nodes.
Message encryption
The message is encrypted with the sender's private key and the recipient's public key. The public key will be obtained from the network, but for this, the recipient's account must be initialized, meaning at least one transaction must exist. A REST request can be used GET /api/accounts/getPublicKey?address={ADAMANT address}, and when loading chats, the public keys of the interlocutors will already be available.

The messenger encrypts messages using the curve25519xsalsa20poly1305 algorithm (). Since the account contains Ed25519 keys, the keys need to be transformed into Curve25519 Diffie-Hellman format to form the box.
Here’s an example in 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)
}
}Creating a transaction with a message
The transaction has the following general structure:
{
"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": {}
} For a transaction-message, the most important value is asset — it needs to contain the message in an object with the structure: chat — we store the encrypted message
messageown_message— nonce— message typetypeMessages are also divided into types. Essentially, the parameter
indicates how to interpret type . You can send just text, or you can send an object with interesting contents — for example, the way the messenger handles cryptocurrency transfers in chats. messageIn the end, we form the transaction:
As a result, we create a transaction:
{
"transaction": {
"type": 8,
"amount": 0,
"senderId": "U12499126640447739963",
"senderPublicKey": "e9cafb1e7b403c4cf247c94f73ee4cada367fcc130cb3888219a0ba0633230b6",
"asset": {
"chat": {
"message": "cb682accceef92d7cddaaddb787d1184ab5428",
"own_message": "e7d8f90ddf7d70efe359c3e4ecfb5ed3802297b248eacbd6",
"type": 1
}
},
"recipientId": "U15677078342684640219",
"timestamp": 63228087,
"signature": "the signature will be here"
}
}Transaction signature
To ensure the authenticity of the sender and recipient, the timestamp, and the content of the message, the transaction is signed. The digital signature allows you to verify the transaction's authenticity using the public key — the private key is not needed for this.
The signature itself is generated using the private key:

The diagram shows that we first hash the transaction using SHA-256, and then we sign it. and obtain the signature signature, and the transaction ID is part of the SHA-256 hash.
Example implementation:
1 — Form the data block, including the message
/**
* 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
break
…
default:
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 — Compute the SHA-256 of the data block
/**
* 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 — Sign the transaction
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'))
}Sending a transaction with a message to a node in the network
Since the network is decentralized, any node with a public API will do. We make a POST request to the endpoint api/transactions:
curl 'api/transactions' -X POST
-d 'TX_DATA'In response, we will receive the transaction ID of type
{
"success": true,
"nodeTimestamp": 63228852,
"transactionId": "6146865104403680934"
}Verifying the transaction's authenticity
The distributed system of nodes based on consensus determines the 'authenticity' of the transaction-message. From whom and to whom, when, whether the message was replaced, and whether the correct timestamp was specified. This is a significant advantage of blockchain — there is no central structure responsible for verification, and the sequence of messages and their contents cannot be forged.
First, one node checks the authenticity, then disseminates to others — if the majority agrees that everything is fine, the transaction will be included in the next block of the chain — this is consensus.

The part of the node's code responsible for verification can be viewed on GitHub — and . Ah, the node runs on Node.js.
Including the transaction with a message in the block
If consensus is reached, the transaction with our message will be included in the next block along with other valid transactions.
Blocks have a strict sequence, with each subsequent block formed based on the hashes of previous blocks.

The essence is that our message is also included in this sequence and cannot be 'rearranged'. If multiple messages enter a block, their order will be defined by timestamp messages.
Reading Messages
The messaging application retrieves transactions from the blockchain that are sent to the recipient. For this, we created the endpoint api/chatrooms.
All transactions are accessible to everyone — encrypted messages can be retrieved. However, only the recipient can decrypt them using their private key and the sender's public key:
**
* 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) : ''
}What else?
Since messages are delivered in about 5 seconds — the time it takes for a new block to appear in the network — we devised a socket connection between client-node and node-node. When a node receives a new transaction, it verifies its validity and forwards it to other nodes. The transaction is available to messaging clients even before consensus is reached and included in a block. This way, we will deliver messages instantly, just like conventional messengers.
To store the address book, we created KVS — Key-Value Storage — this is another type of transaction, in which asset not NaCl-box is encrypted, but Thus, the messenger stores other data as well.
File/image transfers and group chats require much more work. Of course, in a slapdash manner, it can be 'tacked on' quickly, but we want to maintain the same level of privacy.
Yes, there's still work to be done — ideally, real privacy implies that users won't connect to public network nodes but will set up their own. How many users do you think do that? Correct, 0. We partially addressed this issue with the Tor version of the messenger.
We have proven that a blockchain-based messenger can exist. Previously, there was only one attempt in 2012 — which failed due to the long message delivery times, processor load, and lack of mobile applications.
Skepticism arises because blockchain-based messengers are ahead of their time — people are not ready to take responsibility for their own accounts, ownership of personal information is not yet trendy, and current technologies do not allow for high speeds on the blockchain. More technologically advanced alternatives to our project will emerge soon. You'll see.
Source: habr.com
