astreum 0.2.27__tar.gz → 0.2.29__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 (39) hide show
  1. {astreum-0.2.27/src/astreum.egg-info → astreum-0.2.29}/PKG-INFO +1 -1
  2. {astreum-0.2.27 → astreum-0.2.29}/pyproject.toml +1 -1
  3. astreum-0.2.29/src/astreum/lispeum/environment.py +40 -0
  4. astreum-0.2.29/src/astreum/lispeum/expression.py +86 -0
  5. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/node.py +63 -404
  6. astreum-0.2.29/src/astreum/relay/__init__.py +0 -0
  7. astreum-0.2.29/src/astreum/relay/peer.py +9 -0
  8. astreum-0.2.29/src/astreum/relay/route.py +25 -0
  9. astreum-0.2.29/src/astreum/relay/setup.py +58 -0
  10. astreum-0.2.29/src/astreum/storage/__init__.py +0 -0
  11. astreum-0.2.29/src/astreum/storage/object.py +68 -0
  12. astreum-0.2.29/src/astreum/storage/setup.py +15 -0
  13. {astreum-0.2.27 → astreum-0.2.29/src/astreum.egg-info}/PKG-INFO +1 -1
  14. {astreum-0.2.27 → astreum-0.2.29}/src/astreum.egg-info/SOURCES.txt +9 -0
  15. {astreum-0.2.27 → astreum-0.2.29}/LICENSE +0 -0
  16. {astreum-0.2.27 → astreum-0.2.29}/README.md +0 -0
  17. {astreum-0.2.27 → astreum-0.2.29}/setup.cfg +0 -0
  18. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/__init__.py +0 -0
  19. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/crypto/__init__.py +0 -0
  20. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/crypto/ed25519.py +0 -0
  21. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/crypto/quadratic_form.py +0 -0
  22. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/crypto/wesolowski.py +0 -0
  23. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/crypto/x25519.py +0 -0
  24. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/format.py +0 -0
  25. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/lispeum/__init__.py +0 -0
  26. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/lispeum/parser.py +0 -0
  27. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/lispeum/tokenizer.py +0 -0
  28. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/__init__.py +0 -0
  29. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/account.py +0 -0
  30. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/accounts.py +0 -0
  31. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/block.py +0 -0
  32. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/merkle.py +0 -0
  33. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/message.py +0 -0
  34. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/patricia.py +0 -0
  35. {astreum-0.2.27 → astreum-0.2.29}/src/astreum/models/transaction.py +0 -0
  36. {astreum-0.2.27 → astreum-0.2.29}/src/astreum.egg-info/dependency_links.txt +0 -0
  37. {astreum-0.2.27 → astreum-0.2.29}/src/astreum.egg-info/requires.txt +0 -0
  38. {astreum-0.2.27 → astreum-0.2.29}/src/astreum.egg-info/top_level.txt +0 -0
  39. {astreum-0.2.27 → astreum-0.2.29}/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.27
