astreum 0.2.28__tar.gz → 0.2.30__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.
Files changed (39) hide show
  1. {astreum-0.2.28/src/astreum.egg-info → astreum-0.2.30}/PKG-INFO +1 -1
  2. {astreum-0.2.28 → astreum-0.2.30}/pyproject.toml +1 -1
  3. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/node.py +237 -229
  4. astreum-0.2.30/src/astreum/relay/peer.py +9 -0
  5. astreum-0.2.30/src/astreum/relay/route.py +25 -0
  6. astreum-0.2.30/src/astreum/relay/setup.py +58 -0
  7. astreum-0.2.30/src/astreum/storage/__init__.py +0 -0
  8. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/storage/setup.py +1 -2
  9. {astreum-0.2.28 → astreum-0.2.30/src/astreum.egg-info}/PKG-INFO +1 -1
  10. {astreum-0.2.28 → astreum-0.2.30}/src/astreum.egg-info/SOURCES.txt +4 -0
  11. {astreum-0.2.28 → astreum-0.2.30}/LICENSE +0 -0
  12. {astreum-0.2.28 → astreum-0.2.30}/README.md +0 -0
  13. {astreum-0.2.28 → astreum-0.2.30}/setup.cfg +0 -0
  14. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/__init__.py +0 -0
  15. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/crypto/__init__.py +0 -0
  16. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/crypto/ed25519.py +0 -0
  17. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/crypto/quadratic_form.py +0 -0
  18. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/crypto/wesolowski.py +0 -0
  19. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/crypto/x25519.py +0 -0
  20. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/format.py +0 -0
  21. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/lispeum/__init__.py +0 -0
  22. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/lispeum/environment.py +0 -0
  23. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/lispeum/expression.py +0 -0
  24. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/lispeum/parser.py +0 -0
  25. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/lispeum/tokenizer.py +0 -0
  26. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/models/__init__.py +0 -0
  27. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/models/account.py +0 -0
  28. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/models/accounts.py +0 -0
  29. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/models/block.py +0 -0
  30. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/models/merkle.py +0 -0
  31. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/models/message.py +0 -0
  32. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/models/patricia.py +0 -0
  33. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/models/transaction.py +0 -0
  34. {astreum-0.2.28/src/astreum/storage → astreum-0.2.30/src/astreum/relay}/__init__.py +0 -0
  35. {astreum-0.2.28 → astreum-0.2.30}/src/astreum/storage/object.py +0 -0
  36. {astreum-0.2.28 → astreum-0.2.30}/src/astreum.egg-info/dependency_links.txt +0 -0
  37. {astreum-0.2.28 → astreum-0.2.30}/src/astreum.egg-info/requires.txt +0 -0
  38. {astreum-0.2.28 → astreum-0.2.30}/src/astreum.egg-info/top_level.txt +0 -0
  39. {astreum-0.2.28 → astreum-0.2.30}/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.28
3
+ Version: 0.2.30
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.28"
3
+ version = "0.2.30"
4
4
  authors = [
5
5
  { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
6
  ]
@@ -9,6 +9,9 @@ import uuid
9
9
 
10
10
  from astreum.lispeum.environment import Env
11
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
12
15
  from astreum.storage.object import ObjectRequest, ObjectRequestType, ObjectResponse, ObjectResponseType
13
16
  from astreum.storage.setup import storage_setup
14
17
 
@@ -21,39 +24,6 @@ import blake3
21
24
  import struct
22
25
  from .models.message import Message, MessageTopic
23
26
 
24
-
25
-
26
- class Peer:
27
- shared_key: bytes
28
- timestamp: datetime
29
- def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
30
- self.shared_key = my_sec_key.exchange(peer_pub_key)
31
- self.timestamp = datetime.now(timezone.utc)
32
-
33
- class Route:
34
- def __init__(self, relay_public_key: X25519PublicKey, bucket_size: int = 16):
35
- self.relay_public_key_bytes = relay_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
36
- self.bucket_size = bucket_size
37
- self.buckets: Dict[int, List[X25519PublicKey]] = {
38
- i: [] for i in range(len(self.relay_public_key_bytes) * 8)
39
- }
40
- self.peers = {}
41
-
42
- @staticmethod
43
- def _matching_leading_bits(a: bytes, b: bytes) -> int:
44
- for byte_index, (ba, bb) in enumerate(zip(a, b)):
45
- diff = ba ^ bb
46
- if diff:
47
- return byte_index * 8 + (8 - diff.bit_length())
48
- return len(a) * 8
49
-
50
- def add_peer(self, peer_public_key: X25519PublicKey):
51
- peer_public_key_bytes = peer_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
52
- bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
53
- if len(self.buckets[bucket_idx]) < self.bucket_size:
54
- self.buckets[bucket_idx].append(peer_public_key)
55
-
56
-
57
27
  def encode_ip_address(host: str, port: int) -> bytes:
58
28
  ip_bytes = socket.inet_pton(socket.AF_INET6 if ':' in host else socket.AF_INET, host)
59
29
  port_bytes = struct.pack("!H", port)
@@ -95,71 +65,45 @@ class Node:
95
65
  pass
96
66
 
97
67
  def _relay_setup(self, config: dict):
98
- self.use_ipv6 = config.get('use_ipv6', False)
99
- incoming_port = config.get('incoming_port', 7373)
100
-
101
- if 'relay_secret_key' in config:
102
- try:
103
- private_key_bytes = bytes.fromhex(config['relay_secret_key'])
104
- self.relay_secret_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key_bytes)
105
- except Exception as e:
106
- raise Exception(f"Error loading relay secret key provided: {e}")
107
- else:
108
- self.relay_secret_key = ed25519.Ed25519PrivateKey.generate()
109
-
110
- self.relay_public_key = self.relay_secret_key.public_key()
111
-
112
- if 'validation_secret_key' in config:
113
- try:
114
- private_key_bytes = bytes.fromhex(config['validation_secret_key'])
115
- self.validation_secret_key = x25519.X25519PrivateKey.from_private_bytes(private_key_bytes)
116
- except Exception as e:
117
- raise Exception(f"Error loading validation secret key provided: {e}")
118
-
119
- # setup peer route and validation route
120
- self.peer_route = Route(self.relay_public_key)
121
- if self.validation_secret_key:
122
- self.validation_route = Route(self.relay_public_key)
123
-
124
- # Choose address family based on IPv4 or IPv6
125
- family = socket.AF_INET6 if self.use_ipv6 else socket.AF_INET
126
-
127
- self.incoming_socket = socket.socket(family, socket.SOCK_DGRAM)
128
- if self.use_ipv6:
129
- self.incoming_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
130
- bind_address = "::" if self.use_ipv6 else "0.0.0.0"
131
- self.incoming_socket.bind((bind_address, incoming_port or 0))
132
- self.incoming_port = self.incoming_socket.getsockname()[1]
133
- self.incoming_queue = Queue()
134
-
135
- self.incoming_populate_thread = threading.Thread(target=self._relay_incoming_queue_populating)
136
- self.incoming_populate_thread.daemon = True
137
- self.incoming_populate_thread.start()
138
-
139
- self.incoming_process_thread = threading.Thread(target=self._relay_incoming_queue_processing)
140
- self.incoming_process_thread.daemon = True
141
- self.incoming_process_thread.start()
142
-
143
- # outgoing thread
144
- self.outgoing_socket = socket.socket(family, socket.SOCK_DGRAM)
145
- self.outgoing_queue = Queue()
146
- self.outgoing_thread = threading.Thread(target=self._relay_outgoing_queue_processor)
147
- self.outgoing_thread.daemon = True
148
- self.outgoing_thread.start()
149
-
68
+ self.use_ipv6 = config.get('use_ipv6', False)
69
+
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'))
73
+
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
+ )
80
+
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)
88
+
89
+ (self.outgoing_socket,
90
+ self.outgoing_queue,
91
+ self.outgoing_thread
92
+ ) = setup_outgoing(self.use_ipv6)
93
+
94
+ # other workers & maps
150
95
  self.object_request_queue = Queue()
151
-
152
- self.peer_manager_thread = threading.Thread(target=self._relay_peer_manager)
153
- self.peer_manager_thread.daemon = True
96
+ self.peer_manager_thread = threading.Thread(
97
+ target=self._relay_peer_manager,
98
+ daemon=True
99
+ )
154
100
  self.peer_manager_thread.start()
155
101
 
156
- self.peers = Dict[X25519PublicKey, Peer]
157
- self.addresses = Dict[Tuple[str, int], X25519PublicKey]
158
-
159
- if 'bootstrap' in config:
160
- for addr in config['bootstrap']:
161
- self._send_ping(addr)
102
+ self.peers, self.addresses = {}, {} # peers: Dict[X25519PublicKey,Peer], addresses: Dict[(str,int),X25519PublicKey]
162
103
 
