Network
P2P v2 wire protocol
TCP message framing, defensive length validation, network handshake, every command and payload, peer discovery, lifecycle, keepalive, and compatibility boundaries.
This page documents the implemented alpha. Executable source, interoperability vectors, and tests remain authoritative if prose and code ever diverge.
Transport scope
Luracoin P2P v2 is a binary message protocol over TCP. It carries compatibility handshakes, peer discovery, full blocks, complete signed transactions, inventory announcements, data requests, and liveness probes.
It is separate from both HTTP interfaces:
- private wallet RPC uses authenticated loopback HTTP under
/v1; - the public explorer uses read-only HTTP under
/api/v1; - neither HTTP payload format is valid on a P2P socket.
P2P encryption, authenticated peer identity, compression, multiplexing, and transport negotiation are not implemented.
Network namespaces
The first four bytes of every message identify the network:
| Network | Chain ID | P2P magic |
|---|---|---|
| mainnet | 0 | ba77d89f |
| testnet | 1 | fbc0b6db |
| devnet | 2 | faceb00c |
Mainnet values reserve a namespace, but mainnet initialization remains disabled because no audited public genesis exists.
The CLI’s default P2P listen port is 9999 regardless of network. Operators running multiple networks or nodes on one host must assign distinct --port values explicitly; the wire namespace comes from magic and chain ID, not from a port convention.
Network isolation is redundant by design: magic separates message streams, VERSION carries the chain ID, and every transaction carries a one-byte chain ID. A mismatch at any boundary is rejected.
Message header
Every message begins with exactly 24 bytes:
| Offset | Size | Field | Encoding |
|---|---|---|---|
| 0 | 4 | magic |
active network’s exact bytes |
| 4 | 12 | command |
ASCII, right-padded with zero bytes |
| 16 | 4 | payload_length |
unsigned little-endian |
| 20 | 4 | checksum |
first four bytes of SHA256(SHA256(payload)) |
| 24 | declared | payload | command-specific binary body |
The checksum of an empty payload is still calculated from the empty byte string. A receiver validates magic, known command, command-specific length, complete payload read, and checksum.
Allocate only after validation
The receiver parses the fixed header first and rejects impossible lengths before reading or allocating the body. The global maximum is:
MAX_P2P_PAYLOAD = 8,000,004 bytes
The extra four bytes above the maximum block size exist at the global transport layer; a block command itself remains capped at 8,000,000 bytes.
This validation order is security-critical. An implementation must not trust an attacker-supplied uint32 length and allocate gigabytes before checking the command’s exact domain.
Command registry
Only these zero-padded command fields are valid:
| Command | Payload length | Purpose |
|---|---|---|
version |
18 | compatibility and height advertisement |
verack |
0 | complete handshake phase |
getpeers |
0 | request known IPv4 endpoints |
peers |
2–1,538 | return count plus up to 256 endpoints |
getblocks |
6 | request a consecutive height range |
block |
118–8,000,000, aligned | transmit one complete block |
tx |
213 | transmit one complete signed transaction |
inv |
33 | announce one block or transaction ID |
getdata |
33 | request one announced object |
ping |
8 | liveness challenge |
pong |
8 | matching liveness response |
Unknown commands fail closed. A known command with a structurally wrong length also fails before body processing.
For block, (payload_length - 118) % 213 must equal zero. Semantic deserialization later enforces at least one transaction and the height-specific size limit.
VERSION payload
P2P v2 uses exactly 18 little-endian bytes:
| Offset | Size | Field | Requirement |
|---|---|---|---|
| 0 | 4 | protocol_version |
exactly 2 |
| 4 | 4 | chain_id |
equals active network |
| 8 | 4 | height |
remote canonical tip advertisement |
| 12 | 4 | timestamp |
remote Unix seconds advertisement |
| 16 | 2 | listening_port |
1–65,535 |
The timestamp is informational in the implemented handshake; it does not set local time or validate blocks. The height is untrusted scheduling input, not proof that the peer possesses a valid chain.
The advertised listening port replaces an inbound connection’s ephemeral source port when the endpoint is registered.
Handshake state machine
Outbound connection:
local -> VERSION -> remote
local <- VERSION <- remote
local -> VERACK -> remote
local <- VERACK <- remote
Inbound connection:
local <- VERSION <- remote
local -> VERSION -> remote
local -> VERACK -> remote
local <- VERACK <- remote
Before activation, the remote version, chain ID, and listening port are checked. A different protocol or chain is rejected before VERACK completes.
Once activated, another version or verack is a protocol-state error and the peer is removed. Application messages must not be processed before a successful handshake.
PEERS payload
Peer exchange is IPv4-only on wire:
count_le_u16
|| ipv4_bytes_4 || port_le_u16
|| ... repeated count times
Rules:
count <= 256;- payload length is exactly
2 + count × 6; - every port is nonzero;
- each address is decoded as an IPv4 literal;
- invalid, self, duplicate, unspecified, multicast, and reserved addresses are not admitted;
- the local known-peer capacity still applies.
DNS names may exist in local seed/connection configuration but cannot be serialized into this v2 peers format. IPv6 is not represented.
GETBLOCKS payload
The request is:
start_height_le_u32 || count_le_u16
The serving node returns each available block as an individual block message, in ascending consecutive order. Synchronization requests at most 50 per batch even though the field can encode a larger integer.
This is a full-block protocol. There is no locator list, header message, compact block, Merkle proof, or streaming multi-block envelope.
BLOCK and TX payloads
block carries the canonical serialized block exactly as defined in Blocks & validation. It has no extra network wrapper inside the P2P payload.
tx carries the canonical complete 213 bytes described in Transactions. Unsigned 85-byte bodies and JSON objects are invalid.
Transport checksum success proves only that bytes arrived intact. The receiver still performs full deserialization, ID, signature, chain, nonce, balance, block-link, PoW, and state validation as appropriate.
Inventory messages
Both inv and getdata use:
type_u8 || object_hash_32
Valid types are:
| Type | Value | Hash meaning |
|---|---|---|
| block | 0x01 |
canonical block ID |
| transaction | 0x02 |
canonical transaction ID |
An unknown type is malformed. Hashes are raw 32 bytes on wire, not 64-byte hexadecimal ASCII.
inv is an announcement. A node that lacks the object may answer with getdata; the peer then sends the complete block or tx if it can resolve it. The protocol has no explicit “not found” response.
PING and PONG
Both carry one unsigned 64-bit little-endian nonce:
ping: nonce_le_u64
pong: same_nonce_le_u64
The node dispatcher owns the only read loop for an activated connection and correlates pong nonces with pending liveness checks. Concurrent components must not call read() independently on the same stream.
The default periodic ping interval is 60 seconds. A response with a different nonce does not satisfy the outstanding challenge.
Peer admission and capacity
Defaults are:
maximum total peers = 8
maximum inbound peers = 8
Pending inbound and outbound attempts count toward admission decisions so simultaneous handshakes cannot trivially oversubscribe the cap. Activated endpoints are deduplicated.
The same numerical default for total and inbound peers means outbound connections can reduce remaining inbound capacity. Operators may override limits, but this does not add Sybil resistance.
Send and receive behavior
Per-peer sends are serialized with an async lock so message bytes cannot interleave on one TCP stream. The default drain timeout is five seconds. A timeout or socket failure marks the peer disconnected.
Receives are likewise single-owner and bounded by a timeout. Short headers, short bodies, malformed framing, invalid payload structures, and checksum failures terminate useful processing for that connection.
Relay to multiple peers runs concurrently and deduplicates peer objects. One slow peer should not serially block every healthy peer.
Discovery lifecycle
After handshake, a node:
- requests peer addresses with
getpeers; - schedules synchronization if the remote height is ahead;
- replays a bounded slice of pending transaction backlog when appropriate.
The current code ships with no public seed nodes. A fresh node therefore needs explicit peer configuration or a locally arranged topology before it can discover further addresses.
Known peers and cooldown histories are process-local. There is no durable address manager, reputation database, ban list, autonomous DNS-seed refresh policy, or authenticated node identity.
Protocol violations
Disconnect-worthy cases include:
- wrong network magic;
- unknown or noncanonical command field;
- oversized or command-incompatible payload length;
- checksum mismatch;
- malformed structured payload;
- wrong handshake order;
- protocol-version or chain-ID mismatch;
- post-handshake
version/verackreplay; - invalid listening port;
- failed socket read/write or timeout.
Application-level invalid blocks or transactions are rejected too. The alpha does not maintain a persistent misbehavior score or ban duration.
Compatibility boundary
P2P v1 used a different, 14-byte VERSION shape without the chain ID. P2P v2 is deliberately incompatible. A v2 implementation must never infer a missing chain ID or accept an old handshake by length fallback.
Likewise, adding optional bytes to a current command changes its structural validity. Protocol evolution should define a new protocol version and explicit transition rules.
Security limits
Implemented defenses include bounded allocations, exact payload schemas, checksum validation, peer caps, send timeouts, single-reader ownership, address filtering, bounded caches, and cooldowns after synchronization failure.
They do not provide:
- encrypted or private transport;
- peer authentication;
- message signatures independent of block/transaction signatures;
- eclipse or Sybil resistance;
- per-IP connection quotas;
- persistent scoring or bans;
- bandwidth accounting and rate limiting;
- NAT traversal;
- IPv6 discovery;
- chain-proof negotiation.
Do not expose an alpha node as critical infrastructure without a network firewall, OS resource limits, observability, and an explicit threat assessment.
Wire conformance checklist
A second implementation should capture byte fixtures for:
- every exact command field and zero padding;
- empty-payload checksums;
- maximum and off-by-one payload lengths;
- v2
VERSIONon all three chain IDs; - malformed handshake ordering;
- peer count/payload mismatches and zero ports;
- inventory type and raw hash order;
- ping/pong nonce correlation;
- block alignment and the 8,000,000-byte boundary;
- partial reads and concurrent sends;
- mismatched magic, checksum, chain, and protocol version.
Interoperability means matching both valid bytes and rejection behavior.
Source anchors
Primary implementation files used for this chapter:
