astreum 0.2.11__tar.gz → 0.2.13__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of astreum might be problematic. Click here for more details.

Files changed (30) hide show
  1. {astreum-0.2.11/src/astreum.egg-info → astreum-0.2.13}/PKG-INFO +2 -2
  2. {astreum-0.2.11 → astreum-0.2.13}/README.md +1 -1
  3. {astreum-0.2.11 → astreum-0.2.13}/pyproject.toml +1 -1
  4. astreum-0.2.13/src/astreum/models/patricia.py +249 -0
  5. astreum-0.2.13/src/astreum/models/transaction.py +83 -0
  6. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/node.py +2 -81
  7. {astreum-0.2.11 → astreum-0.2.13/src/astreum.egg-info}/PKG-INFO +2 -2
  8. {astreum-0.2.11 → astreum-0.2.13}/src/astreum.egg-info/SOURCES.txt +3 -2
  9. astreum-0.2.11/src/astreum/utils/patricia.py +0 -45
  10. {astreum-0.2.11 → astreum-0.2.13}/LICENSE +0 -0
  11. {astreum-0.2.11 → astreum-0.2.13}/setup.cfg +0 -0
  12. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/__init__.py +0 -0
  13. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/_node/__init__.py +0 -0
  14. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/_node/storage/__init__.py +0 -0
  15. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/_node/storage/merkle.py +0 -0
  16. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/_node/storage/patricia.py +0 -0
  17. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/crypto/__init__.py +0 -0
  18. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/crypto/ed25519.py +0 -0
  19. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/crypto/quadratic_form.py +0 -0
  20. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/crypto/wesolowski.py +0 -0
  21. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/crypto/x25519.py +0 -0
  22. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/format.py +0 -0
  23. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/lispeum/__init__.py +0 -0
  24. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/lispeum/parser.py +0 -0
  25. {astreum-0.2.11 → astreum-0.2.13}/src/astreum/lispeum/tokenizer.py +0 -0
  26. {astreum-0.2.11/src/astreum/utils → astreum-0.2.13/src/astreum/models}/__init__.py +0 -0
  27. {astreum-0.2.11 → astreum-0.2.13}/src/astreum.egg-info/dependency_links.txt +0 -0
  28. {astreum-0.2.11 → astreum-0.2.13}/src/astreum.egg-info/requires.txt +0 -0
  29. {astreum-0.2.11 → astreum-0.2.13}/src/astreum.egg-info/top_level.txt +0 -0
  30. {astreum-0.2.11 → astreum-0.2.13}/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.11
3
+ Version: 0.2.13
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
@@ -118,6 +118,6 @@ except ParseError as e:
118
118
  ## Testing
119
119
 
120
120
  ```bash
121
-
121
+ source venv/bin/activate
122
122
  python3 -m unittest discover -s tests
123
123
  ```
@@ -100,6 +100,6 @@ except ParseError as e:
100
100
  ## Testing
101
101
 