104
+ # bootstrap pings
105
+ for addr in config.get('bootstrap', []):
106
+ self._send_ping(addr)
163
107
 
164
108
  def _local_object_get(self, data_hash: bytes) -> Optional[bytes]:
165
109
  if self.memory_storage is not None:
@@ -519,7 +463,7 @@ class Node:
519
463
  elif first.value == "def":
520
464
  args = expr.elements[1:]
521
465
  if len(args) != 2:
522
- return Expr.Error(message=f"'def' expects exactly 2 arguments, got {len(args)}", origin=expr)
466
+ return Expr.Error("def expects key value", origin=expr)
523
467
  if not isinstance(args[0], Expr.Symbol):
524
468
  return Expr.Error(message="first argument to 'def' must be a symbol", origin=args[0])
525
469
  result = self.machine_expr_eval(env_id=env_id, expr=args[1])
@@ -529,140 +473,191 @@ class Node:
529
473
  self.machine_expr_put(env_id=env_id, name=args[0].value, expr=result)
530
474
  return result
531
475
 
532
- # # List
533
- # elif first.value == "list.new":
534
- # return Expr.ListExpr([self.evaluate_expression(arg, env) for arg in expr.elements[1:]])
535
-
536
- # elif first.value == "list.get":
537
- # args = expr.elements[1:]
538
- # if len(args) != 2:
539
- # return Expr.Error(
540
- # category="SyntaxError",
541
- # message="list.get expects exactly two arguments: a list and an index"
542
- # )
543
- # list_obj = self.evaluate_expression(args[0], env)
544
- # index = self.evaluate_expression(args[1], env)
545
- # return handle_list_get(self, list_obj, index, env)
546
-
547
- # elif first.value == "list.insert":
548
- # args = expr.elements[1:]
549
- # if len(args) != 3:
550
- # return Expr.ListExpr([
551
- # Expr.ListExpr([]),
552
- # Expr.String("list.insert expects exactly three arguments: a list, an index, and a value")
553
- # ])
554
-
555
- # return handle_list_insert(
556
- # list=self.evaluate_expression(args[0], env),
557
- # index=self.evaluate_expression(args[1], env),
558
- # value=self.evaluate_expression(args[2], env),
559
- # )
560
-
561
- # elif first.value == "list.remove":
562
- # args = expr.elements[1:]
563
- # if len(args) != 2:
564
- # return Expr.ListExpr([
565
- # Expr.ListExpr([]),
566
- # Expr.String("list.remove expects exactly two arguments: a list and an index")
567
- # ])
568
-
569
- # return handle_list_remove(
570
- # list=self.evaluate_expression(args[0], env),
571
- # index=self.evaluate_expression(args[1], env),
572
- # )
573
-
574
- # elif first.value == "list.length":
575
- # args = expr.elements[1:]
576
- # if len(args) != 1:
577
- # return Expr.ListExpr([
578
- # Expr.ListExpr([]),
579
- # Expr.String("list.length expects exactly one argument: a list")
580
- # ])
581
-
582
- # list_obj = self.evaluate_expression(args[0], env)
583
- # if not isinstance(list_obj, Expr.ListExpr):
584
- # return Expr.ListExpr([
585
- # Expr.ListExpr([]),
586
- # Expr.String("Argument must be a list")
587
- # ])
588
-
589
- # return Expr.ListExpr([
590
- # Expr.Integer(len(list_obj.elements)),
591
- # Expr.ListExpr([])
592
- # ])
593
-
594
- # elif first.value == "list.fold":
595
- # if len(args) != 3:
596
- # return Expr.ListExpr([
597
- # Expr.ListExpr([]),
598
- # Expr.String("list.fold expects exactly three arguments: a list, an initial value, and a function")
599
- # ])
600
-
601
- # return handle_list_fold(
602
- # machine=self,
603
- # list=self.evaluate_expression(args[0], env),
604
- # initial=self.evaluate_expression(args[1], env),
605
- # func=self.evaluate_expression(args[2], env),
606
- # env=env,
607
- # )
608
-
609
- # elif first.value == "list.map":
610
- # if len(args) != 2:
611
- # return Expr.ListExpr([
612
- # Expr.ListExpr([]),
613
- # Expr.String("list.map expects exactly two arguments: a list and a function")
614
- # ])
615
-
616
- # return handle_list_map(
617
- # machine=self,
618
- # list=self.evaluate_expression(args[0], env),
619
- # func=self.evaluate_expression(args[1], env),
620
- # env=env,
621
- # )
622
-
623
- # elif first.value == "list.position":
624
- # if len(args) != 2:
625
- # return Expr.ListExpr([
626
- # Expr.ListExpr([]),
627
- # Expr.String("list.position expects exactly two arguments: a list and a function")
628
- # ])
629
-
630
- # return handle_list_position(
631
- # machine=self,
632
- # list=self.evaluate_expression(args[0], env),
633
- # predicate=self.evaluate_expression(args[1], env),
634
- # env=env,
635
- # )
636
-
637
- # elif first.value == "list.any":
638
- # if len(args) != 2:
639
- # return Expr.ListExpr([
640
- # Expr.ListExpr([]),
641
- # Expr.String("list.any expects exactly two arguments: a list and a function")
642
- # ])
643
-
644
- # return handle_list_any(
645
- # machine=self,
646
- # list=self.evaluate_expression(args[0], env),
647
- # predicate=self.evaluate_expression(args[1], env),
648
- # env=env,
649
- # )
650
-
651
- # elif first.value == "list.all":
652
- # if len(args) != 2:
653
- # return Expr.ListExpr([
654
- # Expr.ListExpr([]),
655
- # Expr.String("list.all expects exactly two arguments: a list and a function")
656
- # ])
657
-
658
- # return handle_list_all(
659
- # machine=self,
660
- # list=self.evaluate_expression(args[0], env),
661
- # predicate=self.evaluate_expression(args[1], env),
662
- # env=env,
663
- # )
664
-
665
- # Integer arithmetic primitives
476
+ ## List: ints -> (1 2)
477
+ # push: (list.push 3 ints) -> (1 2 3) / (list.push 0 0 ints) -> (0 1 2)
478
+ elif first.value == "list.push":
479
+ args = expr.elements[1:]
480
+ if len(args) == 2:
481
+ val_expr, list_expr = args
482
+ idx = None
483
+ elif len(args) == 3:
484
+ idx_expr, val_expr, list_expr = args
485
+ idx = self.machine_expr_eval(env_id, idx_expr)
486
+ if isinstance(idx, Expr.Error): return idx
487
+ if not isinstance(idx, Expr.IntExpr):
488
+ return Expr.Error("index must be int", origin=idx_expr)
489
+ idx = idx.value
490
+ else:
491
+ return Expr.Error("list.push expects (value list) or (index value list)", origin=expr)
492
+
493
+ lst = self.machine_expr_eval(env_id, list_expr)
494
+ if isinstance(lst, Expr.Error): return lst
495
+ if not isinstance(lst, Expr.ListExpr):
496
+ return Expr.Error("last arg to list.push must be a list", origin=list_expr)
497
+
498
+ val = self.machine_expr_eval(env_id, val_expr)
499
+ if isinstance(val, Expr.Error): return val
500
+
501
+ elems = list(lst.elements)
502
+ if idx is None:
503
+ elems.append(val)
504
+ else:
505
+ if idx < 0 or idx > len(elems):
506
+ return Expr.Error("index out of range", origin=idx_expr)
507
+ elems.insert(idx, val)
508
+ return Expr.ListExpr(elems)
509
+
510
+ # pop: (list.pop 1 ints) -> 2
511
+ elif first.value == "list.pop":
512
+ if len(expr.elements) < 3:
513
+ return Expr.Error("list.pop expects index list", origin=expr)
514
+
515
+ idx_expr, list_expr = expr.elements[1], expr.elements[2]
516
+ idx = self.machine_expr_eval(env_id, idx_expr)
517
+ if isinstance(idx, Expr.Error): return idx
518
+ if not isinstance(idx, Expr.IntExpr):
519
+ return Expr.Error("index must be int", origin=idx_expr)
520
+ idx = idx.value
521
+
522
+ lst = self.machine_expr_eval(env_id, list_expr)
523
+ if isinstance(lst, Expr.Error): return lst
524
+ if not isinstance(lst, Expr.ListExpr):
525
+ return Expr.Error("second arg to list.pop must be a list", origin=list_expr)
526
+
527
+ elems = list(lst.elements)
528
+ if idx < 0 or idx >= len(elems):
529
+ return Expr.Error("index out of range", origin=idx_expr)
530
+ del elems[idx]
531
+ return Expr.ListExpr(elems)
532
+
533
+ # get: (list.get 1 ints) -> 2
534
+ elif first.value == "list.get":
535
+ if len(expr.elements) < 3:
536
+ return Expr.Error("list.get expects index list", origin=expr)
537
+
538
+ idx_expr, list_expr = expr.elements[1], expr.elements[2]
539
+ idx = self.machine_expr_eval(env_id, idx_expr)
540
+ if isinstance(idx, Expr.Error): return idx
541
+ if not isinstance(idx, Expr.IntExpr):
542
+ return Expr.Error("index must be int", origin=idx_expr)
543
+ idx = idx.value
544
+
545
+ lst = self.machine_expr_eval(env_id, list_expr)
546
+ if isinstance(lst, Expr.Error): return lst
547
+ if not isinstance(lst, Expr.ListExpr):
548
+ return Expr.Error("second arg to list.get must be a list", origin=list_expr)
549
+
550
+ if idx < 0 or idx >= len(lst.elements):
551
+ return Expr.Error("index out of range", origin=idx_expr)
552
+ return lst.elements[idx]
553
+
554
+ # set: (list.set 1 3 ints) -> (1 3)
555
+ elif first.value == "list.set":
556
+ if len(expr.elements) < 4:
557
+ return Expr.Error("list.set expects index value list", origin=expr)
558
+ idx_expr, val_expr, list_expr = expr.elements[1], expr.elements[2], expr.elements[3]
559
+ idx = self.machine_expr_eval(env_id, idx_expr)
560
+ if isinstance(idx, Expr.Error): return idx
561
+ if not isinstance(idx, Expr.IntExpr):
562
+ return Expr.Error("index must be int", origin=idx_expr)
563
+ idx = idx.value
564
+
565
+ val = self.machine_expr_eval(env_id, val_expr)
566
+ if isinstance(val, Expr.Error): return val
567
+
568
+ lst = self.machine_expr_eval(env_id, list_expr)
569
+ if isinstance(lst, Expr.Error): return lst
570
+ if not isinstance(lst, Expr.ListExpr):
571
+ return Expr.Error("third arg to list.set must be a list", origin=list_expr)
572
+
573
+ elems = list(lst.elements)
574
+ if idx < 0 or idx >= len(elems):
575
+ return Expr.Error("index out of range", origin=idx_expr)
576
+ elems[idx] = val
577
+ return Expr.ListExpr(elems)
578
+
579
+ ### each: (list.each fn list) -> ()
580
+ elif first.value == "list.each":
581
+ if len(expr.elements) < 3:
582
+ return Expr.Error("list.each expects fn list", origin=expr)
583
+ fn_expr, list_expr = expr.elements[1], expr.elements[2]
584
+ lst = self.machine_expr_eval(env_id, list_expr)
585
+ if isinstance(lst, Expr.Error):
586
+ return lst
587
+ if not isinstance(lst, Expr.ListExpr):
588
+ return Expr.Error("second arg to list.each must be a list", origin=list_expr)
589
+
590
+ for el in lst.elements:
591
+ res = self.machine_expr_eval(env_id, Expr.ListExpr([fn_expr, el]))
592
+ if isinstance(res, Expr.Error):
593
+ return res
594
+ return Expr.ListExpr([])
595
+
596
+ ### fold: (list.fold fn init list) / (list.fold + 0 ints) -> 3
597
+ elif first.value == "list.fold":
598
+ fn_expr, init_expr, list_expr = expr.elements[1], expr.elements[2], expr.elements[3]
599
+ acc = self.machine_expr_eval(env_id, init_expr)
600
+ if isinstance(acc, Expr.Error):
601
+ return acc
602
+
603
+ lst = self.machine_expr_eval(env_id, list_expr)
604
+ if isinstance(lst, Expr.Error):
605
+ return lst
606
+ if not isinstance(lst, Expr.ListExpr):
607
+ return Expr.Error("third arg to list.fold must be a list", origin=list_expr)
608
+
609
+ for el in lst.elements:
610
+ call = Expr.ListExpr([fn_expr, acc, el])
611
+ res = self.machine_expr_eval(env_id, call)
612
+ if isinstance(res, Expr.Error):
613
+ return res
614
+ acc = res
615
+
616
+ return acc
617
+
618
+ ### sort: (list.sort fn list) / (list.sort (fn (a b) (a < b)) ints) -> (2 1)
619
+ elif first.value == "list.sort":
620
+ if len(expr.elements) < 3:
621
+ return Expr.Error("list.sort fn list", origin=expr)
622
+ fn_e, lst_e = expr.elements[1], expr.elements[2]
623
+
624
+ lst = self.machine_expr_eval(env_id, lst_e)
625
+ if isinstance(lst, Expr.Error): return lst
626
+ if not isinstance(lst, Expr.ListExpr):
627
+ return Expr.Error("second arg must be list", origin=lst_e)
628
+
629
+ elems = list(lst.elements)
630
+ for i in range(1, len(elems)):
631
+ j = i
632
+ while j > 0:
633
+ cmp_res = self.machine_expr_eval(
634
+ env_id,
635
+ Expr.ListExpr([fn_e, elems[j-1], elems[j]])
636
+ )
637
+ if isinstance(cmp_res, Expr.Error): return cmp_res
638
+ if not isinstance(cmp_res, Expr.BoolExpr):
639
+ return Expr.Error("comparator must return bool", origin=fn_e)
640
+
641
+ if cmp_res.value:
642
+ elems[j-1], elems[j] = elems[j], elems[j-1]
643
+ j -= 1
644
+ else:
645
+ break
646
+ return Expr.ListExpr(elems)
647
+
648
+ ### len: (list.len list) -> Int / (list.len ints) -> Integer(2)
649
+ elif first.value == "list.len":
650
+ if len(expr.elements) < 2:
651
+ return Expr.Error("list.len list", origin=expr)
652
+ lst_e = expr.elements[1]
653
+ lst = self.machine_expr_eval(env_id, lst_e)
654
+ if isinstance(lst, Expr.Error): return lst
655
+ if not isinstance(lst, Expr.ListExpr):
656
+ return Expr.Error("arg must be list", origin=lst_e)
657
+ return Expr.Integer(len(lst.elements))
658
+
659
+ ## Integer
660
+ ### add
666
661
  elif first.value == "+":
