astreum 0.2.4__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of astreum might be problematic. Click here for more details.

Files changed (63) hide show
  1. {astreum-0.2.4/src/astreum.egg-info → astreum-0.2.6}/PKG-INFO +1 -1
  2. {astreum-0.2.4 → astreum-0.2.6}/pyproject.toml +1 -1
  3. {astreum-0.2.4 → astreum-0.2.6}/src/astreum/node.py +1 -1
  4. {astreum-0.2.4 → astreum-0.2.6/src/astreum.egg-info}/PKG-INFO +1 -1
  5. astreum-0.2.6/src/astreum.egg-info/SOURCES.txt +22 -0
  6. astreum-0.2.4/src/astreum/_node/relay/__init__.py +0 -371
  7. astreum-0.2.4/src/astreum/_node/relay/bucket.py +0 -90
  8. astreum-0.2.4/src/astreum/_node/relay/envelope.py +0 -280
  9. astreum-0.2.4/src/astreum/_node/relay/message.py +0 -110
  10. astreum-0.2.4/src/astreum/_node/relay/peer.py +0 -174
  11. astreum-0.2.4/src/astreum/_node/relay/route.py +0 -161
  12. astreum-0.2.4/src/astreum/_node/storage/storage.py +0 -253
  13. astreum-0.2.4/src/astreum/_node/storage/utils.py +0 -137
  14. astreum-0.2.4/src/astreum/_node/utils.py +0 -34
  15. astreum-0.2.4/src/astreum/_node/validation/_block/__init__.py +0 -0
  16. astreum-0.2.4/src/astreum/_node/validation/_block/create.py +0 -98
  17. astreum-0.2.4/src/astreum/_node/validation/_block/model.py +0 -81
  18. astreum-0.2.4/src/astreum/_node/validation/_block/validate.py +0 -196
  19. astreum-0.2.4/src/astreum/_node/validation/account.py +0 -99
  20. astreum-0.2.4/src/astreum/_node/validation/block.py +0 -21
  21. astreum-0.2.4/src/astreum/_node/validation/constants.py +0 -15
  22. astreum-0.2.4/src/astreum/_node/validation/stake.py +0 -229
  23. astreum-0.2.4/src/astreum/_node/validation/transaction.py +0 -146
  24. astreum-0.2.4/src/astreum/_node/validation/vdf.py +0 -80
  25. astreum-0.2.4/src/astreum/crypto/__init__.py +0 -0
  26. astreum-0.2.4/src/astreum/lispeum/expression.py +0 -95
  27. astreum-0.2.4/src/astreum/lispeum/special/__init__.py +0 -0
  28. astreum-0.2.4/src/astreum/lispeum/special/definition.py +0 -27
  29. astreum-0.2.4/src/astreum/lispeum/special/list/__init__.py +0 -0
  30. astreum-0.2.4/src/astreum/lispeum/special/list/all.py +0 -32
  31. astreum-0.2.4/src/astreum/lispeum/special/list/any.py +0 -32
  32. astreum-0.2.4/src/astreum/lispeum/special/list/fold.py +0 -29
  33. astreum-0.2.4/src/astreum/lispeum/special/list/get.py +0 -20
  34. astreum-0.2.4/src/astreum/lispeum/special/list/insert.py +0 -23
  35. astreum-0.2.4/src/astreum/lispeum/special/list/map.py +0 -30
  36. astreum-0.2.4/src/astreum/lispeum/special/list/position.py +0 -33
  37. astreum-0.2.4/src/astreum/lispeum/special/list/remove.py +0 -22
  38. astreum-0.2.4/src/astreum/lispeum/special/number/__init__.py +0 -0
  39. astreum-0.2.4/src/astreum/lispeum/special/number/addition.py +0 -0
  40. astreum-0.2.4/src/astreum/lispeum/storage.py +0 -410
  41. astreum-0.2.4/src/astreum/machine/__init__.py +0 -352
  42. astreum-0.2.4/src/astreum/machine/environment.py +0 -4
  43. astreum-0.2.4/src/astreum/machine/error.py +0 -0
  44. astreum-0.2.4/src/astreum.egg-info/SOURCES.txt +0 -60
  45. {astreum-0.2.4 → astreum-0.2.6}/LICENSE +0 -0
  46. {astreum-0.2.4 → astreum-0.2.6}/README.md +0 -0
  47. {astreum-0.2.4 → astreum-0.2.6}/setup.cfg +0 -0
  48. {astreum-0.2.4 → astreum-0.2.6}/src/astreum/__init__.py +0 -0
  49. {astreum-0.2.4 → astreum-0.2.6}/src/astreum/_node/__init__.py +0 -0
  50. {astreum-0.2.4 → astreum-0.2.6}/src/astreum/_node/storage/__init__.py +0 -0
  51. {astreum-0.2.4 → astreum-0.2.6}/src/astreum/_node/storage/merkle.py +0 -0
  52. {astreum-0.2.4 → astreum-0.2.6}/src/astreum/_node/storage/patricia.py +0 -0
  53. {astreum-0.2.4/src/astreum/_node/validation → astreum-0.2.6/src/astreum/crypto}/__init__.py +0 -0
  54. {astreum-0.2.4 → astreum-0.2.6}/src/astreum/crypto/ed25519.py +0 -0
  55. {astreum-0.2.4 → astreum-0.2.6}/src/astreum/crypto/x25519.py +0 -0
  56. {astreum-0.2.4 → astreum-0.2.6}/src/astreum/format.py +0 -0
  57. {astreum-0.2.4 → astreum-0.2.6}/src/astreum/lispeum/__init__.py +0 -0
  58. {astreum-0.2.4 → astreum-0.2.6}/src/astreum/lispeum/parser.py +0 -0
  59. {astreum-0.2.4 → astreum-0.2.6}/src/astreum/lispeum/tokenizer.py +0 -0
  60. {astreum-0.2.4 → astreum-0.2.6}/src/astreum.egg-info/dependency_links.txt +0 -0
  61. {astreum-0.2.4 → astreum-0.2.6}/src/astreum.egg-info/requires.txt +0 -0
  62. {astreum-0.2.4 → astreum-0.2.6}/src/astreum.egg-info/top_level.txt +0 -0
  63. {astreum-0.2.4 → astreum-0.2.6}/tests/test_node_machine.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
