Skip to content

directory_server.peer_registry

directory_server.peer_registry

Peer registry for tracking active peers and their metadata.

Implements Single Responsibility Principle: only manages peer state.

Classes

DisplacedPeer dataclass

Source code in directory_server/src/directory_server/peer_registry.py
35
36
37
38
39
@dataclass(frozen=True)
class DisplacedPeer:
    peer_key: str
    peer: PeerInfo
    connection_id: str
Attributes
connection_id: str instance-attribute
peer: PeerInfo instance-attribute
peer_key: str instance-attribute

PeerNotFoundError

Bases: Exception

Source code in directory_server/src/directory_server/peer_registry.py
17
18
class PeerNotFoundError(Exception):
    pass

PeerOwner dataclass

Source code in directory_server/src/directory_server/peer_registry.py
25
26
27
28
29
30
31
32
@dataclass(frozen=True)
class PeerOwner:
    connection_id: str
    verified_pubkey: bytes | str | None = None

    @property
    def verified(self) -> bool:
        return self.verified_pubkey is not None
Attributes
connection_id: str instance-attribute
verified: bool property
verified_pubkey: bytes | str | None = None class-attribute instance-attribute

PeerOwnershipConflictError

Bases: Exception

Raised when a live peer already owns a nick.

Source code in directory_server/src/directory_server/peer_registry.py
21
22
class PeerOwnershipConflictError(Exception):
    """Raised when a live peer already owns a nick."""

PeerRegistry