667
662
  args = expr.elements[1:]
668
663
  if not args:
@@ -747,6 +742,19 @@ class Node:
747
742
 
748
743
  return Expr.Boolean(res)
749
744
 
745
+ if isinstance(first, Expr.Function):
746
+ arg_exprs = expr.elements[1:]
747
+ if len(arg_exprs) != len(first.params):
748
+ return Expr.Error(f"arity mismatch: expected {len(first.params)}, got {len(arg_exprs)}", origin=expr)
749
+
750
+ call_env = self.machine_create_environment(parent_id=env_id)
751
+ for name, aexpr in zip(first.params, arg_exprs):
752
+ val = self.machine_expr_eval(env_id, aexpr)
753
+ if isinstance(val, Expr.Error): return val
754
+ self.machine_expr_put(call_env, name, val)
755
+
756
+ return self.machine_expr_eval(env_id=call_env, expr=first.body)
757
+
750
758
  else:
751
759
  evaluated_elements = [self.machine_expr_eval(env_id=env_id, expr=e) for e in expr.elements]
752
760
  return Expr.ListExpr(evaluated_elements)
@@ -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
@@ -1,8 +1,7 @@
1
1
  from pathlib import Path
2
2
  from typing import Optional, Dict, Tuple, Any
3
3
 
4
- def storage_setup(config: dict
5
- ) -> Tuple[Optional[Path], Dict[bytes, Any], int, Dict[bytes, bytes]]:
4
+ def storage_setup(config: dict) -> Tuple[Optional[Path], Dict[bytes, Any], int, Dict[bytes, bytes]]:
6
5
  storage_path_str = config.get('storage_path')
7
6
  if storage_path_str is None:
8
7
  storage_path, memory_storage = None, {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.28
3
+ Version: 0.2.30
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
@@ -27,6 +27,10 @@ src/astreum/models/merkle.py
27
27
  src/astreum/models/message.py
28
28
  src/astreum/models/patricia.py
29
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
30
34
  src/astreum/storage/__init__.py
31
35
  src/astreum/storage/object.py
32
36
  src/astreum/storage/setup.py
File without changes
File without changes
File without changes
File without changes