astreum 0.3.1__py3-none-any.whl → 0.3.9__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.
- astreum/communication/handlers/handshake.py +89 -83
- astreum/communication/handlers/object_request.py +176 -0
- astreum/communication/handlers/object_response.py +115 -0
- astreum/communication/handlers/ping.py +6 -20
- astreum/communication/handlers/route_request.py +76 -0
- astreum/communication/handlers/route_response.py +53 -0
- astreum/communication/models/message.py +81 -58
- astreum/communication/models/peer.py +42 -14
- astreum/communication/models/route.py +2 -7
- astreum/communication/processors/__init__.py +0 -0
- astreum/communication/processors/incoming.py +98 -0
- astreum/communication/processors/outgoing.py +20 -0
- astreum/communication/setup.py +36 -75
- astreum/communication/start.py +9 -10
- astreum/communication/util.py +7 -0
- astreum/consensus/start.py +9 -10
- astreum/consensus/workers/discovery.py +6 -7
- astreum/consensus/workers/validation.py +307 -291
- astreum/consensus/workers/verify.py +8 -10
- astreum/crypto/chacha20poly1305.py +74 -0
- astreum/machine/evaluations/high_evaluation.py +237 -237
- astreum/machine/evaluations/low_evaluation.py +18 -18
- astreum/node.py +25 -6
- astreum/storage/actions/get.py +183 -69
- astreum/storage/actions/set.py +66 -20
- astreum/storage/requests.py +28 -0
- astreum/storage/setup.py +3 -25
- astreum/utils/config.py +48 -0
- {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/METADATA +3 -3
- {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/RECORD +33 -24
- astreum/communication/handlers/storage_request.py +0 -81
- {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
- {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/top_level.txt +0 -0
|
@@ -11,8 +11,7 @@ def _process_peers_latest_block(
|
|
|
11
11
|
node: Any, latest_block_hash: bytes, peer_ids: Set[Any]
|
|
12
12
|
) -> None:
|
|
13
13
|
"""Assign peers to the fork that matches their reported head."""
|
|
14
|
-
|
|
15
|
-
node_logger.debug(
|
|
14
|
+
node.logger.debug(
|
|
16
15
|
"Processing %d peers reporting block %s",
|
|
17
16
|
len(peer_ids),
|
|
18
17
|
latest_block_hash.hex() if isinstance(latest_block_hash, (bytes, bytearray)) else latest_block_hash,
|
|
@@ -28,7 +27,7 @@ def _process_peers_latest_block(
|
|
|
28
27
|
if new_fork.validated_upto and new_fork.validated_upto in node.forks:
|
|
29
28
|
ref = node.forks[new_fork.validated_upto]
|
|
30
29
|
if getattr(ref, "malicious_block_hash", None):
|
|
31
|
-
|
|
30
|
+
node.logger.warning(
|
|
32
31
|
"Skipping fork from block %s referencing malicious fork %s",
|
|
33
32
|
latest_block_hash.hex() if isinstance(latest_block_hash, (bytes, bytearray)) else latest_block_hash,
|
|
34
33
|
new_fork.validated_upto.hex() if isinstance(new_fork.validated_upto, (bytes, bytearray)) else new_fork.validated_upto,
|
|
@@ -45,7 +44,7 @@ def _process_peers_latest_block(
|
|
|
45
44
|
fork.remove_peer(peer_id)
|
|
46
45
|
|
|
47
46
|
node.forks[latest_block_hash] = new_fork
|
|
48
|
-
|
|
47
|
+
node.logger.debug(
|
|
49
48
|
"Fork %s now has %d peers (total forks %d)",
|
|
50
49
|
latest_block_hash.hex() if isinstance(latest_block_hash, (bytes, bytearray)) else latest_block_hash,
|
|
51
50
|
len(new_fork.peers),
|
|
@@ -57,8 +56,7 @@ def make_verify_worker(node: Any):
|
|
|
57
56
|
"""Build the verify worker bound to the given node."""
|
|
58
57
|
|
|
59
58
|
def _verify_worker() -> None:
|
|
60
|
-
|
|
61
|
-
node_logger.info("Verify worker started")
|
|
59
|
+
node.logger.info("Verify worker started")
|
|
62
60
|
stop = node._validation_stop_event
|
|
63
61
|
while not stop.is_set():
|
|
64
62
|
batch: list[tuple[bytes, Set[Any]]] = []
|
|
@@ -70,14 +68,14 @@ def make_verify_worker(node: Any):
|
|
|
70
68
|
pass
|
|
71
69
|
|
|
72
70
|
if not batch:
|
|
73
|
-
|
|
71
|
+
node.logger.debug("Verify queue empty; sleeping")
|
|
74
72
|
time.sleep(0.1)
|
|
75
73
|
continue
|
|
76
74
|
|
|
77
75
|
for latest_b, peers in batch:
|
|
78
76
|
try:
|
|
79
77
|
_process_peers_latest_block(node, latest_b, peers)
|
|
80
|
-
|
|
78
|
+
node.logger.debug(
|
|
81
79
|
"Updated forks from block %s for %d peers",
|
|
82
80
|
latest_b.hex() if isinstance(latest_b, (bytes, bytearray)) else latest_b,
|
|
83
81
|
len(peers),
|
|
@@ -86,7 +84,7 @@ def make_verify_worker(node: Any):
|
|
|
86
84
|
latest_hex = (
|
|
87
85
|
latest_b.hex() if isinstance(latest_b, (bytes, bytearray)) else latest_b
|
|
88
86
|
)
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
node.logger.exception("Failed processing verification batch for %s", latest_hex)
|
|
88
|
+
node.logger.info("Verify worker stopped")
|
|
91
89
|
|
|
92
90
|
return _verify_worker
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
|
3
|
+
|
|
4
|
+
def encrypt(key: bytes, nonce: bytes, plaintext: bytes, aad: bytes = b"") -> bytes:
|
|
5
|
+
"""
|
|
6
|
+
Encrypt data using the ChaCha20-Poly1305 AEAD construction.
|
|
7
|
+
|
|
8
|
+
This function provides both confidentiality and integrity. The returned
|
|
9
|
+
value contains the ciphertext with the Poly1305 authentication tag
|
|
10
|
+
appended. The same key, nonce, and associated authenticated data (AAD)
|
|
11
|
+
must be provided during decryption.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
key : bytes
|
|
16
|
+
A 32-byte (256-bit) secret key.
|
|
17
|
+
nonce : bytes
|
|
18
|
+
A 12-byte nonce. Must be unique per key; nonce reuse with the same key
|
|
19
|
+
breaks security.
|
|
20
|
+
plaintext : bytes
|
|
21
|
+
The data to be encrypted.
|
|
22
|
+
aad : bytes, optional
|
|
23
|
+
Associated authenticated data that is not encrypted but is included
|
|
24
|
+
in authentication (e.g. headers or metadata). Defaults to empty.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
bytes
|
|
29
|
+
The encrypted output consisting of ciphertext followed by the
|
|
30
|
+
authentication tag.
|
|
31
|
+
|
|
32
|
+
Raises
|
|
33
|
+
------
|
|
34
|
+
ValueError
|
|
35
|
+
If the key or nonce length is invalid.
|
|
36
|
+
"""
|
|
37
|
+
aead = ChaCha20Poly1305(key)
|
|
38
|
+
return aead.encrypt(nonce, plaintext, aad)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def decrypt(key: bytes, nonce: bytes, ciphertext: bytes, aad: bytes = b"") -> bytes:
|
|
42
|
+
"""
|
|
43
|
+
Decrypt data encrypted with ChaCha20-Poly1305 and verify its authenticity.
|
|
44
|
+
|
|
45
|
+
This function verifies the Poly1305 authentication tag before returning
|
|
46
|
+
the plaintext. If the ciphertext, nonce, key, or associated authenticated
|
|
47
|
+
data (AAD) has been altered, decryption fails.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
key : bytes
|
|
52
|
+
The same 32-byte (256-bit) secret key used for encryption.
|
|
53
|
+
nonce : bytes
|
|
54
|
+
The same 12-byte nonce used for encryption.
|
|
55
|
+
ciphertext : bytes
|
|
56
|
+
The encrypted data including the appended authentication tag.
|
|
57
|
+
aad : bytes, optional
|
|
58
|
+
The associated authenticated data used during encryption. Must match
|
|
59
|
+
exactly. Defaults to empty.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
bytes
|
|
64
|
+
The original decrypted plaintext.
|
|
65
|
+
|
|
66
|
+
Raises
|
|
67
|
+
------
|
|
68
|
+
cryptography.exceptions.InvalidTag
|
|
69
|
+
If authentication fails due to tampering or incorrect inputs.
|
|
70
|
+
ValueError
|
|
71
|
+
If the key or nonce length is invalid.
|
|
72
|
+
"""
|
|
73
|
+
aead = ChaCha20Poly1305(key)
|
|
74
|
+
return aead.decrypt(nonce, ciphertext, aad)
|
|
@@ -1,237 +1,237 @@
|
|
|
1
|
-
from typing import List, Optional, Union
|
|
2
|
-
import uuid
|
|
3
|
-
|
|
4
|
-
from ..models.environment import Env
|
|
5
|
-
from ..models.expression import Expr, error_expr, ERROR_SYMBOL
|
|
6
|
-
from ..models.meter import Meter
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _is_error(expr: Expr) -> bool:
|
|
10
|
-
return (
|
|
11
|
-
isinstance(expr, Expr.ListExpr)
|
|
12
|
-
and bool(expr.elements)
|
|
13
|
-
and isinstance(expr.elements[0], Expr.Symbol)
|
|
14
|
-
and expr.elements[0].value == ERROR_SYMBOL
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _hex_symbol_to_bytes(value: Optional[str]) -> Optional[bytes]:
|
|
19
|
-
if not value:
|
|
20
|
-
return None
|
|
21
|
-
data = value.strip()
|
|
22
|
-
if data.startswith(("0x", "0X")):
|
|
23
|
-
data = data[2:]
|
|
24
|
-
if len(data) % 2:
|
|
25
|
-
data = "0" + data
|
|
26
|
-
try:
|
|
27
|
-
return bytes.fromhex(data)
|
|
28
|
-
except ValueError:
|
|
29
|
-
return None
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _expr_to_bytes(expr: Expr) -> Optional[bytes]:
|
|
33
|
-
if isinstance(expr, Expr.Bytes):
|
|
34
|
-
return expr.value
|
|
35
|
-
if isinstance(expr, Expr.Symbol):
|
|
36
|
-
return _hex_symbol_to_bytes(expr.value)
|
|
37
|
-
return None
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def high_eval(self, expr: Expr, env_id: Optional[uuid.UUID] = None, meter = None) -> Expr:
|
|
41
|
-
"""Evaluate high-level expressions with scoped environments and metering."""
|
|
42
|
-
if meter is None:
|
|
43
|
-
meter = Meter()
|
|
44
|
-
|
|
45
|
-
call_env_id = uuid.uuid4()
|
|
46
|
-
self.environments[call_env_id] = Env(parent_id=env_id)
|
|
47
|
-
env_id = call_env_id
|
|
48
|
-
|
|
49
|
-
try:
|
|
50
|
-
# ---------- atoms ----------
|
|
51
|
-
if _is_error(expr):
|
|
52
|
-
return expr
|
|
53
|
-
|
|
54
|
-
if isinstance(expr, Expr.Symbol):
|
|
55
|
-
bound = self.env_get(env_id, expr.value)
|
|
56
|
-
if bound is None:
|
|
57
|
-
return error_expr("eval", f"unbound symbol '{expr.value}'")
|
|
58
|
-
return bound
|
|
59
|
-
|
|
60
|
-
if not isinstance(expr, Expr.ListExpr):
|
|
61
|
-
return expr # Expr.
|
|
62
|
-
|
|
63
|
-
# ---------- empty / single ----------
|
|
64
|
-
if len(expr.elements) == 0:
|
|
65
|
-
return expr
|
|
66
|
-
if len(expr.elements) == 1:
|
|
67
|
-
return self.high_eval(expr=expr.elements[0], env_id=env_id, meter=meter)
|
|
68
|
-
|
|
69
|
-
tail = expr.elements[-1]
|
|
70
|
-
|
|
71
|
-
# ---------- (value name def) ----------
|
|
72
|
-
if isinstance(tail, Expr.Symbol) and tail.value == "def":
|
|
73
|
-
if len(expr.elements) < 3:
|
|
74
|
-
return error_expr("eval", "def expects (value name def)")
|
|
75
|
-
name_e = expr.elements[-2]
|
|
76
|
-
if not isinstance(name_e, Expr.Symbol):
|
|
77
|
-
return error_expr("eval", "def name must be symbol")
|
|
78
|
-
value_e = expr.elements[-3]
|
|
79
|
-
value_res = self.high_eval(expr=value_e, env_id=env_id, meter=meter)
|
|
80
|
-
if _is_error(value_res):
|
|
81
|
-
return value_res
|
|
82
|
-
self.env_set(call_env_id, name_e.value, value_res)
|
|
83
|
-
return value_res
|
|
84
|
-
|
|
85
|
-
# Reference Call
|
|
86
|
-
# (atom_id ref)
|
|
87
|
-
if isinstance(tail, Expr.Symbol) and tail.value == "ref":
|
|
88
|
-
if len(expr.elements) != 2:
|
|
89
|
-
return error_expr("eval", "ref expects (atom_id ref)")
|
|
90
|
-
key_bytes = _expr_to_bytes(expr.elements[0])
|
|
91
|
-
if not key_bytes:
|
|
92
|
-
return error_expr("eval", "ref expects (atom_id ref)")
|
|
93
|
-
stored_list = self.get_expr_list_from_storage(key_bytes)
|
|
94
|
-
if stored_list is None:
|
|
95
|
-
return error_expr("eval", "ref target not found")
|
|
96
|
-
return stored_list
|
|
97
|
-
|
|
98
|
-
# Low Level Call
|
|
99
|
-
# (arg1 arg2 ... ((body) sk))
|
|
100
|
-
if isinstance(tail, Expr.ListExpr):
|
|
101
|
-
inner = tail.elements
|
|
102
|
-
if len(inner) >= 2 and isinstance(inner[-1], Expr.Symbol) and inner[-1].value == "sk":
|
|
103
|
-
body_expr = inner[-2]
|
|
104
|
-
if not isinstance(body_expr, Expr.ListExpr):
|
|
105
|
-
return error_expr("eval", "sk body must be list")
|
|
106
|
-
|
|
107
|
-
# helper: turn an Expr into a contiguous bytes buffer
|
|
108
|
-
def to_bytes(v: Expr) -> Union[bytes, Expr]:
|
|
109
|
-
if isinstance(v, Expr.
|
|
110
|
-
return
|
|
111
|
-
if isinstance(v, Expr.ListExpr):
|
|
112
|
-
# expect a list of Expr.
|
|
113
|
-
out: bytearray = bytearray()
|
|
114
|
-
for el in v.elements:
|
|
115
|
-
if isinstance(el, Expr.
|
|
116
|
-
out.
|
|
117
|
-
else:
|
|
118
|
-
return error_expr("eval", "byte list must contain only
|
|
119
|
-
return bytes(out)
|
|
120
|
-
if _is_error(v):
|
|
121
|
-
return v
|
|
122
|
-
return error_expr("eval", "argument must resolve to
|
|
123
|
-
|
|
124
|
-
# resolve ALL preceding args into bytes (can be
|
|
125
|
-
args_exprs = expr.elements[:-1]
|
|
126
|
-
arg_bytes: List[bytes] = []
|
|
127
|
-
for a in args_exprs:
|
|
128
|
-
v = self.high_eval(expr=a, env_id=env_id, meter=meter)
|
|
129
|
-
if _is_error(v):
|
|
130
|
-
return v
|
|
131
|
-
vb = to_bytes(v)
|
|
132
|
-
if not isinstance(vb, bytes):
|
|
133
|
-
if _is_error(vb):
|
|
134
|
-
return vb
|
|
135
|
-
return error_expr("eval", "unexpected expression while coercing to bytes")
|
|
136
|
-
arg_bytes.append(vb)
|
|
137
|
-
|
|
138
|
-
# build low-level code with $0-based placeholders ($0 = first arg)
|
|
139
|
-
code: List[bytes] = []
|
|
140
|
-
|
|
141
|
-
def emit(tok: Expr) -> Union[None, Expr]:
|
|
142
|
-
if isinstance(tok, Expr.Symbol):
|
|
143
|
-
name = tok.value
|
|
144
|
-
if name.startswith("$"):
|
|
145
|
-
idx_s = name[1:]
|
|
146
|
-
if not idx_s.isdigit():
|
|
147
|
-
return error_expr("eval", "invalid sk placeholder")
|
|
148
|
-
idx = int(idx_s) # $0 is first
|
|
149
|
-
if idx < 0 or idx >= len(arg_bytes):
|
|
150
|
-
return error_expr("eval", "arity mismatch in sk placeholder")
|
|
151
|
-
code.append(arg_bytes[idx])
|
|
152
|
-
return None
|
|
153
|
-
code.append(name.encode())
|
|
154
|
-
return None
|
|
155
|
-
|
|
156
|
-
if isinstance(tok, Expr.
|
|
157
|
-
code.append(
|
|
158
|
-
return None
|
|
159
|
-
|
|
160
|
-
if isinstance(tok, Expr.ListExpr):
|
|
161
|
-
rv = self.high_eval(expr=tok, env_id=env_id, meter=meter)
|
|
162
|
-
if _is_error(rv):
|
|
163
|
-
return rv
|
|
164
|
-
rb = to_bytes(rv)
|
|
165
|
-
if not isinstance(rb, bytes):
|
|
166
|
-
if _is_error(rb):
|
|
167
|
-
return rb
|
|
168
|
-
return error_expr("eval", "unexpected expression while coercing list token to bytes")
|
|
169
|
-
code.append(rb)
|
|
170
|
-
return None
|
|
171
|
-
|
|
172
|
-
if _is_error(tok):
|
|
173
|
-
return tok
|
|
174
|
-
|
|
175
|
-
return error_expr("eval", "invalid token in sk body")
|
|
176
|
-
|
|
177
|
-
for t in body_expr.elements:
|
|
178
|
-
err = emit(t)
|
|
179
|
-
if err is not None and _is_error(err):
|
|
180
|
-
return err
|
|
181
|
-
|
|
182
|
-
# Execute low-level code built from sk-body using the caller's meter
|
|
183
|
-
res = self.low_eval(code, meter=meter)
|
|
184
|
-
return res
|
|
185
|
-
|
|
186
|
-
# High Level Call
|
|
187
|
-
# (arg1 arg2 ... ((body) (params) fn))
|
|
188
|
-
if isinstance(tail, Expr.ListExpr):
|
|
189
|
-
fn_form = tail
|
|
190
|
-
if (len(fn_form.elements) >= 3
|
|
191
|
-
and isinstance(fn_form.elements[-1], Expr.Symbol)
|
|
192
|
-
and fn_form.elements[-1].value == "fn"):
|
|
193
|
-
|
|
194
|
-
body_expr = fn_form.elements[-3]
|
|
195
|
-
params_expr = fn_form.elements[-2]
|
|
196
|
-
|
|
197
|
-
if not isinstance(body_expr, Expr.ListExpr):
|
|
198
|
-
return error_expr("eval", "fn body must be list")
|
|
199
|
-
if not isinstance(params_expr, Expr.ListExpr):
|
|
200
|
-
return error_expr("eval", "fn params must be list")
|
|
201
|
-
|
|
202
|
-
params: List[str] = []
|
|
203
|
-
for p in params_expr.elements:
|
|
204
|
-
if not isinstance(p, Expr.Symbol):
|
|
205
|
-
return error_expr("eval", "fn param must be symbol")
|
|
206
|
-
params.append(p.value)
|
|
207
|
-
|
|
208
|
-
args_exprs = expr.elements[:-1]
|
|
209
|
-
if len(args_exprs) != len(params):
|
|
210
|
-
return error_expr("eval", "arity mismatch")
|
|
211
|
-
|
|
212
|
-
arg_bytes: List[bytes] = []
|
|
213
|
-
for a in args_exprs:
|
|
214
|
-
v = self.high_eval(expr=a, env_id=env_id, meter=meter)
|
|
215
|
-
if _is_error(v):
|
|
216
|
-
return v
|
|
217
|
-
if not isinstance(v, Expr.
|
|
218
|
-
return error_expr("eval", "argument must resolve to
|
|
219
|
-
arg_bytes.append(
|
|
220
|
-
|
|
221
|
-
# child env, bind params -> Expr.
|
|
222
|
-
child_env = uuid.uuid4()
|
|
223
|
-
self.environments[child_env] = Env(parent_id=env_id)
|
|
224
|
-
try:
|
|
225
|
-
for name_b, val_b in zip(params, arg_bytes):
|
|
226
|
-
self.env_set(child_env, name_b, Expr.
|
|
227
|
-
|
|
228
|
-
# evaluate HL body, metered from the top
|
|
229
|
-
return self.high_eval(expr=body_expr, env_id=child_env, meter=meter)
|
|
230
|
-
finally:
|
|
231
|
-
self.environments.pop(child_env, None)
|
|
232
|
-
|
|
233
|
-
# ---------- default: resolve each element and return list ----------
|
|
234
|
-
resolved: List[Expr] = [self.high_eval(expr=e, env_id=env_id, meter=meter) for e in expr.elements]
|
|
235
|
-
return Expr.ListExpr(resolved)
|
|
236
|
-
finally:
|
|
237
|
-
self.environments.pop(call_env_id, None)
|
|
1
|
+
from typing import List, Optional, Union
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from ..models.environment import Env
|
|
5
|
+
from ..models.expression import Expr, error_expr, ERROR_SYMBOL
|
|
6
|
+
from ..models.meter import Meter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _is_error(expr: Expr) -> bool:
|
|
10
|
+
return (
|
|
11
|
+
isinstance(expr, Expr.ListExpr)
|
|
12
|
+
and bool(expr.elements)
|
|
13
|
+
and isinstance(expr.elements[0], Expr.Symbol)
|
|
14
|
+
and expr.elements[0].value == ERROR_SYMBOL
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _hex_symbol_to_bytes(value: Optional[str]) -> Optional[bytes]:
|
|
19
|
+
if not value:
|
|
20
|
+
return None
|
|
21
|
+
data = value.strip()
|
|
22
|
+
if data.startswith(("0x", "0X")):
|
|
23
|
+
data = data[2:]
|
|
24
|
+
if len(data) % 2:
|
|
25
|
+
data = "0" + data
|
|
26
|
+
try:
|
|
27
|
+
return bytes.fromhex(data)
|
|
28
|
+
except ValueError:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _expr_to_bytes(expr: Expr) -> Optional[bytes]:
|
|
33
|
+
if isinstance(expr, Expr.Bytes):
|
|
34
|
+
return expr.value
|
|
35
|
+
if isinstance(expr, Expr.Symbol):
|
|
36
|
+
return _hex_symbol_to_bytes(expr.value)
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def high_eval(self, expr: Expr, env_id: Optional[uuid.UUID] = None, meter = None) -> Expr:
|
|
41
|
+
"""Evaluate high-level expressions with scoped environments and metering."""
|
|
42
|
+
if meter is None:
|
|
43
|
+
meter = Meter()
|
|
44
|
+
|
|
45
|
+
call_env_id = uuid.uuid4()
|
|
46
|
+
self.environments[call_env_id] = Env(parent_id=env_id)
|
|
47
|
+
env_id = call_env_id
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# ---------- atoms ----------
|
|
51
|
+
if _is_error(expr):
|
|
52
|
+
return expr
|
|
53
|
+
|
|
54
|
+
if isinstance(expr, Expr.Symbol):
|
|
55
|
+
bound = self.env_get(env_id, expr.value)
|
|
56
|
+
if bound is None:
|
|
57
|
+
return error_expr("eval", f"unbound symbol '{expr.value}'")
|
|
58
|
+
return bound
|
|
59
|
+
|
|
60
|
+
if not isinstance(expr, Expr.ListExpr):
|
|
61
|
+
return expr # Expr.Bytes or other literals passthrough
|
|
62
|
+
|
|
63
|
+
# ---------- empty / single ----------
|
|
64
|
+
if len(expr.elements) == 0:
|
|
65
|
+
return expr
|
|
66
|
+
if len(expr.elements) == 1:
|
|
67
|
+
return self.high_eval(expr=expr.elements[0], env_id=env_id, meter=meter)
|
|
68
|
+
|
|
69
|
+
tail = expr.elements[-1]
|
|
70
|
+
|
|
71
|
+
# ---------- (value name def) ----------
|
|
72
|
+
if isinstance(tail, Expr.Symbol) and tail.value == "def":
|
|
73
|
+
if len(expr.elements) < 3:
|
|
74
|
+
return error_expr("eval", "def expects (value name def)")
|
|
75
|
+
name_e = expr.elements[-2]
|
|
76
|
+
if not isinstance(name_e, Expr.Symbol):
|
|
77
|
+
return error_expr("eval", "def name must be symbol")
|
|
78
|
+
value_e = expr.elements[-3]
|
|
79
|
+
value_res = self.high_eval(expr=value_e, env_id=env_id, meter=meter)
|
|
80
|
+
if _is_error(value_res):
|
|
81
|
+
return value_res
|
|
82
|
+
self.env_set(call_env_id, name_e.value, value_res)
|
|
83
|
+
return value_res
|
|
84
|
+
|
|
85
|
+
# Reference Call
|
|
86
|
+
# (atom_id ref)
|
|
87
|
+
if isinstance(tail, Expr.Symbol) and tail.value == "ref":
|
|
88
|
+
if len(expr.elements) != 2:
|
|
89
|
+
return error_expr("eval", "ref expects (atom_id ref)")
|
|
90
|
+
key_bytes = _expr_to_bytes(expr.elements[0])
|
|
91
|
+
if not key_bytes:
|
|
92
|
+
return error_expr("eval", "ref expects (atom_id ref)")
|
|
93
|
+
stored_list = self.get_expr_list_from_storage(key_bytes)
|
|
94
|
+
if stored_list is None:
|
|
95
|
+
return error_expr("eval", "ref target not found")
|
|
96
|
+
return stored_list
|
|
97
|
+
|
|
98
|
+
# Low Level Call
|
|
99
|
+
# (arg1 arg2 ... ((body) sk))
|
|
100
|
+
if isinstance(tail, Expr.ListExpr):
|
|
101
|
+
inner = tail.elements
|
|
102
|
+
if len(inner) >= 2 and isinstance(inner[-1], Expr.Symbol) and inner[-1].value == "sk":
|
|
103
|
+
body_expr = inner[-2]
|
|
104
|
+
if not isinstance(body_expr, Expr.ListExpr):
|
|
105
|
+
return error_expr("eval", "sk body must be list")
|
|
106
|
+
|
|
107
|
+
# helper: turn an Expr into a contiguous bytes buffer
|
|
108
|
+
def to_bytes(v: Expr) -> Union[bytes, Expr]:
|
|
109
|
+
if isinstance(v, Expr.Bytes):
|
|
110
|
+
return v.value
|
|
111
|
+
if isinstance(v, Expr.ListExpr):
|
|
112
|
+
# expect a list of Expr.Bytes
|
|
113
|
+
out: bytearray = bytearray()
|
|
114
|
+
for el in v.elements:
|
|
115
|
+
if isinstance(el, Expr.Bytes):
|
|
116
|
+
out.extend(el.value)
|
|
117
|
+
else:
|
|
118
|
+
return error_expr("eval", "byte list must contain only Bytes elements")
|
|
119
|
+
return bytes(out)
|
|
120
|
+
if _is_error(v):
|
|
121
|
+
return v
|
|
122
|
+
return error_expr("eval", "argument must resolve to Bytes or (Bytes ...)")
|
|
123
|
+
|
|
124
|
+
# resolve ALL preceding args into bytes (can be Bytes or List[Bytes])
|
|
125
|
+
args_exprs = expr.elements[:-1]
|
|
126
|
+
arg_bytes: List[bytes] = []
|
|
127
|
+
for a in args_exprs:
|
|
128
|
+
v = self.high_eval(expr=a, env_id=env_id, meter=meter)
|
|
129
|
+
if _is_error(v):
|
|
130
|
+
return v
|
|
131
|
+
vb = to_bytes(v)
|
|
132
|
+
if not isinstance(vb, bytes):
|
|
133
|
+
if _is_error(vb):
|
|
134
|
+
return vb
|
|
135
|
+
return error_expr("eval", "unexpected expression while coercing to bytes")
|
|
136
|
+
arg_bytes.append(vb)
|
|
137
|
+
|
|
138
|
+
# build low-level code with $0-based placeholders ($0 = first arg)
|
|
139
|
+
code: List[bytes] = []
|
|
140
|
+
|
|
141
|
+
def emit(tok: Expr) -> Union[None, Expr]:
|
|
142
|
+
if isinstance(tok, Expr.Symbol):
|
|
143
|
+
name = tok.value
|
|
144
|
+
if name.startswith("$"):
|
|
145
|
+
idx_s = name[1:]
|
|
146
|
+
if not idx_s.isdigit():
|
|
147
|
+
return error_expr("eval", "invalid sk placeholder")
|
|
148
|
+
idx = int(idx_s) # $0 is first
|
|
149
|
+
if idx < 0 or idx >= len(arg_bytes):
|
|
150
|
+
return error_expr("eval", "arity mismatch in sk placeholder")
|
|
151
|
+
code.append(arg_bytes[idx])
|
|
152
|
+
return None
|
|
153
|
+
code.append(name.encode())
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
if isinstance(tok, Expr.Bytes):
|
|
157
|
+
code.append(tok.value)
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
if isinstance(tok, Expr.ListExpr):
|
|
161
|
+
rv = self.high_eval(expr=tok, env_id=env_id, meter=meter)
|
|
162
|
+
if _is_error(rv):
|
|
163
|
+
return rv
|
|
164
|
+
rb = to_bytes(rv)
|
|
165
|
+
if not isinstance(rb, bytes):
|
|
166
|
+
if _is_error(rb):
|
|
167
|
+
return rb
|
|
168
|
+
return error_expr("eval", "unexpected expression while coercing list token to bytes")
|
|
169
|
+
code.append(rb)
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
if _is_error(tok):
|
|
173
|
+
return tok
|
|
174
|
+
|
|
175
|
+
return error_expr("eval", "invalid token in sk body")
|
|
176
|
+
|
|
177
|
+
for t in body_expr.elements:
|
|
178
|
+
err = emit(t)
|
|
179
|
+
if err is not None and _is_error(err):
|
|
180
|
+
return err
|
|
181
|
+
|
|
182
|
+
# Execute low-level code built from sk-body using the caller's meter
|
|
183
|
+
res = self.low_eval(code, meter=meter)
|
|
184
|
+
return res
|
|
185
|
+
|
|
186
|
+
# High Level Call
|
|
187
|
+
# (arg1 arg2 ... ((body) (params) fn))
|
|
188
|
+
if isinstance(tail, Expr.ListExpr):
|
|
189
|
+
fn_form = tail
|
|
190
|
+
if (len(fn_form.elements) >= 3
|
|
191
|
+
and isinstance(fn_form.elements[-1], Expr.Symbol)
|
|
192
|
+
and fn_form.elements[-1].value == "fn"):
|
|
193
|
+
|
|
194
|
+
body_expr = fn_form.elements[-3]
|
|
195
|
+
params_expr = fn_form.elements[-2]
|
|
196
|
+
|
|
197
|
+
if not isinstance(body_expr, Expr.ListExpr):
|
|
198
|
+
return error_expr("eval", "fn body must be list")
|
|
199
|
+
if not isinstance(params_expr, Expr.ListExpr):
|
|
200
|
+
return error_expr("eval", "fn params must be list")
|
|
201
|
+
|
|
202
|
+
params: List[str] = []
|
|
203
|
+
for p in params_expr.elements:
|
|
204
|
+
if not isinstance(p, Expr.Symbol):
|
|
205
|
+
return error_expr("eval", "fn param must be symbol")
|
|
206
|
+
params.append(p.value)
|
|
207
|
+
|
|
208
|
+
args_exprs = expr.elements[:-1]
|
|
209
|
+
if len(args_exprs) != len(params):
|
|
210
|
+
return error_expr("eval", "arity mismatch")
|
|
211
|
+
|
|
212
|
+
arg_bytes: List[bytes] = []
|
|
213
|
+
for a in args_exprs:
|
|
214
|
+
v = self.high_eval(expr=a, env_id=env_id, meter=meter)
|
|
215
|
+
if _is_error(v):
|
|
216
|
+
return v
|
|
217
|
+
if not isinstance(v, Expr.Bytes):
|
|
218
|
+
return error_expr("eval", "argument must resolve to Bytes")
|
|
219
|
+
arg_bytes.append(v.value)
|
|
220
|
+
|
|
221
|
+
# child env, bind params -> Expr.Bytes
|
|
222
|
+
child_env = uuid.uuid4()
|
|
223
|
+
self.environments[child_env] = Env(parent_id=env_id)
|
|
224
|
+
try:
|
|
225
|
+
for name_b, val_b in zip(params, arg_bytes):
|
|
226
|
+
self.env_set(child_env, name_b, Expr.Bytes(val_b))
|
|
227
|
+
|
|
228
|
+
# evaluate HL body, metered from the top
|
|
229
|
+
return self.high_eval(expr=body_expr, env_id=child_env, meter=meter)
|
|
230
|
+
finally:
|
|
231
|
+
self.environments.pop(child_env, None)
|
|
232
|
+
|
|
233
|
+
# ---------- default: resolve each element and return list ----------
|
|
234
|
+
resolved: List[Expr] = [self.high_eval(expr=e, env_id=env_id, meter=meter) for e in expr.elements]
|
|
235
|
+
return Expr.ListExpr(resolved)
|
|
236
|
+
finally:
|
|
237
|
+
self.environments.pop(call_env_id, None)
|