Source code in directory_server/src/directory_server/peer_registry.py
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
class PeerRegistry:
    def __init__(self, max_peers: int = 1000):
        self.max_peers = max_peers
        self._peers: dict[str, PeerInfo] = {}
        self._owners: dict[str, PeerOwner] = {}

    def register(
        self,
        peer: PeerInfo,
        connection_id: str | None = None,
        *,
        verified_pubkey: bytes | str | None = None,
    ) -> RegistrationResult:
        """Atomically reserve a peer's nick for one connection.

        Legacy registrations use first-live-writer ownership. A verified registration may
        replace legacy owners, while replacing a verified owner requires the same full pubkey.
        The self-declared location remains metadata and is never an ownership key.
        """
        connection_id = connection_id or uuid4().hex
        location = peer.location_string
        key = peer.nick

        displaced: list[DisplacedPeer] = []
        existing_peer = self._peers.get(key)
        existing_owner = self._owners.get(key)
        if existing_peer is not None and existing_owner is not None:
            if verified_pubkey is None:
                raise PeerOwnershipConflictError(f"Peer nick already registered: {peer.nick}")
            if existing_owner.verified and existing_owner.verified_pubkey != verified_pubkey:
                raise PeerOwnershipConflictError(f"Verified nick already registered: {peer.nick}")
            displaced.append(
                DisplacedPeer(
                    peer_key=key,
                    peer=existing_peer,
                    connection_id=existing_owner.connection_id,
                )
            )

        if len(self._peers) - len(displaced) >= self.max_peers:
            raise ValueError(f"Maximum peers reached: {self.max_peers}")

        for old in displaced:
            self._remove(old.peer_key)

        self._peers[key] = peer
        self._owners[key] = PeerOwner(
            connection_id=connection_id,
            verified_pubkey=verified_pubkey,
        )

        peer.last_seen = datetime.now(UTC)
        logger.info(f"Registered peer: {peer.nick} at {location}")
        return RegistrationResult(
            peer_key=key,
            connection_id=connection_id,
            displaced=tuple(displaced),
        )

    def unregister(self, key: str, expected_connection_id: str | None = None) -> bool:
        if not self.is_current_owner(key, expected_connection_id):
            return False

        peer = self._remove(key)
        if peer is None:
            return False
        logger.info(f"Unregistered peer: {peer.nick} at {peer.location_string}")
        return True

    def _remove(self, key: str) -> PeerInfo | None:
        peer = self._peers.pop(key, None)
        if peer is None:
            return None
        self._owners.pop(key, None)
        return peer

    def is_current_owner(self, key: str, expected_connection_id: str | None) -> bool:
        owner = self._owners.get(key)
        if owner is None:
            return False
        return expected_connection_id is None or owner.connection_id == expected_connection_id

    def get_owner(self, key: str) -> PeerOwner | None:
        return self._owners.get(key)

    def get_connection_id(self, key: str) -> str | None:
        owner = self.get_owner(key)
        return owner.connection_id if owner else None

    def get_by_key(self, key: str) -> PeerInfo | None:
        return self._peers.get(key)

    def get_by_nick(self, nick: str) -> PeerInfo | None:
        return self._peers.get(nick)

    def get_key_by_nick(self, nick: str) -> str | None:
        """Return the routing key currently reserved for ``nick``."""
        return nick if nick in self._peers else None

    def update_status(
        self,
        key: str,
        status: PeerStatus,
        expected_connection_id: str | None = None,
    ) -> bool:
        if not self.is_current_owner(key, expected_connection_id):
            return False
        peer = self.get_by_key(key)
        if peer:
            peer.status = status
            if status in (PeerStatus.CONNECTED, PeerStatus.HANDSHAKED):
                peer.last_seen = datetime.now(UTC)
            return True
        return False

    def update_last_seen(self, key: str, expected_connection_id: str | None = None) -> bool:
        """Update the last_seen timestamp for a peer.

        Called on every received message to track peer liveness for heartbeat.
        """
        if not self.is_current_owner(key, expected_connection_id):
            return False
        peer = self.get_by_key(key)
        if peer:
            peer.last_seen = datetime.now(UTC)
            return True
        return False

    def _iter_connected(self, network: NetworkType | None = None) -> Iterator[PeerInfo]:
        """Iterator over connected peers.

        Creates a snapshot of peers to avoid RuntimeError if dict is modified during iteration.
        """
        for p in list(self._peers.values()):
            if (
                p.status == PeerStatus.HANDSHAKED
                and not p.is_directory
                and (network is None or p.network == network)
            ):
                yield p

    def iter_connected(self, network: NetworkType | None = None) -> Iterator[PeerInfo]:
        """Public memory-efficient iterator over connected peers."""
        return self._iter_connected(network)

    def iter_connected_owners(
        self, network: NetworkType | None = None
    ) -> Iterator[tuple[str, PeerInfo, str]]:
        """Iterate over handshaked peers with their current ownership generation."""
        for key, peer in list(self._peers.items()):
            owner = self._owners.get(key)
            if (
                owner is not None
                and peer.status == PeerStatus.HANDSHAKED
                and not peer.is_directory
                and (network is None or peer.network == network)
            ):
                yield key, peer, owner.connection_id

    def get_all_connected(self, network: NetworkType | None = None) -> list[PeerInfo]:
        return list(self._iter_connected(network))

    def get_peerlist_for_network(self, network: NetworkType) -> list[tuple[str, str]]:
        # Use generator to avoid intermediate list
        # Include all connected peers, even NOT-SERVING-ONION
        # While they can't be directly connected to, they are reachable via the directory
        # for private messages, so this information is useful
        return [(peer.nick, peer.location_string) for peer in self._iter_connected(network)]

    def get_peerlist_with_features(self, network: NetworkType) -> list[tuple[str, str, FeatureSet]]:
        """
        Get peerlist with features for peers on a network.

        Returns list of (nick, location, features) tuples for connected peers.
        Includes all peers, even NOT-SERVING-ONION, as they are still reachable
        via the directory for private messaging.
        """
        result = []
        for peer in self._iter_connected(network):
            # Build FeatureSet from peer.features dict
            features = FeatureSet(features={k for k, v in peer.features.items() if v is True})
            # Debug: Log when features are extracted for peerlist
            if peer.features and not features.features:
                logger.warning(
                    f"Peer {peer.nick} has features dict {peer.features} but "
                    f"FeatureSet is empty after 'v is True' filter"
                )
            result.append((peer.nick, peer.location_string, features))
        return result

    def count(self) -> int:
        return len(self._peers)

    def clear(self) -> None:
        self._peers.clear()
        self._owners.clear()

    def get_passive_peers(self, network: NetworkType | None = None) -> list[PeerInfo]:
        """
        Get passive peers (NOT-SERVING-ONION).

        These are typically orderbook watchers/takers that don't host their own
        onion service but connect to the directory to watch offers.
        """
        return [p for p in self._iter_connected(network) if p.onion_address == "NOT-SERVING-ONION"]

    def get_active_peers(self, network: NetworkType | None = None) -> list[PeerInfo]:
        """
        Get active peers (serving onion address).

        These are typically makers that host their own onion service and
        publish offers to the orderbook.
        """
        return [p for p in self._iter_connected(network) if p.onion_address != "NOT-SERVING-ONION"]

    def get_stats(self) -> dict[str, int]:
        connected = 0
        passive = 0
        active = 0
        neutrino_compat = 0
        peerlist_features = 0
        push_encrypted = 0

        for p in list(self._peers.values()):
            if p.status == PeerStatus.HANDSHAKED and not p.is_directory:
                connected += 1
                if p.onion_address == "NOT-SERVING-ONION":
                    passive += 1
                else:
                    active += 1
                # Count feature support from features dict
                features = p.features
                if features.get("neutrino_compat"):
                    neutrino_compat += 1
                if features.get("peerlist_features"):
                    peerlist_features += 1
                if features.get("push_encrypted"):
                    push_encrypted += 1

        return {
            "total_peers": len(self._peers),
            "connected_peers": connected,
            "passive_peers": passive,
            "active_peers": active,
            "neutrino_compat_peers": neutrino_compat,
            "peerlist_features_peers": peerlist_features,
            "push_encrypted_peers": push_encrypted,
        }

    def get_neutrino_compat_peers(self, network: NetworkType | None = None) -> list[PeerInfo]:
        """
        Get peers that support neutrino_compat feature.

        These peers advertise extended UTXO metadata (scriptpubkey, blockheight)
        which is required for Neutrino backend verification.
        """
        return [p for p in self._iter_connected(network) if p.neutrino_compat]

    def get_peers_idle_since(self, cutoff: datetime) -> list[tuple[str, PeerInfo, str]]:
        """Get connected peers whose last_seen is older than cutoff.

        Returns list of (peer_key, peer_info, connection_id) tuples.
        """
        result: list[tuple[str, PeerInfo, str]] = []
        for key, peer in list(self._peers.items()):
            owner = self._owners.get(key)
            if (
                owner is not None
                and peer.status == PeerStatus.HANDSHAKED
                and not peer.is_directory
                and peer.last_seen is not None
                and peer.last_seen < cutoff
            ):
                result.append((key, peer, owner.connection_id))
        return result

    def supports_ping(self, key: str, expected_connection_id: str | None = None) -> bool:
        """Check if a peer supports PING/PONG heartbeat."""
        if not self.is_current_owner(key, expected_connection_id):
            return False
        peer = self.get_by_key(key)
        if peer is None:
            return False
        return peer.features.get("ping", False) is True

    def is_maker(self, key: str, expected_connection_id: str | None = None) -> bool:
        """Check if a peer is a maker (serves an onion address)."""
        if not self.is_current_owner(key, expected_connection_id):
            return False
        peer = self.get_by_key(key)
        if peer is None:
            return False
        return peer.onion_address != "NOT-SERVING-ONION"
