Skip to main content
TCP discovery ships gossip and addrman. Peer management is the layer on top: how peers identify themselves on a fresh session, which peers we keep when slots get tight, which peers we preemptively refuse, and how the outbound set is structured to resist eclipse attacks. These features are always on in v2.0 builds; operators who do not care can skip the rest of this page. The defaults match the long-running consensus from Bitcoin Core’s network code.

Session greeting

The first message on every parallax-disc/1 session in both directions is Hello. It carries: Hello is sent before any other message; receiving anything else first is a protocol violation that disconnects the session.

Self-connect detection

Behind a hairpinning NAT, dialing our own external IP returns a TCP connection back to ourselves. The disc/1 layer’s Hello exchange catches this regardless of which dial code path opened the socket: when we receive a Hello whose Nonce matches ours, the session ends with DiscSelf and the connection is torn down before any peer state is recorded. A second guard at the address layer (Server.IsSelfEndpoint) short-circuits dial attempts to our own advertised endpoint at the v2 dial entry point, so most self-connects never even open a TCP socket.

Cross-dial dedup

When two nodes simultaneously dial each other across a bidirectional NAT, both ends see two TCP connections to the same logical neighbour: one outbound on the peer’s listen port, one inbound on the peer’s ephemeral source port. Pre-Hello, neither node-id maps nor (remote.IP, remote.Port) matching can collapse these — the v2 session derives node IDs from ephemeral X25519 keys, and the inbound side’s port is ephemeral. Hello.ListenPort resolves both. After receipt, the address-manager backend looks for any other peer whose effective listen address matches (IP, ListenPort). If one exists, the duplicate is dropped:
  • if exactly one of the two is inbound, drop it;
  • otherwise drop the younger connection (larger created timestamp).
The result is a single peer entry per logical neighbour even under cross-dial races.

Per-peer telemetry

Each peer carries a small set of atomic counters updated by the read loop, the prl protocol handler, and the ping loop. The eviction algorithm consumes them; admin RPC surfaces them for diagnostics.

Slot allocation

Outbound dial slots split into four buckets: DialRatio defaults to 3, so on the default MaxPeers=100 the node keeps roughly 31 full-relay + 2 block-relay-only outbound. Inbound fills the remaining ~67 slots up to MaxPeers.

Network-group diversity

Outbound full-relay and block-relay-only dials enforce one peer per network group: /16 for IPv4 and /32 for IPv6. A second candidate in an already-occupied group is rejected at dial time with errOutboundGroupOccupied. Loopback and link-local addresses are exempt so single-host dev / test setups keep working. This is the dial-side counterpart to the eviction algorithm’s network-group protection round (see below): it spreads outbound peers across distinct groups so no single autonomous system can dominate the outbound set.

Block-relay-only peer behaviour

Block-relay-only peers exist to make eclipse attacks expensive even if all full-relay slots are subverted. Two slots are reserved by default; an attacker has to compromise every slot, including the ones the dialer is choosing without their knowledge, to keep an eclipsed view alive. On a block-relay-only peer the node:
  • disconnects the peer if it pushes or announces transactions (Transactions, NewPooledTransactionHashes, PooledTransactions — a protocol violation, as in Bitcoin Core), and silently ignores its GetPooledTransactions requests, all before any decode;
  • excludes the peer from the tx-broadcast set so we never push tx to it either;
  • clears the RELAY_TX services bit on the outgoing Hello so the remote knows not to expect tx relay;
  • skips the outbound greeting’s self-advertise + GetPeers and silently ignores any incoming GetPeers or Peers — block-relay-only peers do not participate in address gossip in either direction.
Block traffic and the underlying handshake are unchanged.

Inbound saturation: eviction

When the inbound slot pool is full the node runs an eviction algorithm to free a slot for the new peer. The candidate peer set is filtered through six protection rounds, each preserving peers along a different quality axis:
  1. Network-group diversity — preserve up to 4 peers from distinct network groups so a sybiled subnet can’t starve diverse peers out of the inbound pool.
  2. Fastest ping — preserve the 8 peers with the lowest observed minimum RTT.
  3. Newest tx relay — among peers that relay tx, preserve the 4 that delivered tx most recently.
  4. Newest blocks (block-relay-only) — among peers that don’t relay tx (block-relay-only neighbours), preserve the 8 with the most recent block delivery.
  5. Newest blocks overall — preserve the 4 peers with the most recent block delivery across the full inbound set.
  6. Connection age — preserve the oldest 50 % of remaining peers.
