Skip to content

P2P

In the previous chapter, you learned offline-first sync. Now you will learn P2P.

P2P means peer-to-peer. Instead of every node depending on a central server, nodes can discover each other, connect, exchange messages, and synchronize data.

txt
node A ↔ node B
local write → WAL → outbox → P2P transport → peer → ack

Why P2P exists

Real systems do not always have a perfect cloud connection. P2P gives Vix a way to build systems that communicate directly.

Use P2P when you want: local network discovery, direct node-to-node communication, peer synchronization, edge replication, offline-first data exchange, store-and-forward systems.

The mental model

LayerPurpose
DiscoveryFinds available peers on the network.
Peer connectionConnects the local node to another peer.
HandshakeEstablishes peer identity and session metadata.
EnvelopeWraps messages with routing and protocol metadata.
FramingSplits byte streams into complete protocol messages.
DispatchDecodes payloads and routes them to typed handlers.
Sync messagesPushes WAL entries, sends acknowledgments, and pulls work.

Public headers

cpp
#include <vix/p2p.hpp>
#include <vix/p2p_http.hpp>  // for HTTP control routes

Message flow

txt
typed message → payload bytes → envelope → frame → transport
→ peer → decode frame → decode envelope → dispatch typed message

Envelope and framing

cpp
#include <vix/p2p.hpp>

// Create and pack a Ping message
vix::p2p::msg::Ping ping;
ping.nonce = 42;
vix::p2p::Envelope outgoing = vix::p2p::pack::make_envelope(vix::p2p::MessageType::Ping, ping);

// Frame it (length-prefix framing)
vix::p2p::framing::LengthPrefixVarint framer;
vix::p2p::Frame frame = framer.encode(outgoing.encode());

// Decode frame → envelope → message
vix::p2p::FrameDecodeResult decoded = framer.decode(frame.bytes);
vix::p2p::Envelope incoming = vix::p2p::Envelope::decode_or_throw(decoded.frames.front().bytes);
vix::p2p::msg::Ping roundtrip = vix::p2p::msg::Ping::decode_or_throw(incoming.payload);

TCP is a byte stream — framing ensures the receiver knows where each message begins and ends.

Handshake messages

cpp
// Node A → Hello
vix::p2p::msg::Hello hello;
hello.nonce_a = 1001;
hello.node_id = "node-a";
hello.capabilities["proto"] = "1.0";

// Node B → HelloAck
vix::p2p::msg::HelloAck ack;
ack.nonce_a = 1001;
ack.nonce_b = 2002;

// Node A → HelloFinish
vix::p2p::msg::HelloFinish finish;
finish.nonce_a = 1001;
finish.nonce_b = 2002;
finish.signature = /* 64-byte signature */;

Discovery announcement

cpp
vix::p2p::msg::DiscoveryAnnounce announce;
announce.node_id = "node-a";
announce.tcp_port = 9001;
announce.ts_ms = 1710000000000ULL;
announce.nonce = 987654321ULL;
announce.capabilities["proto"] = "1.0";

const std::string json = announce.to_json();
auto parsed = vix::p2p::msg::DiscoveryAnnounce::from_json(json);

Memory router

cpp
vix::p2p::MemoryRouter router;
router.upsert_route("node-b", vix::p2p::Route{"edge-1", false, 8});
router.upsert_route("node-c", vix::p2p::Route{"relay-7", true, 4});

auto route = router.resolve("node-b");  // next_hop="edge-1", via_relay=false
router.remove_route("node-c");

WAL sync messages

cpp
// Push WAL records to a peer
vix::p2p::msg::WalPush push;
push.seq_begin = 10;
push.seq_end = 12;
push.wal_bytes = make_fake_wal_bytes();

// Acknowledge applied sequence
vix::p2p::msg::WalAck ack;
ack.last_applied_seq = 12;

// Ask peer for pending operations
vix::p2p::msg::OutboxPull pull;
pull.target_node_id = "node-b";
pull.max_items = 64;

Start a real P2P node

cpp
vix::p2p::NodeConfig cfg;
cfg.node_id = "node-a";
cfg.listen_port = 9001;
cfg.on_log = [](std::string_view line) { std::cout << line << "\n"; };

auto node = vix::p2p::make_tcp_node(cfg);
node->start();