3
+ Version: 0.2.29
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.27"
3
+ version = "0.2.29"
4
4
  authors = [
5
5
  { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
6
  ]
@@ -0,0 +1,40 @@
1
+ from typing import Dict, Optional
2
+ import uuid
3
+
4
+ from astreum.lispeum.expression import Expr
5
+
6
+
7
+ class Env:
8
+ def __init__(
9
+ self,
10
+ data: Optional[Dict[str, Expr]] = None,
11
+ parent_id: Optional[uuid.UUID] = None,
12
+ max_exprs: Optional[int] = 8,
13
+ ):
14
+ self.data: Dict[str, Expr] = data if data is not None else {}
15
+ self.parent_id: Optional[uuid.UUID] = parent_id
16
+ self.max_exprs: Optional[int] = max_exprs
17
+
18
+ def put(self, name: str, value: Expr) -> None:
19
+ if (
20
+ self.max_exprs is not None
21
+ and name not in self.data
22
+ and len(self.data) >= self.max_exprs
23
+ ):
24
+ raise RuntimeError(
25
+ f"environment full: {len(self.data)} ≥ max_exprs={self.max_exprs}"
26
+ )
27
+ self.data[name] = value
28
+
29
+ def get(self, name: str) -> Optional[Expr]:
30
+ return self.data.get(name)
31
+
32
+ def pop(self, name: str) -> Optional[Expr]:
33
+ return self.data.pop(name, None)
34
+
35
+ def __repr__(self) -> str:
36
+ return (
37
+ f"Env(size={len(self.data)}, "
38
+ f"max_exprs={self.max_exprs}, "
39
+ f"parent_id={self.parent_id})"
40
+ )
@@ -0,0 +1,86 @@
1
+
2
+ from typing import List, Optional, Union
3
+
4
+
5
+ class Expr:
6
+ class ListExpr:
7
+ def __init__(self, elements: List['Expr']):
8
+ self.elements = elements
9
+
10
+ def __eq__(self, other):
11
+ if not isinstance(other, Expr.ListExpr):
12
+ return NotImplemented
13
+ return self.elements == other.elements
14
+
15
+ def __ne__(self, other):
16
+ return not self.__eq__(other)
17
+
18
+ @property
19
+ def value(self):
20
+ inner = " ".join(str(e) for e in self.elements)
21
+ return f"({inner})"
22
+
23
+
24
+ def __repr__(self):
25
+ if not self.elements:
26
+ return "()"
27
+
28
+ inner = " ".join(str(e) for e in self.elements)
29
+ return f"({inner})"
30
+
31
+ def __iter__(self):
32
+ return iter(self.elements)
33
+
34
+ def __getitem__(self, index: Union[int, slice]):
35
+ return self.elements[index]
36
+
37
+ def __len__(self):
38
+ return len(self.elements)
39
+
40
+ class Symbol:
41
+ def __init__(self, value: str):
42
+ self.value = value
43
+
44
+ def __repr__(self):
45
+ return self.value
46
+
47
+ class Integer:
48
+ def __init__(self, value: int):
49
+ self.value = value
50
+
51
+ def __repr__(self):
52
+ return str(self.value)
53
+
54
+ class String:
55
+ def __init__(self, value: str):
56
+ self.value = value
57
+
58
+ def __repr__(self):
59
+ return f'"{self.value}"'
60
+
61
+ class Boolean:
62
+ def __init__(self, value: bool):
63
+ self.value = value
64
+
65
+ def __repr__(self):
66
+ return "true" if self.value else "false"
67
+
68
+ class Function:
69
+ def __init__(self, params: List[str], body: 'Expr'):
70
+ self.params = params
71
+ self.body = body
72
+
73
+ def __repr__(self):
74
+ params_str = " ".join(self.params)
75
+ body_str = str(self.body)
76
+ return f"(fn ({params_str}) {body_str})"
77
+
78
+ class Error:
79
+ def __init__(self, message: str, origin: Optional['Expr'] = None):
80
+ self.message = message
81
+ self.origin = origin
82
+
83
+ def __repr__(self):
84
+ if self.origin is None:
85
+ return f'(error "{self.message}")'
86
+ return f'(error "{self.message}" in {self.origin})'
@@ -7,92 +7,23 @@ from typing import Tuple, Dict, Union, Optional, List
7
7
  from datetime import datetime, timedelta, timezone
8
8
  import uuid
9
9
 
10
+ from astreum.lispeum.environment import Env
11
+ from astreum.lispeum.expression import Expr
12
+ from astreum.relay.peer import Peer
13
+ from astreum.relay.route import Route
14
+ from astreum.relay.setup import load_ed25519, load_x25519, make_routes, setup_outgoing, setup_udp
15
+ from astreum.storage.object import ObjectRequest, ObjectRequestType, ObjectResponse, ObjectResponseType
16
+ from astreum.storage.setup import storage_setup
17
+
10
18
  from .models.transaction import Transaction
11
19
  from .format import encode, decode
12
20
  from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
13
21
  from cryptography.hazmat.primitives import serialization
14
22
  from .crypto import ed25519, x25519
15
- from enum import IntEnum
16
23
  import blake3
17
24
  import struct
18
25
  from .models.message import Message, MessageTopic
19
26
 
20
- class ObjectRequestType(IntEnum):
21
- OBJECT_GET = 0
22
- OBJECT_PUT = 1
23
-
24
- class ObjectRequest:
25
- type: ObjectRequestType
26
- data: bytes
27
- hash: bytes
28
-
29
- def __init__(self, type: ObjectRequestType, data: bytes, hash: bytes = None):
30
- self.type = type
31
- self.data = data
32
- self.hash = hash
33
-
34
- def to_bytes(self):
35
- return encode([self.type.value, self.data, self.hash])
36
-
37
- @classmethod
38
- def from_bytes(cls, data: bytes):
39
- type_val, data_val, hash_val = decode(data)
40
- return cls(type=ObjectRequestType(type_val[0]), data=data_val, hash=hash_val)
41
-
42
- class ObjectResponseType(IntEnum):
43
- OBJECT_FOUND = 0
44
- OBJECT_PROVIDER = 1
45
- OBJECT_NEAREST_PEER = 2
46
-
47
- class ObjectResponse:
48
- type: ObjectResponseType
49
- data: bytes
50
- hash: bytes
51
-
52
- def __init__(self, type: ObjectResponseType, data: bytes, hash: bytes = None):
53
- self.type = type
54
- self.data = data
55
- self.hash = hash
56
-
57
- def to_bytes(self):
58
- return encode([self.type.value, self.data, self.hash])
59
-
60
- @classmethod
61
- def from_bytes(cls, data: bytes):
62
- type_val, data_val, hash_val = decode(data)
63
- return cls(type=ObjectResponseType(type_val[0]), data=data_val, hash=hash_val)
64
-
65
- class Peer:
66
- shared_key: bytes
67
- timestamp: datetime
68
- def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
69
- self.shared_key = my_sec_key.exchange(peer_pub_key)
70
- self.timestamp = datetime.now(timezone.utc)
71
-
72
- class Route:
73
- def __init__(self, relay_public_key: X25519PublicKey, bucket_size: int = 16):
74
- self.relay_public_key_bytes = relay_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
75
- self.bucket_size = bucket_size
76
- self.buckets: Dict[int, List[X25519PublicKey]] = {
77
- i: [] for i in range(len(self.relay_public_key_bytes) * 8)
78
- }
79
- self.peers = {}
80
-
81
- @staticmethod
82
- def _matching_leading_bits(a: bytes, b: bytes) -> int:
83
- for byte_index, (ba, bb) in enumerate(zip(a, b)):
84
- diff = ba ^ bb
85
- if diff:
86
- return byte_index * 8 + (8 - diff.bit_length())
87
- return len(a) * 8
88
-
89
- def add_peer(self, peer_public_key: X25519PublicKey):
90
- peer_public_key_bytes = peer_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
91
- bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
92
- if len(self.buckets[bucket_idx]) < self.bucket_size:
93
- self.buckets[bucket_idx].append(peer_public_key)
94
-
95
-
96
27
  def encode_ip_address(host: str, port: int) -> bytes:
97
28
  ip_bytes = socket.inet_pton(socket.AF_INET6 if ':' in host else socket.AF_INET, host)
98
29
  port_bytes = struct.pack("!H", port)
@@ -109,135 +40,18 @@ def decode_ip_address(data: bytes) -> tuple[str, int]:
109
40
  raise ValueError("Invalid address byte format")
110
41
  return ip, port
111
42
 
112
- # =========
113
- # MACHINE
114
- # =========
115
-
116
- class Expr:
117
- class ListExpr:
118
- def __init__(self, elements: List['Expr']):
119
- self.elements = elements
120
-
121
- def __eq__(self, other):
122
- if not isinstance(other, Expr.ListExpr):
123
- return NotImplemented
124
- return self.elements == other.elements
125
-
126
- def __ne__(self, other):
127
- return not self.__eq__(other)
128
-
129
- @property
130
- def value(self):
131
- inner = " ".join(str(e) for e in self.elements)
132
- return f"({inner})"
133
-
134
-
135
- def __repr__(self):
136
- if not self.elements:
137
- return "()"
138
-
139
- inner = " ".join(str(e) for e in self.elements)
140
- return f"({inner})"
141
-
142
- def __iter__(self):
143
- return iter(self.elements)
144
-
145
- def __getitem__(self, index: Union[int, slice]):
146
- return self.elements[index]
147
-
148
- def __len__(self):
149
- return len(self.elements)
150
-
151
- class Symbol:
152
- def __init__(self, value: str):
153
- self.value = value
154
-
155
- def __repr__(self):
156
- return self.value
157
-
158
- class Integer:
159
- def __init__(self, value: int):
160
- self.value = value
161
-
162
- def __repr__(self):
163
- return str(self.value)
164
-
165
- class String:
166
- def __init__(self, value: str):
167
- self.value = value
168
-
169
- def __repr__(self):
170
- return f'"{self.value}"'
171
-
172
- class Boolean:
173
- def __init__(self, value: bool):
174
- self.value = value
175
-
176
- def __repr__(self):
177
- return "true" if self.value else "false"
178
-
179
- class Function:
180
- def __init__(self, params: List[str], body: 'Expr'):
181
- self.params = params
182
- self.body = body
183
-
184
- def __repr__(self):
185
- params_str = " ".join(self.params)
186
- body_str = str(self.body)
187
- return f"(fn ({params_str}) {body_str})"
188
-
189
- class Error:
190
- def __init__(self, message: str, origin: Optional['Expr'] = None):
191
- self.message = message
192
- self.origin = origin
193
-
194
- def __repr__(self):
195
- if self.origin is None:
196
- return f'(error "{self.message}")'
197
- return f'(error "{self.message}" in {self.origin})'
198
-
199
- class Env:
200
- def __init__(
201
- self,
202
- data: Optional[Dict[str, Expr]] = None,
203
- parent_id: Optional[uuid.UUID] = None,
204
- max_exprs: Optional[int] = 8,
205
- ):
206
- self.data: Dict[str, Expr] = data if data is not None else {}
207
- self.parent_id: Optional[uuid.UUID] = parent_id
208
- self.max_exprs: Optional[int] = max_exprs
209
-
210
- def put(self, name: str, value: Expr) -> None:
211
- if (
212
- self.max_exprs is not None
213
- and name not in self.data
214
- and len(self.data) >= self.max_exprs
215
- ):
216
- raise RuntimeError(
217
- f"environment full: {len(self.data)} ≥ max_exprs={self.max_exprs}"
218
- )
219
- self.data[name] = value
220
-
221
- def get(self, name: str) -> Optional[Expr]:
222
- return self.data.get(name)
223
-
224
- def pop(self, name: str) -> Optional[Expr]:
225
- return self.data.pop(name, None)
226
-
227
- def __repr__(self) -> str:
228
- return (
229
- f"Env(size={len(self.data)}, "
230
- f"max_exprs={self.max_exprs}, "
231
- f"parent_id={self.parent_id})"
232
- )
233
-
234
-
235
43
  class Node:
236
44
  def __init__(self, config: dict = {}):
237
45
  self._machine_setup()
238
46
  machine_only = bool(config.get('machine-only', True))
239
47
  if not machine_only:
240
- self._storage_setup(config=config)
48
+ (
49
+ self.storage_path,
50
+ self.memory_storage,
51
+ self.storage_get_relay_timeout,
52
+ self.storage_index
53
+ ) = storage_setup(config)
54
+
241
55
  self._relay_setup(config=config)
242
56
  self._validation_setup(config=config)
243
57
 
@@ -250,87 +64,46 @@ class Node:
250
64
  def _create_block(self):
251
65
  pass
252
66
 
253
- # STORAGE METHODS
254
- def _storage_setup(self, config: dict):
255
- storage_path_str = config.get('storage_path')
256
- if storage_path_str is None:
257
- self.storage_path = None
258
- self.memory_storage = {}
259
- else:
260
- self.storage_path = Path(storage_path_str)
261
- self.storage_path.mkdir(parents=True, exist_ok=True)
262
- self.memory_storage = None
67
+ def _relay_setup(self, config: dict):
68
+ self.use_ipv6 = config.get('use_ipv6', False)
263
69
 
264
- self.storage_get_relay_timeout = config.get('storage_get_relay_timeout', 5)
265
- # STORAGE INDEX: (object_hash, encoded (provider_public_key, provider_address))
266
- self.storage_index = Dict[bytes, bytes]
70
+ # key loading
71
+ self.relay_secret_key = load_x25519(config.get('relay_secret_key'))
72
+ self.validation_secret_key = load_ed25519(config.get('validation_secret_key'))
267
73
 
268
- def _relay_setup(self, config: dict):
269
- self.use_ipv6 = config.get('use_ipv6', False)
270
- incoming_port = config.get('incoming_port', 7373)
74
+ # derive pubs + routes
75
+ self.relay_public_key = self.relay_secret_key.public_key()
76
+ self.peer_route, self.validation_route = make_routes(
77
+ self.relay_public_key,
78
+ self.validation_secret_key
79
+ )
271
80
 
272
- if 'relay_secret_key' in config:
273
- try:
274
- private_key_bytes = bytes.fromhex(config['relay_secret_key'])
275
- self.relay_secret_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key_bytes)
276
- except Exception as e:
277
- raise Exception(f"Error loading relay secret key provided: {e}")
278
- else:
279
- self.relay_secret_key = ed25519.Ed25519PrivateKey.generate()
280
-
281
- self.relay_public_key = self.relay_secret_key.public_key()
81
+ # sockets + queues + threads
82
+ (self.incoming_socket,
83
+ self.incoming_port,
84
+ self.incoming_queue,
85
+ self.incoming_populate_thread,
86
+ self.incoming_process_thread
87
+ ) = setup_udp(config.get('incoming_port', 7373), self.use_ipv6)
282
88
 
