Tech
This document describes how DePIN Messaging is implemented in Neurai Core, based on the DePIN pool, network gateway, RPCs, and ECIES encryption stack.
System overview
DePIN Messaging is a token-gated, end-to-end encrypted message pool with a TCP/JSON-RPC gateway. The design assumes:
- Messages are encrypted client-side and stored as ciphertext.
- Pool nodes validate token ownership and signatures before storing.
- Recipients decrypt locally using their private keys.
Runtime components
- CDepinMsgPool: in-memory pool for encrypted messages, limits, and expiry.
- CDepinMsgPoolServer: TCP server on the DePIN port (default
19002) with JSON-RPC support. - RPC layer:
depin*RPCs that submit, fetch, inspect, and manage pool contents. - ECIES layer: hybrid encryption using secp256k1 ECDH + AES-256-GCM.
- MCP worker (optional): background bot that polls messages and replies via an MCP AI server.
Message structure
CDepinMessage (outer envelope)
Fields:
token: asset name controlling accesssenderAddress: Neurai addresstimestamp: UNIX timesignature: ECDSA over secp256k1encryptedPayload: serialized ECIES envelope
Signature hash (CHashWriter, double-SHA256 over serialized fields):
hash = SHA256(SHA256(token || senderAddress || timestamp || encryptedPayload))
CECIESEncryptedMessage (inner payload)
Fields:
ephemeralPubKey: 33 bytes (compressed secp256k1)encryptedPayload: [12B nonce || ciphertext || 16B GCM tag]recipientKeys: map ofhash160(address)-> [12B nonce || encrypted_aes_key || 16B tag]
Because recipients are encoded by hash160(address) and stored inside the payload, the pool cannot filter per-recipient. Clients decrypt locally and discard failures.
Encryption and decryption (ECIES hybrid)
Encryption:
- Generate a per-message ephemeral keypair.
- Derive an AES-256 key from the ephemeral private key (KDF-SHA256).
- Encrypt plaintext once with AES-256-GCM (12B nonce, 16B tag).
- For each recipient:
- ECDH with ephemeral privkey and recipient pubkey.
- Derive wrap key (KDF-SHA256).
- Encrypt the AES key with AES-256-GCM.
- Store [nonce || encrypted_aes_key || tag] keyed by
hash160(address).
Decryption:
- Locate recipient package by
hash160(address). - ECDH with recipient private key and message ephemeral pubkey.
- Derive wrap key, decrypt AES key with GCM.
- Decrypt message payload with AES-256-GCM.
Diagram: ECIES encryption/decryption
Client (Sender) Client (Recipient)
----------------- -------------------
ephemeral keypair
(priv_e, pub_e)
|
| KDF-SHA256(priv_e)
v
AES key (K_msg)
|
| AES-256-GCM(plaintext, K_msg, nonce_msg)
v
encryptedPayload = nonce_msg || ciphertext || tag_msg
|
| For each recipient:
| shared = ECDH(priv_e, pub_recipient)
| wrap = KDF-SHA256(shared)
| AES-256-GCM(K_msg, wrap, nonce_r)
v
recipientKeys[hash160(addr)] = nonce_r || enc_K_msg || tag_r
|
+-------------------------------> recipientKeys + encryptedPayload
(via pool)
|
| Find own hash160(addr)
v
nonce_r || enc_K_msg || tag_r
|
| shared = ECDH(priv_recipient, pub_e)
| wrap = KDF-SHA256(shared)
| AES-256-GCM-Decrypt(enc_K_msg, wrap, nonce_r, tag_r)
v
K_msg
|
| AES-256-GCM-Decrypt(ciphertext, K_msg, nonce_msg, tag_msg)
v
plaintext
Pool validation and limits
When adding a message, the pool enforces:
- Token must match the pool token.
- Timestamp cannot be more than 60 seconds in the future.
- Message must not be expired (default 168 hours).
- Payload cannot be empty and must fit size limits.
- Total payload limit:
maxMessageSize * maxRecipients.
- Total payload limit:
- Pool size cannot exceed
maxPoolSizeMB. - Signature must verify (unless skipped by a server-authenticated path).
- Sender must hold the token.
Addresses must have a revealed public key in the pubkeyindex, otherwise:
- They cannot be used as recipients.
- Their signatures cannot be verified.
RPC surface (core)
depingetmsginfo: pool status, limits, cipher, memory usage.depinsubmitmsg: accepts a pre-encrypted, pre-signedCDepinMessage.depinreceivemsg: returns encrypted messages as JSON (client decrypts).depinsendmsg: node prepares encryption and signature on behalf of the sender.depingetmsg: decrypts messages for local wallet addresses (local or remote).depinclearmsg: removes expired messages or clears the pool.depinpoolstats: aggregate pool stats (counts, sizes, age buckets).
depinsubmitmsg verifies the message signature and token ownership before storing.
DePIN port protocol (TCP + JSON-RPC)
The DePIN gateway listens on depinmsgport (default 19002). It supports:
- JSON-RPC:
depinsubmitmsg,depinsendmsg,depingetmsg - Plain text commands (newline-terminated):
AUTH|token|address[|SEND]->CHALLENGE|nonce|timeoutGETMESSAGES|token|addr1,addr2|authAddress|signature|challengeINFO,PING
Challenge/response authentication
- Client requests
AUTH. - Server checks:
- token matches pool token
- address owns the token
- address has a revealed pubkey
- Server issues a short-lived nonce (30s).
- Client signs:
DEPIN-GET|token|address|challengefor receiveDEPIN-SEND|token|address|challengefor send
- Server recovers pubkey from the compact signature and verifies it matches the address.
This reduces unauthenticated sends/reads and limits some DoS vectors.
Local and remote message flows
Local send:
- Client encrypts and signs.
pDepinMsgPool->AddMessage(...).
Remote send (depinsendmsg):
- Client performs challenge/response with the remote pool.
- Node encrypts and signs on the sender wallet.
Remote send (secure depinsubmitmsg):
- Client encrypts and signs.
- Submit serialized hex message to the remote gateway.
Remote receive:
- Client performs challenge/response.
- Pool returns encrypted messages in bulk.
- Client attempts ECIES decryption per address.
Diagram: End-to-end send/receive flow
Sender Client Pool Node Recipient Client
------------- ---------- -----------------
1) Build ECIES payload
2) Build CDepinMessage
3) Sign message hash
4) depinsubmitmsg(hex) -----------> [DePIN gateway]
- validate token
- verify signature
- check ownership
- enforce size/expiry/limits
- store in CDepinMsgPool
- return success
5) depinreceivemsg(token, addr, t) -------------------------------> [Core RPC]
- read from pool (no filtering)
- return encrypted payloads
6) Attempt ECIES decrypt
7) Ignore non-matching payloads
8) Use plaintext message
Persistence and cleanup
If -depinpoolpersist=1:
- Pool is saved to
depinpool.daton shutdown. - File format includes magic bytes
0xD0D1D2D3, version, timestamp, and messages. - Expired messages are dropped during load, and the file is compacted if needed.
Expired messages are also cleaned periodically via -depinmsgcleanupinterval (default 300s).
Configuration flags
Key flags (from init.cpp):
-depinmsg=1enable DePIN messaging.-depinmsgtoken=TOKENrequired token name.-depinmsgport=19002DePIN TCP/JSON-RPC port.-depinmsgmaxusersrecipient limit.-depinmsgsizemax message size (bytes).-depinmsgexpireexpiry hours.-depinpoolsizemax pool size (MB).-depinpoolpersistenable pool persistence.-depinmsgcleanupintervalexpiry sweep interval (seconds).
Required indexes:
-assetindexfor token ownership and holders.-pubkeyindexto verify signatures and encrypt to recipients.
Messaging activation flags
Core messaging features are gated by a version-bits deployment named messaging_restricted. Asset transfers can automatically subscribe wallets to message channels when messaging is enabled.
MCP worker (optional AI integration)
When -depinmcp=1, a background worker:
- Polls the pool every
-depinmcpintervalseconds. - Decrypts messages for the configured node address.
- Filters commands by
-depinmcpkey(default/ai). - Sends prompts to the MCP server and posts responses back to the pool.
- Applies optional per-address rate limiting and deduplicates processed messages.