astreum 0.1.20__py3-none-any.whl → 0.2.1__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/__init__.py +1 -2
- astreum/{node → _node}/relay/envelope.py +1 -1
- astreum/{node → _node}/relay/message.py +1 -1
- astreum/{node → _node}/storage/merkle.py +3 -3
- astreum/{node → _node}/storage/patricia.py +3 -3
- astreum/{node → _node}/storage/storage.py +2 -0
- astreum/{node → _node}/utils.py +1 -1
- astreum/{node → _node}/validation/account.py +3 -3
- astreum/{node → _node}/validation/transaction.py +3 -3
- astreum/lispeum/__init__.py +2 -0
- astreum/lispeum/parser.py +1 -1
- astreum/machine/environment.py +0 -25
- astreum/node.py +1021 -0
- astreum-0.2.1.dist-info/METADATA +123 -0
- astreum-0.2.1.dist-info/RECORD +57 -0
- {astreum-0.1.20.dist-info → astreum-0.2.1.dist-info}/WHEEL +1 -1
- astreum/utils/__init__.py +0 -0
- astreum-0.1.20.dist-info/METADATA +0 -90
- astreum-0.1.20.dist-info/RECORD +0 -57
- /astreum/{node → _node}/__init__.py +0 -0
- /astreum/{node → _node}/relay/__init__.py +0 -0
- /astreum/{node → _node}/relay/bucket.py +0 -0
- /astreum/{node → _node}/relay/peer.py +0 -0
- /astreum/{node → _node}/relay/route.py +0 -0
- /astreum/{node/crypto → _node/storage}/__init__.py +0 -0
- /astreum/{node → _node}/storage/utils.py +0 -0
- /astreum/{node/storage → _node/validation}/__init__.py +0 -0
- /astreum/{node/validation → _node/validation/_block}/__init__.py +0 -0
- /astreum/{node → _node}/validation/_block/create.py +0 -0
- /astreum/{node → _node}/validation/_block/model.py +0 -0
- /astreum/{node → _node}/validation/_block/validate.py +0 -0
- /astreum/{node → _node}/validation/block.py +0 -0
- /astreum/{node → _node}/validation/constants.py +0 -0
- /astreum/{node → _node}/validation/stake.py +0 -0
- /astreum/{node → _node}/validation/vdf.py +0 -0
- /astreum/{node/validation/_block → crypto}/__init__.py +0 -0
- /astreum/{node/crypto → crypto}/ed25519.py +0 -0
- /astreum/{node/crypto → crypto}/x25519.py +0 -0
- /astreum/{utils/bytes_format.py → format.py} +0 -0
- {astreum-0.1.20.dist-info → astreum-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.1.20.dist-info → astreum-0.2.1.dist-info}/top_level.txt +0 -0
astreum/node.py
ADDED
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from queue import Queue
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Tuple, Dict, Union, Optional, List
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
import uuid
|
|
9
|
+
from astreum import format
|
|
10
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
11
|
+
from cryptography.hazmat.primitives import serialization
|
|
12
|
+
from astreum.crypto import ed25519, x25519
|
|
13
|
+
from enum import IntEnum
|
|
14
|
+
import blake3
|
|
15
|
+
import struct
|
|
16
|
+
|
|
17
|
+
class ObjectRequestType(IntEnum):
|
|
18
|
+
OBJECT_GET = 0
|
|
19
|
+
OBJECT_PUT = 1
|
|
20
|
+
|
|
21
|
+
class ObjectRequest:
|
|
22
|
+
type: ObjectRequestType
|
|
23
|
+
data: bytes
|
|
24
|
+
hash: bytes
|
|
25
|
+
|
|
26
|
+
def __init__(self, type: ObjectRequestType, data: bytes, hash: bytes = None):
|
|
27
|
+
self.type = type
|
|
28
|
+
self.data = data
|
|
29
|
+
self.hash = hash
|
|
30
|
+
|
|
31
|
+
def to_bytes(self):
|
|
32
|
+
return format.encode([self.type.value, self.data, self.hash])
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_bytes(cls, data: bytes):
|
|
36
|
+
type_val, data_val, hash_val = format.decode(data)
|
|
37
|
+
return cls(type=ObjectRequestType(type_val[0]), data=data_val, hash=hash_val)
|
|
38
|
+
|
|
39
|
+
class ObjectResponseType(IntEnum):
|
|
40
|
+
OBJECT_FOUND = 0
|
|
41
|
+
OBJECT_PROVIDER = 1
|
|
42
|
+
OBJECT_NEAREST_PEER = 2
|
|
43
|
+
|
|
44
|
+
class ObjectResponse:
|
|
45
|
+
type: ObjectResponseType
|
|
46
|
+
data: bytes
|
|
47
|
+
hash: bytes
|
|
48
|
+
|
|
49
|
+
def __init__(self, type: ObjectResponseType, data: bytes, hash: bytes = None):
|
|
50
|
+
self.type = type
|
|
51
|
+
self.data = data
|
|
52
|
+
self.hash = hash
|
|
53
|
+
|
|
54
|
+
def to_bytes(self):
|
|
55
|
+
return format.encode([self.type.value, self.data, self.hash])
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_bytes(cls, data: bytes):
|
|
59
|
+
type_val, data_val, hash_val = format.decode(data)
|
|
60
|
+
return cls(type=ObjectResponseType(type_val[0]), data=data_val, hash=hash_val)
|
|
61
|
+
|
|
62
|
+
class MessageTopic(IntEnum):
|
|
63
|
+
PING = 0
|
|
64
|
+
OBJECT_REQUEST = 1
|
|
65
|
+
OBJECT_RESPONSE = 2
|
|
66
|
+
ROUTE_REQUEST = 3
|
|
67
|
+
ROUTE_RESPONSE = 4
|
|
68
|
+
|
|
69
|
+
class Message:
|
|
70
|
+
body: bytes
|
|
71
|
+
topic: MessageTopic
|
|
72
|
+
|
|
73
|
+
def to_bytes(self):
|
|
74
|
+
return format.encode([self.body, [self.topic.value]])
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_bytes(cls, data: bytes):
|
|
78
|
+
body, topic = format.decode(data)
|
|
79
|
+
return cls(body=body, topic=MessageTopic(topic[0]))
|
|
80
|
+
|
|
81
|
+
class Envelope:
|
|
82
|
+
encrypted: bool
|
|
83
|
+
message: Message
|
|
84
|
+
nonce: int
|
|
85
|
+
sender: X25519PublicKey
|
|
86
|
+
timestamp: datetime
|
|
87
|
+
|
|
88
|
+
def __init__(self, message: Message, sender: X25519PublicKey, encrypted: bool = False, nonce: int = 0, timestamp: Union[int, datetime, None] = None, difficulty: int = 1):
|
|
89
|
+
self.encrypted = encrypted
|
|
90
|
+
encrypted_bytes = b'\x01' if self.encrypted else b'\x00'
|
|
91
|
+
|
|
92
|
+
self.message = message
|
|
93
|
+
|
|
94
|
+
self.sender = sender
|
|
95
|
+
self.sender_bytes = sender.public_bytes()
|
|
96
|
+
|
|
97
|
+
self.nonce = nonce
|
|
98
|
+
|
|
99
|
+
if timestamp is None:
|
|
100
|
+
self.timestamp = datetime.now(timezone.utc)
|
|
101
|
+
timestamp_int = int(self.timestamp.timestamp())
|
|
102
|
+
elif isinstance(timestamp, int):
|
|
103
|
+
self.timestamp = datetime.fromtimestamp(timestamp, timezone.utc)
|
|
104
|
+
timestamp_int = timestamp
|
|
105
|
+
elif isinstance(timestamp, datetime):
|
|
106
|
+
self.timestamp = timestamp
|
|
107
|
+
timestamp_int = int(timestamp.timestamp())
|
|
108
|
+
else:
|
|
109
|
+
raise TypeError("Timestamp must be an int (Unix timestamp), datetime object, or None")
|
|
110
|
+
|
|
111
|
+
def count_leading_zero_bits(data: bytes) -> int:
|
|
112
|
+
count = 0
|
|
113
|
+
for b in data:
|
|
114
|
+
if b == 0:
|
|
115
|
+
count += 8
|
|
116
|
+
else:
|
|
117
|
+
count += 8 - b.bit_length()
|
|
118
|
+
break
|
|
119
|
+
return count
|
|
120
|
+
|
|
121
|
+
while True:
|
|
122
|
+
envelope_bytes = format.encode([
|
|
123
|
+
encrypted_bytes,
|
|
124
|
+
message_bytes,
|
|
125
|
+
self.nonce,
|
|
126
|
+
self.sender_bytes,
|
|
127
|
+
timestamp_int
|
|
128
|
+
])
|
|
129
|
+
envelope_hash = utils.blake3_hash(envelope_bytes)
|
|
130
|
+
if count_leading_zero_bits(envelope_hash) >= difficulty:
|
|
131
|
+
self.hash = envelope_hash
|
|
132
|
+
break
|
|
133
|
+
self.nonce += 1
|
|
134
|
+
|
|
135
|
+
def to_bytes(self):
|
|
136
|
+
encrypted_bytes = b'\x01' if self.encrypted else b'\x00'
|
|
137
|
+
|
|
138
|
+
return format.encode([
|
|
139
|
+
encrypted_bytes,
|
|
140
|
+
self.message.to_bytes(),
|
|
141
|
+
self.nonce,
|
|
142
|
+
self.sender.public_bytes(),
|
|
143
|
+
int(self.timestamp.timestamp())
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def from_bytes(cls, data: bytes):
|
|
148
|
+
encrypted_bytes, message_bytes, nonce, sender_bytes, timestamp_int = format.decode(data)
|
|
149
|
+
return cls(
|
|
150
|
+
encrypted=(encrypted_bytes == b'\x01'),
|
|
151
|
+
message=Message.from_bytes(message_bytes),
|
|
152
|
+
nonce=nonce,
|
|
153
|
+
sender=X25519PublicKey.from_public_bytes(sender_bytes),
|
|
154
|
+
timestamp=datetime.fromtimestamp(timestamp_int, timezone.utc)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
class Peer:
|
|
158
|
+
def __init__(self, node_secret_key: X25519PrivateKey, peer_public_key: X25519PublicKey, address: Tuple[str, int]):
|
|
159
|
+
self.shared_key = x25519.generate_shared_key(node_secret_key, peer_public_key)
|
|
160
|
+
self.address = address
|
|
161
|
+
self.timestamp = datetime.now(timezone.utc)
|
|
162
|
+
|
|
163
|
+
class Route:
|
|
164
|
+
def __init__(self, relay_public_key: X25519PublicKey, bucket_size: int = 16):
|
|
165
|
+
self.relay_public_key_bytes = relay_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
|
|
166
|
+
self.bucket_size = bucket_size
|
|
167
|
+
self.buckets: Dict[int, List[X25519PublicKey]] = {
|
|
168
|
+
i: [] for i in range(len(self.relay_public_key_bytes) * 8)
|
|
169
|
+
}
|
|
170
|
+
self.peers = {}
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _matching_leading_bits(a: bytes, b: bytes) -> int:
|
|
174
|
+
for byte_index, (ba, bb) in enumerate(zip(a, b)):
|
|
175
|
+
diff = ba ^ bb
|
|
176
|
+
if diff:
|
|
177
|
+
return byte_index * 8 + (8 - diff.bit_length())
|
|
178
|
+
return len(a) * 8
|
|
179
|
+
|
|
180
|
+
def add_peer(self, peer_public_key: X25519PublicKey):
|
|
181
|
+
peer_public_key_bytes = peer_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
|
|
182
|
+
bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
|
|
183
|
+
if len(self.buckets[bucket_idx]) < self.bucket_size:
|
|
184
|
+
self.buckets[bucket_idx].append(peer_public_key)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def encode_ip_address(host: str, port: int) -> bytes:
|
|
188
|
+
ip_bytes = socket.inet_pton(socket.AF_INET6 if ':' in host else socket.AF_INET, host)
|
|
189
|
+
port_bytes = struct.pack("!H", port)
|
|
190
|
+
return ip_bytes + port_bytes
|
|
191
|
+
|
|
192
|
+
def decode_ip_address(data: bytes) -> tuple[str, int]:
|
|
193
|
+
if len(data) == 6:
|
|
194
|
+
ip = socket.inet_ntop(socket.AF_INET, data[:4])
|
|
195
|
+
port = struct.unpack("!H", data[4:6])[0]
|
|
196
|
+
elif len(data) == 18:
|
|
197
|
+
ip = socket.inet_ntop(socket.AF_INET6, data[:16])
|
|
198
|
+
port = struct.unpack("!H", data[16:18])[0]
|
|
199
|
+
else:
|
|
200
|
+
raise ValueError("Invalid address byte format")
|
|
201
|
+
return ip, port
|
|
202
|
+
|
|
203
|
+
# =========
|
|
204
|
+
# MACHINE
|
|
205
|
+
# =========
|
|
206
|
+
|
|
207
|
+
class Expr:
|
|
208
|
+
class ListExpr:
|
|
209
|
+
def __init__(self, elements: List['Expr']):
|
|
210
|
+
self.elements = elements
|
|
211
|
+
|
|
212
|
+
def __eq__(self, other):
|
|
213
|
+
if not isinstance(other, Expr.ListExpr):
|
|
214
|
+
return NotImplemented
|
|
215
|
+
return self.elements == other.elements
|
|
216
|
+
|
|
217
|
+
def __ne__(self, other):
|
|
218
|
+
return not self.__eq__(other)
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def value(self):
|
|
222
|
+
inner = " ".join(str(e) for e in self.elements)
|
|
223
|
+
return f"({inner})"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def __repr__(self):
|
|
227
|
+
if not self.elements:
|
|
228
|
+
return "()"
|
|
229
|
+
|
|
230
|
+
inner = " ".join(str(e) for e in self.elements)
|
|
231
|
+
return f"({inner})"
|
|
232
|
+
|
|
233
|
+
def __iter__(self):
|
|
234
|
+
return iter(self.elements)
|
|
235
|
+
|
|
236
|
+
def __getitem__(self, index: Union[int, slice]):
|
|
237
|
+
return self.elements[index]
|
|
238
|
+
|
|
239
|
+
def __len__(self):
|
|
240
|
+
return len(self.elements)
|
|
241
|
+
|
|
242
|
+
class Symbol:
|
|
243
|
+
def __init__(self, value: str):
|
|
244
|
+
self.value = value
|
|
245
|
+
|
|
246
|
+
def __repr__(self):
|
|
247
|
+
return self.value
|
|
248
|
+
|
|
249
|
+
class Integer:
|
|
250
|
+
def __init__(self, value: int):
|
|
251
|
+
self.value = value
|
|
252
|
+
|
|
253
|
+
def __repr__(self):
|
|
254
|
+
return str(self.value)
|
|
255
|
+
|
|
256
|
+
class String:
|
|
257
|
+
def __init__(self, value: str):
|
|
258
|
+
self.value = value
|
|
259
|
+
|
|
260
|
+
def __repr__(self):
|
|
261
|
+
return f'"{self.value}"'
|
|
262
|
+
|
|
263
|
+
class Boolean:
|
|
264
|
+
def __init__(self, value: bool):
|
|
265
|
+
self.value = value
|
|
266
|
+
|
|
267
|
+
def __repr__(self):
|
|
268
|
+
return "true" if self.value else "false"
|
|
269
|
+
|
|
270
|
+
class Function:
|
|
271
|
+
def __init__(self, params: List[str], body: 'Expr'):
|
|
272
|
+
self.params = params
|
|
273
|
+
self.body = body
|
|
274
|
+
|
|
275
|
+
def __repr__(self):
|
|
276
|
+
params_str = " ".join(self.params)
|
|
277
|
+
body_str = str(self.body)
|
|
278
|
+
return f"(fn ({params_str}) {body_str})"
|
|
279
|
+
|
|
280
|
+
class Error:
|
|
281
|
+
def __init__(self, message: str, origin: 'Expr' | None = None):
|
|
282
|
+
self.message = message
|
|
283
|
+
self.origin = origin
|
|
284
|
+
|
|
285
|
+
def __repr__(self):
|
|
286
|
+
if self.origin is None:
|
|
287
|
+
return f'(error "{self.message}")'
|
|
288
|
+
return f'(error "{self.message}" in {self.origin})'
|
|
289
|
+
|
|
290
|
+
class Env:
|
|
291
|
+
def __init__(self, parent: 'Env' = None):
|
|
292
|
+
self.data: Dict[str, Expr] = {}
|
|
293
|
+
self.parent = parent
|
|
294
|
+
|
|
295
|
+
def put(self, name: str, value: Expr):
|
|
296
|
+
self.data[name] = value
|
|
297
|
+
|
|
298
|
+
def get(self, name: str) -> Optional[Expr]:
|
|
299
|
+
if name in self.data:
|
|
300
|
+
return self.data[name]
|
|
301
|
+
elif self.parent is not None:
|
|
302
|
+
return self.parent.get(name)
|
|
303
|
+
else:
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
def __repr__(self):
|
|
307
|
+
return f"Env({self.data})"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class Node:
|
|
311
|
+
def __init__(self, config: dict):
|
|
312
|
+
self._machine_setup()
|
|
313
|
+
machine_only = bool(config.get('machine-only', True))
|
|
314
|
+
if not machine_only:
|
|
315
|
+
self._storage_setup(config=config)
|
|
316
|
+
self._relay_setup(config=config)
|
|
317
|
+
|
|
318
|
+
# STORAGE METHODS
|
|
319
|
+
def _storage_setup(self, config: dict):
|
|
320
|
+
storage_path_str = config.get('storage_path')
|
|
321
|
+
if storage_path_str is None:
|
|
322
|
+
self.storage_path = None
|
|
323
|
+
self.memory_storage = {}
|
|
324
|
+
else:
|
|
325
|
+
self.storage_path = Path(storage_path_str)
|
|
326
|
+
self.storage_path.mkdir(parents=True, exist_ok=True)
|
|
327
|
+
self.memory_storage = None
|
|
328
|
+
|
|
329
|
+
self.storage_get_relay_timeout = config.get('storage_get_relay_timeout', 5)
|
|
330
|
+
# STORAGE INDEX: (object_hash, encoded (provider_public_key, provider_address))
|
|
331
|
+
self.storage_index = Dict[bytes, bytes]
|
|
332
|
+
|
|
333
|
+
def _relay_setup(self, config: dict):
|
|
334
|
+
self.use_ipv6 = config.get('use_ipv6', False)
|
|
335
|
+
incoming_port = config.get('incoming_port', 7373)
|
|
336
|
+
|
|
337
|
+
if 'relay_secret_key' in config:
|
|
338
|
+
try:
|
|
339
|
+
private_key_bytes = bytes.fromhex(config['relay_secret_key'])
|
|
340
|
+
self.relay_secret_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key_bytes)
|
|
341
|
+
except Exception as e:
|
|
342
|
+
raise Exception(f"Error loading relay secret key provided: {e}")
|
|
343
|
+
else:
|
|
344
|
+
self.relay_secret_key = ed25519.Ed25519PrivateKey.generate()
|
|
345
|
+
|
|
346
|
+
self.relay_public_key = self.relay_secret_key.public_key()
|
|
347
|
+
|
|
348
|
+
if 'validation_secret_key' in config:
|
|
349
|
+
try:
|
|
350
|
+
private_key_bytes = bytes.fromhex(config['validation_secret_key'])
|
|
351
|
+
self.validation_secret_key = x25519.X25519PrivateKey.from_private_bytes(private_key_bytes)
|
|
352
|
+
except Exception as e:
|
|
353
|
+
raise Exception(f"Error loading validation secret key provided: {e}")
|
|
354
|
+
|
|
355
|
+
# setup peer route and validation route
|
|
356
|
+
self.peer_route = Route(self.relay_public_key)
|
|
357
|
+
if self.validation_secret_key:
|
|
358
|
+
self.validation_route = Route(self.relay_public_key)
|
|
359
|
+
|
|
360
|
+
# Choose address family based on IPv4 or IPv6
|
|
361
|
+
family = socket.AF_INET6 if self.use_ipv6 else socket.AF_INET
|
|
362
|
+
|
|
363
|
+
self.incoming_socket = socket.socket(family, socket.SOCK_DGRAM)
|
|
364
|
+
if self.use_ipv6:
|
|
365
|
+
self.incoming_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
|
366
|
+
bind_address = "::" if self.use_ipv6 else "0.0.0.0"
|
|
367
|
+
self.incoming_socket.bind((bind_address, incoming_port or 0))
|
|
368
|
+
self.incoming_port = self.incoming_socket.getsockname()[1]
|
|
369
|
+
self.incoming_queue = Queue()
|
|
370
|
+
|
|
371
|
+
self.incoming_populate_thread = threading.Thread(target=self._relay_incoming_queue_populating)
|
|
372
|
+
self.incoming_populate_thread.daemon = True
|
|
373
|
+
self.incoming_populate_thread.start()
|
|
374
|
+
|
|
375
|
+
self.incoming_process_thread = threading.Thread(target=self._relay_incoming_queue_processing)
|
|
376
|
+
self.incoming_process_thread.daemon = True
|
|
377
|
+
self.incoming_process_thread.start()
|
|
378
|
+
|
|
379
|
+
# outgoing thread
|
|
380
|
+
self.outgoing_socket = socket.socket(family, socket.SOCK_DGRAM)
|
|
381
|
+
self.outgoing_queue = Queue()
|
|
382
|
+
self.outgoing_thread = threading.Thread(target=self._relay_outgoing_queue_processor)
|
|
383
|
+
self.outgoing_thread.daemon = True
|
|
384
|
+
self.outgoing_thread.start()
|
|
385
|
+
|
|
386
|
+
self.object_request_queue = Queue()
|
|
387
|
+
|
|
388
|
+
self.peer_manager_thread = threading.Thread(target=self._relay_peer_manager)
|
|
389
|
+
self.peer_manager_thread.daemon = True
|
|
390
|
+
self.peer_manager_thread.start()
|
|
391
|
+
|
|
392
|
+
self.peers = Dict[X25519PublicKey, Peer]
|
|
393
|
+
|
|
394
|
+
if 'bootstrap' in config:
|
|
395
|
+
for addr in config['bootstrap']:
|
|
396
|
+
self._send_ping(addr)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _local_object_get(self, data_hash: bytes) -> Optional[bytes]:
|
|
400
|
+
if self.memory_storage is not None:
|
|
401
|
+
return self.memory_storage.get(data_hash)
|
|
402
|
+
|
|
403
|
+
file_path = self.storage_path / data_hash.hex()
|
|
404
|
+
if file_path.exists():
|
|
405
|
+
return file_path.read_bytes()
|
|
406
|
+
return None
|
|
407
|
+
|
|
408
|
+
def _local_object_put(self, hash: bytes, data: bytes) -> bool:
|
|
409
|
+
if self.memory_storage is not None:
|
|
410
|
+
self.memory_storage[hash] = data
|
|
411
|
+
return True
|
|
412
|
+
|
|
413
|
+
file_path = self.storage_path / hash.hex()
|
|
414
|
+
file_path.write_bytes(data)
|
|
415
|
+
return True
|
|
416
|
+
|
|
417
|
+
def _object_get(self, hash: bytes) -> Optional[bytes]:
|
|
418
|
+
local_data = self._local_object_get(hash)
|
|
419
|
+
if local_data:
|
|
420
|
+
return local_data
|
|
421
|
+
|
|
422
|
+
# find the nearest peer route node to the hash and send an object request
|
|
423
|
+
closest_peer = self._get_closest_local_peer(hash)
|
|
424
|
+
if closest_peer:
|
|
425
|
+
object_request_message = Message(topic=MessageTopic.OBJECT_REQUEST, body=hash)
|
|
426
|
+
object_request_envelope = Envelope(message=object_request_message, sender=self.relay_public_key)
|
|
427
|
+
self.outgoing_queue.put((object_request_envelope.to_bytes(), self.peers[closest_peer].address))
|
|
428
|
+
|
|
429
|
+
# wait for upto self.storage_get_relay_timeout seconds for the object to be stored/until local_object_get returns something
|
|
430
|
+
start_time = time.time()
|
|
431
|
+
while time.time() - start_time < self.storage_get_relay_timeout:
|
|
432
|
+
# Check if the object has been stored locally
|
|
433
|
+
local_data = self._local_object_get(hash)
|
|
434
|
+
if local_data:
|
|
435
|
+
return local_data
|
|
436
|
+
# Sleep briefly to avoid hammering the local storage
|
|
437
|
+
time.sleep(0.1)
|
|
438
|
+
|
|
439
|
+
# If we reach here, the object was not received within the timeout period
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
# RELAY METHODS
|
|
443
|
+
def _relay_incoming_queue_populating(self):
|
|
444
|
+
while True:
|
|
445
|
+
try:
|
|
446
|
+
data, addr = self.incoming_socket.recvfrom(4096)
|
|
447
|
+
self.incoming_queue.put((data, addr))
|
|
448
|
+
except Exception as e:
|
|
449
|
+
print(f"Error in _relay_populate_incoming_queue: {e}")
|
|
450
|
+
|
|
451
|
+
def _relay_incoming_queue_processing(self):
|
|
452
|
+
while True:
|
|
453
|
+
try:
|
|
454
|
+
data, addr = self.incoming_queue.get()
|
|
455
|
+
envelope = Envelope.from_bytes(data)
|
|
456
|
+
match envelope.message.topic:
|
|
457
|
+
case MessageTopic.PING:
|
|
458
|
+
if envelope.sender in self.peers:
|
|
459
|
+
self.peers[envelope.sender].timestamp = datetime.now(timezone.utc)
|
|
460
|
+
continue
|
|
461
|
+
|
|
462
|
+
is_validator_flag = format.decode(envelope.message.body)
|
|
463
|
+
|
|
464
|
+
if envelope.sender not in self.peers:
|
|
465
|
+
self._send_ping(addr)
|
|
466
|
+
|
|
467
|
+
peer = Peer(self.relay_secret_key, envelope.sender, addr)
|
|
468
|
+
self.peers[peer.sender] = peer
|
|
469
|
+
self.peer_route.add_peer(envelope.sender)
|
|
470
|
+
if is_validator_flag == [1]:
|
|
471
|
+
self.validation_route.add_peer(envelope.sender)
|
|
472
|
+
|
|
473
|
+
if peer.timestamp < datetime.now(timezone.utc) - timedelta(minutes=5.0):
|
|
474
|
+
self._send_ping(addr)
|
|
475
|
+
|
|
476
|
+
case MessageTopic.OBJECT_REQUEST:
|
|
477
|
+
try:
|
|
478
|
+
object_request = ObjectRequest.from_bytes(envelope.message.body)
|
|
479
|
+
|
|
480
|
+
match object_request.type:
|
|
481
|
+
# -------------- OBJECT_GET --------------
|
|
482
|
+
case ObjectRequestType.OBJECT_GET:
|
|
483
|
+
object_hash = object_request.hash
|
|
484
|
+
|
|
485
|
+
# 1. If we already have the object, return it.
|
|
486
|
+
local_data = self._local_object_get(object_hash)
|
|
487
|
+
if local_data is not None:
|
|
488
|
+
resp = ObjectResponse(
|
|
489
|
+
type=ObjectResponseType.OBJECT_FOUND,
|
|
490
|
+
data=local_data,
|
|
491
|
+
hash=object_hash
|
|
492
|
+
)
|
|
493
|
+
msg = Message(topic=MessageTopic.OBJECT_RESPONSE, body=resp.to_bytes())
|
|
494
|
+
env = Envelope(message=msg, sender=self.relay_public_key)
|
|
495
|
+
self.outgoing_queue.put((env.to_bytes(), addr))
|
|
496
|
+
return # done
|
|
497
|
+
|
|
498
|
+
# 2. If we know a provider, tell the requester.
|
|
499
|
+
if not hasattr(self, "storage_index") or not isinstance(self.storage_index, dict):
|
|
500
|
+
self.storage_index = {}
|
|
501
|
+
if object_hash in self.storage_index:
|
|
502
|
+
provider_bytes = self.storage_index[object_hash]
|
|
503
|
+
resp = ObjectResponse(
|
|
504
|
+
type=ObjectResponseType.OBJECT_PROVIDER,
|
|
505
|
+
data=provider_bytes,
|
|
506
|
+
hash=object_hash
|
|
507
|
+
)
|
|
508
|
+
msg = Message(topic=MessageTopic.OBJECT_RESPONSE, body=resp.to_bytes())
|
|
509
|
+
env = Envelope(message=msg, sender=self.relay_public_key)
|
|
510
|
+
self.outgoing_queue.put((env.to_bytes(), addr))
|
|
511
|
+
return # done
|
|
512
|
+
|
|
513
|
+
# 3. Otherwise, direct the requester to a peer nearer to the hash.
|
|
514
|
+
nearest = self._get_closest_local_peer(object_hash)
|
|
515
|
+
if nearest:
|
|
516
|
+
nearest_key, nearest_peer = nearest
|
|
517
|
+
peer_info = format.encode([
|
|
518
|
+
nearest_key.public_bytes(
|
|
519
|
+
encoding=serialization.Encoding.Raw,
|
|
520
|
+
format=serialization.PublicFormat.Raw
|
|
521
|
+
),
|
|
522
|
+
encode_ip_address(*nearest_peer.address)
|
|
523
|
+
])
|
|
524
|
+
resp = ObjectResponse(
|
|
525
|
+
type=ObjectResponseType.OBJECT_NEAREST_PEER,
|
|
526
|
+
data=peer_info,
|
|
527
|
+
hash=object_hash
|
|
528
|
+
)
|
|
529
|
+
msg = Message(topic=MessageTopic.OBJECT_RESPONSE, body=resp.to_bytes())
|
|
530
|
+
env = Envelope(message=msg, sender=self.relay_public_key)
|
|
531
|
+
self.outgoing_queue.put((env.to_bytes(), addr))
|
|
532
|
+
|
|
533
|
+
# -------------- OBJECT_PUT --------------
|
|
534
|
+
case ObjectRequestType.OBJECT_PUT:
|
|
535
|
+
# Ensure the hash is present / correct.
|
|
536
|
+
obj_hash = object_request.hash or blake3.blake3(object_request.data).digest()
|
|
537
|
+
|
|
538
|
+
nearest = self._get_closest_local_peer(obj_hash)
|
|
539
|
+
# If a strictly nearer peer exists, forward the PUT.
|
|
540
|
+
if nearest and self._is_closer_than_local_peers(obj_hash, nearest[0]):
|
|
541
|
+
fwd_req = ObjectRequest(
|
|
542
|
+
type=ObjectRequestType.OBJECT_PUT,
|
|
543
|
+
data=object_request.data,
|
|
544
|
+
hash=obj_hash
|
|
545
|
+
)
|
|
546
|
+
fwd_msg = Message(topic=MessageTopic.OBJECT_REQUEST, body=fwd_req.to_bytes())
|
|
547
|
+
fwd_env = Envelope(message=fwd_msg, sender=self.relay_public_key)
|
|
548
|
+
self.outgoing_queue.put((fwd_env.to_bytes(), nearest[1].address))
|
|
549
|
+
else:
|
|
550
|
+
# We are closest → remember who can provide the object.
|
|
551
|
+
provider_record = format.encode([
|
|
552
|
+
envelope.sender.public_bytes(),
|
|
553
|
+
encode_ip_address(*addr)
|
|
554
|
+
])
|
|
555
|
+
if not hasattr(self, "storage_index") or not isinstance(self.storage_index, dict):
|
|
556
|
+
self.storage_index = {}
|
|
557
|
+
self.storage_index[obj_hash] = provider_record
|
|
558
|
+
|
|
559
|
+
except Exception as e:
|
|
560
|
+
print(f"Error processing OBJECT_REQUEST: {e}")
|
|
561
|
+
|
|
562
|
+
case MessageTopic.OBJECT_RESPONSE:
|
|
563
|
+
try:
|
|
564
|
+
object_response = ObjectResponse.from_bytes(envelope.message.body)
|
|
565
|
+
if object_response.hash not in self.object_request_queue:
|
|
566
|
+
continue
|
|
567
|
+
|
|
568
|
+
match object_response.type:
|
|
569
|
+
case ObjectResponseType.OBJECT_FOUND:
|
|
570
|
+
if object_response.hash != blake3.blake3(object_response.data).digest():
|
|
571
|
+
continue
|
|
572
|
+
self.object_request_queue.remove(object_response.hash)
|
|
573
|
+
self._local_object_put(object_response.hash, object_response.data)
|
|
574
|
+
|
|
575
|
+
case ObjectResponseType.OBJECT_PROVIDER:
|
|
576
|
+
_provider_public_key, provider_address = format.decode(object_response.data)
|
|
577
|
+
provider_ip, provider_port = decode_ip_address(provider_address)
|
|
578
|
+
object_request_message = Message(topic=MessageTopic.OBJECT_REQUEST, body=object_hash)
|
|
579
|
+
object_request_envelope = Envelope(message=object_request_message, sender=self.relay_public_key)
|
|
580
|
+
self.outgoing_queue.put((object_request_envelope.to_bytes(), (provider_ip, provider_port)))
|
|
581
|
+
|
|
582
|
+
case ObjectResponseType.OBJECT_NEAREST_PEER:
|
|
583
|
+
# -- decode the peer info sent back
|
|
584
|
+
nearest_peer_public_key_bytes, nearest_peer_address = (
|
|
585
|
+
format.decode(object_response.data)
|
|
586
|
+
)
|
|
587
|
+
nearest_peer_public_key = X25519PublicKey.from_public_bytes(
|
|
588
|
+
nearest_peer_public_key_bytes
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
# -- XOR-distance between the object hash and the candidate peer
|
|
592
|
+
peer_bytes = nearest_peer_public_key.public_bytes(
|
|
593
|
+
encoding=serialization.Encoding.Raw,
|
|
594
|
+
format=serialization.PublicFormat.Raw,
|
|
595
|
+
)
|
|
596
|
+
object_response_xor = sum(
|
|
597
|
+
a ^ b for a, b in zip(object_response.hash, peer_bytes)
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
# -- forward only if that peer is strictly nearer than any local peer
|
|
601
|
+
if self._is_closer_than_local_peers(
|
|
602
|
+
object_response.hash, nearest_peer_public_key
|
|
603
|
+
):
|
|
604
|
+
nearest_peer_ip, nearest_peer_port = decode_ip_address(
|
|
605
|
+
nearest_peer_address
|
|
606
|
+
)
|
|
607
|
+
object_request_message = Message(
|
|
608
|
+
topic=MessageTopic.OBJECT_REQUEST,
|
|
609
|
+
body=object_response.hash,
|
|
610
|
+
)
|
|
611
|
+
object_request_envelope = Envelope(
|
|
612
|
+
message=object_request_message,
|
|
613
|
+
sender=self.relay_public_key,
|
|
614
|
+
)
|
|
615
|
+
self.outgoing_queue.put(
|
|
616
|
+
(
|
|
617
|
+
object_request_envelope.to_bytes(),
|
|
618
|
+
(nearest_peer_ip, nearest_peer_port),
|
|
619
|
+
)
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
except Exception as e:
|
|
624
|
+
print(f"Error processing OBJECT_RESPONSE: {e}")
|
|
625
|
+
|
|
626
|
+
except Exception as e:
|
|
627
|
+
print(f"Error processing message: {e}")
|
|
628
|
+
|
|
629
|
+
def _relay_outgoing_queue_processor(self):
|
|
630
|
+
while True:
|
|
631
|
+
try:
|
|
632
|
+
data, addr = self.outgoing_queue.get()
|
|
633
|
+
self.outgoing_socket.sendto(data, addr)
|
|
634
|
+
except Exception as e:
|
|
635
|
+
print(f"Error sending message: {e}")
|
|
636
|
+
|
|
637
|
+
def _relay_peer_manager(self):
|
|
638
|
+
while True:
|
|
639
|
+
try:
|
|
640
|
+
time.sleep(60)
|
|
641
|
+
for peer in self.peers.values():
|
|
642
|
+
if (datetime.now(timezone.utc) - peer.timestamp).total_seconds() > 900:
|
|
643
|
+
del self.peers[peer.sender]
|
|
644
|
+
self.peer_route.remove_peer(peer.sender)
|
|
645
|
+
if peer.sender in self.validation_route.buckets:
|
|
646
|
+
self.validation_route.remove_peer(peer.sender)
|
|
647
|
+
except Exception as e:
|
|
648
|
+
print(f"Error in _peer_manager_thread: {e}")
|
|
649
|
+
|
|
650
|
+
def _send_ping(self, addr: Tuple[str, int]):
|
|
651
|
+
is_validator_flag = format.encode([1] if self.validation_secret_key else [0])
|
|
652
|
+
ping_message = Message(topic=MessageTopic.PING, body=is_validator_flag)
|
|
653
|
+
ping_envelope = Envelope(message=ping_message, sender=self.relay_public_key)
|
|
654
|
+
self.outgoing_queue.put((ping_envelope.to_bytes(), addr))
|
|
655
|
+
|
|
656
|
+
def _get_closest_local_peer(self, hash: bytes) -> Optional[(X25519PublicKey, Peer)]:
|
|
657
|
+
# Find the globally closest peer using XOR distance
|
|
658
|
+
closest_peer = None
|
|
659
|
+
closest_distance = None
|
|
660
|
+
|
|
661
|
+
# Check all peers
|
|
662
|
+
for peer_key, peer in self.peers.items():
|
|
663
|
+
# Calculate XOR distance between hash and peer's public key
|
|
664
|
+
peer_bytes = peer_key.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)
|
|
665
|
+
# XOR each byte and sum them to get a total distance
|
|
666
|
+
distance = sum(a ^ b for a, b in zip(hash, peer_bytes))
|
|
667
|
+
# Update the closest peer if the distance is smaller
|
|
668
|
+
if closest_distance is None or distance < closest_distance:
|
|
669
|
+
closest_distance = distance
|
|
670
|
+
closest_peer = (peer_key, peer)
|
|
671
|
+
|
|
672
|
+
return closest_peer
|
|
673
|
+
|
|
674
|
+
def _is_closer_than_local_peers(self, hash: bytes, foreign_peer_public_key: X25519PublicKey) -> bool:
|
|
675
|
+
|
|
676
|
+
# Get the closest local peer
|
|
677
|
+
closest_local_peer = self._get_closest_local_peer(hash)
|
|
678
|
+
|
|
679
|
+
# If we have no local peers, the foreign peer is closer by default
|
|
680
|
+
if closest_local_peer is None:
|
|
681
|
+
return True
|
|
682
|
+
|
|
683
|
+
# Calculate XOR distance for the foreign peer
|
|
684
|
+
foreign_peer_bytes = foreign_peer_public_key.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)
|
|
685
|
+
foreign_distance = sum(a ^ b for a, b in zip(hash, foreign_peer_bytes))
|
|
686
|
+
|
|
687
|
+
# Get the closest local peer key and calculate its distance
|
|
688
|
+
closest_peer_key, _ = closest_local_peer
|
|
689
|
+
closest_peer_bytes = closest_peer_key.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)
|
|
690
|
+
local_distance = sum(a ^ b for a, b in zip(hash, closest_peer_bytes))
|
|
691
|
+
|
|
692
|
+
# Return True if the foreign peer is closer (has smaller XOR distance)
|
|
693
|
+
return foreign_distance < local_distance
|
|
694
|
+
|
|
695
|
+
# MACHINE
|
|
696
|
+
def _machine_setup(self):
|
|
697
|
+
self.sessions: Dict[uuid.UUID, Env] = {}
|
|
698
|
+
self.lock = threading.Lock()
|
|
699
|
+
|
|
700
|
+
def machine_session_create(self) -> uuid.UUID:
|
|
701
|
+
session_id = uuid.uuid4()
|
|
702
|
+
with self.lock:
|
|
703
|
+
self.sessions[session_id] = Env()
|
|
704
|
+
return session_id
|
|
705
|
+
|
|
706
|
+
def machine_session_delete(self, session_id: str) -> bool:
|
|
707
|
+
with self.lock:
|
|
708
|
+
if session_id in self.sessions:
|
|
709
|
+
del self.sessions[session_id]
|
|
710
|
+
return True
|
|
711
|
+
else:
|
|
712
|
+
return False
|
|
713
|
+
|
|
714
|
+
def machine_expr_get(self, session_id: uuid.UUID, name: str) -> Optional[Expr]:
|
|
715
|
+
with self.lock:
|
|
716
|
+
env = self.sessions.get(session_id)
|
|
717
|
+
if env is None:
|
|
718
|
+
return None
|
|
719
|
+
return env.get(name)
|
|
720
|
+
|
|
721
|
+
def machine_expr_put(self, session_id: uuid.UUID, name: str, expr: Expr):
|
|
722
|
+
with self.lock:
|
|
723
|
+
env = self.sessions.get(session_id)
|
|
724
|
+
if env is None:
|
|
725
|
+
return False
|
|
726
|
+
env.put(name, expr)
|
|
727
|
+
return True
|
|
728
|
+
|
|
729
|
+
def machine_expr_eval(self, env: Env, expr: Expr) -> Expr:
|
|
730
|
+
if isinstance(expr, Expr.Boolean) or isinstance(expr, Expr.Integer) or isinstance(expr, Expr.String) or isinstance(expr, Expr.Error):
|
|
731
|
+
return expr
|
|
732
|
+
|
|
733
|
+
elif isinstance(expr, Expr.Symbol):
|
|
734
|
+
value = env.get(expr.value)
|
|
735
|
+
if value:
|
|
736
|
+
return value
|
|
737
|
+
else:
|
|
738
|
+
return Expr.Error(message=f"unbound symbol '{expr.value}'", origin=expr)
|
|
739
|
+
|
|
740
|
+
elif isinstance(expr, Expr.ListExpr):
|
|
741
|
+
if len(expr.elements) == 0:
|
|
742
|
+
return expr
|
|
743
|
+
if len(expr.elements) == 1:
|
|
744
|
+
return self.machine_expr_eval(expr=expr.elements[0], env=env)
|
|
745
|
+
first = expr.elements[0]
|
|
746
|
+
if isinstance(first, Expr.Symbol):
|
|
747
|
+
first_symbol_value = env.get(first.value)
|
|
748
|
+
|
|
749
|
+
if first_symbol_value and not isinstance(first_symbol_value, Expr.Function):
|
|
750
|
+
evaluated_elements = [self.evaluate_expression(e, env) for e in expr.elements]
|
|
751
|
+
return Expr.ListExpr(evaluated_elements)
|
|
752
|
+
|
|
753
|
+
elif first.value == "def":
|
|
754
|
+
if len(args) != 2:
|
|
755
|
+
return Expr.Error(message=f"'def' expects exactly 2 arguments, got {len(args)}", origin=expr)
|
|
756
|
+
if not isinstance(args[0], Expr.Symbol):
|
|
757
|
+
return Expr.Error(message="first argument to 'def' must be a symbol", origin=args[0])
|
|
758
|
+
result = self.machine_expr_eval(env=env, expr=args[1])
|
|
759
|
+
if isinstance(result, Expr.Error):
|
|
760
|
+
return result
|
|
761
|
+
env.put(name=args[0].value, value=result)
|
|
762
|
+
return result
|
|
763
|
+
|
|
764
|
+
# # List
|
|
765
|
+
# elif first.value == "list.new":
|
|
766
|
+
# return Expr.ListExpr([self.evaluate_expression(arg, env) for arg in expr.elements[1:]])
|
|
767
|
+
|
|
768
|
+
# elif first.value == "list.get":
|
|
769
|
+
# args = expr.elements[1:]
|
|
770
|
+
# if len(args) != 2:
|
|
771
|
+
# return Expr.Error(
|
|
772
|
+
# category="SyntaxError",
|
|
773
|
+
# message="list.get expects exactly two arguments: a list and an index"
|
|
774
|
+
# )
|
|
775
|
+
# list_obj = self.evaluate_expression(args[0], env)
|
|
776
|
+
# index = self.evaluate_expression(args[1], env)
|
|
777
|
+
# return handle_list_get(self, list_obj, index, env)
|
|
778
|
+
|
|
779
|
+
# elif first.value == "list.insert":
|
|
780
|
+
# args = expr.elements[1:]
|
|
781
|
+
# if len(args) != 3:
|
|
782
|
+
# return Expr.ListExpr([
|
|
783
|
+
# Expr.ListExpr([]),
|
|
784
|
+
# Expr.String("list.insert expects exactly three arguments: a list, an index, and a value")
|
|
785
|
+
# ])
|
|
786
|
+
|
|
787
|
+
# return handle_list_insert(
|
|
788
|
+
# list=self.evaluate_expression(args[0], env),
|
|
789
|
+
# index=self.evaluate_expression(args[1], env),
|
|
790
|
+
# value=self.evaluate_expression(args[2], env),
|
|
791
|
+
# )
|
|
792
|
+
|
|
793
|
+
# elif first.value == "list.remove":
|
|
794
|
+
# args = expr.elements[1:]
|
|
795
|
+
# if len(args) != 2:
|
|
796
|
+
# return Expr.ListExpr([
|
|
797
|
+
# Expr.ListExpr([]),
|
|
798
|
+
# Expr.String("list.remove expects exactly two arguments: a list and an index")
|
|
799
|
+
# ])
|
|
800
|
+
|
|
801
|
+
# return handle_list_remove(
|
|
802
|
+
# list=self.evaluate_expression(args[0], env),
|
|
803
|
+
# index=self.evaluate_expression(args[1], env),
|
|
804
|
+
# )
|
|
805
|
+
|
|
806
|
+
# elif first.value == "list.length":
|
|
807
|
+
# args = expr.elements[1:]
|
|
808
|
+
# if len(args) != 1:
|
|
809
|
+
# return Expr.ListExpr([
|
|
810
|
+
# Expr.ListExpr([]),
|
|
811
|
+
# Expr.String("list.length expects exactly one argument: a list")
|
|
812
|
+
# ])
|
|
813
|
+
|
|
814
|
+
# list_obj = self.evaluate_expression(args[0], env)
|
|
815
|
+
# if not isinstance(list_obj, Expr.ListExpr):
|
|
816
|
+
# return Expr.ListExpr([
|
|
817
|
+
# Expr.ListExpr([]),
|
|
818
|
+
# Expr.String("Argument must be a list")
|
|
819
|
+
# ])
|
|
820
|
+
|
|
821
|
+
# return Expr.ListExpr([
|
|
822
|
+
# Expr.Integer(len(list_obj.elements)),
|
|
823
|
+
# Expr.ListExpr([])
|
|
824
|
+
# ])
|
|
825
|
+
|
|
826
|
+
# elif first.value == "list.fold":
|
|
827
|
+
# if len(args) != 3:
|
|
828
|
+
# return Expr.ListExpr([
|
|
829
|
+
# Expr.ListExpr([]),
|
|
830
|
+
# Expr.String("list.fold expects exactly three arguments: a list, an initial value, and a function")
|
|
831
|
+
# ])
|
|
832
|
+
|
|
833
|
+
# return handle_list_fold(
|
|
834
|
+
# machine=self,
|
|
835
|
+
# list=self.evaluate_expression(args[0], env),
|
|
836
|
+
# initial=self.evaluate_expression(args[1], env),
|
|
837
|
+
# func=self.evaluate_expression(args[2], env),
|
|
838
|
+
# env=env,
|
|
839
|
+
# )
|
|
840
|
+
|
|
841
|
+
# elif first.value == "list.map":
|
|
842
|
+
# if len(args) != 2:
|
|
843
|
+
# return Expr.ListExpr([
|
|
844
|
+
# Expr.ListExpr([]),
|
|
845
|
+
# Expr.String("list.map expects exactly two arguments: a list and a function")
|
|
846
|
+
# ])
|
|
847
|
+
|
|
848
|
+
# return handle_list_map(
|
|
849
|
+
# machine=self,
|
|
850
|
+
# list=self.evaluate_expression(args[0], env),
|
|
851
|
+
# func=self.evaluate_expression(args[1], env),
|
|
852
|
+
# env=env,
|
|
853
|
+
# )
|
|
854
|
+
|
|
855
|
+
# elif first.value == "list.position":
|
|
856
|
+
# if len(args) != 2:
|
|
857
|
+
# return Expr.ListExpr([
|
|
858
|
+
# Expr.ListExpr([]),
|
|
859
|
+
# Expr.String("list.position expects exactly two arguments: a list and a function")
|
|
860
|
+
# ])
|
|
861
|
+
|
|
862
|
+
# return handle_list_position(
|
|
863
|
+
# machine=self,
|
|
864
|
+
# list=self.evaluate_expression(args[0], env),
|
|
865
|
+
# predicate=self.evaluate_expression(args[1], env),
|
|
866
|
+
# env=env,
|
|
867
|
+
# )
|
|
868
|
+
|
|
869
|
+
# elif first.value == "list.any":
|
|
870
|
+
# if len(args) != 2:
|
|
871
|
+
# return Expr.ListExpr([
|
|
872
|
+
# Expr.ListExpr([]),
|
|
873
|
+
# Expr.String("list.any expects exactly two arguments: a list and a function")
|
|
874
|
+
# ])
|
|
875
|
+
|
|
876
|
+
# return handle_list_any(
|
|
877
|
+
# machine=self,
|
|
878
|
+
# list=self.evaluate_expression(args[0], env),
|
|
879
|
+
# predicate=self.evaluate_expression(args[1], env),
|
|
880
|
+
# env=env,
|
|
881
|
+
# )
|
|
882
|
+
|
|
883
|
+
# elif first.value == "list.all":
|
|
884
|
+
# if len(args) != 2:
|
|
885
|
+
# return Expr.ListExpr([
|
|
886
|
+
# Expr.ListExpr([]),
|
|
887
|
+
# Expr.String("list.all expects exactly two arguments: a list and a function")
|
|
888
|
+
# ])
|
|
889
|
+
|
|
890
|
+
# return handle_list_all(
|
|
891
|
+
# machine=self,
|
|
892
|
+
# list=self.evaluate_expression(args[0], env),
|
|
893
|
+
# predicate=self.evaluate_expression(args[1], env),
|
|
894
|
+
# env=env,
|
|
895
|
+
# )
|
|
896
|
+
|
|
897
|
+
# Integer
|
|
898
|
+
elif first.value == "+":
|
|
899
|
+
args = expr.elements[1:]
|
|
900
|
+
if len(args) == 0:
|
|
901
|
+
return Expr.Error(message="'+' expects at least 1 argument", origin=expr)
|
|
902
|
+
evaluated_args = []
|
|
903
|
+
for arg in args:
|
|
904
|
+
val = self.evaluate_expression(arg, env)
|
|
905
|
+
if isinstance(val, Expr.Error):
|
|
906
|
+
return val
|
|
907
|
+
evaluated_args.append(val)
|
|
908
|
+
if not all(isinstance(val, Expr.Integer) for val in evaluated_args):
|
|
909
|
+
offending = next(val for val in evaluated_args if not isinstance(val, Expr.Integer))
|
|
910
|
+
return Expr.Error(message="'+' only accepts integer operands", origin=offending)
|
|
911
|
+
result = sum(val.value for val in evaluated_args)
|
|
912
|
+
return Expr.Integer(result)
|
|
913
|
+
|
|
914
|
+
# # Subtraction
|
|
915
|
+
# elif first.value == "-":
|
|
916
|
+
# evaluated_args = [self.evaluate_expression(arg, env) for arg in expr.elements[1:]]
|
|
917
|
+
|
|
918
|
+
# # Check for non-integer arguments
|
|
919
|
+
# if not all(isinstance(arg, Expr.Integer) for arg in evaluated_args):
|
|
920
|
+
# return Expr.Error(
|
|
921
|
+
# category="TypeError",
|
|
922
|
+
# message="All arguments to - must be integers"
|
|
923
|
+
# )
|
|
924
|
+
|
|
925
|
+
# # With only one argument, negate it
|
|
926
|
+
# if len(evaluated_args) == 1:
|
|
927
|
+
# return Expr.Integer(-evaluated_args[0].value)
|
|
928
|
+
|
|
929
|
+
# # With multiple arguments, subtract all from the first
|
|
930
|
+
# result = evaluated_args[0].value
|
|
931
|
+
# for arg in evaluated_args[1:]:
|
|
932
|
+
# result -= arg.value
|
|
933
|
+
|
|
934
|
+
# return Expr.Integer(result)
|
|
935
|
+
|
|
936
|
+
# # Multiplication
|
|
937
|
+
# elif first.value == "*":
|
|
938
|
+
# evaluated_args = [self.evaluate_expression(arg, env) for arg in expr.elements[1:]]
|
|
939
|
+
|
|
940
|
+
# # Check for non-integer arguments
|
|
941
|
+
# if not all(isinstance(arg, Expr.Integer) for arg in evaluated_args):
|
|
942
|
+
# return Expr.Error(
|
|
943
|
+
# category="TypeError",
|
|
944
|
+
# message="All arguments to * must be integers"
|
|
945
|
+
# )
|
|
946
|
+
|
|
947
|
+
# # Multiply all values
|
|
948
|
+
# result = 1
|
|
949
|
+
# for arg in evaluated_args:
|
|
950
|
+
# result *= arg.value
|
|
951
|
+
|
|
952
|
+
# return Expr.Integer(result)
|
|
953
|
+
|
|
954
|
+
# # Division (integer division)
|
|
955
|
+
# elif first.value == "/":
|
|
956
|
+
# evaluated_args = [self.evaluate_expression(arg, env) for arg in expr.elements[1:]]
|
|
957
|
+
|
|
958
|
+
# # Check for non-integer arguments
|
|
959
|
+
# if not all(isinstance(arg, Expr.Integer) for arg in evaluated_args):
|
|
960
|
+
# return Expr.Error(
|
|
961
|
+
# category="TypeError",
|
|
962
|
+
# message="All arguments to / must be integers"
|
|
963
|
+
# )
|
|
964
|
+
|
|
965
|
+
# # Need exactly two arguments
|
|
966
|
+
# if len(evaluated_args) != 2:
|
|
967
|
+
# return Expr.Error(
|
|
968
|
+
# category="ArgumentError",
|
|
969
|
+
# message="The / operation requires exactly two arguments"
|
|
970
|
+
# )
|
|
971
|
+
|
|
972
|
+
# dividend = evaluated_args[0].value
|
|
973
|
+
# divisor = evaluated_args[1].value
|
|
974
|
+
|
|
975
|
+
# if divisor == 0:
|
|
976
|
+
# return Expr.Error(
|
|
977
|
+
# category="DivisionError",
|
|
978
|
+
# message="Division by zero"
|
|
979
|
+
# )
|
|
980
|
+
|
|
981
|
+
# return Expr.Integer(dividend // divisor) # Integer division
|
|
982
|
+
|
|
983
|
+
# # Remainder (modulo)
|
|
984
|
+
# elif first.value == "%":
|
|
985
|
+
# evaluated_args = [self.evaluate_expression(arg, env) for arg in expr.elements[1:]]
|
|
986
|
+
|
|
987
|
+
# # Check for non-integer arguments
|
|
988
|
+
# if not all(isinstance(arg, Expr.Integer) for arg in evaluated_args):
|
|
989
|
+
# return Expr.Error(
|
|
990
|
+
# category="TypeError",
|
|
991
|
+
# message="All arguments to % must be integers"
|
|
992
|
+
# )
|
|
993
|
+
|
|
994
|
+
# # Need exactly two arguments
|
|
995
|
+
# if len(evaluated_args) != 2:
|
|
996
|
+
# return Expr.Error(
|
|
997
|
+
# category="ArgumentError",
|
|
998
|
+
# message="The % operation requires exactly two arguments"
|
|
999
|
+
# )
|
|
1000
|
+
|
|
1001
|
+
# dividend = evaluated_args[0].value
|
|
1002
|
+
# divisor = evaluated_args[1].value
|
|
1003
|
+
|
|
1004
|
+
# if divisor == 0:
|
|
1005
|
+
# return Expr.Error(
|
|
1006
|
+
# category="DivisionError",
|
|
1007
|
+
# message="Modulo by zero"
|
|
1008
|
+
# )
|
|
1009
|
+
|
|
1010
|
+
# return Expr.Integer(dividend % divisor)
|
|
1011
|
+
|
|
1012
|
+
else:
|
|
1013
|
+
evaluated_elements = [self.evaluate_expression(e, env) for e in expr.elements]
|
|
1014
|
+
return Expr.ListExpr(evaluated_elements)
|
|
1015
|
+
|
|
1016
|
+
elif isinstance(expr, Expr.Function):
|
|
1017
|
+
return expr
|
|
1018
|
+
|
|
1019
|
+
else:
|
|
1020
|
+
raise ValueError(f"Unknown expression type: {type(expr)}")
|
|
1021
|
+
|