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.