astreum 0.2.36__tar.gz → 0.2.38__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.
- {astreum-0.2.36/src/astreum.egg-info → astreum-0.2.38}/PKG-INFO +4 -2
- {astreum-0.2.36 → astreum-0.2.38}/README.md +3 -1
- {astreum-0.2.36 → astreum-0.2.38}/pyproject.toml +1 -1
- astreum-0.2.38/src/astreum/__init__.py +9 -0
- astreum-0.2.38/src/astreum/_communication/__init__.py +9 -0
- astreum-0.2.38/src/astreum/_communication/peer.py +11 -0
- astreum-0.2.38/src/astreum/_communication/setup.py +113 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/_lispeum/__init__.py +3 -3
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/_lispeum/environment.py +5 -2
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/_lispeum/expression.py +7 -7
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/_lispeum/high_evaluation.py +8 -8
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/_lispeum/low_evaluation.py +10 -8
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/_lispeum/parser.py +25 -9
- astreum-0.2.38/src/astreum/_node.py +58 -0
- astreum-0.2.38/src/astreum/_storage/__init__.py +5 -0
- astreum-0.2.36/src/astreum/_node.py → astreum-0.2.38/src/astreum/_storage/atom.py +17 -64
- astreum-0.2.38/src/astreum/_validation/__init__.py +12 -0
- astreum-0.2.38/src/astreum/_validation/block.py +296 -0
- astreum-0.2.38/src/astreum/_validation/chain.py +63 -0
- astreum-0.2.38/src/astreum/_validation/fork.py +98 -0
- astreum-0.2.38/src/astreum/_validation/setup.py +221 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/models/block.py +18 -9
- astreum-0.2.38/src/astreum/relay/route.py +25 -0
- astreum-0.2.38/src/astreum/storage/__init__.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38/src/astreum.egg-info}/PKG-INFO +4 -2
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum.egg-info/SOURCES.txt +13 -2
- astreum-0.2.36/src/astreum/__init__.py +0 -1
- astreum-0.2.36/tests/test_node_machine.py +0 -56
- {astreum-0.2.36 → astreum-0.2.38}/LICENSE +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/setup.cfg +0 -0
- {astreum-0.2.36/src/astreum/relay → astreum-0.2.38/src/astreum/_communication}/route.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/_lispeum/meter.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/_lispeum/tokenizer.py +0 -0
- /astreum-0.2.36/src/astreum/crypto/__init__.py → /astreum-0.2.38/src/astreum/_validation/genesis.py +0 -0
- {astreum-0.2.36/src/astreum/lispeum → astreum-0.2.38/src/astreum/crypto}/__init__.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/crypto/ed25519.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/crypto/quadratic_form.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/crypto/wesolowski.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/crypto/x25519.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/format.py +0 -0
- {astreum-0.2.36/src/astreum/models → astreum-0.2.38/src/astreum/lispeum}/__init__.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/lispeum/environment.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/lispeum/expression.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/lispeum/parser.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/lispeum/tokenizer.py +0 -0
- {astreum-0.2.36/src/astreum/relay → astreum-0.2.38/src/astreum/models}/__init__.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/models/account.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/models/accounts.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/models/merkle.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/models/message.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/models/patricia.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/models/transaction.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/node.py +0 -0
- {astreum-0.2.36/src/astreum/storage → astreum-0.2.38/src/astreum/relay}/__init__.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/relay/peer.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/relay/setup.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/storage/object.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum/storage/setup.py +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum.egg-info/dependency_links.txt +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum.egg-info/requires.txt +0 -0
- {astreum-0.2.36 → astreum-0.2.38}/src/astreum.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astreum
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.38
|
|
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
|
|
@@ -79,7 +79,7 @@ The Lispeum virtual machine (VM) is embedded inside `astreum.Node`. You feed it
|
|
|
79
79
|
# Define a named function int.add (stack body) and call it with bytes 1 and 2
|
|
80
80
|
|
|
81
81
|
import uuid
|
|
82
|
-
from astreum
|
|
82
|
+
from astreum import Node, Env, Expr
|
|
83
83
|
|
|
84
84
|
# 1) Spin‑up a stand‑alone VM
|
|
85
85
|
node = Node()
|
|
@@ -139,6 +139,8 @@ except ParseError as e:
|
|
|
139
139
|
## Testing
|
|
140
140
|
|
|
141
141
|
```bash
|
|
142
|
+
python3 -m venv venv
|
|
142
143
|
source venv/bin/activate
|
|
144
|
+
pip install -e .
|
|
143
145
|
python3 -m unittest discover -s tests
|
|
144
146
|
```
|
|
@@ -61,7 +61,7 @@ The Lispeum virtual machine (VM) is embedded inside `astreum.Node`. You feed it
|
|
|
61
61
|
# Define a named function int.add (stack body) and call it with bytes 1 and 2
|
|
62
62
|
|
|
63
63
|
import uuid
|
|
64
|
-
from astreum
|
|
64
|
+
from astreum import Node, Env, Expr
|
|
65
65
|
|
|
66
66
|
# 1) Spin‑up a stand‑alone VM
|
|
67
67
|
node = Node()
|
|
@@ -121,6 +121,8 @@ except ParseError as e:
|
|
|
121
121
|
## Testing
|
|
122
122
|
|
|
123
123
|
```bash
|
|
124
|
+
python3 -m venv venv
|
|
124
125
|
source venv/bin/activate
|
|
126
|
+
pip install -e .
|
|
125
127
|
python3 -m unittest discover -s tests
|
|
126
128
|
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Lightweight package initializer to avoid circular imports during tests.
|
|
2
|
+
|
|
3
|
+
Exports are intentionally minimal; import submodules directly as needed:
|
|
4
|
+
- Node, Expr, Env, tokenize, parse -> from astreum._node or astreum.lispeum
|
|
5
|
+
- Validation types -> from astreum._validation
|
|
6
|
+
- Storage types -> from astreum._storage
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
latest_block: bytes
|
|
8
|
+
|
|
9
|
+
def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
|
|
10
|
+
self.shared_key = my_sec_key.exchange(peer_pub_key)
|
|
11
|
+
self.timestamp = datetime.now(timezone.utc)
|
|
@@ -0,0 +1,113 @@
|
|
|
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 import serialization
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import (
|
|
7
|
+
X25519PrivateKey,
|
|
8
|
+
X25519PublicKey,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .. import Node
|
|
14
|
+
|
|
15
|
+
from .import Route
|
|
16
|
+
|
|
17
|
+
def load_x25519(hex_key: Optional[str]) -> X25519PrivateKey:
|
|
18
|
+
"""DH key for relaying (always X25519)."""
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
def load_ed25519(hex_key: Optional[str]) -> Optional[ed25519.Ed25519PrivateKey]:
|
|
22
|
+
"""Signing key for validation (Ed25519), or None if absent."""
|
|
23
|
+
return ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key)) \
|
|
24
|
+
if hex_key else None
|
|
25
|
+
|
|
26
|
+
def make_routes(
|
|
27
|
+
relay_pk: X25519PublicKey,
|
|
28
|
+
val_sk: Optional[ed25519.Ed25519PrivateKey]
|
|
29
|
+
) -> Tuple[Route, Optional[Route]]:
|
|
30
|
+
"""Peer route (DH pubkey) + optional validation route (ed pubkey)."""
|
|
31
|
+
peer_rt = Route(relay_pk)
|
|
32
|
+
val_rt = Route(val_sk.public_key()) if val_sk else None
|
|
33
|
+
return peer_rt, val_rt
|
|
34
|
+
|
|
35
|
+
def setup_udp(
|
|
36
|
+
bind_port: int,
|
|
37
|
+
use_ipv6: bool
|
|
38
|
+
) -> Tuple[socket.socket, int, Queue, threading.Thread, threading.Thread]:
|
|
39
|
+
fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
|
|
40
|
+
sock = socket.socket(fam, socket.SOCK_DGRAM)
|
|
41
|
+
if use_ipv6:
|
|
42
|
+
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
|
43
|
+
sock.bind(("::" if use_ipv6 else "0.0.0.0", bind_port or 0))
|
|
44
|
+
port = sock.getsockname()[1]
|
|
45
|
+
|
|
46
|
+
q = Queue()
|
|
47
|
+
pop = threading.Thread(target=lambda: None, daemon=True)
|
|
48
|
+
proc = threading.Thread(target=lambda: None, daemon=True)
|
|
49
|
+
pop.start(); proc.start()
|
|
50
|
+
return sock, port, q, pop, proc
|
|
51
|
+
|
|
52
|
+
def setup_outgoing(
|
|
53
|
+
use_ipv6: bool
|
|
54
|
+
) -> Tuple[socket.socket, Queue, threading.Thread]:
|
|
55
|
+
fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
|
|
56
|
+
sock = socket.socket(fam, socket.SOCK_DGRAM)
|
|
57
|
+
q = Queue()
|
|
58
|
+
thr = threading.Thread(target=lambda: None, daemon=True)
|
|
59
|
+
thr.start()
|
|
60
|
+
return sock, q, thr
|
|
61
|
+
|
|
62
|
+
def make_maps():
|
|
63
|
+
"""Empty lookup maps: peers and addresses."""
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
def communication_setup(node: "Node", config: dict):
|
|
67
|
+
node.use_ipv6 = config.get('use_ipv6', False)
|
|
68
|
+
|
|
69
|
+
# key loading
|
|
70
|
+
node.relay_secret_key = load_x25519(config.get('relay_secret_key'))
|
|
71
|
+
node.validation_secret_key = load_ed25519(config.get('validation_secret_key'))
|
|
72
|
+
|
|
73
|
+
# derive pubs + routes
|
|
74
|
+
node.relay_public_key = node.relay_secret_key.public_key()
|
|
75
|
+
node.validation_public_key = (
|
|
76
|
+
node.validation_secret_key.public_key().public_bytes(
|
|
77
|
+
encoding=serialization.Encoding.Raw,
|
|
78
|
+
format=serialization.PublicFormat.Raw,
|
|
79
|
+
)
|
|
80
|
+
if node.validation_secret_key
|
|
81
|
+
else None
|
|
82
|
+
)
|
|
83
|
+
node.peer_route, node.validation_route = make_routes(
|
|
84
|
+
node.relay_public_key,
|
|
85
|
+
node.validation_secret_key
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# sockets + queues + threads
|
|
89
|
+
(node.incoming_socket,
|
|
90
|
+
node.incoming_port,
|
|
91
|
+
node.incoming_queue,
|
|
92
|
+
node.incoming_populate_thread,
|
|
93
|
+
node.incoming_process_thread
|
|
94
|
+
) = setup_udp(config.get('incoming_port', 7373), node.use_ipv6)
|
|
95
|
+
|
|
96
|
+
(node.outgoing_socket,
|
|
97
|
+
node.outgoing_queue,
|
|
98
|
+
node.outgoing_thread
|
|
99
|
+
) = setup_outgoing(node.use_ipv6)
|
|
100
|
+
|
|
101
|
+
# other workers & maps
|
|
102
|
+
node.object_request_queue = Queue()
|
|
103
|
+
node.peer_manager_thread = threading.Thread(
|
|
104
|
+
target=node._relay_peer_manager,
|
|
105
|
+
daemon=True
|
|
106
|
+
)
|
|
107
|
+
node.peer_manager_thread.start()
|
|
108
|
+
|
|
109
|
+
node.peers, node.addresses = {}, {} # peers: Dict[X25519PublicKey,Peer], addresses: Dict[(str,int),X25519PublicKey]
|
|
110
|
+
|
|
111
|
+
# bootstrap pings
|
|
112
|
+
for addr in config.get('bootstrap', []):
|
|
113
|
+
node._send_ping(addr)
|
|
@@ -2,7 +2,7 @@ from .expression import Expr
|
|
|
2
2
|
from .environment import Env
|
|
3
3
|
from .low_evaluation import low_eval
|
|
4
4
|
from .meter import Meter
|
|
5
|
-
from .parser import parse
|
|
5
|
+
from .parser import parse, ParseError
|
|
6
6
|
from .tokenizer import tokenize
|
|
7
7
|
|
|
8
8
|
__all__ = [
|
|
@@ -11,6 +11,6 @@ __all__ = [
|
|
|
11
11
|
"low_eval",
|
|
12
12
|
"Meter",
|
|
13
13
|
"parse",
|
|
14
|
-
"tokenize"
|
|
14
|
+
"tokenize",
|
|
15
|
+
"ParseError",
|
|
15
16
|
]
|
|
16
|
-
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from ast import Expr
|
|
2
2
|
from typing import Dict, Optional
|
|
3
|
+
import uuid
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class Env:
|
|
6
7
|
def __init__(
|
|
7
8
|
self,
|
|
8
|
-
data: Optional[Dict[str, Expr]] = None
|
|
9
|
+
data: Optional[Dict[str, Expr]] = None,
|
|
10
|
+
parent_id: Optional[uuid.UUID] = None,
|
|
9
11
|
):
|
|
10
|
-
self.data: Dict[bytes, Expr] = {} if data is None else data
|
|
12
|
+
self.data: Dict[bytes, Expr] = {} if data is None else data
|
|
13
|
+
self.parent_id = parent_id
|
|
@@ -10,21 +10,21 @@ class Expr:
|
|
|
10
10
|
if not self.elements:
|
|
11
11
|
return "()"
|
|
12
12
|
inner = " ".join(str(e) for e in self.elements)
|
|
13
|
-
return f"({inner})"
|
|
13
|
+
return f"({inner} list)"
|
|
14
14
|
|
|
15
15
|
class Symbol:
|
|
16
16
|
def __init__(self, value: str):
|
|
17
17
|
self.value = value
|
|
18
18
|
|
|
19
19
|
def __repr__(self):
|
|
20
|
-
return self.value
|
|
20
|
+
return f"({self.value} symbol)"
|
|
21
21
|
|
|
22
|
-
class
|
|
23
|
-
def __init__(self, value:
|
|
22
|
+
class Bytes:
|
|
23
|
+
def __init__(self, value: bytes):
|
|
24
24
|
self.value = value
|
|
25
25
|
|
|
26
26
|
def __repr__(self):
|
|
27
|
-
return self.value
|
|
27
|
+
return f"({self.value} bytes)"
|
|
28
28
|
|
|
29
29
|
class Error:
|
|
30
30
|
def __init__(self, topic: str, origin: Optional['Expr'] = None):
|
|
@@ -33,5 +33,5 @@ class Expr:
|
|
|
33
33
|
|
|
34
34
|
def __repr__(self):
|
|
35
35
|
if self.origin is None:
|
|
36
|
-
return f'({self.topic}
|
|
37
|
-
return f'({self.origin} {self.topic}
|
|
36
|
+
return f'({self.topic} error)'
|
|
37
|
+
return f'({self.origin} {self.topic} error)'
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
from typing import List, Union
|
|
2
|
-
import uuid
|
|
3
|
-
|
|
4
|
-
from
|
|
1
|
+
from typing import List, Union
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from .environment import Env
|
|
5
|
+
from .expression import Expr
|
|
6
|
+
from .meter import Meter
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
def high_eval(self, env_id: uuid.UUID, expr: Expr, meter = None) -> Expr:
|
|
@@ -125,9 +127,7 @@ def high_eval(self, env_id: uuid.UUID, expr: Expr, meter = None) -> Expr:
|
|
|
125
127
|
|
|
126
128
|
# Execute low-level code built from sk-body using the caller's meter
|
|
127
129
|
res = self.low_eval(code, meter=meter)
|
|
128
|
-
|
|
129
|
-
return res
|
|
130
|
-
return Expr.ListExpr([Expr.Byte(b) for b in res])
|
|
130
|
+
return res
|
|
131
131
|
|
|
132
132
|
# ---------- (... (body params fn)) HIGH-LEVEL CALL ----------
|
|
133
133
|
if isinstance(tail, Expr.ListExpr):
|
|
@@ -174,4 +174,4 @@ def high_eval(self, env_id: uuid.UUID, expr: Expr, meter = None) -> Expr:
|
|
|
174
174
|
|
|
175
175
|
# ---------- default: resolve each element and return list ----------
|
|
176
176
|
resolved: List[Expr] = [self.high_eval(env_id, e, meter=meter) for e in expr.elements]
|
|
177
|
-
return Expr.ListExpr(resolved)
|
|
177
|
+
return Expr.ListExpr(resolved)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
from typing import Dict, List, Union
|
|
2
|
-
from
|
|
1
|
+
from typing import Dict, List, Union
|
|
2
|
+
from .expression import Expr
|
|
3
|
+
from .meter import Meter
|
|
3
4
|
|
|
4
5
|
def tc_to_int(b: bytes) -> int:
|
|
5
6
|
"""bytes -> int using two's complement (width = len(b)*8)."""
|
|
@@ -34,18 +35,19 @@ def nand_bytes(a: bytes, b: bytes) -> bytes:
|
|
|
34
35
|
resu = (~(au & bu)) & mask
|
|
35
36
|
return resu.to_bytes(w, "big", signed=False)
|
|
36
37
|
|
|
37
|
-
def low_eval(self, code: List[bytes], meter: Meter) ->
|
|
38
|
+
def low_eval(self, code: List[bytes], meter: Meter) -> Expr:
|
|
38
39
|
|
|
39
40
|
heap: Dict[bytes, bytes] = {}
|
|
40
41
|
|
|
41
42
|
stack: List[bytes] = []
|
|
42
43
|
pc = 0
|
|
43
44
|
|
|
44
|
-
while True:
|
|
45
|
-
if pc >= len(code):
|
|
46
|
-
if len(stack) != 1:
|
|
47
|
-
return Expr.Error("bad stack")
|
|
48
|
-
|
|
45
|
+
while True:
|
|
46
|
+
if pc >= len(code):
|
|
47
|
+
if len(stack) != 1:
|
|
48
|
+
return Expr.Error("bad stack")
|
|
49
|
+
# wrap successful result as an Expr.Bytes
|
|
50
|
+
return Expr.Bytes(stack.pop())
|
|
49
51
|
|
|
50
52
|
tok = code[pc]
|
|
51
53
|
pc += 1
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from typing import List, Tuple
|
|
2
|
-
from src.astreum._lispeum import Expr
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
from src.astreum._lispeum import Expr
|
|
3
3
|
|
|
4
4
|
class ParseError(Exception):
|
|
5
5
|
pass
|
|
@@ -27,14 +27,30 @@ def _parse_one(tokens: List[str], pos: int = 0) -> Tuple[Expr, int]:
|
|
|
27
27
|
if tok == ')':
|
|
28
28
|
raise ParseError("unexpected ')'")
|
|
29
29
|
|
|
30
|
-
# try integer →
|
|
31
|
-
try:
|
|
32
|
-
n = int(tok)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
# try integer → Bytes (variable-length two's complement)
|
|
31
|
+
try:
|
|
32
|
+
n = int(tok)
|
|
33
|
+
# encode as minimal-width signed two's complement, big-endian
|
|
34
|
+
def int_to_min_tc(v: int) -> bytes:
|
|
35
|
+
"""Return the minimal-width signed two's complement big-endian
|
|
36
|
+
byte encoding of integer v. Width expands just enough so that
|
|
37
|
+
decoding with signed=True yields the same value and sign.
|
|
38
|
+
Example: 0 -> b"\x00", 127 -> b"\x7f", 128 -> b"\x00\x80".
|
|
39
|
+
"""
|
|
40
|
+
if v == 0:
|
|
41
|
+
return b"\x00"
|
|
42
|
+
w = 1
|
|
43
|
+
while True:
|
|
44
|
+
try:
|
|
45
|
+
return v.to_bytes(w, "big", signed=True)
|
|
46
|
+
except OverflowError:
|
|
47
|
+
w += 1
|
|
48
|
+
|
|
49
|
+
return Expr.Bytes(int_to_min_tc(n)), pos + 1
|
|
50
|
+
except ValueError:
|
|
51
|
+
return Expr.Symbol(tok), pos + 1
|
|
36
52
|
|
|
37
53
|
def parse(tokens: List[str]) -> Tuple[Expr, List[str]]:
|
|
38
54
|
"""Parse tokens into an Expr and return (expr, remaining_tokens)."""
|
|
39
55
|
expr, next_pos = _parse_one(tokens, 0)
|
|
40
|
-
return expr, tokens[next_pos:]
|
|
56
|
+
return expr, tokens[next_pos:]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Dict, Optional
|
|
3
|
+
import uuid
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
from src.astreum._storage.atom import Atom
|
|
7
|
+
from src.astreum._lispeum import Env, Expr, low_eval, parse, tokenize, ParseError
|
|
8
|
+
|
|
9
|
+
def bytes_touched(*vals: bytes) -> int:
|
|
10
|
+
"""For metering: how many bytes were manipulated (max of operands)."""
|
|
11
|
+
return max((len(v) for v in vals), default=1)
|
|
12
|
+
|
|
13
|
+
class Node:
|
|
14
|
+
def __init__(self, config: dict):
|
|
15
|
+
# Storage Setup
|
|
16
|
+
self.in_memory_storage: Dict[bytes, Atom] = {}
|
|
17
|
+
self.in_memory_storage_lock = threading.RLock()
|
|
18
|
+
# Lispeum Setup
|
|
19
|
+
self.environments: Dict[uuid.UUID, Env] = {}
|
|
20
|
+
self.machine_environments_lock = threading.RLock()
|
|
21
|
+
self.low_eval = low_eval
|
|
22
|
+
# Communication and Validation Setup (import lazily to avoid heavy deps during parsing tests)
|
|
23
|
+
try:
|
|
24
|
+
from astreum._communication import communication_setup # type: ignore
|
|
25
|
+
communication_setup(node=self, config=config)
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
try:
|
|
29
|
+
from astreum._validation import validation_setup # type: ignore
|
|
30
|
+
validation_setup(node=self)
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
# ---- Env helpers ----
|
|
35
|
+
def env_get(self, env_id: uuid.UUID, key: bytes) -> Optional[Expr]:
|
|
36
|
+
cur = self.environments.get(env_id)
|
|
37
|
+
while cur is not None:
|
|
38
|
+
if key in cur.data:
|
|
39
|
+
return cur.data[key]
|
|
40
|
+
cur = self.environments.get(cur.parent_id) if cur.parent_id else None
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
def env_set(self, env_id: uuid.UUID, key: bytes, value: Expr) -> bool:
|
|
44
|
+
with self.machine_environments_lock:
|
|
45
|
+
env = self.environments.get(env_id)
|
|
46
|
+
if env is None:
|
|
47
|
+
return False
|
|
48
|
+
env.data[key] = value
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
# Storage
|
|
52
|
+
def _local_get(self, key: bytes) -> Optional[Atom]:
|
|
53
|
+
with self.in_memory_storage_lock:
|
|
54
|
+
return self.in_memory_storage.get(key)
|
|
55
|
+
|
|
56
|
+
def _local_set(self, key: bytes, value: Atom) -> None:
|
|
57
|
+
with self.in_memory_storage_lock:
|
|
58
|
+
self.in_memory_storage[key] = value
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
from typing import Dict, List, Optional, Tuple, Union
|
|
3
|
-
import uuid
|
|
4
|
-
import threading
|
|
5
1
|
|
|
6
|
-
from src.astreum._lispeum import Env, Expr, Meter, low_eval
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
"""For metering: how many bytes were manipulated (max of operands)."""
|
|
10
|
-
return max((len(v) for v in vals), default=1)
|
|
3
|
+
from typing import List, Optional, Tuple
|
|
11
4
|
|
|
5
|
+
from .._lispeum.expression import Expr
|
|
12
6
|
from blake3 import blake3
|
|
13
7
|
|
|
14
8
|
ZERO32 = b"\x00"*32
|
|
@@ -48,8 +42,6 @@ class Atom:
|
|
|
48
42
|
return object_id == blake3(data_hash + next_hash + u64_le(size)).digest()
|
|
49
43
|
|
|
50
44
|
def to_bytes(self) -> bytes:
|
|
51
|
-
if len(self.next) != len(ZERO32):
|
|
52
|
-
raise ValueError("next must be 32 bytes")
|
|
53
45
|
return self.next + self.data
|
|
54
46
|
|
|
55
47
|
@staticmethod
|
|
@@ -60,29 +52,23 @@ class Atom:
|
|
|
60
52
|
data = buf[len(ZERO32):]
|
|
61
53
|
return Atom(data=data, next=next_hash, size=len(data))
|
|
62
54
|
|
|
63
|
-
def u32_le(n: int) -> bytes:
|
|
64
|
-
return int(n).to_bytes(4, "little", signed=False)
|
|
65
|
-
|
|
66
55
|
def expr_to_atoms(e: Expr) -> Tuple[bytes, List[Atom]]:
|
|
67
|
-
def
|
|
68
|
-
val =
|
|
69
|
-
val_atom = Atom.from_data(
|
|
56
|
+
def symbol(value: str) -> Tuple[bytes, List[Atom]]:
|
|
57
|
+
val = value.encode("utf-8")
|
|
58
|
+
val_atom = Atom.from_data(data=val)
|
|
70
59
|
typ_atom = Atom.from_data(b"symbol", val_atom.object_id())
|
|
71
60
|
return typ_atom.object_id(), [val_atom, typ_atom]
|
|
72
61
|
|
|
73
|
-
def
|
|
74
|
-
val_atom = Atom.from_data(
|
|
62
|
+
def bytes(data: bytes) -> Tuple[bytes, List[Atom]]:
|
|
63
|
+
val_atom = Atom.from_data(data=data)
|
|
75
64
|
typ_atom = Atom.from_data(b"byte", val_atom.object_id())
|
|
76
65
|
return typ_atom.object_id(), [val_atom, typ_atom]
|
|
77
66
|
|
|
78
67
|
def err(topic: str, origin: Optional[Expr]) -> Tuple[bytes, List[Atom]]:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
val_atom = Atom.from_data(u32_le(len(t)) + t + origin_hash)
|
|
84
|
-
typ_atom = Atom.from_data(b"error", val_atom.object_id())
|
|
85
|
-
return typ_atom.object_id(), acc + [val_atom, typ_atom]
|
|
68
|
+
topic_bytes = topic.encode("utf-8")
|
|
69
|
+
topic_atom = Atom.from_data(data=topic_bytes)
|
|
70
|
+
typ_atom = Atom.from_data(data=b"error", next_hash=topic_atom.object_id())
|
|
71
|
+
return typ_atom.object_id(), [topic_atom, typ_atom]
|
|
86
72
|
|
|
87
73
|
def lst(items: List[Expr]) -> Tuple[bytes, List[Atom]]:
|
|
88
74
|
acc: List[Atom] = []
|
|
@@ -99,49 +85,16 @@ def expr_to_atoms(e: Expr) -> Tuple[bytes, List[Atom]]:
|
|
|
99
85
|
elem_atoms.append(a)
|
|
100
86
|
elem_atoms.reverse()
|
|
101
87
|
head = next_hash
|
|
102
|
-
val_atom = Atom.from_data(u64_le(len(items)), head)
|
|
103
|
-
typ_atom = Atom.from_data(b"list", val_atom.object_id())
|
|
88
|
+
val_atom = Atom.from_data(data=u64_le(len(items)), next_hash=head)
|
|
89
|
+
typ_atom = Atom.from_data(data=b"list", next_hash=val_atom.object_id())
|
|
104
90
|
return typ_atom.object_id(), acc + elem_atoms + [val_atom, typ_atom]
|
|
105
91
|
|
|
106
92
|
if isinstance(e, Expr.Symbol):
|
|
107
|
-
return
|
|
108
|
-
if isinstance(e, Expr.
|
|
109
|
-
return
|
|
93
|
+
return symbol(e.value)
|
|
94
|
+
if isinstance(e, Expr.Bytes):
|
|
95
|
+
return bytes(e.value)
|
|
110
96
|
if isinstance(e, Expr.Error):
|
|
111
97
|
return err(e.topic, e.origin)
|
|
112
98
|
if isinstance(e, Expr.ListExpr):
|
|
113
99
|
return lst(e.elements)
|
|
114
|
-
raise TypeError("unknown Expr variant")
|
|
115
|
-
|
|
116
|
-
class Node:
|
|
117
|
-
def __init__(self):
|
|
118
|
-
# Storage Setup
|
|
119
|
-
self.in_memory_storage: Dict[bytes, bytes] = {}
|
|
120
|
-
# Lispeum Setup
|
|
121
|
-
self.environments: Dict[uuid.UUID, Env] = {}
|
|
122
|
-
self.machine_environments_lock = threading.RLock()
|
|
123
|
-
self.low_eval = low_eval
|
|
124
|
-
|
|
125
|
-
# ---- Env helpers ----
|
|
126
|
-
def env_get(self, env_id: uuid.UUID, key: bytes) -> Optional[Expr]:
|
|
127
|
-
cur = self.environments.get(env_id)
|
|
128
|
-
while cur is not None:
|
|
129
|
-
if key in cur.data:
|
|
130
|
-
return cur.data[key]
|
|
131
|
-
cur = self.environments.get(cur.parent_id) if cur.parent_id else None
|
|
132
|
-
return None
|
|
133
|
-
|
|
134
|
-
def env_set(self, env_id: uuid.UUID, key: bytes, value: Expr) -> bool:
|
|
135
|
-
with self.machine_environments_lock:
|
|
136
|
-
env = self.environments.get(env_id)
|
|
137
|
-
if env is None:
|
|
138
|
-
return False
|
|
139
|
-
env.data[key] = value
|
|
140
|
-
return True
|
|
141
|
-
|
|
142
|
-
# ---- Storage (persistent) ----
|
|
143
|
-
def _local_get(self, key: bytes) -> Optional[bytes]:
|
|
144
|
-
return self.in_memory_storage.get(key)
|
|
145
|
-
|
|
146
|
-
def _local_set(self, key: bytes, value: bytes) -> None:
|
|
147
|
-
self.in_memory_storage[key] = value
|
|
100
|
+
raise TypeError("unknown Expr variant")
|