astreum 0.2.29__py3-none-any.whl → 0.2.61__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.
Files changed (58) hide show
  1. astreum/__init__.py +9 -1
  2. astreum/_communication/__init__.py +11 -0
  3. astreum/{models → _communication}/message.py +101 -64
  4. astreum/_communication/peer.py +23 -0
  5. astreum/_communication/ping.py +33 -0
  6. astreum/_communication/route.py +95 -0
  7. astreum/_communication/setup.py +322 -0
  8. astreum/_communication/util.py +42 -0
  9. astreum/_consensus/__init__.py +20 -0
  10. astreum/_consensus/account.py +95 -0
  11. astreum/_consensus/accounts.py +38 -0
  12. astreum/_consensus/block.py +311 -0
  13. astreum/_consensus/chain.py +66 -0
  14. astreum/_consensus/fork.py +100 -0
  15. astreum/_consensus/genesis.py +72 -0
  16. astreum/_consensus/receipt.py +136 -0
  17. astreum/_consensus/setup.py +115 -0
  18. astreum/_consensus/transaction.py +215 -0
  19. astreum/_consensus/workers/__init__.py +9 -0
  20. astreum/_consensus/workers/discovery.py +48 -0
  21. astreum/_consensus/workers/validation.py +125 -0
  22. astreum/_consensus/workers/verify.py +63 -0
  23. astreum/_lispeum/__init__.py +16 -0
  24. astreum/_lispeum/environment.py +13 -0
  25. astreum/_lispeum/expression.py +190 -0
  26. astreum/_lispeum/high_evaluation.py +236 -0
  27. astreum/_lispeum/low_evaluation.py +123 -0
  28. astreum/_lispeum/meter.py +18 -0
  29. astreum/_lispeum/parser.py +51 -0
  30. astreum/_lispeum/tokenizer.py +22 -0
  31. astreum/_node.py +198 -0
  32. astreum/_storage/__init__.py +7 -0
  33. astreum/_storage/atom.py +109 -0
  34. astreum/_storage/patricia.py +478 -0
  35. astreum/_storage/setup.py +35 -0
  36. astreum/models/block.py +48 -39
  37. astreum/node.py +755 -563
  38. astreum/utils/bytes.py +24 -0
  39. astreum/utils/integer.py +25 -0
  40. astreum/utils/logging.py +219 -0
  41. {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/METADATA +50 -14
  42. astreum-0.2.61.dist-info/RECORD +57 -0
  43. astreum/lispeum/__init__.py +0 -2
  44. astreum/lispeum/environment.py +0 -40
  45. astreum/lispeum/expression.py +0 -86
  46. astreum/lispeum/parser.py +0 -41
  47. astreum/lispeum/tokenizer.py +0 -52
  48. astreum/models/account.py +0 -91
  49. astreum/models/accounts.py +0 -34
  50. astreum/models/transaction.py +0 -106
  51. astreum/relay/__init__.py +0 -0
  52. astreum/relay/peer.py +0 -9
  53. astreum/relay/route.py +0 -25
  54. astreum/relay/setup.py +0 -58
  55. astreum-0.2.29.dist-info/RECORD +0 -33
  56. {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/WHEEL +0 -0
  57. {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/licenses/LICENSE +0 -0
  58. {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from queue import Empty
5
+ from typing import Any, Callable
6
+
7
+ from ..block import Block
8
+ from ..transaction import apply_transaction
9
+ from ..._storage.atom import bytes_list_to_atoms
10
+ from ..._storage.patricia import PatriciaTrie
11
+ from ..._communication.message import Message, MessageTopic
12
+ from ..._communication.ping import Ping
13
+
14
+
15
+ def make_validation_worker(
16
+ node: Any,
17
+ *,
18
+ current_validator: Callable[[Any], bytes],
19
+ ) -> Callable[[], None]:
20
+ """Build the validation worker bound to the given node."""
21
+
22
+ def _validation_worker() -> None:
23
+ stop = node._validation_stop_event
24
+ while not stop.is_set():
25
+ validation_public_key = getattr(node, "validation_public_key", None)
26
+ if not validation_public_key:
27
+ time.sleep(0.5)
28
+ continue
29
+
30
+ scheduled_validator = current_validator(node)
31
+
32
+ if scheduled_validator != validation_public_key:
33
+ time.sleep(0.5)
34
+ continue
35
+
36
+ try:
37
+ current_hash = node._validation_transaction_queue.get_nowait()
38
+ except Empty:
39
+ time.sleep(0.1)
40
+ continue
41
+
42
+ # create thread to perform vdf
43
+
44
+ new_block = Block()
45
+ new_block.validator_public_key = validation_public_key
46
+ new_block.previous_block_hash = node.latest_block_hash
47
+ try:
48
+ new_block.previous_block = Block.from_atom(node, new_block.previous_block_hash)
49
+ except Exception:
50
+ continue
51
+ new_block.accounts = PatriciaTrie(root_hash=new_block.previous_block.accounts_hash)
52
+
53
+ # we may want to add a timer to process part of the txs only on a slow computer
54
+ while True:
55
+ try:
56
+ apply_transaction(node, new_block, current_hash)
57
+ except NotImplementedError:
58
+ node._validation_transaction_queue.put(current_hash)
59
+ time.sleep(0.5)
60
+ break
61
+ except Exception:
62
+ pass
63
+
64
+ try:
65
+ current_hash = node._validation_transaction_queue.get_nowait()
66
+ except Empty:
67
+ break
68
+
69
+ # create an atom list of transactions, save the list head hash as the block's transactions_hash
70
+ transactions = new_block.transactions or []
71
+ tx_hashes = [bytes(tx.hash) for tx in transactions if tx.hash]
72
+ head_hash, _ = bytes_list_to_atoms(tx_hashes)
73
+ new_block.transactions_hash = head_hash
74
+
75
+ receipts = new_block.receipts or []
76
+ receipt_hashes = [bytes(rcpt.hash) for rcpt in receipts if rcpt.hash]
77
+ receipts_head, _ = bytes_list_to_atoms(receipt_hashes)
78
+ new_block.receipts_hash = receipts_head
79
+
80
+ # get vdf result, default to 0 for now
81
+
82
+ # get timestamp or wait for a the next second from the previous block, rule is the next block must be atleast 1 second after the previous
83
+ now = time.time()
84
+ min_allowed = new_block.previous_block.timestamp + 1
85
+ if now < min_allowed:
86
+ time.sleep(max(0.0, min_allowed - now))
87
+ now = time.time()
88
+ new_block.timestamp = max(int(now), min_allowed)
89
+
90
+ # atomize block
91
+ new_block_hash, new_block_atoms = new_block.to_atom()
92
+ # put as own latest block hash
93
+ node.latest_block_hash = new_block_hash
94
+
95
+ # ping peers in the validation route to update there records
96
+ if node.validation_route and node.outgoing_queue and node.addresses:
97
+ route_peers = {
98
+ peer_key
99
+ for bucket in getattr(node.validation_route, "buckets", {}).values()
100
+ for peer_key in bucket
101
+ }
102
+ if route_peers:
103
+ ping_payload = Ping(
104
+ is_validator=True,
105
+ latest_block=new_block_hash,
106
+ ).to_bytes()
107
+
108
+ message_bytes = Message(
109
+ topic=MessageTopic.PING,
110
+ content=ping_payload,
111
+ ).to_bytes()
112
+
113
+ for address, peer_key in node.addresses.items():
114
+ if peer_key in route_peers:
115
+ try:
116
+ node.outgoing_queue.put((message_bytes, address))
117
+ except Exception:
118
+ pass
119
+
120
+ # upload block atoms
121
+
122
+ # upload receipt atoms
123
+ # upload account atoms
124
+
125
+ return _validation_worker
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from queue import Empty
5
+ from typing import Any, Set
6
+
7
+ from ..fork import Fork
8
+
9
+
10
+ def _process_peers_latest_block(
11
+ node: Any, latest_block_hash: bytes, peer_ids: Set[Any]
12
+ ) -> None:
13
+ """Assign peers to the fork that matches their reported head."""
14
+ new_fork = Fork(head=latest_block_hash)
15
+
16
+ current_fork_heads = {
17
+ fk.head for fk in node.forks.values() if fk.head != latest_block_hash
18
+ }
19
+
20
+ new_fork.validate(storage_get=node.storage_get, stop_heads=current_fork_heads)
21
+
22
+ if new_fork.validated_upto and new_fork.validated_upto in node.forks:
23
+ ref = node.forks[new_fork.validated_upto]
24
+ if getattr(ref, "malicious_block_hash", None):
25
+ return
26
+ new_fork.root = ref.root
27
+ new_fork.validated_upto = ref.validated_upto
28
+ new_fork.chain_fork_position = ref.chain_fork_position
29
+
30
+ for peer_id in peer_ids:
31
+ new_fork.add_peer(peer_id)
32
+ for head, fork in list(node.forks.items()):
33
+ if head != latest_block_hash:
34
+ fork.remove_peer(peer_id)
35
+
36
+ node.forks[latest_block_hash] = new_fork
37
+
38
+
39
+ def make_verify_worker(node: Any):
40
+ """Build the verify worker bound to the given node."""
41
+
42
+ def _verify_worker() -> None:
43
+ stop = node._validation_stop_event
44
+ while not stop.is_set():
45
+ batch: list[tuple[bytes, Set[Any]]] = []
46
+ try:
47
+ while True:
48
+ latest_b, peers = node._validation_verify_queue.get_nowait()
49
+ batch.append((latest_b, peers))
50
+ except Empty:
51
+ pass
52
+
53
+ if not batch:
54
+ time.sleep(0.1)
55
+ continue
56
+
57
+ for latest_b, peers in batch:
58
+ try:
59
+ _process_peers_latest_block(node, latest_b, peers)
60
+ except Exception:
61
+ pass
62
+
63
+ return _verify_worker
@@ -0,0 +1,16 @@
1
+ from .expression import Expr
2
+ from .environment import Env
3
+ from .low_evaluation import low_eval
4
+ from .meter import Meter
5
+ from .parser import parse, ParseError
6
+ from .tokenizer import tokenize
7
+
8
+ __all__ = [
9
+ "Env",
10
+ "Expr",
11
+ "low_eval",
12
+ "Meter",
13
+ "parse",
14
+ "tokenize",
15
+ "ParseError",
16
+ ]
@@ -0,0 +1,13 @@
1
+ from ast import Expr
2
+ from typing import Dict, Optional
3
+ import uuid
4
+
5
+
6
+ class Env:
7
+ def __init__(
8
+ self,
9
+ data: Optional[Dict[str, Expr]] = None,
10
+ parent_id: Optional[uuid.UUID] = None,
11
+ ):
12
+ self.data: Dict[bytes, Expr] = {} if data is None else data
13
+ self.parent_id = parent_id
@@ -0,0 +1,190 @@
1
+ from typing import Any, List, Optional, Tuple
2
+
3
+ from .._storage.atom import Atom, AtomKind
4
+
5
+ ZERO32 = b"\x00" * 32
6
+ ERROR_SYMBOL = "error"
7
+
8
+
9
+ class Expr:
10
+ class ListExpr:
11
+ def __init__(self, elements: List['Expr']):
12
+ self.elements = elements
13
+
14
+ def __repr__(self):
15
+ if not self.elements:
16
+ return "()"
17
+ inner = " ".join(str(e) for e in self.elements)
18
+ return f"({inner})"
19
+
20
+ def to_atoms(self):
21
+ return Expr.to_atoms(self)
22
+
23
+ class Symbol:
24
+ def __init__(self, value: str):
25
+ self.value = value
26
+
27
+ def __repr__(self):
28
+ return f"{self.value}"
29
+
30
+ def to_atoms(self):
31
+ return Expr.to_atoms(self)
32
+
33
+ class Bytes:
34
+ def __init__(self, value: bytes):
35
+ self.value = value
36
+
37
+ def __repr__(self):
38
+ int_value = int.from_bytes(self.value, "big") if self.value else 0
39
+ return f"{int_value}"
40
+
41
+ def to_atoms(self):
42
+ return Expr.to_atoms(self)
43
+ @classmethod
44
+ def from_atoms(cls, node: Any, root_hash: bytes) -> "Expr":
45
+ """Rebuild an expression tree from stored atoms."""
46
+ if not isinstance(root_hash, (bytes, bytearray)):
47
+ raise TypeError("root hash must be bytes-like")
48
+
49
+ storage_get = getattr(node, "storage_get", None)
50
+ if not callable(storage_get):
51
+ raise TypeError("node must provide a callable 'storage_get'")
52
+
53
+ expr_id = bytes(root_hash)
54
+
55
+ def _require(atom_id: Optional[bytes], context: str):
56
+ if not atom_id:
57
+ raise ValueError(f"missing atom id while decoding {context}")
58
+ atom = storage_get(atom_id)
59
+ if atom is None:
60
+ raise ValueError(f"missing atom data while decoding {context}")
61
+ return atom
62
+
63
+ def _atom_kind(atom: Any) -> Optional[AtomKind]:
64
+ kind_value = getattr(atom, "kind", None)
65
+ if isinstance(kind_value, AtomKind):
66
+ return kind_value
67
+ if isinstance(kind_value, int):
68
+ try:
69
+ return AtomKind(kind_value)
70
+ except ValueError:
71
+ return None
72
+ return None
73
+
74
+ type_atom = _require(expr_id, "expression atom")
75
+
76
+ kind_enum = _atom_kind(type_atom)
77
+ if kind_enum is None:
78
+ raise ValueError("expression atom missing kind")
79
+
80
+ if kind_enum is AtomKind.SYMBOL:
81
+ try:
82
+ return cls.Symbol(type_atom.data.decode("utf-8"))
83
+ except UnicodeDecodeError as exc:
84
+ raise ValueError("symbol atom is not valid utf-8") from exc
85
+
86
+ if kind_enum is AtomKind.BYTES:
87
+ return cls.Bytes(type_atom.data)
88
+
89
+ if kind_enum is AtomKind.LIST:
90
+ # Empty list sentinel: zero-length payload and no next pointer.
91
+ if len(type_atom.data) == 0 and type_atom.next == ZERO32:
92
+ return cls.ListExpr([])
93
+
94
+ elements: List[Expr] = []
95
+ current_atom = type_atom
96
+ idx = 0
97
+ while True:
98
+ child_hash = current_atom.data
99
+ if not child_hash:
100
+ raise ValueError("list element missing child hash")
101
+ if len(child_hash) != len(ZERO32):
102
+ raise ValueError("list element hash has unexpected length")
103
+ child_expr = cls.from_atoms(node, child_hash)
104
+ elements.append(child_expr)
105
+ next_id = current_atom.next
106
+ if not next_id or next_id == ZERO32:
107
+ break
108
+ next_atom = _require(next_id, f"list element {idx}")
109
+ next_kind = _atom_kind(next_atom)
110
+ if next_kind is not AtomKind.LIST:
111
+ raise ValueError("list chain contains non-list atom")
112
+ current_atom = next_atom
113
+ idx += 1
114
+ return cls.ListExpr(elements)
115
+
116
+ raise ValueError(f"unknown expression kind: {kind_enum}")
117
+
118
+ @staticmethod
119
+ def to_atoms(e: "Expr") -> Tuple[bytes, List[Atom]]:
120
+ def symbol(value: str) -> Tuple[bytes, List[Atom]]:
121
+ atom = Atom.from_data(data=value.encode("utf-8"), kind=AtomKind.SYMBOL)
122
+ return atom.object_id(), [atom]
123
+
124
+ def bytes_value(data: bytes) -> Tuple[bytes, List[Atom]]:
125
+ atom = Atom.from_data(data=data, kind=AtomKind.BYTES)
126
+ return atom.object_id(), [atom]
127
+
128
+ def lst(items: List["Expr"]) -> Tuple[bytes, List[Atom]]:
129
+ acc: List[Atom] = []
130
+ child_hashes: List[bytes] = []
131
+ for it in items:
132
+ h, atoms = Expr.to_atoms(it)
133
+ acc.extend(atoms)
134
+ child_hashes.append(h)
135
+ next_hash = ZERO32
136
+ elem_atoms: List[Atom] = []
137
+ for h in reversed(child_hashes):
138
+ a = Atom.from_data(h, next_hash, kind=AtomKind.LIST)
139
+ next_hash = a.object_id()
140
+ elem_atoms.append(a)
141
+ elem_atoms.reverse()
142
+ if elem_atoms:
143
+ head = elem_atoms[0].object_id()
144
+ else:
145
+ empty_atom = Atom.from_data(data=b"", next_hash=ZERO32, kind=AtomKind.LIST)
146
+ elem_atoms = [empty_atom]
147
+ head = empty_atom.object_id()
148
+ return head, acc + elem_atoms
149
+
150
+ if isinstance(e, Expr.Symbol):
151
+ return symbol(e.value)
152
+ if isinstance(e, Expr.Bytes):
153
+ return bytes_value(e.value)
154
+ if isinstance(e, Expr.ListExpr):
155
+ return lst(e.elements)
156
+ raise TypeError("unknown Expr variant")
157
+
158
+ def _expr_generate_id(expr) -> bytes:
159
+ expr_id, _ = Expr.to_atoms(expr)
160
+ return expr_id
161
+
162
+
163
+ def _expr_cached_id(expr) -> bytes:
164
+ cached = getattr(expr, "_cached_id", None)
165
+ if cached is None:
166
+ cached = _expr_generate_id(expr)
167
+ setattr(expr, "_cached_id", cached)
168
+ return cached
169
+
170
+
171
+ for _expr_cls in (Expr.ListExpr, Expr.Symbol, Expr.Bytes):
172
+ _expr_cls.generate_id = _expr_generate_id # type: ignore[attr-defined]
173
+ _expr_cls.id = property(_expr_cached_id) # type: ignore[attr-defined]
174
+
175
+
176
+ def error_expr(topic: str, message: str) -> Expr.ListExpr:
177
+ """Encode an error as (error <topic-bytes> <message-bytes>)."""
178
+ try:
179
+ topic_bytes = topic.encode("utf-8")
180
+ except UnicodeEncodeError as exc:
181
+ raise ValueError("error topic must be valid utf-8") from exc
182
+ try:
183
+ message_bytes = message.encode("utf-8")
184
+ except UnicodeEncodeError as exc:
185
+ raise ValueError("error message must be valid utf-8") from exc
186
+ return Expr.ListExpr([
187
+ Expr.Symbol(ERROR_SYMBOL),
188
+ Expr.Bytes(topic_bytes),
189
+ Expr.Bytes(message_bytes),
190
+ ])
@@ -0,0 +1,236 @@
1
+ from typing import List, Optional, Union
2
+ import uuid
3
+
4
+ from .environment import Env
5
+ from .expression import Expr, error_expr, ERROR_SYMBOL
6
+ from .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, env_id: uuid.UUID, expr: Expr, meter = None) -> Expr:
41
+ if meter is None:
42
+ meter = Meter()
43
+
44
+ call_env_id = uuid.uuid4()
45
+ self.environments[call_env_id] = Env(parent_id=env_id)
46
+ env_id = call_env_id
47
+
48
+ try:
49
+ # ---------- atoms ----------
50
+ if _is_error(expr):
51
+ return expr
52
+
53
+ if isinstance(expr, Expr.Symbol):
54
+ bound = self.env_get(env_id, expr.value.encode())
55
+ if bound is None:
56
+ return error_expr("eval", f"unbound symbol '{expr.value}'")
57
+ return bound
58
+
59
+ if not isinstance(expr, Expr.ListExpr):
60
+ return expr # Expr.Byte or other literals passthrough
61
+
62
+ # ---------- empty / single ----------
63
+ if len(expr.elements) == 0:
64
+ return expr
65
+ if len(expr.elements) == 1:
66
+ return self.high_eval(env_id, expr.elements[0], meter)
67
+
68
+ tail = expr.elements[-1]
69
+
70
+ # ---------- (value name def) ----------
71
+ if isinstance(tail, Expr.Symbol) and tail.value == "def":
72
+ if len(expr.elements) < 3:
73
+ return error_expr("eval", "def expects (value name def)")
74
+ name_e = expr.elements[-2]
75
+ if not isinstance(name_e, Expr.Symbol):
76
+ return error_expr("eval", "def name must be symbol")
77
+ value_e = expr.elements[-3]
78
+ value_res = self.high_eval(env_id, value_e, meter)
79
+ if _is_error(value_res):
80
+ return value_res
81
+ self.env_set(env_id, name_e.value.encode(), value_res)
82
+ return value_res
83
+
84
+ # Reference Call
85
+ # (atom_id ref)
86
+ if isinstance(tail, Expr.Symbol) and tail.value == "ref":
87
+ if len(expr.elements) != 2:
88
+ return error_expr("eval", "ref expects (atom_id ref)")
89
+ key_bytes = _expr_to_bytes(expr.elements[0])
90
+ if not key_bytes:
91
+ return error_expr("eval", "ref expects (atom_id ref)")
92
+ stored_list = self.get_expr_list_from_storage(key_bytes)
93
+ if stored_list is None:
94
+ return error_expr("eval", "ref target not found")
95
+ return stored_list
96
+
97
+ # Low Level Call
98
+ # (arg1 arg2 ... ((body) sk))
99
+ if isinstance(tail, Expr.ListExpr):
100
+ inner = tail.elements
101
+ if len(inner) >= 2 and isinstance(inner[-1], Expr.Symbol) and inner[-1].value == "sk":
102
+ body_expr = inner[-2]
103
+ if not isinstance(body_expr, Expr.ListExpr):
104
+ return error_expr("eval", "sk body must be list")
105
+
106
+ # helper: turn an Expr into a contiguous bytes buffer
107
+ def to_bytes(v: Expr) -> Union[bytes, Expr]:
108
+ if isinstance(v, Expr.Byte):
109
+ return bytes([v.value & 0xFF])
110
+ if isinstance(v, Expr.ListExpr):
111
+ # expect a list of Expr.Byte
112
+ out: bytearray = bytearray()
113
+ for el in v.elements:
114
+ if isinstance(el, Expr.Byte):
115
+ out.append(el.value & 0xFF)
116
+ else:
117
+ return error_expr("eval", "byte list must contain only Byte elements")
118
+ return bytes(out)
119
+ if _is_error(v):
120
+ return v
121
+ return error_expr("eval", "argument must resolve to Byte or (Byte ...)")
122
+
123
+ # resolve ALL preceding args into bytes (can be Byte or List[Byte])
124
+ args_exprs = expr.elements[:-1]
125
+ arg_bytes: List[bytes] = []
126
+ for a in args_exprs:
127
+ v = self.high_eval(env_id, a, meter)
128
+ if _is_error(v):
129
+ return v
130
+ vb = to_bytes(v)
131
+ if not isinstance(vb, bytes):
132
+ if _is_error(vb):
133
+ return vb
134
+ return error_expr("eval", "unexpected expression while coercing to bytes")
135
+ arg_bytes.append(vb)
136
+
137
+ # build low-level code with $0-based placeholders ($0 = first arg)
138
+ code: List[bytes] = []
139
+
140
+ def emit(tok: Expr) -> Union[None, Expr]:
141
+ if isinstance(tok, Expr.Symbol):
142
+ name = tok.value
143
+ if name.startswith("$"):
144
+ idx_s = name[1:]
145
+ if not idx_s.isdigit():
146
+ return error_expr("eval", "invalid sk placeholder")
147
+ idx = int(idx_s) # $0 is first
148
+ if idx < 0 or idx >= len(arg_bytes):
149
+ return error_expr("eval", "arity mismatch in sk placeholder")
150
+ code.append(arg_bytes[idx])
151
+ return None
152
+ code.append(name.encode())
153
+ return None
154
+
155
+ if isinstance(tok, Expr.Byte):
156
+ code.append(bytes([tok.value & 0xFF]))
157
+ return None
158
+
159
+ if isinstance(tok, Expr.ListExpr):
160
+ rv = self.high_eval(env_id, tok, meter)
161
+ if _is_error(rv):
162
+ return rv
163
+ rb = to_bytes(rv)
164
+ if not isinstance(rb, bytes):
165
+ if _is_error(rb):
166
+ return rb
167
+ return error_expr("eval", "unexpected expression while coercing list token to bytes")
168
+ code.append(rb)
169
+ return None
170
+
171
+ if _is_error(tok):
172
+ return tok
173
+
174
+ return error_expr("eval", "invalid token in sk body")
175
+
176
+ for t in body_expr.elements:
177
+ err = emit(t)
178
+ if err is not None and _is_error(err):
179
+ return err
180
+
181
+ # Execute low-level code built from sk-body using the caller's meter
182
+ res = self.low_eval(code, meter=meter)
183
+ return res
184
+
185
+ # High Level Call
186
+ # (arg1 arg2 ... ((body) (params) fn))
187
+ if isinstance(tail, Expr.ListExpr):
188
+ fn_form = tail
189
+ if (len(fn_form.elements) >= 3
190
+ and isinstance(fn_form.elements[-1], Expr.Symbol)
191
+ and fn_form.elements[-1].value == "fn"):
192
+
193
+ body_expr = fn_form.elements[-3]
194
+ params_expr = fn_form.elements[-2]
195
+
196
+ if not isinstance(body_expr, Expr.ListExpr):
197
+ return error_expr("eval", "fn body must be list")
198
+ if not isinstance(params_expr, Expr.ListExpr):
199
+ return error_expr("eval", "fn params must be list")
200
+
201
+ params: List[bytes] = []
202
+ for p in params_expr.elements:
203
+ if not isinstance(p, Expr.Symbol):
204
+ return error_expr("eval", "fn param must be symbol")
205
+ params.append(p.value.encode())
206
+
207
+ args_exprs = expr.elements[:-1]
208
+ if len(args_exprs) != len(params):
209
+ return error_expr("eval", "arity mismatch")
210
+
211
+ arg_bytes: List[bytes] = []
212
+ for a in args_exprs:
213
+ v = self.high_eval(env_id, a, meter)
214
+ if _is_error(v):
215
+ return v
216
+ if not isinstance(v, Expr.Byte):
217
+ return error_expr("eval", "argument must resolve to Byte")
218
+ arg_bytes.append(bytes([v.value & 0xFF]))
219
+
220
+ # child env, bind params -> Expr.Byte
221
+ child_env = uuid.uuid4()
222
+ self.environments[child_env] = Env(parent_id=env_id)
223
+ try:
224
+ for name_b, val_b in zip(params, arg_bytes):
225
+ self.env_set(child_env, name_b, Expr.Byte(val_b[0]))
226
+
227
+ # evaluate HL body, metered from the top
228
+ return self.high_eval(child_env, body_expr, meter)
229
+ finally:
230
+ self.environments.pop(child_env, None)
231
+
232
+ # ---------- default: resolve each element and return list ----------
233
+ resolved: List[Expr] = [self.high_eval(env_id, e, meter) for e in expr.elements]
234
+ return Expr.ListExpr(resolved)
235
+ finally:
236
+ self.environments.pop(call_env_id, None)