astreum 0.2.12__tar.gz → 0.2.14__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.12/src/astreum.egg-info → astreum-0.2.14}/PKG-INFO +2 -2
- {astreum-0.2.12 → astreum-0.2.14}/README.md +1 -1
- {astreum-0.2.12 → astreum-0.2.14}/pyproject.toml +1 -1
- astreum-0.2.14/src/astreum/models/block.py +98 -0
- astreum-0.2.14/src/astreum/models/merkle.py +233 -0
- astreum-0.2.14/src/astreum/models/transaction.py +83 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/node.py +2 -175
- {astreum-0.2.12 → astreum-0.2.14/src/astreum.egg-info}/PKG-INFO +2 -2
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum.egg-info/SOURCES.txt +5 -2
- {astreum-0.2.12 → astreum-0.2.14}/LICENSE +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/setup.cfg +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/__init__.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/_node/__init__.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/_node/storage/__init__.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/_node/storage/merkle.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/_node/storage/patricia.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/crypto/__init__.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/crypto/ed25519.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/crypto/quadratic_form.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/crypto/wesolowski.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/crypto/x25519.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/format.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/lispeum/__init__.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/lispeum/parser.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum/lispeum/tokenizer.py +0 -0
- {astreum-0.2.12/src/astreum/utils → astreum-0.2.14/src/astreum/models}/__init__.py +0 -0
- {astreum-0.2.12/src/astreum/utils → astreum-0.2.14/src/astreum/models}/patricia.py +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum.egg-info/dependency_links.txt +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum.egg-info/requires.txt +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/src/astreum.egg-info/top_level.txt +0 -0
- {astreum-0.2.12 → astreum-0.2.14}/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.
|
|
3
|
+
Version: 0.2.14
|
|
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
|
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from ..format import encode, decode
|
|
2
|
+
from ..crypto import ed25519
|
|
3
|
+
import blake3
|
|
4
|
+
|
|
5
|
+
class Block:
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
number: int,
|
|
9
|
+
prev_block_hash: bytes,
|
|
10
|
+
timestamp: int,
|
|
11
|
+
accounts_hash: bytes,
|
|
12
|
+
transactions_total_fees: int,
|
|
13
|
+
transaction_limit: int,
|
|
14
|
+
transactions_root_hash: bytes,
|
|
15
|
+
vdf_difficulty: int,
|
|
16
|
+
vdf_output: bytes,
|
|
17
|
+
vdf_proof: bytes,
|
|
18
|
+
validator_pk: bytes,
|
|
19
|
+
signature: bytes,
|
|
20
|
+
) -> None:
|
|
21
|
+
self.accounts_hash = accounts_hash
|
|
22
|
+
self.number = int(number)
|
|
23
|
+
self.prev_block_hash = prev_block_hash
|
|
24
|
+
self.timestamp = int(timestamp)
|
|
25
|
+
self.transactions_total_fees = int(transactions_total_fees)
|
|
26
|
+
self.transaction_limit = int(transaction_limit)
|
|
27
|
+
self.transactions_root_hash = transactions_root_hash
|
|
28
|
+
self.validator_pk = validator_pk
|
|
29
|
+
self.vdf_difficulty = int(vdf_difficulty)
|
|
30
|
+
self.vdf_output = vdf_output
|
|
31
|
+
self.vdf_proof = vdf_proof
|
|
32
|
+
self.signature = signature
|
|
33
|
+
self.body_hash = self._compute_body_hash()
|
|
34
|
+
|
|
35
|
+
def _body_fields_without_sig(self) -> list:
|
|
36
|
+
return [
|
|
37
|
+
self.accounts_hash,
|
|
38
|
+
self.number,
|
|
39
|
+
self.prev_block_hash,
|
|
40
|
+
self.timestamp,
|
|
41
|
+
self.transactions_total_fees,
|
|
42
|
+
self.transaction_limit,
|
|
43
|
+
self.transactions_root_hash,
|
|
44
|
+
self.validator_pk,
|
|
45
|
+
self.vdf_difficulty,
|
|
46
|
+
self.vdf_output,
|
|
47
|
+
self.vdf_proof,
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def _compute_body_hash(self) -> bytes:
|
|
51
|
+
return blake3.blake3(encode(self._body_fields_without_sig())).digest()
|
|
52
|
+
|
|
53
|
+
def to_bytes(self) -> bytes:
|
|
54
|
+
return encode(self._body_fields_without_sig() + [self.signature])
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_bytes(cls, blob: bytes) -> "Block":
|
|
58
|
+
(
|
|
59
|
+
accounts_hash,
|
|
60
|
+
number,
|
|
61
|
+
prev_block_hash,
|
|
62
|
+
timestamp,
|
|
63
|
+
transactions_total_fees,
|
|
64
|
+
transaction_limit,
|
|
65
|
+
transactions_root_hash,
|
|
66
|
+
validator_pk,
|
|
67
|
+
vdf_difficulty,
|
|
68
|
+
vdf_output,
|
|
69
|
+
vdf_proof,
|
|
70
|
+
signature
|
|
71
|
+
) = decode(blob)
|
|
72
|
+
return cls(
|
|
73
|
+
number=int(number),
|
|
74
|
+
prev_block_hash=prev_block_hash,
|
|
75
|
+
timestamp=int(timestamp),
|
|
76
|
+
accounts_hash=accounts_hash,
|
|
77
|
+
transactions_total_fees=int(transactions_total_fees),
|
|
78
|
+
transaction_limit=int(transaction_limit),
|
|
79
|
+
transactions_root_hash=transactions_root_hash,
|
|
80
|
+
vdf_difficulty=int(vdf_difficulty),
|
|
81
|
+
vdf_output=vdf_output,
|
|
82
|
+
vdf_proof=vdf_proof,
|
|
83
|
+
validator_pk=validator_pk,
|
|
84
|
+
signature=signature,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def hash(self) -> bytes:
|
|
89
|
+
return blake3.blake3(self.body_hash + self.signature).digest()
|
|
90
|
+
|
|
91
|
+
def verify_block_signature(self) -> bool:
|
|
92
|
+
try:
|
|
93
|
+
pub = ed25519.Ed25519PublicKey.from_public_bytes(self.validator_pk)
|
|
94
|
+
pub.verify(self.signature, self.body_hash)
|
|
95
|
+
return True
|
|
96
|
+
except Exception:
|
|
97
|
+
return False
|
|
98
|
+
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import blake3 # type: ignore
|
|
6
|
+
from ..format import encode, decode
|
|
7
|
+
|
|
8
|
+
class MerkleNode:
|
|
9
|
+
"""A node in a binary Merkle tree.
|
|
10
|
+
|
|
11
|
+
*Leaf* : ``value`` is **not** ``None``.
|
|
12
|
+
*Interior*: ``value`` is ``None``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__slots__ = ("left", "right", "value", "_hash")
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
left: Optional[bytes],
|
|
20
|
+
right: Optional[bytes],
|
|
21
|
+
value: Optional[bytes],
|
|
22
|
+
) -> None:
|
|
23
|
+
self.left = left
|
|
24
|
+
self.right = right
|
|
25
|
+
self.value = value
|
|
26
|
+
self._hash: Optional[bytes] = None
|
|
27
|
+
|
|
28
|
+
# ------------------------------------------------------------------
|
|
29
|
+
# serialisation helpers
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
def to_bytes(self) -> bytes:
|
|
32
|
+
return encode([self.left, self.right, self.value])
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_bytes(cls, blob: bytes) -> "MerkleNode":
|
|
36
|
+
left, right, value = decode(blob)
|
|
37
|
+
return cls(left, right, value)
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
# content hash (blake3)
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
def _compute_hash(self) -> bytes:
|
|
43
|
+
if self.value is not None: # leaf
|
|
44
|
+
return blake3.blake3(self.value).digest()
|
|
45
|
+
left = self.left or b""
|
|
46
|
+
right = self.right or b""
|
|
47
|
+
return blake3.blake3(left + right).digest()
|
|
48
|
+
|
|
49
|
+
def hash(self) -> bytes:
|
|
50
|
+
if self._hash is None:
|
|
51
|
+
self._hash = self._compute_hash()
|
|
52
|
+
return self._hash
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
# Merkle tree – fixed‑height, no dynamic growth
|
|
57
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MerkleTree:
|
|
61
|
+
"""Binary Merkle tree addressed by *leaf position* (0‑based).
|
|
62
|
+
|
|
63
|
+
* The number of levels is fixed once the tree is built (``_height``).
|
|
64
|
+
* ``put`` can **only update** existing leaves; it never adds capacity.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# construction helpers
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
node_get: Callable[[bytes], Optional[bytes]],
|
|
73
|
+
root_hash: Optional[bytes] = None,
|
|
74
|
+
height: Optional[int] = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
self._node_get = node_get
|
|
77
|
+
self.nodes: Dict[bytes, MerkleNode] = {}
|
|
78
|
+
self.root_hash = root_hash
|
|
79
|
+
self._height: Optional[int] = height
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_leaves(
|
|
83
|
+
cls,
|
|
84
|
+
leaves: List[bytes],
|
|
85
|
+
node_get: Callable[[bytes], Optional[bytes]] | None = None,
|
|
86
|
+
) -> "MerkleTree":
|
|
87
|
+
"""Build a complete tree from *leaves* (left‑to‑right order).
|
|
88
|
+
|
|
89
|
+
Missing right siblings in the top levels are allowed; they are encoded
|
|
90
|
+
as interior nodes with a single child.
|
|
91
|
+
"""
|
|
92
|
+
if not leaves:
|
|
93
|
+
raise ValueError("must supply at least one leaf")
|
|
94
|
+
|
|
95
|
+
node_get = node_get or (lambda _h: None)
|
|
96
|
+
tree = cls(node_get=node_get)
|
|
97
|
+
|
|
98
|
+
# Step 1 – create leaf nodes list[bytes]
|
|
99
|
+
level_hashes: List[bytes] = []
|
|
100
|
+
for val in leaves:
|
|
101
|
+
leaf = MerkleNode(None, None, val)
|
|
102
|
+
h = leaf.hash()
|
|
103
|
+
tree.nodes[h] = leaf
|
|
104
|
+
level_hashes.append(h)
|
|
105
|
+
|
|
106
|
+
height = 1 # current level (leaves)
|
|
107
|
+
|
|
108
|
+
# Step 2 – build upper levels until single root remains
|
|
109
|
+
while len(level_hashes) > 1:
|
|
110
|
+
next_level: List[bytes] = []
|
|
111
|
+
it = iter(level_hashes)
|
|
112
|
+
for left_hash in it:
|
|
113
|
+
try:
|
|
114
|
+
right_hash = next(it)
|
|
115
|
+
except StopIteration:
|
|
116
|
+
right_hash = None
|
|
117
|
+
parent = MerkleNode(left_hash, right_hash, None)
|
|
118
|
+
ph = parent.hash()
|
|
119
|
+
tree.nodes[ph] = parent
|
|
120
|
+
next_level.append(ph)
|
|
121
|
+
level_hashes = next_level
|
|
122
|
+
height += 1
|
|
123
|
+
|
|
124
|
+
tree.root_hash = level_hashes[0]
|
|
125
|
+
tree._height = height
|
|
126
|
+
return tree
|
|
127
|
+
|
|
128
|
+
# ------------------------------------------------------------------
|
|
129
|
+
# internal helpers
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
def _fetch(self, h: bytes | None) -> Optional[MerkleNode]:
|
|
132
|
+
if h is None:
|
|
133
|
+
return None
|
|
134
|
+
node = self.nodes.get(h)
|
|
135
|
+
if node is None:
|
|
136
|
+
raw = self._node_get(h)
|
|
137
|
+
if raw is None:
|
|
138
|
+
return None
|
|
139
|
+
node = MerkleNode.from_bytes(raw)
|
|
140
|
+
self.nodes[h] = node
|
|
141
|
+
return node
|
|
142
|
+
|
|
143
|
+
def _invalidate(self, node: MerkleNode) -> None:
|
|
144
|
+
node._hash = None # type: ignore[attr-defined]
|
|
145
|
+
|
|
146
|
+
def _ensure_height(self) -> None:
|
|
147
|
+
if self._height is None:
|
|
148
|
+
# Recompute by traversing leftmost branch
|
|
149
|
+
h = 0
|
|
150
|
+
nh = self.root_hash
|
|
151
|
+
while nh is not None:
|
|
152
|
+
node = self._fetch(nh)
|
|
153
|
+
nh = node.left if node and node.value is None else None
|
|
154
|
+
h += 1
|
|
155
|
+
self._height = h or 1
|
|
156
|
+
|
|
157
|
+
def _capacity(self) -> int:
|
|
158
|
+
self._ensure_height()
|
|
159
|
+
assert self._height is not None
|
|
160
|
+
return 1 << (self._height - 1)
|
|
161
|
+
|
|
162
|
+
def _path_bits(self, index: int) -> List[int]:
|
|
163
|
+
self._ensure_height()
|
|
164
|
+
assert self._height is not None
|
|
165
|
+
bits = []
|
|
166
|
+
for shift in range(self._height - 2, -1, -1):
|
|
167
|
+
bits.append((index >> shift) & 1)
|
|
168
|
+
return bits
|
|
169
|
+
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
# get / put
|
|
172
|
+
# ------------------------------------------------------------------
|
|
173
|
+
def get(self, index: int) -> Optional[bytes]:
|
|
174
|
+
if index < 0 or self.root_hash is None or index >= self._capacity():
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
node_hash = self.root_hash
|
|
178
|
+
for bit in self._path_bits(index):
|
|
179
|
+
node = self._fetch(node_hash)
|
|
180
|
+
if node is None:
|
|
181
|
+
return None
|
|
182
|
+
node_hash = node.right if bit else node.left
|
|
183
|
+
if node_hash is None:
|
|
184
|
+
return None
|
|
185
|
+
leaf = self._fetch(node_hash)
|
|
186
|
+
return leaf.value if leaf else None
|
|
187
|
+
|
|
188
|
+
def put(self, index: int, value: bytes) -> None:
|
|
189
|
+
"""Overwrite *value* at existing leaf *index*.
|
|
190
|
+
|
|
191
|
+
Raises ``IndexError`` if *index* is outside current capacity **or** if
|
|
192
|
+
the path to that leaf is missing in the stored structure.
|
|
193
|
+
"""
|
|
194
|
+
if index < 0:
|
|
195
|
+
raise IndexError("negative index")
|
|
196
|
+
if self.root_hash is None:
|
|
197
|
+
raise IndexError("tree is empty – build it first with from_leaves()")
|
|
198
|
+
if index >= self._capacity():
|
|
199
|
+
raise IndexError("index beyond tree capacity")
|
|
200
|
+
|
|
201
|
+
node_hash = self.root_hash
|
|
202
|
+
stack: List[Tuple[MerkleNode, bytes, bool]] = []
|
|
203
|
+
for bit in self._path_bits(index):
|
|
204
|
+
node = self._fetch(node_hash)
|
|
205
|
+
if node is None:
|
|
206
|
+
raise IndexError("missing node along path")
|
|
207
|
+
went_right = bool(bit)
|
|
208
|
+
child_hash = node.right if went_right else node.left
|
|
209
|
+
if child_hash is None:
|
|
210
|
+
raise IndexError("path leads into non‑existent branch")
|
|
211
|
+
stack.append((node, node.hash(), went_right))
|
|
212
|
+
node_hash = child_hash
|
|
213
|
+
|
|
214
|
+
leaf = self._fetch(node_hash)
|
|
215
|
+
if leaf is None or leaf.value is None:
|
|
216
|
+
raise IndexError("target leaf missing")
|
|
217
|
+
leaf.value = value
|
|
218
|
+
self._invalidate(leaf)
|
|
219
|
+
new_hash = leaf.hash()
|
|
220
|
+
|
|
221
|
+
# bubble updated hashes
|
|
222
|
+
for parent, old_hash, went_right in reversed(stack):
|
|
223
|
+
if went_right:
|
|
224
|
+
parent.right = new_hash
|
|
225
|
+
else:
|
|
226
|
+
parent.left = new_hash
|
|
227
|
+
self._invalidate(parent)
|
|
228
|
+
new_hash = parent.hash()
|
|
229
|
+
if new_hash != old_hash:
|
|
230
|
+
del self.nodes[old_hash]
|
|
231
|
+
self.nodes[new_hash] = parent
|
|
232
|
+
self.root_hash = new_hash
|
|
233
|
+
|
|
@@ -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
|
|
@@ -324,181 +326,6 @@ class Env:
|
|
|
324
326
|
)
|
|
325
327
|
|
|
326
328
|
|
|
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
|
-
class Block:
|
|
408
|
-
def __init__(
|
|
409
|
-
self,
|
|
410
|
-
*,
|
|
411
|
-
number: int,
|
|
412
|
-
prev_block_hash: bytes,
|
|
413
|
-
timestamp: int,
|
|
414
|
-
accounts_hash: bytes,
|
|
415
|
-
total_astre_burned: int,
|
|
416
|
-
tx_limit: int,
|
|
417
|
-
tx_root: bytes,
|
|
418
|
-
vdf_difficulty: int,
|
|
419
|
-
vdf_output: bytes,
|
|
420
|
-
vdf_proof: bytes,
|
|
421
|
-
validator_pk: bytes,
|
|
422
|
-
signature: bytes,
|
|
423
|
-
) -> None:
|
|
424
|
-
self.accounts_hash = accounts_hash
|
|
425
|
-
self.number = int(number)
|
|
426
|
-
self.prev_block_hash = prev_block_hash
|
|
427
|
-
self.timestamp = int(timestamp)
|
|
428
|
-
self.total_astre_burned = int(total_astre_burned)
|
|
429
|
-
self.tx_limit = int(tx_limit)
|
|
430
|
-
self.tx_root = tx_root
|
|
431
|
-
self.validator_pk = validator_pk
|
|
432
|
-
self.vdf_difficulty = int(vdf_difficulty)
|
|
433
|
-
self.vdf_output = vdf_output
|
|
434
|
-
self.vdf_proof = vdf_proof
|
|
435
|
-
self.signature = signature
|
|
436
|
-
self.body_hash = self._compute_body_hash()
|
|
437
|
-
|
|
438
|
-
def _body_fields_without_sig(self) -> list:
|
|
439
|
-
return [
|
|
440
|
-
self.accounts_hash,
|
|
441
|
-
self.number,
|
|
442
|
-
self.prev_block_hash,
|
|
443
|
-
self.timestamp,
|
|
444
|
-
self.total_astre_burned,
|
|
445
|
-
self.tx_limit,
|
|
446
|
-
self.tx_root,
|
|
447
|
-
self.validator_pk,
|
|
448
|
-
self.vdf_difficulty,
|
|
449
|
-
self.vdf_output,
|
|
450
|
-
self.vdf_proof,
|
|
451
|
-
]
|
|
452
|
-
|
|
453
|
-
def _compute_body_hash(self) -> bytes:
|
|
454
|
-
return blake3.blake3(encode(self._body_fields_without_sig())).digest()
|
|
455
|
-
|
|
456
|
-
def to_bytes(self) -> bytes:
|
|
457
|
-
return encode(self._body_fields_without_sig() + [self.signature])
|
|
458
|
-
|
|
459
|
-
@classmethod
|
|
460
|
-
def from_bytes(cls, blob: bytes) -> "Block":
|
|
461
|
-
(
|
|
462
|
-
accounts_hash,
|
|
463
|
-
number,
|
|
464
|
-
prev_block_hash,
|
|
465
|
-
timestamp,
|
|
466
|
-
total_astre_burned,
|
|
467
|
-
tx_limit,
|
|
468
|
-
tx_root,
|
|
469
|
-
validator_pk,
|
|
470
|
-
vdf_difficulty,
|
|
471
|
-
vdf_output,
|
|
472
|
-
vdf_proof,
|
|
473
|
-
signature
|
|
474
|
-
) = decode(blob)
|
|
475
|
-
return cls(
|
|
476
|
-
number=int(number),
|
|
477
|
-
prev_block_hash=prev_block_hash,
|
|
478
|
-
timestamp=int(timestamp),
|
|
479
|
-
accounts_hash=accounts_hash,
|
|
480
|
-
total_astre_burned=int(total_astre_burned),
|
|
481
|
-
tx_limit=int(tx_limit),
|
|
482
|
-
tx_root=tx_root,
|
|
483
|
-
vdf_difficulty=int(vdf_difficulty),
|
|
484
|
-
vdf_output=vdf_output,
|
|
485
|
-
vdf_proof=vdf_proof,
|
|
486
|
-
validator_pk=validator_pk,
|
|
487
|
-
signature=signature,
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
@property
|
|
491
|
-
def hash(self) -> bytes:
|
|
492
|
-
return blake3.blake3(self.body_hash + self.signature).digest()
|
|
493
|
-
|
|
494
|
-
def verify_block_signature(self) -> bool:
|
|
495
|
-
try:
|
|
496
|
-
pub = ed25519.Ed25519PublicKey.from_public_bytes(self.validator_pk)
|
|
497
|
-
pub.verify(self.signature, self.body_hash)
|
|
498
|
-
return True
|
|
499
|
-
except Exception:
|
|
500
|
-
return False
|
|
501
|
-
|
|
502
329
|
class Node:
|
|
503
330
|
def __init__(self, config: dict = {}):
|
|
504
331
|
self._machine_setup()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astreum
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.14
|
|
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,9 @@ 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/
|
|
25
|
-
src/astreum/
|
|
24
|
+
src/astreum/models/__init__.py
|
|
25
|
+
src/astreum/models/block.py
|
|
26
|
+
src/astreum/models/merkle.py
|
|
27
|
+
src/astreum/models/patricia.py
|
|
28
|
+
src/astreum/models/transaction.py
|
|
26
29
|
tests/test_node_machine.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|