Attributes
max_peers = max_peers instance-attribute
Methods:
__init__(max_peers: int = 1000)
Source code in directory_server/src/directory_server/peer_registry.py
50
51
52
53
def __init__(self, max_peers: int = 1000):
    self.max_peers = max_peers
    self._peers: dict[str, PeerInfo] = {}
    self._owners: dict[str, PeerOwner] = {}
clear() -> None
Source code in directory_server/src/directory_server/peer_registry.py
242
243
244
def clear(self) -> None:
    self._peers.clear()
    self._owners.clear()
count() -> int
Source code in directory_server/src/directory_server/peer_registry.py
239
240
def count(self) -> int:
    return len(self._peers)
get_active_peers(network: NetworkType | None = None) -> list[PeerInfo]

Get active peers (serving onion address).

These are typically makers that host their own onion service and publish offers to the orderbook.

Source code in directory_server/src/directory_server/peer_registry.py
255
256
257
258
259
260
261
262
def get_active_peers(self, network: NetworkType | None = None) -> list[PeerInfo]:
    """
    Get active peers (serving onion address).

    These are typically makers that host their own onion service and
    publish offers to the orderbook.
    """
    return [p for p in self._iter_connected(network) if p.onion_address != "NOT-SERVING-ONION"]
get_all_connected(network: NetworkType | None = None) -> list[PeerInfo]
Source code in directory_server/src/directory_server/peer_registry.py
208
209
def get_all_connected(self, network: NetworkType | None = None) -> list[PeerInfo]:
    return list(self._iter_connected(network))
get_by_key(key: str) -> PeerInfo | None
Source code in directory_server/src/directory_server/peer_registry.py
138
139
def get_by_key(self, key: str) -> PeerInfo | None:
    return self._peers.get(key)
