Skip to content

DLT P2P Architecture

VIZ Ledger's P2P layer was redesigned from the legacy Graphene synopsis-based protocol (node.cpp) to a dedicated DLT-native protocol (dlt_p2p_node.cpp). The public plugin API is unchanged — only the internal implementation was replaced.


Overview

Before:  p2p_plugin → graphene::network::node    (node.cpp, 6978 lines, STCP, inventory gossip)
After:   p2p_plugin → dlt_p2p_node               (dlt_p2p_node.cpp, 2627 lines, raw TCP, range-based sync)

The replacement is in-place: same plugin name "p2p", same port (2001/4243), same public API. All dependent plugins (validator, snapshot, etc.) required zero changes.


Wire Protocol

Raw TCP — no STCP encryption layer. Each message on the wire:

[4 bytes: data size (uint32_t)] [4 bytes: msg_type (uint32_t)] [N bytes: fc::raw::pack(T)]

Message Types (5100–5116)

TypeIDDescription
dlt_hello_message5100Handshake: protocol version, head/LIB, DLT range, node/fork status
dlt_hello_reply_message5101Handshake reply: exchange_enabled, fork_alignment
dlt_range_request_message5102Request a range of block IDs
dlt_range_reply_message5103Reply with available block range
dlt_get_block_range_message5104Fetch blocks start..end with prev_block_id check
dlt_block_range_reply_message5105Reply: blocks vector + is_last flag
dlt_get_block_message5106Fetch a single block by ID
dlt_block_reply_message5107Reply: block + next_available + is_last
dlt_not_available_message5108Block not available
dlt_fork_status_message5109Broadcast current fork/node status to peers
dlt_peer_exchange_request5110Request known peers list
dlt_peer_exchange_reply5111Reply with peers
dlt_peer_exchange_rate_limited5112Rate limit notice: wait N seconds
dlt_transaction_message5113Broadcast a signed transaction
dlt_soft_ban_message5114Notification before disconnecting a banned peer
dlt_gap_fill_request5115Request specific block numbers to fill a gap
dlt_gap_fill_reply5116Reply with requested blocks

Fiber Architecture

All I/O runs on a single fc::thread using cooperative fibers — no mutexes needed for shared state:

FiberRole
Accept loopWaits for incoming connections; rejects duplicate IPs
Read loop (per peer)Reads messages; dispatches to on_message()
Periodic taskReconnects, checks stagnation, peer stats, mempool cleanup

Fibers yield on blocking I/O (readsome(), writesome()), allowing multiple peers on one thread without contention.


Node Status and Peer Lifecycle

Node statuses: SYNC (catching up) / FORWARD (live, exchanging blocks)

Peer lifecycle states:

CONNECTING → HANDSHAKING → SYNCING → ACTIVE → DISCONNECTED → BANNED

Timeouts: connecting=5s, handshaking=10s. Reconnect backoff: 30s → 60s → … → 3600s with ±25% jitter, reset after 5 minutes stable uptime. Peers removed after 8 hours of non-response.


Block Sync: SYNC Mode

A node in SYNC mode fetches blocks sequentially from a peer with a higher head:

  1. request_blocks_from_peer() — sends dlt_get_block_range_message for up to 200 blocks after our head.
  2. on_dlt_block_range_reply() — validates prev_block_id hash chain, applies each block.
  3. check_sync_catchup() — compares our head against all peers' heads; transitions to FORWARD when caught up.
  4. sync_stagnation_check() — after 30s with no new block, retries up to 3 times then transitions to FORWARD with a warning.

Gap Fill

When a contiguous gap exists between our head and the earliest available block on the syncing peer, request_gap_fill() sends a dlt_gap_fill_request (up to 100 blocks per request) to any peer whose DLT range covers the gap. Gap fill works in both SYNC and FORWARD modes:

  • Triggered from on_dlt_block_reply() (out-of-order block detected) and periodic_task() (every 5s).
  • Falls back from exchange-enabled peers to any active peer with a higher head.
  • Falls back to SYNC mode if no peer has the needed blocks.
  • Large gaps handled in 100-block chunks with a 5s cooldown between requests.

Block Exchange: FORWARD Mode

In FORWARD mode, peers exchange live blocks and transactions:

  • exchange_enabled flag controls whether a peer receives new blocks from us.
  • On FORWARD transition, dlt_fork_status_message is sent to all peers (not just exchange-enabled) to notify them of our readiness.
  • on_dlt_fork_status() re-evaluates exchange_enabled when a peer transitions from SYNC to FORWARD.
  • check_forward_stagnation() — if head hasn't advanced in 30s AND at least one peer is ahead, transitions to SYNC.

Fork Alignment and Exchange Eligibility

During the hello handshake, check_fork_alignment() performs multi-tier block ID matching to determine if peers are on the same fork:

CheckCondition
Empty peerhead_block_num == 0 → aligned (new node)
Range overlapOur DLT log covers peer's head → is_block_known(head_id)
Boundary linkpeer_head + 1 == our_earliest → check our earliest block's previous == peer_head_id
LIB fallbackAlways check is_block_known(lib_id)

