Introduction
A P2P Encrypted Network is a network of nodes that communicate with each other using a secure, encrypted protocol.
The network is designed to be decentralized, with no central authority or single point of failure.
It is also designed to be lightweight for low end devices, but also resistant to censorship and surveillance, with strong privacy and security guarantees.
Seed nodes are used to help nodes find each other and are connected by default when no peerlist available.
XELIS uses ChaCha20-Poly1305 AEAD cipher for symmetric encryption with key rotation happening for each 1 GB of data transferred.
All transferred data uses a custom serializer / deserializer made by hand to transform a struct representation in raw bytes directly.
This serialization is done using the fixed position of each field and their corresponding bit size.
Before sending a packet to the peer, the local node encrypts it completely to prevent network traffic analysis and to authenticate each transferred data.
All data is transferred using this encrypted protocol which allows for efficient reading, transferring, and serializing of the data.
Protocol
The protocol has a simple design with a packet system that allows transferring data between peers.
To create a connection between with a potential peer, the party that initiated the connection must send its symmetric encryption key first.
The other party will wait on it and will send its own key after receiving the first one.
Once both parties have received the other's symmetric encryption key, they can start the handshake and send / receive data encrypted.
The client that initiated the connection will send a handshake packet to the other party to be upgraded as a Peer, and the other party will respond with a handshake packet to confirm the upgrade.
In the handshake packet, a peer can inform others peers if it accepts that its IP is shared to extend peerlist of others and / or to be returned in RPC API.
To reduce overall bandwidth of network, nodes shouldn't:
- send twice a transaction during propagation time to the same peer.
- send twice a block during propagation time to the same peer.
For block and transaction propagation, the local node keeps a cache of the last N elements sent or received from a peer to not send the same data twice during propagation.
Implementation
As a note, the connection for a new peer (took from the queue or a new incoming connections) is executed through a unique tokio task with the same allocated buffer for handshake. This prevents any DoS attack on creating multiple task and verifying connection.
When the peer is verified and valid, two task are created for it. One reads incoming packets and the other writes packets to the peer. Separating both directions into two tasks prevents incoming packets from blocking outgoing packets.
Chain Sync
The local node randomly select a peer in the peer list which has a greater height than us and send him a chain request.
The chain request includes last CHAIN_SYNC_REQUEST_MAX_BLOCKS
blocks hashes of the local node's chain with their topoheight spaced exponentially.
This data is used by the selected peer to try to find a common point with the local node's chain and his own (block hash must be at same topoheight as other peer).
If the selected peer finds a common point, it adds up to CHAIN_SYNC_RESPONSE_MAX_BLOCKS
blocks hashes ordered by block topoheight.
Through the "ask and await" request object system, the local node asks for the complete block (block header with transactions included) and add it to the chain directly.
Chain sync is requested with a minimum interval of CHAIN_SYNC_DELAY
seconds.
Packets
This part explains all packets used in the XELIS network to communicate over P2p.
Key Exchange
Key Exchange is the first real packet to be sent when creating a new connection. This allow exchanging symmetric encryption keys between peer to establish an encrypted communication channel over TCP.
Currently, the ChaCha20-Poly1305 algorithm is used to encrypt / decrypt all packets.
This packet can be sent later to rotate the key of a peer. This is currently done every 1 GB of data sent.
We're using two different symmetric keys for encryption per Peer. One key is from the local node, to encrypt its packets, and the other key is to decrypt peer's packets.
Handshake
The handshake packet must be the first packet sent with the blockchain state inside to upgrade a connection to a peer. If valid, the peer will send the same packet with is own blockchain state.
This packet should only ever be sent at the beginning of a connection.
Transaction Propagation
The transaction propagation packet contains the hash only to prevent sending the TX. Its also backed by a cache per peer to know if the transaction had already been sent/received.
Block Propagation
The block propagation packet contains the block header only. Its sent to all peers who have their height minus our height less than STABLE_LIMIT
.
To build the block, transactions are retrieved from the mempool.
If a transaction is not found in the mempool, the local node requests it from the same peer in order to build it.
Chain Request
This packet is sent to a peer to request a chain sync.
It contains a maximum allowed response size and the last CHAIN_SYNC_REQUEST_MAX_BLOCKS
blocks hashes of the local node's chain with their topoheight spaced exponentially.
Chain Response
This packet is sent to a peer in response to a chain request.
It contains the last CHAIN_SYNC_RESPONSE_MAX_BLOCKS
blocks hashes of the peer's chain ordered by block topoheight.
Only block hashes are sent in this packet, allowing a peer to request the full block if don't have any block matching the hash. It also prevent any DDoS attack by sending a lot of blocks to a peer or having high bandwidth usage during high traffic. Blocks are requested one by one to the peer to prevent any overload and controling the memory and network usage.
Ping
The ping packet is sent at an regular interval and informs peers of the local node's blockchain state.
Every 15 minutes, the packet can contain up to MAX_LEN
sockets addresses (IPv4 or IPv6) to help others nodes to extends their peer lists.
Object Request
This packet is sent to a peer to request an object (block header, full block or transaction) by its hash. It is mainly used for chain synchronization, block / transaction propagation.
Object Response
This packet is sent to a peer in response to an object request. It contains the requested object (block header, full block or transaction) or the requested hash if the object was not found.
Notify Inventory Request
Inventory request packet is requested when we've connected to a new synced peer or when we've finished to sync the chain with the peer. It is used to request the mempool of the peer to have the same transactions in the mempool.
The inventory is paginated using one (optional) byte that contains the page number.
Notify Inventory Response
Inventory response packet is sent in response to an inventory request. It contains the list of all transactions hashes from the mempool of the peer.
An optional byte is used to indicate if the inventory is paginated and if there are more pages to request.
Bootstrap Chain Request
Bootstrap chain request packet is a specific packet that is split in several sub-variants to efficiently request the top chain state from a peer.
This packet should be used only with a trusted peer, to prevent any bad behavior that could occurs (like sending fake datas, etc).
Requests variants must follow the following proceed:
- Chain Info: The chain info contains the chain state (common point, stable height, stable topoheight, stable hash, etc)
- Assets: retrieve all assets present in the chain
- Keys: all accounts registered in the chain
- Nonces: nonce of each account
- Balances: all balances summary of the accounts for a specific asset
- Balances Details: all balances details of the accounts for a specific asset
- Blocks metadata: 80 blocks below stable topoheight containing hash, supply, reward, difficulty, cumulative difficulty, difficulty P variable.
Bootstrap Chain Response
Bootsrap chain response contains all the needed data to bootstrap the chain from a peer.
It is split in several sub-variants to efficiently request each part of the chain.
Disconnect
The disconnect packet is sent when a peer is disconnected from the local node and we have it in common with another peer.
This is used to keep synced the "propagation map predicate".