get_by_nick(nick: str) -> PeerInfo | None
Source code in directory_server/src/directory_server/peer_registry.py
141
142
def get_by_nick(self, nick: str) -> PeerInfo | None:
    return self._peers.get(nick)
get_connection_id(key: str) -> str | None
Source code in directory_server/src/directory_server/peer_registry.py
134
135
136
def get_connection_id(self, key: str) -> str | None:
    owner = self.get_owner(key)
    return owner.connection_id if owner else None
get_key_by_nick(nick: str) -> str | None

Return the routing key currently reserved for nick.

Source code in directory_server/src/directory_server/peer_registry.py
144
145
146
def get_key_by_nick(self, nick: str) -> str | None:
    """Return the routing key currently reserved for ``nick``."""
    return nick if nick in self._peers else None
get_neutrino_compat_peers(network: NetworkType | None = None) -> list[PeerInfo]

Get peers that support neutrino_compat feature.

These peers advertise extended UTXO metadata (scriptpubkey, blockheight) which is required for Neutrino backend verification.

Source code in directory_server/src/directory_server/peer_registry.py
298
299
300
301
302
303
304
305
def get_neutrino_compat_peers(self, network: NetworkType | None = None) -> list[PeerInfo]:
    """
    Get peers that support neutrino_compat feature.

    These peers advertise extended UTXO metadata (scriptpubkey, blockheight)
    which is required for Neutrino backend verification.
    """
    return [p for p in self._iter_connected(network) if p.neutrino_compat]
get_owner(key: str) -> PeerOwner | None
Source code in directory_server/src/directory_server/peer_registry.py
131
132
def get_owner(self, key: str) -> PeerOwner | None:
    return self._owners.get(key)
get_passive_peers(network: NetworkType | None = None) -> list[PeerInfo]

Get passive peers (NOT-SERVING-ONION).

These are typically orderbook watchers/takers that don't host their own onion service but connect to the directory to watch offers.

Source code in directory_server/src/directory_server/peer_registry.py
246
247
248
249
250
251
252
253
def get_passive_peers(self, network: NetworkType | None = None) -> list[PeerInfo]:
    """
    Get passive peers (NOT-SERVING-ONION).

    These are typically orderbook watchers/takers that don't host their own
    onion service but connect to the directory to watch offers.
    """
    return [p for p in self._iter_connected(network) if p.onion_address == "NOT-SERVING-ONION"]
get_peerlist_for_network(network: NetworkType) -> list[tuple[str, str]]
Source code in directory_server/src/directory_server/peer_registry.py
211
212
213
214
215
216
def get_peerlist_for_network(self, network: NetworkType) -> list[tuple[str, str]]:
    # Use generator to avoid intermediate list
    # Include all connected peers, even NOT-SERVING-ONION
    # While they can't be directly connected to, they are reachable via the directory
    # for private messages, so this information is useful
    return [(peer.nick, peer.location_string) for peer in self._iter_connected(network)]
get_peerlist_with_features(network: NetworkType) -> list[tuple[str, str, FeatureSet]]

Get peerlist with features for peers on a network.

Returns list of (nick, location, features) tuples for connected peers. Includes all peers, even NOT-SERVING-ONION, as they are still reachable via the directory for private messaging.

Source code in directory_server/src/directory_server/peer_registry.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
def get_peerlist_with_features(self, network: NetworkType) -> list[tuple[str, str, FeatureSet]]:
    """
    Get peerlist with features for peers on a network.

    Returns list of (nick, location, features) tuples for connected peers.
    Includes all peers, even NOT-SERVING-ONION, as they are still reachable
    via the directory for private messaging.
    """
    result = []
    for peer in self._iter_connected(network):
        # Build FeatureSet from peer.features dict
        features = FeatureSet(features={k for k, v in peer.features.items() if v is True})
        # Debug: Log when features are extracted for peerlist
        if peer.features and not features.features:
            logger.warning(
                f"Peer {peer.nick} has features dict {peer.features} but "
                f"FeatureSet is empty after 'v is True' filter"
            )
        result.append((peer.nick, peer.location_string, features))
    return result
get_peers_idle_since(cutoff: datetime) -> list[tuple[str, PeerInfo, str]]

Get connected peers whose last_seen is older than cutoff.

Returns list of (peer_key, peer_info, connection_id) tuples.

