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 everyparallax-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’sHello 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
createdtimestamp).
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 itsGetPooledTransactionsrequests, all before any decode; - excludes the peer from the tx-broadcast set so we never push tx to it either;
- clears the
RELAY_TXservices bit on the outgoingHelloso the remote knows not to expect tx relay; - skips the outbound greeting’s self-advertise +
GetPeersand silently ignores any incomingGetPeersorPeers— block-relay-only peers do not participate in address gossip in either direction.
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:- 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.
- Fastest ping — preserve the 8 peers with the lowest observed minimum RTT.
- Newest tx relay — among peers that relay tx, preserve the 4 that delivered tx most recently.
- 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.
- Newest blocks overall — preserve the 4 peers with the most recent block delivery across the full inbound set.
- Connection age — preserve the oldest 50 % of remaining peers.
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-shotaddrfetch 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.subnetaccepts 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). Onaddany currently-connected peer in the matching IP range is also disconnected.bantimedefaults to 24 h on add; passing 0 (or a negative value) also selects the default. Withabsolute=truethe 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 longerbantimeon a ban that would expire sooner extends it.admin_listbanned— return active (non-expired) entries as JSON.admin_clearbanned— wipebanlist.json. Does not touch the in-memory discourage filter.
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).
