astreum 0.2.3__py3-none-any.whl → 0.2.5__py3-none-any.whl
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.
- astreum/node.py +10 -0
- {astreum-0.2.3.dist-info → astreum-0.2.5.dist-info}/METADATA +1 -1
- astreum-0.2.5.dist-info/RECORD +18 -0
- astreum/_node/relay/__init__.py +0 -371
- astreum/_node/relay/bucket.py +0 -90
- astreum/_node/relay/envelope.py +0 -280
- astreum/_node/relay/message.py +0 -110
- astreum/_node/relay/peer.py +0 -174
- astreum/_node/relay/route.py +0 -161
- astreum/_node/storage/storage.py +0 -253
- astreum/_node/storage/utils.py +0 -137
- astreum/_node/utils.py +0 -34
- astreum/_node/validation/__init__.py +0 -0
- astreum/_node/validation/_block/__init__.py +0 -0
- astreum/_node/validation/_block/create.py +0 -98
- astreum/_node/validation/_block/model.py +0 -81
- astreum/_node/validation/_block/validate.py +0 -196
- astreum/_node/validation/account.py +0 -99
- astreum/_node/validation/block.py +0 -21
- astreum/_node/validation/constants.py +0 -15
- astreum/_node/validation/stake.py +0 -229
- astreum/_node/validation/transaction.py +0 -146
- astreum/_node/validation/vdf.py +0 -80
- astreum/lispeum/expression.py +0 -95
- astreum/lispeum/special/__init__.py +0 -0
- astreum/lispeum/special/definition.py +0 -27
- astreum/lispeum/special/list/__init__.py +0 -0
- astreum/lispeum/special/list/all.py +0 -32
- astreum/lispeum/special/list/any.py +0 -32
- astreum/lispeum/special/list/fold.py +0 -29
- astreum/lispeum/special/list/get.py +0 -20
- astreum/lispeum/special/list/insert.py +0 -23
- astreum/lispeum/special/list/map.py +0 -30
- astreum/lispeum/special/list/position.py +0 -33
- astreum/lispeum/special/list/remove.py +0 -22
- astreum/lispeum/special/number/__init__.py +0 -0
- astreum/lispeum/special/number/addition.py +0 -0
- astreum/lispeum/storage.py +0 -410
- astreum/machine/__init__.py +0 -352
- astreum/machine/environment.py +0 -4
- astreum/machine/error.py +0 -0
- astreum-0.2.3.dist-info/RECORD +0 -56
- {astreum-0.2.3.dist-info → astreum-0.2.5.dist-info}/WHEEL +0 -0
- {astreum-0.2.3.dist-info → astreum-0.2.5.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.3.dist-info → astreum-0.2.5.dist-info}/top_level.txt +0 -0
astreum/_node/relay/envelope.py
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Envelope related classes and utilities for Astreum node network.
|
|
3
|
-
|
|
4
|
-
Message Structure:
|
|
5
|
-
+ - - - - - - - +
|
|
6
|
-
| Envelope |
|
|
7
|
-
+ - - - - - - - +
|
|
8
|
-
^
|
|
9
|
-
. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - .
|
|
10
|
-
^ ^ ^ ^
|
|
11
|
-
+ - - - - - - - + + - - - - - - - + + - - - - - - - + + - - - - - - - +
|
|
12
|
-
| Time | | Encrypted | | Nonce | | Message |
|
|
13
|
-
+ - - - - - - - + + - - - - - - - + + - - - - - - - + + - - - - - - - +
|
|
14
|
-
^
|
|
15
|
-
. - - - - - - - - - - - .
|
|
16
|
-
^ ^
|
|
17
|
-
+ - - - - - - - + + - - - - - - - +
|
|
18
|
-
| Topic | | Body |
|
|
19
|
-
+ - - - - - - - + + - - - - - - - +
|
|
20
|
-
|
|
21
|
-
The Envelope uses a Merkle tree structure with the following leaves:
|
|
22
|
-
- Timestamp
|
|
23
|
-
- Encrypted flag
|
|
24
|
-
- Nonce
|
|
25
|
-
- Message bytes
|
|
26
|
-
|
|
27
|
-
The root hash of this Merkle tree must have a specified number of leading zero bits,
|
|
28
|
-
determined by the difficulty parameter. The nonce is adjusted until this requirement is met.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
import struct
|
|
32
|
-
import time
|
|
33
|
-
import os
|
|
34
|
-
from dataclasses import dataclass
|
|
35
|
-
from typing import Optional, Tuple, List
|
|
36
|
-
from .message import Message, Topic
|
|
37
|
-
from astreum.format import encode, decode
|
|
38
|
-
from ..utils import hash_data
|
|
39
|
-
|
|
40
|
-
@dataclass
|
|
41
|
-
class Envelope:
|
|
42
|
-
"""
|
|
43
|
-
Represents an envelope that wraps a message with additional metadata.
|
|
44
|
-
|
|
45
|
-
Attributes:
|
|
46
|
-
encrypted (bool): True if the message is encrypted, False otherwise
|
|
47
|
-
message (Message): The message being sent
|
|
48
|
-
nonce (bytes): Nonce for encryption and proof of work
|
|
49
|
-
timestamp (int): Time when the envelope was created
|
|
50
|
-
"""
|
|
51
|
-
encrypted: bool
|
|
52
|
-
message: Message
|
|
53
|
-
nonce: bytes
|
|
54
|
-
timestamp: int
|
|
55
|
-
|
|
56
|
-
@classmethod
|
|
57
|
-
def create(cls, body: bytes, topic: Topic, encrypted: bool = False, difficulty: int = 1) -> 'Envelope':
|
|
58
|
-
"""
|
|
59
|
-
Create a new envelope with the current timestamp and a nonce that satisfies
|
|
60
|
-
the given difficulty level using a Merkle tree structure.
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
body (bytes): The message body
|
|
64
|
-
topic (Topic): The message topic
|
|
65
|
-
encrypted (bool): Whether the message is encrypted
|
|
66
|
-
difficulty (int): Number of leading zero bits required in the Merkle root hash
|
|
67
|
-
|
|
68
|
-
Returns:
|
|
69
|
-
Envelope: A new envelope with a valid nonce
|
|
70
|
-
"""
|
|
71
|
-
timestamp = int(time.time())
|
|
72
|
-
message = Message(body=body, topic=topic)
|
|
73
|
-
|
|
74
|
-
# Generate a valid nonce for the Merkle tree
|
|
75
|
-
nonce = cls._generate_nonce(message, timestamp, encrypted, difficulty)
|
|
76
|
-
|
|
77
|
-
return cls(
|
|
78
|
-
encrypted=encrypted,
|
|
79
|
-
message=message,
|
|
80
|
-
nonce=nonce,
|
|
81
|
-
timestamp=timestamp
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
@staticmethod
|
|
85
|
-
def _generate_nonce(message: Message, timestamp: int, encrypted: bool, difficulty: int) -> bytes:
|
|
86
|
-
"""
|
|
87
|
-
Generate a nonce that results in a Merkle tree root hash with the specified
|
|
88
|
-
number of leading zero bits.
|
|
89
|
-
|
|
90
|
-
Args:
|
|
91
|
-
message (Message): The message to include in the Merkle tree
|
|
92
|
-
timestamp (int): The timestamp to include in the Merkle tree
|
|
93
|
-
encrypted (bool): Whether the message is encrypted
|
|
94
|
-
difficulty (int): Number of leading zero bits required
|
|
95
|
-
|
|
96
|
-
Returns:
|
|
97
|
-
bytes: A valid nonce
|
|
98
|
-
"""
|
|
99
|
-
# Prepare the message data
|
|
100
|
-
message_data = message.to_bytes()
|
|
101
|
-
timestamp_data = struct.pack('!Q', timestamp)
|
|
102
|
-
encrypted_flag = b'\x01' if encrypted else b'\x00'
|
|
103
|
-
|
|
104
|
-
# Calculate how many bytes need to be zero
|
|
105
|
-
zero_bytes = difficulty // 8
|
|
106
|
-
# Calculate how many bits in the last byte need to be zero
|
|
107
|
-
remaining_bits = difficulty % 8
|
|
108
|
-
|
|
109
|
-
# Create a mask for the remaining bits
|
|
110
|
-
mask = 0
|
|
111
|
-
if remaining_bits > 0:
|
|
112
|
-
mask = 0xFF >> remaining_bits
|
|
113
|
-
|
|
114
|
-
while True:
|
|
115
|
-
# Generate a random nonce
|
|
116
|
-
nonce = os.urandom(32)
|
|
117
|
-
|
|
118
|
-
# Calculate the Merkle root using the leaves
|
|
119
|
-
merkle_root = Envelope._calculate_merkle_root([
|
|
120
|
-
timestamp_data,
|
|
121
|
-
encrypted_flag,
|
|
122
|
-
nonce,
|
|
123
|
-
message_data
|
|
124
|
-
])
|
|
125
|
-
|
|
126
|
-
# Check if it meets the difficulty requirement
|
|
127
|
-
valid = True
|
|
128
|
-
|
|
129
|
-
# Check full zero bytes
|
|
130
|
-
for i in range(zero_bytes):
|
|
131
|
-
if merkle_root[i] != 0:
|
|
132
|
-
valid = False
|
|
133
|
-
break
|
|
134
|
-
|
|
135
|
-
# If we need to check partial bits in a byte
|
|
136
|
-
if valid and remaining_bits > 0:
|
|
137
|
-
# The next byte should have required number of leading zeros
|
|
138
|
-
if (merkle_root[zero_bytes] & (0xFF ^ mask)) != 0:
|
|
139
|
-
valid = False
|
|
140
|
-
|
|
141
|
-
if valid:
|
|
142
|
-
return nonce
|
|
143
|
-
|
|
144
|
-
@staticmethod
|
|
145
|
-
def _calculate_merkle_root(leaves: List[bytes]) -> bytes:
|
|
146
|
-
"""
|
|
147
|
-
Calculate the Merkle root hash from a list of leaf node data.
|
|
148
|
-
|
|
149
|
-
Args:
|
|
150
|
-
leaves (List[bytes]): List of leaf node data
|
|
151
|
-
|
|
152
|
-
Returns:
|
|
153
|
-
bytes: The Merkle root hash
|
|
154
|
-
"""
|
|
155
|
-
if not leaves:
|
|
156
|
-
return hash_data(b'')
|
|
157
|
-
|
|
158
|
-
if len(leaves) == 1:
|
|
159
|
-
return hash_data(leaves[0])
|
|
160
|
-
|
|
161
|
-
# Hash all leaf nodes
|
|
162
|
-
hashed_leaves = [hash_data(leaf) for leaf in leaves]
|
|
163
|
-
|
|
164
|
-
# Build the Merkle tree
|
|
165
|
-
while len(hashed_leaves) > 1:
|
|
166
|
-
if len(hashed_leaves) % 2 != 0:
|
|
167
|
-
# Duplicate the last element if there's an odd number
|
|
168
|
-
hashed_leaves.append(hashed_leaves[-1])
|
|
169
|
-
|
|
170
|
-
# Combine adjacent pairs and hash them
|
|
171
|
-
next_level = []
|
|
172
|
-
for i in range(0, len(hashed_leaves), 2):
|
|
173
|
-
combined = hashed_leaves[i] + hashed_leaves[i+1]
|
|
174
|
-
next_level.append(hash_data(combined))
|
|
175
|
-
|
|
176
|
-
hashed_leaves = next_level
|
|
177
|
-
|
|
178
|
-
# Return the root hash
|
|
179
|
-
return hashed_leaves[0]
|
|
180
|
-
|
|
181
|
-
def verify_nonce(self, difficulty: int = 1) -> bool:
|
|
182
|
-
"""
|
|
183
|
-
Verify that the nonce produces a valid Merkle tree root hash
|
|
184
|
-
with the specified number of leading zero bits.
|
|
185
|
-
|
|
186
|
-
Args:
|
|
187
|
-
difficulty (int): Number of leading zero bits required in the root hash
|
|
188
|
-
|
|
189
|
-
Returns:
|
|
190
|
-
bool: True if the nonce is valid, False otherwise
|
|
191
|
-
"""
|
|
192
|
-
# Prepare the message data
|
|
193
|
-
message_data = self.message.to_bytes()
|
|
194
|
-
timestamp_data = struct.pack('!Q', self.timestamp)
|
|
195
|
-
encrypted_flag = b'\x01' if self.encrypted else b'\x00'
|
|
196
|
-
|
|
197
|
-
# Calculate the Merkle root
|
|
198
|
-
merkle_root = self._calculate_merkle_root([
|
|
199
|
-
timestamp_data,
|
|
200
|
-
encrypted_flag,
|
|
201
|
-
self.nonce,
|
|
202
|
-
message_data
|
|
203
|
-
])
|
|
204
|
-
|
|
205
|
-
# Calculate how many bytes need to be zero
|
|
206
|
-
zero_bytes = difficulty // 8
|
|
207
|
-
# Calculate how many bits in the last byte need to be zero
|
|
208
|
-
remaining_bits = difficulty % 8
|
|
209
|
-
|
|
210
|
-
# Create a mask for the remaining bits
|
|
211
|
-
mask = 0
|
|
212
|
-
if remaining_bits > 0:
|
|
213
|
-
mask = 0xFF >> remaining_bits
|
|
214
|
-
|
|
215
|
-
# Check if it meets the difficulty requirement
|
|
216
|
-
valid = True
|
|
217
|
-
|
|
218
|
-
# Check full zero bytes
|
|
219
|
-
for i in range(zero_bytes):
|
|
220
|
-
if merkle_root[i] != 0:
|
|
221
|
-
valid = False
|
|
222
|
-
break
|
|
223
|
-
|
|
224
|
-
# If we need to check partial bits in a byte
|
|
225
|
-
if valid and remaining_bits > 0:
|
|
226
|
-
# The next byte should have required number of leading zeros
|
|
227
|
-
if (merkle_root[zero_bytes] & (0xFF ^ mask)) != 0:
|
|
228
|
-
valid = False
|
|
229
|
-
|
|
230
|
-
return valid
|
|
231
|
-
|
|
232
|
-
def to_bytes(self) -> bytes:
|
|
233
|
-
"""
|
|
234
|
-
Convert this Envelope to bytes.
|
|
235
|
-
|
|
236
|
-
Returns:
|
|
237
|
-
bytes: Serialized envelope
|
|
238
|
-
"""
|
|
239
|
-
return encode([
|
|
240
|
-
struct.pack('!Q', self.timestamp),
|
|
241
|
-
b'\x01' if self.encrypted else b'\x00',
|
|
242
|
-
self.nonce,
|
|
243
|
-
self.message.to_bytes()
|
|
244
|
-
])
|
|
245
|
-
|
|
246
|
-
@classmethod
|
|
247
|
-
def from_bytes(cls, data: bytes) -> Optional['Envelope']:
|
|
248
|
-
"""
|
|
249
|
-
Create an Envelope from its serialized form.
|
|
250
|
-
|
|
251
|
-
Args:
|
|
252
|
-
data (bytes): Serialized envelope
|
|
253
|
-
|
|
254
|
-
Returns:
|
|
255
|
-
Optional[Envelope]: The deserialized envelope, or None if the data is invalid
|
|
256
|
-
"""
|
|
257
|
-
try:
|
|
258
|
-
parts = decode(data)
|
|
259
|
-
if len(parts) != 4:
|
|
260
|
-
return None
|
|
261
|
-
|
|
262
|
-
timestamp_data, encrypted_flag, nonce, message_data = parts
|
|
263
|
-
|
|
264
|
-
timestamp = struct.unpack('!Q', timestamp_data)[0]
|
|
265
|
-
encrypted = encrypted_flag == b'\x01'
|
|
266
|
-
nonce = nonce
|
|
267
|
-
message = Message.from_bytes(message_data)
|
|
268
|
-
|
|
269
|
-
if not message:
|
|
270
|
-
return None
|
|
271
|
-
|
|
272
|
-
return cls(
|
|
273
|
-
encrypted=encrypted,
|
|
274
|
-
message=message,
|
|
275
|
-
nonce=nonce,
|
|
276
|
-
timestamp=timestamp
|
|
277
|
-
)
|
|
278
|
-
except (ValueError, struct.error) as e:
|
|
279
|
-
print(f"Error deserializing envelope: {e}")
|
|
280
|
-
return None
|
astreum/_node/relay/message.py
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Message related classes and utilities for Astreum node network.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import struct
|
|
6
|
-
from enum import IntEnum, auto
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from typing import Optional
|
|
9
|
-
from astreum.format import encode, decode
|
|
10
|
-
|
|
11
|
-
class Topic(IntEnum):
|
|
12
|
-
"""
|
|
13
|
-
Enum for different message topics in the Astreum network.
|
|
14
|
-
"""
|
|
15
|
-
PEER_ROUTE = 1
|
|
16
|
-
LATEST_BLOCK_REQUEST = 2
|
|
17
|
-
LATEST_BLOCK_RESPONSE = 3
|
|
18
|
-
GET_BLOCKS = 4
|
|
19
|
-
BLOCKS = 5
|
|
20
|
-
TRANSACTION = 6
|
|
21
|
-
BLOCK_COMMIT = 7
|
|
22
|
-
OBJECT_REQUEST = 8
|
|
23
|
-
OBJECT_RESPONSE = 9
|
|
24
|
-
PING = 10
|
|
25
|
-
PONG = 11
|
|
26
|
-
ROUTE = 12
|
|
27
|
-
ROUTE_REQUEST = 13
|
|
28
|
-
LATEST_BLOCK = 14
|
|
29
|
-
BLOCK = 15
|
|
30
|
-
|
|
31
|
-
def to_bytes(self) -> bytes:
|
|
32
|
-
"""
|
|
33
|
-
Convert this Topic enum value to bytes.
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
bytes: Single byte representing the topic
|
|
37
|
-
"""
|
|
38
|
-
return struct.pack('!B', self.value)
|
|
39
|
-
|
|
40
|
-
@classmethod
|
|
41
|
-
def from_bytes(cls, data: bytes) -> Optional['Topic']:
|
|
42
|
-
"""
|
|
43
|
-
Create a Topic from its serialized form.
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
data (bytes): Serialized topic (single byte)
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
Optional[Topic]: The deserialized topic, or None if the data is invalid
|
|
50
|
-
"""
|
|
51
|
-
if not data or len(data) != 1:
|
|
52
|
-
return None
|
|
53
|
-
|
|
54
|
-
try:
|
|
55
|
-
topic_value = struct.unpack('!B', data)[0]
|
|
56
|
-
return cls(topic_value)
|
|
57
|
-
except (struct.error, ValueError) as e:
|
|
58
|
-
print(f"Error deserializing topic: {e}")
|
|
59
|
-
return None
|
|
60
|
-
|
|
61
|
-
@dataclass
|
|
62
|
-
class Message:
|
|
63
|
-
"""
|
|
64
|
-
Represents a message in the Astreum network.
|
|
65
|
-
|
|
66
|
-
Attributes:
|
|
67
|
-
body (bytes): The actual content of the message
|
|
68
|
-
topic (Topic): The topic/type of the message
|
|
69
|
-
"""
|
|
70
|
-
body: bytes
|
|
71
|
-
topic: Topic
|
|
72
|
-
|
|
73
|
-
def to_bytes(self) -> bytes:
|
|
74
|
-
"""
|
|
75
|
-
Convert this Message to bytes using bytes_format.
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
bytes: Serialized message
|
|
79
|
-
"""
|
|
80
|
-
return encode([
|
|
81
|
-
self.topic.to_bytes(),
|
|
82
|
-
self.body
|
|
83
|
-
])
|
|
84
|
-
|
|
85
|
-
@classmethod
|
|
86
|
-
def from_bytes(cls, data: bytes) -> Optional['Message']:
|
|
87
|
-
"""
|
|
88
|
-
Create a Message from its serialized form using bytes_format.
|
|
89
|
-
|
|
90
|
-
Args:
|
|
91
|
-
data (bytes): Serialized message
|
|
92
|
-
|
|
93
|
-
Returns:
|
|
94
|
-
Optional[Message]: The deserialized message, or None if the data is invalid
|
|
95
|
-
"""
|
|
96
|
-
try:
|
|
97
|
-
parts = decode(data)
|
|
98
|
-
if len(parts) != 2:
|
|
99
|
-
return None
|
|
100
|
-
|
|
101
|
-
topic_data, body = parts
|
|
102
|
-
topic = Topic.from_bytes(topic_data)
|
|
103
|
-
|
|
104
|
-
if not topic:
|
|
105
|
-
return None
|
|
106
|
-
|
|
107
|
-
return cls(body=body, topic=topic)
|
|
108
|
-
except (ValueError, struct.error) as e:
|
|
109
|
-
print(f"Error deserializing message: {e}")
|
|
110
|
-
return None
|
astreum/_node/relay/peer.py
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Peer management for Astreum node's Kademlia-style network.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
from typing import Tuple, Optional
|
|
7
|
-
import time
|
|
8
|
-
|
|
9
|
-
@dataclass
|
|
10
|
-
class Peer:
|
|
11
|
-
"""
|
|
12
|
-
Represents a peer in the Astreum network.
|
|
13
|
-
|
|
14
|
-
Attributes:
|
|
15
|
-
address (Tuple[str, int]): The network address (host, port)
|
|
16
|
-
public_key (bytes): The public key of the peer
|
|
17
|
-
difficulty (int): The proof-of-work difficulty required for this peer
|
|
18
|
-
last_seen (int): Timestamp when the peer was last seen
|
|
19
|
-
failed_attempts (int): Number of consecutive failed communication attempts
|
|
20
|
-
"""
|
|
21
|
-
address: Tuple[str, int]
|
|
22
|
-
public_key: bytes
|
|
23
|
-
difficulty: int = 1
|
|
24
|
-
last_seen: int = 0
|
|
25
|
-
failed_attempts: int = 0
|
|
26
|
-
|
|
27
|
-
def __post_init__(self):
|
|
28
|
-
if self.last_seen == 0:
|
|
29
|
-
self.last_seen = int(time.time())
|
|
30
|
-
|
|
31
|
-
def update_last_seen(self):
|
|
32
|
-
"""Update the last seen timestamp to current time."""
|
|
33
|
-
self.last_seen = int(time.time())
|
|
34
|
-
self.failed_attempts = 0
|
|
35
|
-
|
|
36
|
-
def register_failed_attempt(self):
|
|
37
|
-
"""Register a failed communication attempt with this peer."""
|
|
38
|
-
self.failed_attempts += 1
|
|
39
|
-
|
|
40
|
-
def is_active(self, max_age: int = 3600, max_failures: int = 3) -> bool:
|
|
41
|
-
"""
|
|
42
|
-
Check if the peer is considered active.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
max_age (int): Maximum age in seconds before peer is considered inactive
|
|
46
|
-
max_failures (int): Maximum consecutive failures before peer is considered inactive
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
bool: True if the peer is active, False otherwise
|
|
50
|
-
"""
|
|
51
|
-
current_time = int(time.time())
|
|
52
|
-
return (
|
|
53
|
-
(current_time - self.last_seen) <= max_age and
|
|
54
|
-
self.failed_attempts < max_failures
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
class PeerManager:
|
|
58
|
-
"""
|
|
59
|
-
Manages a collection of peers and provides utilities for peer operations.
|
|
60
|
-
"""
|
|
61
|
-
|
|
62
|
-
def __init__(self, our_node_id: bytes):
|
|
63
|
-
"""
|
|
64
|
-
Initialize a peer manager.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
our_node_id (bytes): Our node's unique identifier
|
|
68
|
-
"""
|
|
69
|
-
self.our_node_id = our_node_id
|
|
70
|
-
self.peers_by_id = {} # node_id: Peer
|
|
71
|
-
self.peers_by_address = {} # address: Peer
|
|
72
|
-
|
|
73
|
-
def add_or_update_peer(self, address: Tuple[str, int], public_key: bytes) -> Peer:
|
|
74
|
-
"""
|
|
75
|
-
Add a new peer or update an existing one.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
address (Tuple[str, int]): Network address (host, port)
|
|
79
|
-
public_key (bytes): The public key of the peer
|
|
80
|
-
|
|
81
|
-
Returns:
|
|
82
|
-
Peer: The added or updated peer
|
|
83
|
-
"""
|
|
84
|
-
# Check if we already know this peer by ID
|
|
85
|
-
if public_key in self.peers_by_id:
|
|
86
|
-
peer = self.peers_by_id[public_key]
|
|
87
|
-
# Update address if changed
|
|
88
|
-
if peer.address != address:
|
|
89
|
-
if peer.address in self.peers_by_address:
|
|
90
|
-
del self.peers_by_address[peer.address]
|
|
91
|
-
peer.address = address
|
|
92
|
-
self.peers_by_address[address] = peer
|
|
93
|
-
peer.update_last_seen()
|
|
94
|
-
return peer
|
|
95
|
-
|
|
96
|
-
# Check if address exists with different ID
|
|
97
|
-
if address in self.peers_by_address:
|
|
98
|
-
old_peer = self.peers_by_address[address]
|
|
99
|
-
if old_peer.public_key in self.peers_by_id:
|
|
100
|
-
del self.peers_by_id[old_peer.public_key]
|
|
101
|
-
|
|
102
|
-
# Create and add new peer
|
|
103
|
-
peer = Peer(address=address, public_key=public_key)
|
|
104
|
-
self.peers_by_id[public_key] = peer
|
|
105
|
-
self.peers_by_address[address] = peer
|
|
106
|
-
return peer
|
|
107
|
-
|
|
108
|
-
def get_peer_by_id(self, public_key: bytes) -> Optional[Peer]:
|
|
109
|
-
"""
|
|
110
|
-
Get a peer by its public key.
|
|
111
|
-
|
|
112
|
-
Args:
|
|
113
|
-
public_key (bytes): The peer's public key
|
|
114
|
-
|
|
115
|
-
Returns:
|
|
116
|
-
Optional[Peer]: The peer if found, None otherwise
|
|
117
|
-
"""
|
|
118
|
-
return self.peers_by_id.get(public_key)
|
|
119
|
-
|
|
120
|
-
def get_peer_by_address(self, address: Tuple[str, int]) -> Optional[Peer]:
|
|
121
|
-
"""
|
|
122
|
-
Get a peer by its network address.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
address (Tuple[str, int]): The peer's network address
|
|
126
|
-
|
|
127
|
-
Returns:
|
|
128
|
-
Optional[Peer]: The peer if found, None otherwise
|
|
129
|
-
"""
|
|
130
|
-
return self.peers_by_address.get(address)
|
|
131
|
-
|
|
132
|
-
def remove_peer(self, public_key: bytes) -> bool:
|
|
133
|
-
"""
|
|
134
|
-
Remove a peer by its public key.
|
|
135
|
-
|
|
136
|
-
Args:
|
|
137
|
-
public_key (bytes): The peer's public key
|
|
138
|
-
|
|
139
|
-
Returns:
|
|
140
|
-
bool: True if the peer was removed, False otherwise
|
|
141
|
-
"""
|
|
142
|
-
if public_key in self.peers_by_id:
|
|
143
|
-
peer = self.peers_by_id[public_key]
|
|
144
|
-
del self.peers_by_id[public_key]
|
|
145
|
-
if peer.address in self.peers_by_address:
|
|
146
|
-
del self.peers_by_address[peer.address]
|
|
147
|
-
return True
|
|
148
|
-
return False
|
|
149
|
-
|
|
150
|
-
def calculate_distance(self, public_key: bytes) -> int:
|
|
151
|
-
"""
|
|
152
|
-
Calculate the XOR distance between our node ID and the given public key.
|
|
153
|
-
|
|
154
|
-
In Kademlia, peers are organized into buckets based on the XOR distance.
|
|
155
|
-
The bucket index (0-255) represents the position of the first bit that differs.
|
|
156
|
-
|
|
157
|
-
Args:
|
|
158
|
-
public_key (bytes): The remote node's public key
|
|
159
|
-
|
|
160
|
-
Returns:
|
|
161
|
-
int: XOR distance (0-255)
|
|
162
|
-
"""
|
|
163
|
-
# Assuming IDs are 256-bit (32 bytes)
|
|
164
|
-
for i in range(min(len(self.our_node_id), len(public_key))):
|
|
165
|
-
xor_byte = self.our_node_id[i] ^ public_key[i]
|
|
166
|
-
if xor_byte == 0:
|
|
167
|
-
continue
|
|
168
|
-
|
|
169
|
-
# Find the most significant bit
|
|
170
|
-
for bit in range(7, -1, -1):
|
|
171
|
-
if (xor_byte >> bit) & 1:
|
|
172
|
-
return (i * 8) + (7 - bit)
|
|
173
|
-
|
|
174
|
-
return 255 # Default maximum distance
|