Source code in directory_server/src/directory_server/peer_registry.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
def get_peers_idle_since(self, cutoff: datetime) -> list[tuple[str, PeerInfo, str]]:
    """Get connected peers whose last_seen is older than cutoff.

    Returns list of (peer_key, peer_info, connection_id) tuples.
    """
    result: list[tuple[str, PeerInfo, str]] = []
    for key, peer in list(self._peers.items()):
        owner = self._owners.get(key)
        if (
            owner is not None
            and peer.status == PeerStatus.HANDSHAKED
            and not peer.is_directory
            and peer.last_seen is not None
            and peer.last_seen < cutoff
        ):
            result.append((key, peer, owner.connection_id))
    return result
get_stats() -> dict[str, int]
Source code in directory_server/src/directory_server/peer_registry.py
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def get_stats(self) -> dict[str, int]:
    connected = 0
    passive = 0
    active = 0
    neutrino_compat = 0
    peerlist_features = 0
    push_encrypted = 0

    for p in list(self._peers.values()):
        if p.status == PeerStatus.HANDSHAKED and not p.is_directory:
            connected += 1
            if p.onion_address == "NOT-SERVING-ONION":
                passive += 1
            else:
                active += 1
            # Count feature support from features dict
            features = p.features
            if features.get("neutrino_compat"):
                neutrino_compat += 1
            if features.get("peerlist_features"):
                peerlist_features += 1
            if features.get("push_encrypted"):
                push_encrypted += 1

    return {
        "total_peers": len(self._peers),
        "connected_peers": connected,
        "passive_peers": passive,
        "active_peers": active,
        "neutrino_compat_peers": neutrino_compat,
        "peerlist_features_peers": peerlist_features,
        "push_encrypted_peers": push_encrypted,
    }
is_current_owner(key: str, expected_connection_id: str | None) -> bool
Source code in directory_server/src/directory_server/peer_registry.py
125
126
127
128
129
def is_current_owner(self, key: str, expected_connection_id: str | None) -> bool:
    owner = self._owners.get(key)
    if owner is None:
        return False
    return expected_connection_id is None or owner.connection_id == expected_connection_id
is_maker(key: str, expected_connection_id: str | None = None) -> bool

Check if a peer is a maker (serves an onion address).

Source code in directory_server/src/directory_server/peer_registry.py
334
335
336
337
338
339
340
341
def is_maker(self, key: str, expected_connection_id: str | None = None) -> bool:
    """Check if a peer is a maker (serves an onion address)."""
    if not self.is_current_owner(key, expected_connection_id):
        return False
    peer = self.get_by_key(key)
    if peer is None:
        return False
    return peer.onion_address != "NOT-SERVING-ONION"
iter_connected(network: NetworkType | None = None) -> Iterator[PeerInfo]

Public memory-efficient iterator over connected peers.

Source code in directory_server/src/directory_server/peer_registry.py
190
191
192
def iter_connected(self, network: NetworkType | None = None) -> Iterator[PeerInfo]:
    """Public memory-efficient iterator over connected peers."""
    return self._iter_connected(network)
iter_connected_owners(network: NetworkType | None = None) -> Iterator[tuple[str, PeerInfo, str]]

Iterate over handshaked peers with their current ownership generation.

Source code in directory_server/src/directory_server/peer_registry.py
194
195
196
197
198
199
200
201
202
203
204
205
206
def iter_connected_owners(
    self, network: NetworkType | None = None
) -> Iterator[tuple[str, PeerInfo, str]]:
    """Iterate over handshaked peers with their current ownership generation."""
    for key, peer in list(self._peers.items()):
        owner = self._owners.get(key)
        if (
            owner is not None
            and peer.status == PeerStatus.HANDSHAKED
            and not peer.is_directory
            and (network is None or peer.network == network)
        ):
            yield key, peer, owner.connection_id
register(peer: PeerInfo, connection_id: str | None = None, *, verified_pubkey: bytes | str | None = None) -> RegistrationResult

Atomically reserve a peer's nick for one connection.

Legacy registrations use first-live-writer ownership. A verified registration may replace legacy owners, while replacing a verified owner requires the same full pubkey. The self-declared location remains metadata and is never an ownership key.