const auto stats = node->stats();
// stats.peers_total, peers_connected, handshakes_started, handshakes_completed

P2PRuntime and connect

cpp
vix::p2p::P2PRuntime runtime(node);
runtime.start();

vix::p2p::PeerEndpoint ep;
ep.host = "127.0.0.1";
ep.port = 9001;
ep.scheme = "tcp";

runtime.connect(ep);
// runtime.stats(), runtime.stop()

UDP discovery

cpp
vix::p2p::DiscoveryConfig cfg;
cfg.self_node_id = "node-a";
cfg.self_tcp_port = 9001;
cfg.discovery_port = 37020;
cfg.mode = vix::p2p::DiscoveryMode::Broadcast;
cfg.announce_interval_ms = 1000;

auto on_peer = [](const vix::p2p::DiscoveryAnnouncement &a)
{
  std::cout << "discovered: " << a.node_id << " at " << a.host << ":" << a.port << "\n";
};

auto discovery = vix::p2p::make_udp_discovery(cfg, on_peer);
discovery->start();
auto snapshot = discovery->snapshot();
discovery->stop();

P2P HTTP control surface

cpp
#include <vix.hpp>
#include <vix/p2p.hpp>
#include <vix/p2p_http.hpp>

vix::p2p::P2PRuntime runtime(node);
runtime.start();

vix::p2p_http::P2PHttpOptions opt;
opt.prefix = "/p2p";
opt.enable_ping = true;
opt.enable_status = true;
opt.enable_peers = true;

vix::App app;
vix::p2p_http::registerRoutes(app, runtime, opt);
app.listen(8081, []() { std::cout << "HTTP API listening on 8081\n"; });

HTTP control routes

RoutePurpose
GET /p2p/pingRuns a simple P2P smoke test.
GET /p2p/statusReturns current runtime statistics.
GET /p2p/peersLists known peers for this node.
POST /p2p/connectConnects this node to another peer.
GET /p2p/logsReturns recent runtime log entries.
bash
curl http://127.0.0.1:8081/p2p/ping
curl http://127.0.0.1:8081/p2p/status

curl -X POST http://127.0.0.1:8083/p2p/connect \
  -H "content-type: application/json" \
  -d '{"host":"127.0.0.1","port":9201,"scheme":"tcp"}'

Important vix run rule

bash
# Correct — --run passes args to your program
vix run p2p_manual_connect.cpp --run server
vix run p2p_manual_connect.cpp --run client 127.0.0.1 9101

# Wrong — -- is for compiler/linker flags
vix run p2p_manual_connect.cpp -- server

How P2P connects to sync

P2P can become one transport for the sync engine:

txt
WAL → WalPush → peer receives → peer applies → WalAck

The P2P layer moves sync data between nodes. The sync layer decides what must be durable, retried, replayed, and acknowledged.

P2P vs WebSocket vs HTTP

CriteriaWebSocketP2PHTTP
ConnectionClient -> server.Node -> node.Request -> response.
Best forBrowser real-time apps.Distributed systems.Standard APIs.
DiscoveryManual configuration.UDP discovery or registry.Manual configuration.

Common mistakes

Sending raw bytes without framing

TCP does not preserve message boundaries. Use framing before decoding.

Trusting unknown peers

Use handshake, identity, and security checks before trusting a peer.

Forgetting idempotency

A peer can receive the same sync message more than once. Use operation ids and WAL sequence numbers to deduplicate.

Exposing P2P HTTP control routes without auth

Routes like POST /p2p/connect must be protected in production.

Production notes

  • Use stable node ids
  • Protect control routes with authentication
  • Use secure transport
  • Make sync messages idempotent
  • Monitor peer counts and handshake failures
  • Log peer ids and operation ids
json
{
  "ok": true,
  "p2p": {
    "node_id": "node-a",
    "peers_total": 3,
    "peers_connected": 2,
    "handshakes_completed": 2
  }
}

What you should remember

The basic message flow: typed message → envelope → frame → transport → peer → decode → dispatch

The runtime flow: node starts → peer discovered → handshake → messages exchanged

The sync flow: WAL → WalPush → peer applies → WalAck

P2P does not replace offline-first sync — it complements it.

The core idea: Vix Sync preserves intent. Vix P2P moves that intent between nodes.

Next chapter

Next: Production deployment

Released under the MIT License.