283
- if 'validation_secret_key' in config:
284
- try:
285
- private_key_bytes = bytes.fromhex(config['validation_secret_key'])
286
- self.validation_secret_key = x25519.X25519PrivateKey.from_private_bytes(private_key_bytes)
287
- except Exception as e:
288
- raise Exception(f"Error loading validation secret key provided: {e}")
289
-
290
- # setup peer route and validation route
291
- self.peer_route = Route(self.relay_public_key)
292
- if self.validation_secret_key:
293
- self.validation_route = Route(self.relay_public_key)
294
-
295
- # Choose address family based on IPv4 or IPv6
296
- family = socket.AF_INET6 if self.use_ipv6 else socket.AF_INET
297
-
298
- self.incoming_socket = socket.socket(family, socket.SOCK_DGRAM)
299
- if self.use_ipv6:
300
- self.incoming_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
301
- bind_address = "::" if self.use_ipv6 else "0.0.0.0"
302
- self.incoming_socket.bind((bind_address, incoming_port or 0))
303
- self.incoming_port = self.incoming_socket.getsockname()[1]
304
- self.incoming_queue = Queue()
305
-
306
- self.incoming_populate_thread = threading.Thread(target=self._relay_incoming_queue_populating)
307
- self.incoming_populate_thread.daemon = True
308
- self.incoming_populate_thread.start()
309
-
310
- self.incoming_process_thread = threading.Thread(target=self._relay_incoming_queue_processing)
311
- self.incoming_process_thread.daemon = True
312
- self.incoming_process_thread.start()
313
-
314
- # outgoing thread
315
- self.outgoing_socket = socket.socket(family, socket.SOCK_DGRAM)
316
- self.outgoing_queue = Queue()
317
- self.outgoing_thread = threading.Thread(target=self._relay_outgoing_queue_processor)
318
- self.outgoing_thread.daemon = True
319
- self.outgoing_thread.start()
89
+ (self.outgoing_socket,
90
+ self.outgoing_queue,
91
+ self.outgoing_thread
92
+ ) = setup_outgoing(self.use_ipv6)
320
93
 