102
102
  ```bash
103
-
103
+ source venv/bin/activate
104
104
  python3 -m unittest discover -s tests
105
105
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "astreum"
3
- version = "0.2.11"
3
+ version = "0.2.13"
4
4
  authors = [
5
5
  { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
6
  ]
@@ -0,0 +1,249 @@
1
+ import blake3
2
+ from typing import Callable, Dict, List, Optional, Tuple
3
+ from astreum import format
4
+
5
+ class PatriciaNode:
6
+ def __init__(
7
+ self,
8
+ key_len: int,
9
+ key: bytes,
10
+ value: Optional[bytes],
11
+ child_0: Optional[bytes],
12
+ child_1: Optional[bytes]
13
+ ):
14
+ self.key_len = key_len
15
+ self.key = key
16
+ self.value = value
17
+ self.child_0 = child_0
18
+ self.child_1 = child_1
19
+ self._hash: bytes | None = None
20
+
21
+ def to_bytes(self) -> bytes:
22
+ return format.encode([self.key_len, self.key, self.value, self.child_0, self.child_1])
23
+
24
+ @classmethod
25
+ def from_bytes(cls, blob: bytes) -> "PatriciaNode":
26
+ key_len, key, value, child_0, child_1 = format.decode(blob)
27
+ return cls(key_len, key, value, child_0, child_1)
28
+
29
+ def hash(self) -> bytes:
30
+ if self._hash is None:
31
+ self._hash = blake3.blake3(self.to_bytes()).digest()
32
+ return self._hash
33
+
34
+ class PatriciaTrie:
35
+ def __init__(
36
+ self,
37
+ node_get: Callable[[bytes], Optional[bytes]],
38
+ root_hash: Optional[bytes] = None,
39
+ ) -> None:
40
+ self._node_get = node_get
41
+ self.nodes: Dict[bytes, PatriciaNode] = {}
42
+ self.root_hash: Optional[bytes] = root_hash
43
+
44
+ @staticmethod
45
+ def _bit(buf: bytes, idx: int) -> bool:
46
+ byte_i, offset = divmod(idx, 8)
47
+ return ((buf[byte_i] >> (7 - offset)) & 1) == 1
48
+
49
+ @classmethod
50
+ def _match_prefix(
51
+ cls,
52
+ prefix: bytes,
53
+ prefix_len: int,
54
+ key: bytes,
55
+ key_bit_offset: int,
56
+ ) -> bool:
57
+ if key_bit_offset + prefix_len > len(key) * 8:
58
+ return False
59
+
60
+ for i in range(prefix_len):
61
+ if cls._bit(prefix, i) != cls._bit(key, key_bit_offset + i):
62
+ return False
63
+ return True
64
+
65
+ def _fetch(self, h: bytes) -> Optional[PatriciaNode]:
66
+ node = self.nodes.get(h)
67
+ if node is None:
68
+ raw = self._node_get(h)
69
+ if raw is None:
70
+ return None
71
+ node = PatriciaNode.from_bytes(raw)
72
+ self.nodes[h] = node
73
+ return node
74
+
75
+ def get(self, key: bytes) -> Optional["PatriciaNode"]:
76
+ """Return the node that stores *key*, or ``None`` if absent."""
77
+ if self.root_hash is None:
78
+ return None
79
+
80
+ node = self._fetch(self.root_hash)
81
+ if node is None:
82
+ return None
83
+
84
+ key_pos = 0
85
+
86
+ while node is not None:
87
+ # 1️⃣ Verify that this node's (possibly sub‑byte) prefix matches.
88
+ if not self._match_prefix(node.key, node.key_len, key, key_pos):
89
+ return None
90
+ key_pos += node.key_len
91
+
92
+ # 2️⃣ If every bit of *key* has been matched, success only if the
93
+ # node actually stores a value.
94
+ if key_pos == len(key) * 8:
95
+ return node if node.value is not None else None
96
+
97
+ # 3️⃣ Decide which branch to follow using the next bit of *key*.
98
+ try:
99
+ next_bit = self._bit(key, key_pos)
100
+ except IndexError: # key ended prematurely
101
+ return None
102
+
103
+ child_hash = node.child_1 if next_bit else node.child_0
104
+ if child_hash is None: # dead end – key not present
105
+ return None
106
+
107
+ # 4️⃣ Fetch the child node via unified helper.
108
+ node = self._fetch(child_hash)
109
+ if node is None: # dangling pointer
110
+ return None
111
+
112
+ key_pos += 1 # we just consumed one routing bit
113
+
114
+ return None
115
+
116
+ def put(self, key: bytes, value: bytes) -> None:
117
+ """Insert or update ``key`` with ``value`` in‑place."""
118
+ total_bits = len(key) * 8
119
+
120
+ # S1 – Empty trie → create root leaf
121
+ if self.root_hash is None:
122
+ leaf = self._make_node(key, total_bits, value, None, None)
123
+ self.root_hash = leaf.hash()
124
+ return
125
+
126
+ # S2 – traversal bookkeeping
127
+ stack: List[Tuple[PatriciaNode, bytes, int]] = [] # (parent, parent_hash, dir_bit)
128
+ node = self._fetch(self.root_hash)
129
+ assert node is not None # root must exist now
130
+ key_pos = 0
131
+
132
+ # S4 – main descent loop
133
+ while True:
134
+ # 4.1 – prefix mismatch? → split
135
+ if not self._match_prefix(node.key, node.key_len, key, key_pos):
136
+ self._split_and_insert(node, stack, key, key_pos, value)
137
+ return
138
+
139
+ # 4.2 – consume this prefix
140
+ key_pos += node.key_len
141
+
142
+ # 4.3 – matched entire key → update value
143
+ if key_pos == total_bits:
144
+ self._invalidate_hash(node)
145
+ node.value = value
146
+ new_hash = node.hash()
147
+ self._bubble(stack, new_hash)
148
+ return
149
+
150
+ # 4.4 – routing bit
151
+ next_bit = self._bit(key, key_pos)
152
+ child_hash = node.child_1 if next_bit else node.child_0
153
+
154
+ # 4.6 – no child → easy append leaf
155
+ if child_hash is None:
156
+ self._append_leaf(node, next_bit, key, key_pos, value, stack)
157
+ return
158
+
159
+ # 4.7 – push current node onto stack
160
+ stack.append((node, node.hash(), int(next_bit)))
161
+
162
+ # 4.8 – fetch child and continue
163
+ node = self._fetch(child_hash)
164
+ if node is None:
165
+ # Dangling pointer – treat as append missing leaf
166
+ self._append_leaf(stack[-1][0], next_bit, key, key_pos, value, stack[:-1])
167
+ return
168
+ key_pos += 1 # consumed routing bit
169
+
170
+ def _append_leaf(
171
+ self,
172
+ parent: PatriciaNode,
173
+ dir_bit: bool,
174
+ key: bytes,
175
+ key_pos: int,
176
+ value: bytes,
177
+ stack: List[Tuple[PatriciaNode, bytes, int]],
178
+ ) -> None:
179
+ # key_pos points to routing bit; leaf stores the *rest* after that bit
180
+ tail_len = len(key) * 8 - (key_pos + 1)
181
+ tail_bits, tail_len = self._bit_slice(key, key_pos + 1, tail_len)
182
+ leaf = self._make_node(tail_bits, tail_len, value, None, None)
183
+
184
+ # attach
185
+ if dir_bit:
186
+ parent.child_1 = leaf.hash()
187
+ else:
188
+ parent.child_0 = leaf.hash()
189
+ self._invalidate_hash(parent)
190
+ new_parent_hash = parent.hash()
191
+ self._bubble(stack, new_parent_hash)
192
+
193
+ def _split_and_insert(
194
+ self,
195
+ node: PatriciaNode,
196
+ stack: List[Tuple[PatriciaNode, bytes, int]],
197
+ key: bytes,
198
+ key_pos: int,
199
+ value: bytes,
200
+ ) -> None:
201
+ """Split ``node`` at first divergent bit and insert new leaf for *key*."""
202
+ # Compute LCP between node.key and remaining key bits
203
+ max_lcp = min(node.key_len, len(key) * 8 - key_pos)
204
+ lcp = 0
205
+ while lcp < max_lcp and self._bit(node.key, lcp) == self._bit(key, key_pos + lcp):
206
+ lcp += 1
207
+
208
+ # Common prefix bits → new internal node
209
+ common_bits, common_len = self._bit_slice(node.key, 0, lcp)
210
+ internal = self._make_node(common_bits, common_len, None, None, None)
211
+
212
+ # Trim old node prefix
213
+ old_suffix_bits, old_suffix_len = self._bit_slice(node.key, lcp, node.key_len - lcp)
214
+ node.key = old_suffix_bits
215
+ node.key_len = old_suffix_len
216
+ self._invalidate_hash(node) # will be re‑hashed when attached
217
+ old_div_bit = self._bit(node.key, 0) if old_suffix_len > 0 else False
218
+
219
+ # New key leaf
220
+ new_key_tail_len = len(key) * 8 - (key_pos + lcp + 1)
221
+ new_tail_bits, new_tail_len = self._bit_slice(key, key_pos + lcp + 1, new_key_tail_len)
222
+ leaf = self._make_node(new_tail_bits, new_tail_len, value, None, None)
223
+ new_div_bit = self._bit(key, key_pos + lcp)
224
+
225
+ # Attach children to internal
226
+ if old_div_bit:
227
+ internal.child_1 = node.hash()
228
+ internal.child_0 = leaf.hash() if not new_div_bit else internal.child_0
229
+ else:
230
+ internal.child_0 = node.hash()
231
+ internal.child_1 = leaf.hash() if new_div_bit else internal.child_1
232
+ self._invalidate_hash(internal)
233
+ internal_hash = internal.hash()
234
+
235
+ # Rewire parent link or set as root
236
+ if not stack:
237
+ self.root_hash = internal_hash
238
+ return
239
+
240
+ parent, parent_old_hash, dir_bit = stack.pop()
241
+ if dir_bit == 0:
242
+ parent.child_0 = internal_hash
243
+ else:
244
+ parent.child_1 = internal_hash
245
+ self._invalidate_hash(parent)
246
+ parent_new_hash = parent.hash()
247
+ self._bubble(stack, parent_new_hash)
248
+
249
+
@@ -0,0 +1,83 @@
1
+ from ..format import encode, decode
2
+ from ..crypto import ed25519
3
+ import blake3
4
+
5
+ class Transaction:
6
+ def __init__(
7
+ self,
8
+ sender_pk: bytes,
9
+ recipient_pk: bytes,
10
+ amount: int,
11
+ fee: int,
12
+ nonce: int,
13
+ signature: bytes | None = None,
14
+ ) -> None:
15
+ self.sender_pk = sender_pk
16
+ self.recipient_pk = recipient_pk
17
+ self.amount = amount
18
+ self.fee = fee
19
+ self.nonce = nonce
20
+ self.signature = signature
21
+
22
+ if self.amount < 0 or self.fee < 0:
23
+ raise ValueError("amount and fee must be non-negative")
24
+
25
+ if self.fee % 2 != 0:
26
+ raise ValueError("fee must be divisible by two")
27
+
28
+ self.tx_body_hash: bytes = blake3.blake3(self._body_bytes()).digest()
29
+
30
+ if self.signature is not None:
31
+ self.tx_hash = blake3.blake3(self.tx_body_hash + self.signature).digest()
32
+ else:
33
+ self.tx_hash = None
34
+
35
+ def sign(self, priv_key: ed25519.Ed25519PrivateKey) -> None:
36
+ if self.signature is not None:
37
+ raise ValueError("transaction already signed")
38
+ sig = priv_key.sign(self.tx_body_hash)
39
+ self.signature = sig
40
+ self.tx_hash = blake3.blake3(self.tx_body_hash + sig).digest()
41
+
42
+ def verify_signature(self) -> bool:
43
+ if self.signature is None:
44
+ return False
45
+ try:
46
+ pub = ed25519.Ed25519PublicKey.from_public_bytes(self.sender_pk)
47
+ pub.verify(self.signature, self.tx_body_hash)
48
+ return True
49
+ except Exception:
50
+ return False
51
+
52
+ def to_bytes(self) -> bytes:
53
+ sig = self.signature or b""
54
+ return encode([
55
+ self.sender_pk,
56
+ self.recipient_pk,
57
+ self.amount,
58
+ self.fee,
59
+ self.nonce,
60
+ sig,
61
+ ])
62
+
63
+ @classmethod
64
+ def from_bytes(cls, blob: bytes) -> 'Transaction':
65
+ sender, recipient, amount, fee, nonce, sig = decode(blob)
66
+ return cls(sender, recipient, int(amount), int(fee), int(nonce), sig)
67
+
68
+ def _body_bytes(self) -> bytes:
69
+ return encode([
70
+ self.sender_pk,
71
+ self.recipient_pk,
72
+ self.amount,
73
+ self.fee,
74
+ self.nonce,
75
+ ])
76
+
77
+ def __eq__(self, other: "Transaction") -> bool:
78
+ if not isinstance(other, Transaction):
79
+ return NotImplemented
80
+ return self.tx_hash == other.tx_hash
81
+
82
+ def __hash__(self) -> int:
83
+ return int.from_bytes(self.tx_hash, 'big')
@@ -6,6 +6,8 @@ from pathlib import Path
6
6
  from typing import Tuple, Dict, Union, Optional, List
7
7
  from datetime import datetime, timedelta, timezone
8
8
  import uuid
9
+
10
+ from .models.transaction import Transaction
9
11
  from .format import encode, decode
10
12
  from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
11
13
  from cryptography.hazmat.primitives import serialization
@@ -323,87 +325,6 @@ class Env:
323
325
  f"parent_id={self.parent_id})"
324
326
  )
325
327
 
326
-
327
- class Transaction:
328
- def __init__(
329
- self,
330
- sender_pk: bytes,
331
- recipient_pk: bytes,
332
- amount: int,
333
- fee: int,
334
- nonce: int,
335
- signature: bytes | None = None,
336
- ) -> None:
337
- self.sender_pk = sender_pk
338
- self.recipient_pk = recipient_pk
339
- self.amount = amount
340
- self.fee = fee
341
- self.nonce = nonce
342
- self.signature = signature
343
-
344
- if self.amount < 0 or self.fee < 0:
345
- raise ValueError("amount and fee must be non-negative")
346
-
347
- if self.fee % 2 != 0:
348
- raise ValueError("fee must be divisible by two")
349
-
350
- self.tx_body_hash: bytes = blake3.blake3(self._body_bytes()).digest()
351
-
352
- if self.signature is not None:
353
- self.tx_hash = blake3.blake3(self.tx_body_hash + self.signature).digest()
354
- else:
355
- self.tx_hash = None
356
-
357
- def sign(self, priv_key: ed25519.Ed25519PrivateKey) -> None:
358
- if self.signature is not None:
359
- raise ValueError("transaction already signed")
360
- sig = priv_key.sign(self.tx_body_hash)
361
- self.signature = sig
362
- self.tx_hash = blake3.blake3(self.tx_body_hash + sig).digest()
363
-
364
- def verify_signature(self) -> bool:
365
- if self.signature is None:
366
- return False
367
- try:
368
- pub = ed25519.Ed25519PublicKey.from_public_bytes(self.sender_pk)
369
- pub.verify(self.signature, self.tx_body_hash)
370
- return True
371
- except Exception:
372
- return False
373
-
374
- def to_bytes(self) -> bytes:
375
- sig = self.signature or b""
376
- return encode([
377
- self.sender_pk,
378
- self.recipient_pk,
379
- self.amount,
380
- self.fee,
381
- self.nonce,
382
- sig,
383
- ])
384
-
385
- @classmethod
386
- def from_bytes(cls, blob: bytes) -> 'Transaction':
387
- sender, recipient, amount, fee, nonce, sig = decode(blob)
388
- return cls(sender, recipient, int(amount), int(fee), int(nonce), sig)
389
-
390
- def _body_bytes(self) -> bytes:
391
- return encode([
392
- self.sender_pk,
393
- self.recipient_pk,
394
- self.amount,
395
- self.fee,
396
- self.nonce,
397
- ])
398
-
399
- def __eq__(self, other: Any) -> bool:
400
- if not isinstance(other, Transaction):
401
- return NotImplemented
402
- return self.tx_hash == other.tx_hash
403
-
404
- def __hash__(self) -> int:
405
- return int.from_bytes(self.tx_hash, 'big')
406
-
407
328
  class Block:
408
329
  def __init__(
409
330
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.11
3
+ Version: 0.2.13
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
@@ -118,6 +118,6 @@ except ParseError as e:
118
118
  ## Testing
119
119
 
120
120
  ```bash