5
5
  Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
6
6
  Project-URL: Homepage, https://github.com/astreum/lib
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "astreum"
3
- version = "0.2.4"
3
+ version = "0.2.6"
4
4
  authors = [
5
5
  { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
6
  ]
@@ -309,7 +309,7 @@ class Env:
309
309
 
310
310
 
311
311
  class Node:
312
- def __init__(self, config: dict):
312
+ def __init__(self, config: dict = {}):
313
313
  self._machine_setup()
314
314
  machine_only = bool(config.get('machine-only', True))
315
315
  if not machine_only:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
5
5
  Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
6
6
  Project-URL: Homepage, https://github.com/astreum/lib
@@ -0,0 +1,22 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/astreum/__init__.py
5
+ src/astreum/format.py
6
+ src/astreum/node.py
7
+ src/astreum.egg-info/PKG-INFO
8
+ src/astreum.egg-info/SOURCES.txt
9
+ src/astreum.egg-info/dependency_links.txt
10
+ src/astreum.egg-info/requires.txt
11
+ src/astreum.egg-info/top_level.txt
12
+ src/astreum/_node/__init__.py
13
+ src/astreum/_node/storage/__init__.py
14
+ src/astreum/_node/storage/merkle.py
15
+ src/astreum/_node/storage/patricia.py
16
+ src/astreum/crypto/__init__.py
17
+ src/astreum/crypto/ed25519.py
18
+ src/astreum/crypto/x25519.py
19
+ src/astreum/lispeum/__init__.py
20
+ src/astreum/lispeum/parser.py
21
+ src/astreum/lispeum/tokenizer.py
22
+ tests/test_node_machine.py
@@ -1,371 +0,0 @@
1
- """
2
- Relay module for handling network communication in the Astreum node.
3
- """
4
-
5
- import socket
6
- import threading
7
- import random
8
- import time
9
- from queue import Queue
10
- from typing import Tuple, Callable, Dict, Set, Optional, List
11
- from .message import Message, Topic
12
- from .envelope import Envelope
13
- from .bucket import KBucket
14
- from .peer import Peer, PeerManager
15
- from .route import RouteTable
16
- import json
17
- from cryptography.hazmat.primitives.asymmetric import ed25519
18
-
19
- class Relay:
20
- def __init__(self, config: dict):
21
- """Initialize relay with configuration."""
22
- self.config = config
23
- self.use_ipv6 = config.get('use_ipv6', False)
24
- incoming_port = config.get('incoming_port', 7373)
25
- self.max_message_size = config.get('max_message_size', 65536) # Max UDP datagram size
26
- self.num_workers = config.get('num_workers', 4)
27
-
28
- # Generate Ed25519 keypair for this node
29
- if 'relay_private_key' in config:
30
- # Load existing private key if provided
31
- try:
32
- private_key_bytes = bytes.fromhex(config['relay_private_key'])
33
- self.private_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key_bytes)
34
- except Exception as e:
35
- print(f"Error loading private key: {e}, generating new one")
36
- self.private_key = ed25519.Ed25519PrivateKey.generate()
37
- else:
38
- # Generate new keypair
39
- self.private_key = ed25519.Ed25519PrivateKey.generate()
40
-
41
- # Use public key as node ID
42
- self.public_key = self.private_key.public_key()
43
- self.node_id = self.public_key.public_bytes_raw()
44
-
45
- # Save private key bytes for config persistence
46
- self.private_key_bytes = self.private_key.private_bytes_raw()
47
-
48
- # Routes that this node participates in
49
- # 0 = peer route, 1 = validation route
50
- # All routes are tracked by default, but we only join some
51
- self.routes: List[int] = []
52
- self.tracked_routes: List[int] = [0, 1] # Track all routes
53
-
54
- # Always join peer route
55
- self.routes.append(0) # Peer route
56
-
57
- # Check if this node should join validation route
58
- if config.get('validation_route', False):
59
- self.routes.append(1) # Validation route
60
-
61
- # Choose address family based on IPv4 or IPv6
62
- family = socket.AF_INET6 if self.use_ipv6 else socket.AF_INET
63
-
64
- # Create a UDP socket
65
- self.incoming_socket = socket.socket(family, socket.SOCK_DGRAM)
66
-
67
- # Allow dual-stack support (IPv4-mapped addresses on IPv6)
68
- if self.use_ipv6:
69
- self.incoming_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
70
-
71
- # Bind to an address (IPv6 "::" or IPv4 "0.0.0.0") and port
72
- bind_address = "::" if self.use_ipv6 else "0.0.0.0"
73
- self.incoming_socket.bind((bind_address, incoming_port or 0))
74
-
75
- # Get the actual port assigned
76
- self.incoming_port = self.incoming_socket.getsockname()[1]
77
-
78
- # Create a UDP socket for sending messages
79
- self.outgoing_socket = socket.socket(family, socket.SOCK_DGRAM)
80
-
81
- # Message queues
82
- self.incoming_queue = Queue()
83
- self.outgoing_queue = Queue()
84
-
85
- # Message handling
86
- self.message_handlers: Dict[Topic, Callable] = {
87
- Topic.PEER_ROUTE: None, # set by Node later
88
- Topic.OBJECT_REQUEST: None, # set by Node later
89
- Topic.OBJECT_RESPONSE: None, # set by Node later
90
- }
91
-
92
- # Route buckets (peers for each route)
93
- self.peer_route_bucket = KBucket(k=20) # Bucket for peer route
94
- self.validation_route_bucket = KBucket(k=20) # Bucket for validation route
95
-
96
- # Initialize route table with our node ID
97
- self.route_table = RouteTable(self)
98
-
99
- # Initialize storage index
100
- self.storage_index: Dict[bytes, bytes] = {}
101
-
102
- # Start worker threads
103
- self._start_workers()
104
-
105
- def is_in_peer_route(self) -> bool:
106
- """Check if this node is part of the peer route."""
107
- return 0 in self.routes
108
-
109
- def is_in_validation_route(self) -> bool:
110
- """Check if this node is part of the validation route."""
111
- return 1 in self.routes
112
-
113
- def is_tracking_route(self, route_type: int) -> bool:
114
- """Check if this node is tracking a specific route."""
115
- return route_type in self.tracked_routes
116
-
117
- def get_random_peers_from_route(self, route_type: int, count: int = 3) -> List[Peer]:
118
- """
119
- Get a list of random peers from different buckets in the specified route.
120
-
121
- Args:
122
- route_type (int): Route type (0 for peer, 1 for validation)
123
- count (int): Number of random peers to select (one from each bucket)
124
-
125
- Returns:
126
- List[Peer]: List of randomly selected peers from different buckets
127
- """
128
- result = []
129
- route_id = self._get_route_id(route_type)
130
-
131
- # Get all buckets that have peers for this route
132
- buckets_with_peers = []
133
- for i, bucket in enumerate(self.routing_table):
134
- # For each bucket, collect peers that are in this route
135
- route_peers_in_bucket = [peer for peer in bucket.values()
136
- if peer.routes and route_id in peer.routes]
137
- if route_peers_in_bucket:
138
- buckets_with_peers.append((i, route_peers_in_bucket))
139
-
140
- # If we don't have any buckets with peers, return empty list
141
- if not buckets_with_peers:
142
- return []
143
-
144
- # If we have fewer buckets than requested count, adjust count
145
- sample_count = min(count, len(buckets_with_peers))
146
-
147
- # Sample random buckets
148
- selected_buckets = random.sample(buckets_with_peers, sample_count)
149
-
150
- # For each selected bucket, pick one random peer
151
- for bucket_idx, peers in selected_buckets:
152
- # Select one random peer from this bucket
153
- selected_peer = random.choice(peers)
154
- result.append(selected_peer)
155
-
156
- return result
157
-
158
- def get_peers_in_route(self, route_type: int) -> List[Peer]:
159
- """
160
- Get all peers in a specific route.
161
-
162
- Args:
163
- route_type (int): Route type (0 for peer, 1 for validation)
164
-
165
- Returns:
166
- List[Peer]: List of peers in the route
167
- """
168
- if route_type == 0: # Peer route
169
- return self.peer_route_bucket.get_peers()
170
- elif route_type == 1: # Validation route
171
- return self.validation_route_bucket.get_peers()
172
- return []
173
-
174
- def add_peer_to_route(self, peer: Peer, route_types: List[int]):
175
- """
176
- Add a peer to specified routes.
177
-
178
- Args:
179
- peer (Peer): The peer to add
180
- route_types (List[int]): List of route types to add the peer to (0 = peer, 1 = validation)
181
- """
182
- for route_type in route_types:
183
- if route_type == 0: # Peer route
184
- # Add to top of bucket, eject last if at capacity
185
- self.peer_route_bucket.add(peer, to_front=True)
186
- elif route_type == 1: # Validation route
187
- # Add to top of bucket, eject last if at capacity
188
- self.validation_route_bucket.add(peer, to_front=True)
189
-
190
- def register_message_handler(self, topic: Topic, handler_func):
191
- """Register a handler function for a specific message topic."""
192
- self.message_handlers[topic] = handler_func
193
-
194
- def _start_workers(self):
195
- """Start worker threads for processing incoming and outgoing messages."""
196
- self.running = True
197
-
198
- # Start receiver thread
199
- self.receiver_thread = threading.Thread(target=self._receive_messages)
200
- self.receiver_thread.daemon = True
201
- self.receiver_thread.start()
202
-
203
- # Start sender thread
204
- self.sender_thread = threading.Thread(target=self._send_messages)
205
- self.sender_thread.daemon = True
206
- self.sender_thread.start()
207
-
208
- # Start worker threads for processing incoming messages
209
- self.worker_threads = []
210
- for _ in range(self.num_workers):
211
- thread = threading.Thread(target=self._process_messages)
212
- thread.daemon = True
213
- thread.start()
214
- self.worker_threads.append(thread)
215
-
216
- def _receive_messages(self):
217
- """Continuously receive messages and add them to the incoming queue."""
218
- while self.running:
219
- try:
220
- data, addr = self.incoming_socket.recvfrom(self.max_message_size)
221
- self.incoming_queue.put((data, addr))
222
- except Exception as e:
223
- # Log error but continue running
224
- print(f"Error receiving message: {e}")
225
-
226
- def _send_messages(self):
227
- """Continuously send messages from the outgoing queue."""
228
- while self.running:
229
- try:
230
- data, addr = self.outgoing_queue.get()
231
- self.outgoing_socket.sendto(data, addr)
232
- self.outgoing_queue.task_done()
233
- except Exception as e:
234
- # Log error but continue running
235
- print(f"Error sending message: {e}")
236
-
237
- def _process_messages(self):
238
- """Process messages from the incoming queue."""
239
- while self.running:
240
- try:
241
- data, addr = self.incoming_queue.get()
242
- self._handle_message(data, addr)
243
- self.incoming_queue.task_done()
244
- except Exception as e:
245
- # Log error but continue running
246
- print(f"Error processing message: {e}")
247
-
248
- def _handle_message(self, data: bytes, addr: Tuple[str, int]):
249
- """Handle an incoming message."""
250
- envelope = Envelope.from_bytes(data)
251
- if envelope and envelope.message.topic in self.message_handlers:
252
- # For transaction messages, only process if we're in validation route
253
- if envelope.message.topic == Topic.TRANSACTION:
254
- if self.is_in_validation_route():
255
- self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
256
- # For block messages, only process if we're in validation route
257
- elif envelope.message.topic == Topic.BLOCK:
258
- if self.is_in_validation_route():
259
- self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
260
- # Latest block and latest block requests can be handled by any node tracking the routes
261
- elif envelope.message.topic in (Topic.LATEST_BLOCK, Topic.LATEST_BLOCK_REQUEST):
262
- self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
263
- # For other message types, always process
264
- else:
265
- self.message_handlers[envelope.message.topic](envelope.message.body, addr, envelope)
266
-
267
- def send(self, data: bytes, addr: Tuple[str, int]):
268
- """Send raw data to a specific address."""
269
- self.outgoing_queue.put((data, addr))
270
-
271
- def get_address(self) -> Tuple[str, int]:
272
- """
273
- Get the local address of this relay node.
274
-
275
- Returns:
276
- Tuple[str, int]: The local address (host, port)
277
- """
278
- # This is a simplification - in a real implementation this would determine the
279
- # actual public-facing IP address, which may be different from the binding address
280
- return ("localhost", self.incoming_port)
281
-
282
- def get_routes(self) -> bytes:
283
- """
284
- Get the routes this node is part of as a bytes object.
285
-
286
- Returns:
287
- bytes: List of route types (0 for peer, 1 for validation)
288
- """
289
- return bytes(self.routes)
290
-
291
- def send_message(self, body: bytes, topic: Topic, addr: Tuple[str, int], encrypted: bool = False, difficulty: int = 1):
292
- """
293
- Create and send a message to a specific address.
294
-
295
- Args:
296
- body (bytes): The message body
297
- topic (Topic): The message topic
298
- addr (Tuple[str, int]): The recipient's address (host, port)
299
- encrypted (bool): Whether the message is encrypted
300
- difficulty (int): Number of leading zero bits required in the nonce hash
301
- """
302
- envelope = Envelope.create(body, topic, encrypted, difficulty)
303
- encoded_data = envelope.to_bytes()
304
- self.send(encoded_data, addr)
305
-
306
- def send_message_to_addr(self, addr: tuple, topic: Topic, body: bytes):
307
- """
308
- Send a message to a specific address.
309
-
310
- Args:
311
- addr: Tuple of (ip, port) to send to
312
- topic: Message topic
313
- body: Message body
314
- """
315
- try:
316
- # Create an envelope with our node id and the message
317
- message = Message(self.node_id, topic, body)
318
- envelope = Envelope(message)
319
-
320
- # Serialize and send
321
- self.outgoing_socket.sendto(envelope.to_bytes(), addr)
322
- except Exception as e:
323
- print(f"Error sending message to {addr}: {e}")
324
-
325
- def send_message_to_peer(self, peer: Peer, topic: Topic, body):
326
- """
327
- Send a message to a specific peer.
328
-
329
- Args:
330
- peer: Peer to send to
331
- topic: Message topic
332
- body: Message body (bytes or JSON serializable)
333
- """
334
- # Convert body to bytes if it's not already
335
- if not isinstance(body, bytes):
336
- if isinstance(body, dict) or isinstance(body, list):
337
- body = json.dumps(body).encode('utf-8')
338
- else:
339
- body = str(body).encode('utf-8')
340
-
341
- # Send to the peer's address
342
- self.send_message_to_addr(peer.address, topic, body)
343
-
344
- def stop(self):
345
- """Stop all worker threads."""
346
- self.running = False
347
- # Wait for queues to be processed
348
- self.incoming_queue.join()
349
- self.outgoing_queue.join()
350
-
351
- # RouteTable wrapper methods
352
- def add_peer(self, addr, public_key, difficulty):
353
- """Add a peer to the routing table."""
354
- return self.route_table.update_peer(addr, public_key, difficulty)
355
-
356
- def get_closest_peers(self, target_id, count=3):
357
- """Get the closest peers to the target ID."""
358
- return self.route_table.get_closest_peers(target_id, count=count)
359
-
360
- @property
361
- def num_buckets(self):
362
- """Get the number of buckets in the routing table."""
363
- return self.route_table.num_buckets
364
-
365
- def get_bucket_peers(self, bucket_index):
366
- """Get peers from a specific bucket."""
367
- return self.route_table.get_bucket_peers(bucket_index)
368
-
369
- def has_peer(self, addr):
370
- """Check if a peer with the given address exists in the routing table."""
371
- return self.route_table.has_peer(addr)
@@ -1,90 +0,0 @@
1
- """
2
- K-bucket implementation for Kademlia-style routing in Astreum node.
3
- """
4
-
5
- from typing import List, Set
6
- from .peer import Peer
7
-
8
- class KBucket:
9
- """
10
- A Kademlia k-bucket that stores peers.
11
-
12
- K-buckets are used to store contact information for nodes in the DHT.
13
- When a new node is added, it's placed at the tail of the list.
14
- If a node is already in the list, it is moved to the tail.
15
- This creates a least-recently seen eviction policy.
16
- """
17
-
18
- def __init__(self, k: int = 20):
19
- """
20
- Initialize a k-bucket with a fixed size.
21
-
22
- Args:
23
- k (int): Maximum number of peers in the bucket
24
- """
25
- self.k = k
26
- self.peers: List[Peer] = []
27
- self._peer_ids: Set[bytes] = set() # Track peer IDs for quick lookup
28
-
29
- def add(self, peer: Peer) -> bool:
30
- """
31
- Add peer to bucket if not full or if peer exists.
32
-
33
- Args:
34
- peer (Peer): Peer to add to the bucket
35
-
36
- Returns:
37
- bool: True if added/exists, False if bucket full and peer not in bucket
38
- """
39
- # If peer already in bucket, move to end (most recently seen)
40
- if peer.public_key in self._peer_ids:
41
- # Find and remove the peer
42
- for i, existing_peer in enumerate(self.peers):
43
- if existing_peer.public_key == peer.public_key:
44
- del self.peers[i]
45
- break
46
-
47
- # Add back at the end (most recently seen)
48
- self.peers.append(peer)
49
- peer.update_last_seen()
50
- return True
51
-
52
- # If bucket not full, add peer
53
- if len(self.peers) < self.k:
54
- self.peers.append(peer)
55
- self._peer_ids.add(peer.public_key)
56
- peer.update_last_seen()
57
- return True
58
-
59
- # Bucket full and peer not in bucket
60
- return False
61
-
62
- def remove(self, peer: Peer) -> bool:
63
- """
64
- Remove peer from bucket.
65
-
66
- Args:
67
- peer (Peer): Peer to remove
68
-
69
- Returns:
70
- bool: True if removed, False if not in bucket
71
- """
72
- if peer.public_key in self._peer_ids:
73
- for i, existing_peer in enumerate(self.peers):
74
- if existing_peer.public_key == peer.public_key:
75
- del self.peers[i]
76
- self._peer_ids.remove(peer.public_key)
77
- return True
78
- return False
79
-
80
- def get_peers(self) -> List[Peer]:
81
- """Get all peers in the bucket."""
82
- return self.peers.copy()
83
-
84
- def contains(self, peer_id: bytes) -> bool:
85
- """Check if a peer ID is in the bucket."""
86
- return peer_id in self._peer_ids
87
-
88
- def __len__(self) -> int:
89
- """Get the number of peers in the bucket."""
90
- return len(self.peers)