> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parallaxprotocol.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Peer management

PIP-0006 ships TCP 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:

| Field          | Bytes    | Meaning                                                                                                                                                   |
| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ProtoVersion` | 2        | Currently `1`. A floor of `HelloMinProtoVersion` is enforced; older peers are refused.                                                                    |
| `Nonce`        | 8        | Random per `Server` lifetime. Echoing it back identifies a self-connect (dialed our own external IP through a hairpinning NAT).                           |
| `ListenPort`   | 2        | The peer's TCP listen port. `0` means "unknown" (peer behind opaque NAT).                                                                                 |
| `Services`     | 4        | Bit flags. `NODE_NETWORK = 1<<0` (serves blocks), `NODE_RELAY_TX = 1<<2` (accepts tx relay). Block-relay-only peers clear `RELAY_TX` on outbound `Hello`. |
| `Tail`         | RLP-tail | Forward-compat slot for future fields.                                                                                                                    |

`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.

| Field          | What it measures                                                                                                                                                                                                                                                                                          |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MinPing`      | Smallest ping RTT observed across the session, in nanoseconds.                                                                                                                                                                                                                                            |
| `LastBlockRx`  | Monotonic time of the last block *accepted* from this peer — stamped when a propagated `NewBlock` imports successfully, not on mere receipt of block-bearing messages. Mirrors Bitcoin Core's `m_last_block_time` (acceptance, not traffic), so a peer serving headers/bodies during sync earns no stamp. |
| `LastTxRx`     | Monotonic time of the last transaction from this peer *accepted into the pool* — stamped on pool admission, not message receipt, mirroring Core's `m_last_tx_time`. Block-relay-only peers leave this at zero.                                                                                            |
| `BytesRx`      | Cumulative payload bytes received. Framing overhead excluded so v1/v2 transports compare meaningfully.                                                                                                                                                                                                    |
| `RelayTxs`     | Mirror of the peer's `Hello.Services & RELAY_TX` bit. Defaults true; flipped to false on block-relay-only outbound.                                                                                                                                                                                       |
| `NetworkGroup` | Cached `/16`-IPv4 or `/32`-IPv6 prefix bytes, computed once at peer attach.                                                                                                                                                                                                                               |

## Slot allocation

Outbound dial slots split into four buckets:

| Bucket           | Default                                                 | What it's for                                                                                      |
| ---------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Full-relay       | `MaxPeers / DialRatio - MaxBlockRelayPeers`             | Normal peers — block + tx + addr gossip both ways.                                                 |
| Block-relay-only | 2 (`MaxBlockRelayPeers`)                                | Anti-eclipse insurance. Block traffic only; no tx, no addr gossip.                                 |
| Feeler           | 1, \~120 s cadence (only while outbound slots are full) | Probes one addrman entry per tick to keep `LastSuccess` fresh. Disconnects after a short lifetime. |
| Anchor           | up to 2, startup-only                                   | Re-dials block-relay-only peers from the previous clean shutdown.                                  |

`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:

| Set             | Storage                                | Source                                        | Rejects                                                                                                                                                                                                                                                  |
| --------------- | -------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Banned**      | persistent (`<datadir>/banlist.json`)  | operator (`setban` RPC)                       | every inbound from a matching IP/CIDR, and every automatic outbound dial to one.                                                                                                                                                                         |
| **Discouraged** | in-memory Bloom filter, \~50k capacity | automatic (peer misbehavior during a session) | inbound from a matching IP **only when the inbound pool is at saturation**; automatic outbound dials always (operator-initiated `addPeer` / `dialV2` dials are exempt). Under saturation, discouraged survivors are also the preferred eviction victims. |

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
[PIP-0006](./pip-0006#operator-rpc).

* `admin_setban <subnet> <add|remove> [bantime] [absolute]` — add
  or remove an entry. `subnet` accepts a plain IP (`1.2.3.4` →
  `/32`) or a CIDR (`10.0.0.0/24`). On `add` any currently-connected
  peer in the matching 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

| Field                | Default                                        | Notes                                                                                                                                                             |
| -------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MaxBlockRelayPeers` | 2                                              | Outbound slots reserved for block-relay-only. Capped at `maxDialedConns / 2`. Negative disables the bucket entirely; every outbound dial then becomes full-relay. |
| `AnchorsPath`        | `<datadir>/anchors.dat`                        | Empty disables anchor persistence.                                                                                                                                |
| `BanList`            | non-`nil` BanMan with `<datadir>/banlist.json` | `nil` disables ban / discourage gating. Useful in ephemeral tests.                                                                                                |

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).