94
+ # other workers & maps
321
95
  self.object_request_queue = Queue()
322
-
323
- self.peer_manager_thread = threading.Thread(target=self._relay_peer_manager)
324
- self.peer_manager_thread.daemon = True
96
+ self.peer_manager_thread = threading.Thread(
97
+ target=self._relay_peer_manager,
98
+ daemon=True
99
+ )
325
100
  self.peer_manager_thread.start()
326
101
 
327
- self.peers = Dict[X25519PublicKey, Peer]
328
- self.addresses = Dict[Tuple[str, int], X25519PublicKey]
329
-
330
- if 'bootstrap' in config:
331
- for addr in config['bootstrap']:
332
- self._send_ping(addr)
102
+ self.peers, self.addresses = {}, {} # peers: Dict[X25519PublicKey,Peer], addresses: Dict[(str,int),X25519PublicKey]
333
103
 
104
+ # bootstrap pings
105
+ for addr in config.get('bootstrap', []):
106
+ self._send_ping(addr)
334
107
 
335
108
  def _local_object_get(self, data_hash: bytes) -> Optional[bytes]:
336
109
  if self.memory_storage is not None:
@@ -701,137 +474,10 @@ class Node:
701
474
  return result
702
475
 
703
476
  # # List
704
- # elif first.value == "list.new":
705
- # return Expr.ListExpr([self.evaluate_expression(arg, env) for arg in expr.elements[1:]])
706
-
707
- # elif first.value == "list.get":
708
- # args = expr.elements[1:]
709
- # if len(args) != 2:
710
- # return Expr.Error(
711
- # category="SyntaxError",
712
- # message="list.get expects exactly two arguments: a list and an index"
713
- # )
714
- # list_obj = self.evaluate_expression(args[0], env)
715
- # index = self.evaluate_expression(args[1], env)
716
- # return handle_list_get(self, list_obj, index, env)
717
-
718
- # elif first.value == "list.insert":
719
- # args = expr.elements[1:]
720
- # if len(args) != 3:
721
- # return Expr.ListExpr([
722
- # Expr.ListExpr([]),
723
- # Expr.String("list.insert expects exactly three arguments: a list, an index, and a value")
724
- # ])
725
-
726
- # return handle_list_insert(
727
- # list=self.evaluate_expression(args[0], env),
728
- # index=self.evaluate_expression(args[1], env),
729
- # value=self.evaluate_expression(args[2], env),
730
- # )
731
-
732
- # elif first.value == "list.remove":
733
- # args = expr.elements[1:]
734
- # if len(args) != 2:
735
- # return Expr.ListExpr([
736
- # Expr.ListExpr([]),
737
- # Expr.String("list.remove expects exactly two arguments: a list and an index")
738
- # ])
739
-
740
- # return handle_list_remove(
741
- # list=self.evaluate_expression(args[0], env),
742
- # index=self.evaluate_expression(args[1], env),
743
- # )
744
-
745
- # elif first.value == "list.length":
746
- # args = expr.elements[1:]
747
- # if len(args) != 1:
748
- # return Expr.ListExpr([
749
- # Expr.ListExpr([]),
750
- # Expr.String("list.length expects exactly one argument: a list")
751
- # ])
752
-
753
- # list_obj = self.evaluate_expression(args[0], env)
754
- # if not isinstance(list_obj, Expr.ListExpr):
755
- # return Expr.ListExpr([
756
- # Expr.ListExpr([]),
757
- # Expr.String("Argument must be a list")
758
- # ])
759
-
760
- # return Expr.ListExpr([
761
- # Expr.Integer(len(list_obj.elements)),
762
- # Expr.ListExpr([])
763
- # ])
764
-
765
- # elif first.value == "list.fold":
766
- # if len(args) != 3:
767
- # return Expr.ListExpr([
768
- # Expr.ListExpr([]),
769
- # Expr.String("list.fold expects exactly three arguments: a list, an initial value, and a function")
770
- # ])
771
-
772
- # return handle_list_fold(
773
- # machine=self,
774
- # list=self.evaluate_expression(args[0], env),
775
- # initial=self.evaluate_expression(args[1], env),
776
- # func=self.evaluate_expression(args[2], env),
777
- # env=env,
778
- # )
779
-
780
- # elif first.value == "list.map":
781
- # if len(args) != 2:
782
- # return Expr.ListExpr([
783
- # Expr.ListExpr([]),
784
- # Expr.String("list.map expects exactly two arguments: a list and a function")
785
- # ])
786
-
787
- # return handle_list_map(
788
- # machine=self,
789
- # list=self.evaluate_expression(args[0], env),
790
- # func=self.evaluate_expression(args[1], env),
791
- # env=env,
792
- # )
793
-
794
- # elif first.value == "list.position":
795
- # if len(args) != 2:
796
- # return Expr.ListExpr([
797
- # Expr.ListExpr([]),
798
- # Expr.String("list.position expects exactly two arguments: a list and a function")
799
- # ])
800
-
801
- # return handle_list_position(
802
- # machine=self,
803
- # list=self.evaluate_expression(args[0], env),
804
- # predicate=self.evaluate_expression(args[1], env),
805
- # env=env,
806
- # )
807
-
808
- # elif first.value == "list.any":
809
- # if len(args) != 2:
810
- # return Expr.ListExpr([
811
- # Expr.ListExpr([]),
812
- # Expr.String("list.any expects exactly two arguments: a list and a function")
813
- # ])
814
-
815
- # return handle_list_any(
816
- # machine=self,
817
- # list=self.evaluate_expression(args[0], env),
818
- # predicate=self.evaluate_expression(args[1], env),
819
- # env=env,
820
- # )
821
-
822
- # elif first.value == "list.all":
823
- # if len(args) != 2:
824
- # return Expr.ListExpr([
825
- # Expr.ListExpr([]),
826
- # Expr.String("list.all expects exactly two arguments: a list and a function")
827
- # ])
828
-
829
- # return handle_list_all(
830
- # machine=self,
831
- # list=self.evaluate_expression(args[0], env),
832
- # predicate=self.evaluate_expression(args[1], env),
833
- # env=env,
834
- # )
477
+ elif first.value == "list.each":
478
+ internal_function = expr.elements[1]
479
+
480
+
835
481
 