Source code in directory_server/src/directory_server/peer_registry.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def register(
    self,
    peer: PeerInfo,
    connection_id: str | None = None,
    *,
    verified_pubkey: bytes | str | None = None,
) -> RegistrationResult:
    """Atomically reserve a peer's nick for one connection.

    Legacy registrations use first-live-writer ownership. A verified registration may
    replace legacy owners, while replacing a verified owner requires the same full pubkey.
    The self-declared location remains metadata and is never an ownership key.
    """
    connection_id = connection_id or uuid4().hex
    location = peer.location_string
    key = peer.nick

    displaced: list[DisplacedPeer] = []
    existing_peer = self._peers.get(key)
    existing_owner = self._owners.get(key)
    if existing_peer is not None and existing_owner is not None:
        if verified_pubkey is None:
            raise PeerOwnershipConflictError(f"Peer nick already registered: {peer.nick}")
        if existing_owner.verified and existing_owner.verified_pubkey != verified_pubkey:
            raise PeerOwnershipConflictError(f"Verified nick already registered: {peer.nick}")
        displaced.append(
            DisplacedPeer(
                peer_key=key,
                peer=existing_peer,
                connection_id=existing_owner.connection_id,
            )
        )

    if len(self._peers) - len(displaced) >= self.max_peers:
        raise ValueError(f"Maximum peers reached: {self.max_peers}")

    for old in displaced:
        self._remove(old.peer_key)

    self._peers[key] = peer
    self._owners[key] = PeerOwner(
        connection_id=connection_id,
        verified_pubkey=verified_pubkey,
    )

    peer.last_seen = datetime.now(UTC)
    logger.info(f"Registered peer: {peer.nick} at {location}")
    return RegistrationResult(
        peer_key=key,
        connection_id=connection_id,
        displaced=tuple(displaced),
    )
supports_ping(key: str, expected_connection_id: str | None = None) -> bool

Check if a peer supports PING/PONG heartbeat.

Source code in directory_server/src/directory_server/peer_registry.py
325
326
327
328
329
330
331
332
def supports_ping(self, key: str, expected_connection_id: str | None = None) -> bool:
    """Check if a peer supports PING/PONG heartbeat."""
    if not self.is_current_owner(key, expected_connection_id):
        return False
    peer = self.get_by_key(key)
    if peer is None:
        return False
    return peer.features.get("ping", False) is True
unregister(key: str, expected_connection_id: str | None = None) -> bool
Source code in directory_server/src/directory_server/peer_registry.py
108
109
110
111
112
113
114
115
116
def unregister(self, key: str, expected_connection_id: str | None = None) -> bool:
    if not self.is_current_owner(key, expected_connection_id):
        return False

    peer = self._remove(key)
    if peer is None:
        return False
    logger.info(f"Unregistered peer: {peer.nick} at {peer.location_string}")
    return True
update_last_seen(key: str, expected_connection_id: str | None = None) -> bool

Update the last_seen timestamp for a peer.

Called on every received message to track peer liveness for heartbeat.

Source code in directory_server/src/directory_server/peer_registry.py
164
165
166
167
168
169
170
171
172
173
174
175
def update_last_seen(self, key: str, expected_connection_id: str | None = None) -> bool:
    """Update the last_seen timestamp for a peer.

    Called on every received message to track peer liveness for heartbeat.
    """
    if not self.is_current_owner(key, expected_connection_id):
        return False
    peer = self.get_by_key(key)
    if peer:
        peer.last_seen = datetime.now(UTC)
        return True
    return False
update_status(key: str, status: PeerStatus, expected_connection_id: str | None = None) -> bool
Source code in directory_server/src/directory_server/peer_registry.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def update_status(
    self,
    key: str,
    status: PeerStatus,
    expected_connection_id: str | None = None,
) -> bool:
    if not self.is_current_owner(key, expected_connection_id):
        return False
    peer = self.get_by_key(key)
    if peer:
        peer.status = status
        if status in (PeerStatus.CONNECTED, PeerStatus.HANDSHAKED):
            peer.last_seen = datetime.now(UTC)
        return True
    return False

RegistrationResult dataclass

Source code in directory_server/src/directory_server/peer_registry.py
42
43
44
45
46
@dataclass(frozen=True)
class RegistrationResult:
    peer_key: str
    connection_id: str
    displaced: tuple[DisplacedPeer, ...] = ()
Attributes
connection_id: str instance-attribute
displaced: tuple[DisplacedPeer, ...] = () class-attribute instance-attribute
peer_key: str instance-attribute