A prefer-evict pass then runs over the survivors: if any of them misbehaved this session or connected from an address in the discourage filter, the candidate set narrows to those peers — a peer we already caught misbehaving absorbs the eviction before any well-behaved survivor does. Finally, the surviving candidates are bucketed by network group; the most populated group is selected (ties broken by youngest representative connection), and the youngest peer in that group is disconnected with DiscTooManyPeers. Trusted and static peers are always exempt, as are peers whose disconnect is already in flight. There is no minimum connection age before a peer becomes eligible — Bitcoin Core’s eviction has no age floor either, and brand-new connections are legitimate candidates (the connection-age protection round already favors older peers). If every candidate is protected — eviction can find no victim — the new connection is hard-rejected with DiscTooManyPeers.

addrman maintenance

Feeler dials

Once every ~2 minutes — and only while the outbound full-relay and block-relay slots are at target, matching Bitcoin Core’s feeler scheduling (below target the connection effort goes to real peers instead) — the node picks one addrman entry, dials it briefly to verify reachability, then disconnects. Selection prefers test-before-evict candidates (SelectTriedCollision) so the addrman can promote new tried entries cleanly when an old one stops answering. Successful feeler dials refresh LastSuccess via addrman.Good; failures bump the failure counter so unreachable entries eventually become IsTerrible and drop out of Select. ResolveCollisions runs on every feeler tick to walk the collision set and either promote the new entry or evict the stale one.

addrfetch on cold start

When the addrman holds fewer than 1000 entries on startup the node runs a one-shot addrfetch against BootstrapNodesV2: dial each once, let the disc/1 outbound greeting solicit a Peers reply, then disconnect after a short lifetime. This is the bootstrap-only counterpart to feeler — feeler keeps a steady-state addrman fresh, addrfetch warms a cold one.

Anchor persistence

On clean shutdown the node writes <datadir>/anchors.dat: the (IP, listen-port) of each currently-connected block-relay-only outbound peer, capped at 2 entries. On the next startup those peers are re-dialled as block-relay-only and the file is deleted immediately. A crash mid-startup therefore replays anchors at most once. If block-relay-only peers are disabled (MaxBlockRelayPeers < 0), a leftover anchors.dat is deleted without being replayed. The wire format is RLP with a schema-version byte. Anchors are ephemeral hints: like Bitcoin Core, the file is deleted after every startup read regardless of parse outcome, so a file written by a newer schema is discarded rather than replayed.

Misbehavior, discourage, and ban

The node tracks two distinct rejection sets: Matching addresses are additionally filtered out of addrman dial selection, so neither set is reachable through the dial scheduler either. A peer is flagged for discourage when the disc/1 handler observes a protocol-discipline violation — oversized message, double Hello / YourAddr, or a decode or validation failure. The peer is disconnected immediately and its source IP is added to the discourage Bloom on session-end. Two rejections are deliberately not discourage offenses: a message before Hello disconnects without a stamp (tolerance for the 2.0 flag-day window, where an old node may talk first), and unsolicited Peers messages are legal in any number — they are how address relay works, bounded by the per-peer ingest rate limit instead. Banned IPs are hard-rejected at the listener before the handshake, with no trusted exemption — at that stage the node cannot yet know whether the remote is trusted, so a banned IP is rejected even for a would-be trusted peer (operators should unban rather than work around it). Only the discourage check exempts trusted peers: discouraged IPs are rejected only at saturation — when the node still has free inbound slots there’s no benefit to turning them away. The discourage filter is in-memory and reset on every restart. The ban list persists.

Operator RPCs

Three RPCs manage the persistent ban list. They sit alongside the existing addrbook RPCs documented under TCP Peer Discovery.
  • admin_setban <subnet> <add|remove> [bantime] [absolute] — add or remove an entry. subnet accepts a plain IP (1.2.3.4/32), a CIDR (10.0.0.0/24), or a Tor v3 onion address (<addr>.onion, an exact-host row — there is no subnet concept for onion space, and enforcement happens at the outbound dial gate since inbound onion streams cannot be attributed to an address; see Tor Integration). On add any currently-connected peer in the matching IP range is also disconnected. bantime defaults to 24 h on add; passing 0 (or a negative value) also selects the default. With absolute=true the value is a Unix timestamp instead and must lie in the future — 0 is an error there. Re-adding an already-active ban is an error (IP/subnet already banned); a longer bantime on a ban that would expire sooner extends it.
  • admin_listbanned — return active (non-expired) entries as JSON.
  • admin_clearbanned — wipe banlist.json. Does not touch the in-memory discourage filter.
The corresponding parallax-cli subcommands are setban, listbanned, and clearbanned.

Configuration knobs

The ban duration is not a configuration knob: it is chosen per call via the bantime argument to setban, and bantime=0 falls back to the fixed internal default DefaultBanDuration (24 h).