This multi-tier check prevents false "different fork" disconnections in DLT mode, where old blocks are pruned and the old single-ID check would fail for peers on the same chain.


Fork Resolution

The fork resolution subsystem tracks competing chain tips:

  • Threshold: 42 blocks of divergence triggers resolve_fork() (= CHAIN_MAX_VALIDATORS × 2, one full schedule rotation).
  • Selection: Heaviest branch by vote weight.
  • Hysteresis: 6 consecutive blocks as winner before switching (CONFIRMATION_BLOCKS).
  • Status: _fork_status exposed via is_on_majority_fork() for the validator plugin to check before producing blocks.

Anti-Spam

MechanismDescription
spam_strikes counterSingle counter per peer; reset on good packet; soft-ban at threshold=10
Soft banSets BANNED state for 3600s; sends dlt_soft_ban_message before closing
Per-IP dedupRejects duplicate connections from the same IP (both inbound and outbound)
Broadcast dedupsend_to_all_our_fork_peers() tracks std::set<ip::address> to skip duplicate IPs

Duplicate blocks and out-of-order blocks from range replies are silently skipped — not counted as spam. Deserialization errors do not increment spam strikes.


P2P Mempool

A separate in-process mempool (distinct from the chain's _pending_tx) provides early transaction filtering before chain acceptance:

  • Dedup by tx_id.
  • Eviction by oldest expiry when limits are reached.
  • Limits (configurable): max 10,000 entries, 100 MB total, 64 KB per transaction.
  • Provisional entries tagged during SYNC mode; revalidated on FORWARD transition.
  • Cleanup on block receipt (remove_transactions_in_block) and fork switch (prune_mempool_on_fork_switch).

Peer Exchange

Rate-limited peer discovery:

  • Max 3 requests per 5-minute window per peer.
  • Subnet diversity filter: max 2 peers per /24 prefix in each reply.
  • Only peers with ≥600s uptime are shared.
  • Inbound peers (ephemeral ports) excluded from exchange replies.

Recovery Mechanisms

Peer isolation (P53)

When zero active peers exist for 60 seconds, emergency_peer_reset():

  • Clears all soft bans (BANNED → DISCONNECTED, resets spam strikes).
  • Resets all disconnected peer backoffs to minimum with immediate reconnect.

Block processing pause/resume

pause_block_processing() / resume_block_processing() allow the snapshot plugin to halt P2P block intake during state serialization. The periodic task skips DB-accessing operations while paused.

Startup grace period (P22)

For the first 60 seconds after startup, blocks within 10 of the head are treated as FORK_DB_ONLY instead of DEAD_FORK — preventing cascade rejections while the fork_db rebuilds from the block log.


Block Accept Results

dlt_block_accept_result enum replaces the old boolean return:

ValueMeaning
ACCEPTEDBlock applied to chain (became new head)
FORK_DB_ONLYStored in fork_db but not applied (unlinkable, competing fork)
DEAD_FORKBlock at/below head from a dead fork — peer is soft-banned
ALREADY_KNOWNAlready have this block (duplicates, block_too_old_exception)
REJECTEDFailed validation entirely

Configuration Reference

OptionDefaultDescription
dlt-block-log-max-blocks100,000Max blocks in DLT rolling block log
dlt-peer-max-disconnect-hours8Remove peer after this many hours non-response
dlt-mempool-max-tx10,000Hard cap on mempool entries
dlt-mempool-max-bytes100 MBHard cap on total mempool memory
dlt-mempool-max-tx-size64 KBReject oversized transactions
dlt-mempool-max-expiration-hours24Reject far-future expiration
dlt-peer-exchange-max-per-reply10Max peers per exchange reply
dlt-peer-exchange-max-per-subnet2Anti-sybil: max 2 peers per /24
dlt-peer-exchange-min-uptime-sec600Min uptime before peer is shared
dlt-stats-interval-sec300Peer stats log interval (min 30)

Color-Coded Logging

ColorMeaning
GreenSync progress and block production
WhiteNormal block exchange
RedFork events
Dark grayTransaction handling
OrangeWarnings (soft bans, stagnation, gaps)
CyanPeer statistics output

Delegate Pattern

The network library links only fc and graphene_protocol — not graphene_chain. The dlt_p2p_delegate abstract interface bridges this gap:

dlt_p2p_node (network lib)  ←→  dlt_p2p_delegate (interface)  ←→  dlt_delegate (p2p_plugin)

The dlt_delegate in p2p_plugin.cpp implements:

  • read_block_by_num() — checks dlt_block_log, then fork_db.
  • accept_block() — calls push_block(); catches unlinkable_block_exception → stores in fork_db.
  • get_fork_branch_tips() — fetches from fork_db around current head.
  • is_tapos_block_known() — delegates to db.is_known_block().

See also: P2P Overview, Sync Scenarios, Snapshot Plugin, Block Log.