836
482
  # Integer arithmetic primitives
837
483
  elif first.value == "+":
@@ -918,6 +564,19 @@ class Node:
918
564
 
919
565
  return Expr.Boolean(res)
920
566
 
567
+ if isinstance(first, Expr.Function):
568
+ arg_exprs = expr.elements[1:]
569
+ if len(arg_exprs) != len(first.params):
570
+ return Expr.Error(f"arity mismatch: expected {len(first.params)}, got {len(arg_exprs)}", origin=expr)
571
+
572
+ call_env = self.machine_create_environment(parent_id=env_id)
573
+ for name, aexpr in zip(first.params, arg_exprs):
574
+ val = self.machine_expr_eval(env_id, aexpr)
575
+ if isinstance(val, Expr.Error): return val
576
+ self.machine_expr_put(call_env, name, val)
577
+
578
+ return self.machine_expr_eval(env_id=call_env, expr=first.body)
579
+
921
580
  else:
922
581
  evaluated_elements = [self.machine_expr_eval(env_id=env_id, expr=e) for e in expr.elements]
923
582
  return Expr.ListExpr(evaluated_elements)
File without changes
@@ -0,0 +1,9 @@
1
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
2
+ from datetime import datetime, timezone
3
+
4
+ class Peer:
5
+ shared_key: bytes
6
+ timestamp: datetime
7
+ def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
8
+ self.shared_key = my_sec_key.exchange(peer_pub_key)
9
+ self.timestamp = datetime.now(timezone.utc)
@@ -0,0 +1,25 @@
1
+ from typing import Dict, List
2
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
3
+
4
+ class Route:
5
+ def __init__(self, relay_public_key: X25519PublicKey, bucket_size: int = 16):
6
+ self.relay_public_key_bytes = relay_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
7
+ self.bucket_size = bucket_size
8
+ self.buckets: Dict[int, List[X25519PublicKey]] = {
9
+ i: [] for i in range(len(self.relay_public_key_bytes) * 8)
10
+ }
11
+ self.peers = {}
12
+
13
+ @staticmethod
14
+ def _matching_leading_bits(a: bytes, b: bytes) -> int:
15
+ for byte_index, (ba, bb) in enumerate(zip(a, b)):
16
+ diff = ba ^ bb
17
+ if diff:
18
+ return byte_index * 8 + (8 - diff.bit_length())
19
+ return len(a) * 8
20
+
21
+ def add_peer(self, peer_public_key: X25519PublicKey):
22
+ peer_public_key_bytes = peer_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
23
+ bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
24
+ if len(self.buckets[bucket_idx]) < self.bucket_size:
25
+ self.buckets[bucket_idx].append(peer_public_key)
@@ -0,0 +1,58 @@
1
+ import socket, threading
2
+ from queue import Queue
3
+ from typing import Tuple, Optional
4
+ from cryptography.hazmat.primitives.asymmetric import ed25519
5
+ from cryptography.hazmat.primitives.asymmetric.x25519 import (
6
+ X25519PrivateKey,
7
+ X25519PublicKey,
8
+ )
9
+ from yourproject.routes import Route
10
+
11
+ def load_x25519(hex_key: Optional[str]) -> X25519PrivateKey:
12
+ """DH key for relaying (always X25519)."""
13
+ return
14
+
15
+ def load_ed25519(hex_key: Optional[str]) -> Optional[ed25519.Ed25519PrivateKey]:
16
+ """Signing key for validation (Ed25519), or None if absent."""
17
+ return ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key)) \
18
+ if hex_key else None
19
+
20
+ def make_routes(
21
+ relay_pk: X25519PublicKey,
22
+ val_sk: Optional[ed25519.Ed25519PrivateKey]
23
+ ) -> Tuple[Route, Optional[Route]]:
24
+ """Peer route (DH pubkey) + optional validation route (ed pubkey)."""
25
+ peer_rt = Route(relay_pk)
26
+ val_rt = Route(val_sk.public_key()) if val_sk else None
27
+ return peer_rt, val_rt
28
+
29
+ def setup_udp(
30
+ bind_port: int,
31
+ use_ipv6: bool
32
+ ) -> Tuple[socket.socket, int, Queue, threading.Thread, threading.Thread]:
33
+ fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
34
+ sock = socket.socket(fam, socket.SOCK_DGRAM)
35
+ if use_ipv6:
36
+ sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
37
+ sock.bind(("::" if use_ipv6 else "0.0.0.0", bind_port or 0))
38
+ port = sock.getsockname()[1]
39
+
40
+ q = Queue()
41
+ pop = threading.Thread(target=lambda: None, daemon=True)
42
+ proc = threading.Thread(target=lambda: None, daemon=True)
43
+ pop.start(); proc.start()
44
+ return sock, port, q, pop, proc
45
+
46
+ def setup_outgoing(
47
+ use_ipv6: bool
48
+ ) -> Tuple[socket.socket, Queue, threading.Thread]:
49
+ fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
50
+ sock = socket.socket(fam, socket.SOCK_DGRAM)
51
+ q = Queue()
52
+ thr = threading.Thread(target=lambda: None, daemon=True)
53
+ thr.start()
54
+ return sock, q, thr
55
+
56
+ def make_maps():
57
+ """Empty lookup maps: peers and addresses."""
58
+ return
File without changes
@@ -0,0 +1,68 @@
1
+ from enum import IntEnum
2
+
3
+ class ObjectRequestType(IntEnum):
4
+ OBJECT_GET = 0
5
+ OBJECT_PUT = 1
6
+
7
+ class ObjectRequest:
8
+ type: ObjectRequestType
9
+ data: bytes
10
+ hash: bytes
11
+
12
+ def __init__(self, type: ObjectRequestType, data: bytes, hash: bytes = None):
13
+ self.type = type
14
+ self.data = data
15
+ self.hash = hash
16
+
17
+ def to_bytes(self):
18
+ return [self.type.value] + self.hash + self.data
19
+
20
+ @classmethod
21
+ def from_bytes(cls, data: bytes) -> "ObjectRequest":
22
+ # need at least 1 byte for type + 32 bytes for hash
23
+ if len(data) < 1 + 32:
24
+ raise ValueError(f"Too short for ObjectRequest ({len(data)} bytes)")
25
+
26
+ type_val = data[0]
27
+ try:
28
+ req_type = ObjectRequestType(type_val)
29
+ except ValueError:
30
+ raise ValueError(f"Unknown ObjectRequestType: {type_val!r}")
31
+
32
+ hash_bytes = data[1:33]
33
+ payload = data[33:]
34
+ return cls(req_type, payload, hash_bytes)
35
+
36
+ class ObjectResponseType(IntEnum):
37
+ OBJECT_FOUND = 0
38
+ OBJECT_PROVIDER = 1
39
+ OBJECT_NEAREST_PEER = 2
40
+
41
+ class ObjectResponse:
42
+ type: ObjectResponseType
43
+ data: bytes
44
+ hash: bytes
45
+
46
+ def __init__(self, type: ObjectResponseType, data: bytes, hash: bytes = None):
47
+ self.type = type
48
+ self.data = data
49
+ self.hash = hash
50
+
51
+ def to_bytes(self):
52
+ return [self.type.value] + self.hash + self.data
53
+
54
+ @classmethod
55
+ def from_bytes(cls, data: bytes) -> "ObjectResponse":
56
+ # need at least 1 byte for type + 32 bytes for hash
57
+ if len(data) < 1 + 32:
58
+ raise ValueError(f"Too short to be a valid ObjectResponse ({len(data)} bytes)")
59
+
60
+ type_val = data[0]
61
+ try:
62
+ resp_type = ObjectResponseType(type_val)
63
+ except ValueError:
64
+ raise ValueError(f"Unknown ObjectResponseType: {type_val}")
65
+
66
+ hash_bytes = data[1:33]
67
+ payload = data[33:]
68
+ return cls(resp_type, payload, hash_bytes)
@@ -0,0 +1,15 @@
1
+ from pathlib import Path
2
+ from typing import Optional, Dict, Tuple, Any
3
+
4
+ def storage_setup(config: dict) -> Tuple[Optional[Path], Dict[bytes, Any], int, Dict[bytes, bytes]]:
5
+ storage_path_str = config.get('storage_path')
6
+ if storage_path_str is None:
7
+ storage_path, memory_storage = None, {}
8
+ else:
9
+ storage_path = Path(storage_path_str)
10
+ storage_path.mkdir(parents=True, exist_ok=True)
11
+ memory_storage = None
12
+
13
+ timeout = config.get('storage_get_relay_timeout', 5)
14
+ storage_index: Dict[bytes, bytes] = {}
15
+ return storage_path, memory_storage, timeout, storage_index
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.27
3
+ Version: 0.2.29
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
@@ -15,6 +15,8 @@ src/astreum/crypto/quadratic_form.py
15
15
  src/astreum/crypto/wesolowski.py
16
16
  src/astreum/crypto/x25519.py
17
17
  src/astreum/lispeum/__init__.py
18
+ src/astreum/lispeum/environment.py
19
+ src/astreum/lispeum/expression.py
18
20
  src/astreum/lispeum/parser.py
19
21
  src/astreum/lispeum/tokenizer.py
20
22
  src/astreum/models/__init__.py
@@ -25,4 +27,11 @@ src/astreum/models/merkle.py
25
27
  src/astreum/models/message.py
26
28
  src/astreum/models/patricia.py
27
29
  src/astreum/models/transaction.py
30
+ src/astreum/relay/__init__.py
31
+ src/astreum/relay/peer.py
32
+ src/astreum/relay/route.py
33
+ src/astreum/relay/setup.py
34
+ src/astreum/storage/__init__.py
35
+ src/astreum/storage/object.py
36
+ src/astreum/storage/setup.py
28
37
  tests/test_node_machine.py
File without changes
File without changes
File without changes
File without changes