This document describes the "stubnet" p2p communication protocol for ZkVM blockchain.
It uses proper p2p transaction and block broadcast, but uses a single pre-determinate party to announce blocks (centralized block signer).
However, to make transition to decentralized consensus easier, nothing else in the protocol assumes the central party. All peers are equal and signed block can originate from any node.
A member of the network that maintains a blockchain state and sends/receives messages to/from its peers.
Another node that’s connected to your node.
A connection initiated by a peer to your node.
A connection initiated by your node to a peer.
A transaction envelope format that contains pure ZkVM transaction and a list of Utreexo proofs.
A block envelope format that contains a BlockID and a list of BlockchainTx objects.
A 6-byte transaction ID, specified for a given nonce (little-endian u64).
- Initialize SipHash-2-4 with k0 set to nonce, k1 set to the first 8 bytes as little-endian u64 of the recipient’s Peer ID.
- Feed transaction ID as an input to SipHash.
- Read u64 output, drop two most significant bytes.
See also BIP-152.
The node maintains the following state:
- Blockchain state and mempool.
- Target tip.
- Current nonce for short IDs
- States of connected peers.
- Configuration parameter
max_msg_size
that limits amount of data to be sent or received.
Each peer has the following state:
- Peer's tip.
- Flag:
needs_inventory
. - List of short IDs that are missing in the mempool, along with their nonce.
- Timestamp of the last inventory received.
Upon receiving an inbound connection, or making an outbound connection, a node sends GetInventory
to the peer
with the same random nonce across all peers (so responses contain comparable short IDs). The random nonce is rotated every minute.
When receiving a GetInventory
message, the peer is marked as needs_inventory
.
Required delay allows avoiding resource exhaustion with repeated request and probing the state of the node.
When receiving an Inventory
message:
- Peer's tip is remembered per-peer.
- If the tip block header is higher than the current target one, it is verified and remembered as a new target one.
- If the tip matches, the list of mempool transactions is remembered per-peer and filtered down against already present transactions, so it only contains transactions that the node does not have, but the peer does have.
- Bump the timestamp of the inventory for the peer.
Periodically, every 2 seconds:
- The peers who have
needs_inventory=true
are sent a newInventory
message. - If the target tip does not match the current state, the node requests the next block using
GetBlock
from the random peer. - If the target tip is the latest, the node walks all peers in round-robin and constructs lists of short IDs to request from each peer, keeping track of already used IDs. Once all requests are constructed, the
GetMempoolTxs
messages are sent out to respective peers. - For peers who have not sent inventory for over a minute, we send
GetInventory
again.
Periodically, every 60 seconds:
- Set a new random short ID nonce.
- Clear all the short IDs stored per peer.
When GetBlock
message is received,
we reply immediately with the block requested using Block
message.
When Block
message is received:
- If the block is a direct descendant:
- It is verified and advances the state.
- Orphan blocks from other peers are tried to be applied.
- Duplicates or conflicting transactions are removed from mempool.
- Missing block is sent unsolicited to the peers who have
tip
set to one less than the current block and latest message timestamp less than 10 seconds ago. This ensures that blocks propagate quickly among live nodes while not spending bandwidth too aggressively. Lagging nodes would request missing blocks at their pace.
- Earlier blocks are discarded.
- Orphan blocks are stored in a LRU buffer per peer.
When MempoolTxs
message is received:
- If the tip matches the current state, transactions are applied to the mempool.
- Otherwise, the message is discarded as stale.
"Get inventory". Requests the state of the node: its blockchain state and transactions in the mempool.
struct GetInventory {
version: u64,
shortid_nonce: u64
}
Sends the inventory of a node back to the peer who requested it with GetInventory
message.
Contains the block tip and the contents of mempool as a list of short IDs.
struct Inventory {
version: u64,
tip: BlockHeader,
tip_signature: starsig::Signature,
shortid_nonce: u64,
shortid_list: Vec<u8>,
}
Requests a block at a given height.
struct GetBlock {
height: u64,
}
Sends a block requested with GetBlock
message.
struct Block {
header: BlockHeader,
signature: starsig::Signature,
txs: Vec<BlockTx>,
}
Requests a subset of mempool transactions with the given short IDs after receiving the Inventory
message.
struct GetMempoolTxs {
shortid_nonce: u64,
shortids: Vec<ShortID>
}
Sends a subset of mempool transactions in response to GetMempoolTxs
message.
The node sends a list of blockchain transaction packages matching the short IDs requested.
struct MempoolTxs {
tip: BlockID,
txs: Vec<BlockchainTx>
}