121
-
121
+ source venv/bin/activate
122
122
  python3 -m unittest discover -s tests
123
123
  ```
@@ -21,6 +21,7 @@ src/astreum/crypto/x25519.py
21
21
  src/astreum/lispeum/__init__.py
22
22
  src/astreum/lispeum/parser.py
23
23
  src/astreum/lispeum/tokenizer.py
24
- src/astreum/utils/__init__.py
25
- src/astreum/utils/patricia.py
24
+ src/astreum/models/__init__.py
25
+ src/astreum/models/patricia.py
26
+ src/astreum/models/transaction.py
26
27
  tests/test_node_machine.py
@@ -1,45 +0,0 @@
1
- import blake3
2
- from typing import Callable, Dict, List, Optional
3
- from astreum import format
4
-
5
- EMPTY_HASH = b"\x00" * 32
6
-
7
- class PatriciaNode:
8
- def __init__(
9
- self,
10
- key_len: int,
11
- key_bits: bytes,
12
- value: bytes,
13
- children: List[bytes]|None = None
14
- ):
15
- self.key_len = key_len
16
- self.key_bits = key_bits
17
- self.value = value
18
- self.children = children
19
- self._hash: bytes | None = None
20
-
21
- def to_bytes(self) -> bytes:
22
- key_field = bytes([self.key_len]) + self.key_bits
23
- return format.encode([key_field, self.value, self.children])
24
-
25
- @classmethod
26
- def from_bytes(cls, blob: bytes) -> "PatriciaNode":
27
- key_field, value, children = format.decode(blob)
28
- key_len = key_field[0]
29
- key_bits = key_field[1:]
30
- return cls(key_len, key_bits, value, children)
31
-
32
- def hash(self) -> bytes:
33
- if self._hash is None:
34
- self._hash = blake3.blake3(self.to_bytes()).digest()
35
- return self._hash
36
-
37
- class PatriciaTrie:
38
- def __init__(
39
- self,
40
- node_get: Callable[[bytes], Optional[bytes]],
41
- root_hash: Optional[bytes] = None,
42
- ) -> None:
43
- self._node_get = node_get
44
- self.nodes: Dict[bytes, bytes] = {}
45
- self.root_hash: Optional[bytes] = root_hash
File without changes
File without changes
File without changes