astreum 0.2.23__tar.gz → 0.2.25__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.23/src/astreum.egg-info → astreum-0.2.25}/PKG-INFO +1 -1
- {astreum-0.2.23 → astreum-0.2.25}/pyproject.toml +1 -1
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/crypto/wesolowski.py +2 -2
- astreum-0.2.25/src/astreum/models/block.py +347 -0
- {astreum-0.2.23 → astreum-0.2.25/src/astreum.egg-info}/PKG-INFO +1 -1
- astreum-0.2.23/src/astreum/models/block.py +0 -170
- {astreum-0.2.23 → astreum-0.2.25}/LICENSE +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/README.md +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/setup.cfg +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/__init__.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/crypto/__init__.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/crypto/ed25519.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/crypto/quadratic_form.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/crypto/x25519.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/format.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/lispeum/__init__.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/lispeum/parser.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/lispeum/tokenizer.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/models/__init__.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/models/account.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/models/accounts.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/models/merkle.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/models/patricia.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/models/transaction.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum/node.py +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum.egg-info/SOURCES.txt +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum.egg-info/dependency_links.txt +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum.egg-info/requires.txt +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/src/astreum.egg-info/top_level.txt +0 -0
- {astreum-0.2.23 → astreum-0.2.25}/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.25
|
|
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
|
|
@@ -109,7 +109,7 @@ def deserialize(data: bytes, D: int) -> QuadraticForm:
|
|
|
109
109
|
|
|
110
110
|
# --- Public VDF API -----------------------------------------------------
|
|
111
111
|
|
|
112
|
-
def
|
|
112
|
+
def vdf_generate(
|
|
113
113
|
old_output: bytes,
|
|
114
114
|
T: int,
|
|
115
115
|
D: int
|
|
@@ -136,7 +136,7 @@ def generate(
|
|
|
136
136
|
return y_bytes, proof_bytes
|
|
137
137
|
|
|
138
138
|
|
|
139
|
-
def
|
|
139
|
+
def vdf_verify(
|
|
140
140
|
old_output: bytes,
|
|
141
141
|
new_output: bytes,
|
|
142
142
|
proof: bytes,
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from threading import Thread
|
|
4
|
+
from typing import List, Dict, Any, Optional, Union
|
|
5
|
+
|
|
6
|
+
from astreum.crypto.wesolowski import vdf_generate
|
|
7
|
+
from astreum.models.account import Account
|
|
8
|
+
from astreum.models.accounts import Accounts
|
|
9
|
+
from astreum.models.patricia import PatriciaTrie
|
|
10
|
+
from astreum.models.transaction import Transaction
|
|
11
|
+
from ..crypto import ed25519
|
|
12
|
+
from .merkle import MerkleTree
|
|
13
|
+
|
|
14
|
+
# Constants for integer field names
|
|
15
|
+
_INT_FIELDS = {
|
|
16
|
+
"delay_difficulty",
|
|
17
|
+
"number",
|
|
18
|
+
"timestamp",
|
|
19
|
+
"transaction_limit",
|
|
20
|
+
"transactions_total_fees",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class Block:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
block_hash: bytes,
|
|
27
|
+
*,
|
|
28
|
+
number: Optional[int] = None,
|
|
29
|
+
prev_block_hash: Optional[bytes] = None,
|
|
30
|
+
timestamp: Optional[int] = None,
|
|
31
|
+
accounts_hash: Optional[bytes] = None,
|
|
32
|
+
accounts: Optional[Accounts] = None,
|
|
33
|
+
transaction_limit: Optional[int] = None,
|
|
34
|
+
transactions_total_fees: Optional[int] = None,
|
|
35
|
+
transactions_root_hash: Optional[bytes] = None,
|
|
36
|
+
transactions_count: Optional[int] = None,
|
|
37
|
+
delay_difficulty: Optional[int] = None,
|
|
38
|
+
delay_output: Optional[bytes] = None,
|
|
39
|
+
delay_proof: Optional[bytes] = None,
|
|
40
|
+
validator_pk: Optional[bytes] = None,
|
|
41
|
+
body_tree: Optional[MerkleTree] = None,
|
|
42
|
+
signature: Optional[bytes] = None,
|
|
43
|
+
):
|
|
44
|
+
self.hash = block_hash
|
|
45
|
+
self.number = number
|
|
46
|
+
self.prev_block_hash = prev_block_hash
|
|
47
|
+
self.timestamp = timestamp
|
|
48
|
+
self.accounts_hash = accounts_hash
|
|
49
|
+
self.accounts = accounts
|
|
50
|
+
self.transaction_limit = transaction_limit
|
|
51
|
+
self.transactions_total_fees = transactions_total_fees
|
|
52
|
+
self.transactions_root_hash = transactions_root_hash
|
|
53
|
+
self.transactions_count = transactions_count
|
|
54
|
+
self.delay_difficulty = delay_difficulty
|
|
55
|
+
self.delay_output = delay_output
|
|
56
|
+
self.delay_proof = delay_proof
|
|
57
|
+
self.validator_pk = validator_pk
|
|
58
|
+
self.body_tree = body_tree
|
|
59
|
+
self.signature = signature
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def hash(self) -> bytes:
|
|
63
|
+
return self._block_hash
|
|
64
|
+
|
|
65
|
+
def get_body_hash(self) -> bytes:
|
|
66
|
+
"""Return the Merkle root of the body fields."""
|
|
67
|
+
if not self._body_tree:
|
|
68
|
+
raise ValueError("Body tree not available for this block instance.")
|
|
69
|
+
return self._body_tree.root_hash
|
|
70
|
+
|
|
71
|
+
def get_signature(self) -> bytes:
|
|
72
|
+
"""Return the block's signature leaf."""
|
|
73
|
+
if self._signature is None:
|
|
74
|
+
raise ValueError("Signature not available for this block instance.")
|
|
75
|
+
return self._signature
|
|
76
|
+
|
|
77
|
+
def get_field(self, name: str) -> Union[int, bytes]:
|
|
78
|
+
"""Query a single body field by name, returning an int or bytes."""
|
|
79
|
+
if name not in self._field_names:
|
|
80
|
+
raise KeyError(f"Unknown field: {name}")
|
|
81
|
+
if not self._body_tree:
|
|
82
|
+
raise ValueError("Body tree not available for field queries.")
|
|
83
|
+
idx = self._field_names.index(name)
|
|
84
|
+
leaf_bytes = self._body_tree.leaves[idx]
|
|
85
|
+
if name in _INT_FIELDS:
|
|
86
|
+
return int.from_bytes(leaf_bytes, "big")
|
|
87
|
+
return leaf_bytes
|
|
88
|
+
|
|
89
|
+
def verify_block_signature(self) -> bool:
|
|
90
|
+
"""Verify the block's Ed25519 signature against its body root."""
|
|
91
|
+
pub = ed25519.Ed25519PublicKey.from_public_bytes(
|
|
92
|
+
self.get_field("validator_pk")
|
|
93
|
+
)
|
|
94
|
+
try:
|
|
95
|
+
pub.verify(self.get_signature(), self.get_body_hash())
|
|
96
|
+
return True
|
|
97
|
+
except Exception:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def genesis(cls, validator_addr: bytes) -> "Block":
|
|
102
|
+
# 1 . validator-stakes sub-trie
|
|
103
|
+
stake_trie = PatriciaTrie()
|
|
104
|
+
stake_trie.put(validator_addr, (1).to_bytes(32, "big"))
|
|
105
|
+
stake_root = stake_trie.root_hash
|
|
106
|
+
|
|
107
|
+
# 2 . build the two Account bodies
|
|
108
|
+
validator_acct = Account.create(balance=0, data=b"", nonce=0)
|
|
109
|
+
treasury_acct = Account.create(balance=1, data=stake_root, nonce=0)
|
|
110
|
+
|
|
111
|
+
# 3 . global Accounts structure
|
|
112
|
+
accts = Accounts()
|
|
113
|
+
accts.set_account(validator_addr, validator_acct)
|
|
114
|
+
accts.set_account(b"\x11" * 32, treasury_acct)
|
|
115
|
+
accounts_hash = accts.root_hash
|
|
116
|
+
|
|
117
|
+
# 4 . constant body fields for genesis
|
|
118
|
+
body_kwargs = dict(
|
|
119
|
+
number = 0,
|
|
120
|
+
prev_block_hash = b"\x00" * 32,
|
|
121
|
+
timestamp = 0,
|
|
122
|
+
accounts_hash = accounts_hash,
|
|
123
|
+
transactions_total_fees = 0,
|
|
124
|
+
transaction_limit = 0,
|
|
125
|
+
transactions_root_hash = b"\x00" * 32,
|
|
126
|
+
delay_difficulty = 0,
|
|
127
|
+
delay_output = b"",
|
|
128
|
+
delay_proof = b"",
|
|
129
|
+
validator_pk = validator_addr,
|
|
130
|
+
signature = b"",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# 5 . build and return the block
|
|
134
|
+
return cls.create(**body_kwargs)
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def build(
|
|
138
|
+
cls,
|
|
139
|
+
previous_block: "Block",
|
|
140
|
+
transactions: List[Transaction],
|
|
141
|
+
*,
|
|
142
|
+
validator_sk, # private key for signing
|
|
143
|
+
natural_rate: float = 0.618,
|
|
144
|
+
) -> "Block":
|
|
145
|
+
TREASURY = b"\x11" * 32
|
|
146
|
+
BURN = b"\x00" * 32
|
|
147
|
+
|
|
148
|
+
blk = cls(
|
|
149
|
+
block_hash=b"",
|
|
150
|
+
number=previous_block.number + 1,
|
|
151
|
+
prev_block_hash=previous_block.hash,
|
|
152
|
+
timestamp=previous_block.timestamp + 1,
|
|
153
|
+
accounts_hash=previous_block.accounts_hash,
|
|
154
|
+
transaction_limit=previous_block.transaction_limit,
|
|
155
|
+
transactions_count=0,
|
|
156
|
+
validator_pk=validator_sk.public_key().public_bytes(),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# ------------------ difficulty via natural_rate -----------------------
|
|
160
|
+
prev_bt = previous_block.block_time or 0
|
|
161
|
+
prev_diff = previous_block.delay_difficulty or 1
|
|
162
|
+
if prev_bt <= 1:
|
|
163
|
+
blk.delay_difficulty = max(1, int(prev_diff / natural_rate)) # increase
|
|
164
|
+
else:
|
|
165
|
+
blk.delay_difficulty = max(1, int(prev_diff * natural_rate)) # decrease
|
|
166
|
+
|
|
167
|
+
# ------------------ launch VDF in background --------------------------
|
|
168
|
+
vdf_result: dict[str, bytes] = {}
|
|
169
|
+
|
|
170
|
+
def _vdf_worker():
|
|
171
|
+
y, p = vdf_generate(previous_block.delay_output, blk.delay_difficulty, -4)
|
|
172
|
+
vdf_result["y"] = y
|
|
173
|
+
vdf_result["p"] = p
|
|
174
|
+
|
|
175
|
+
Thread(target=_vdf_worker, daemon=True).start()
|
|
176
|
+
|
|
177
|
+
# ------------------ process transactions -----------------------------
|
|
178
|
+
for tx in transactions:
|
|
179
|
+
try:
|
|
180
|
+
blk.apply_tx(tx)
|
|
181
|
+
except ValueError:
|
|
182
|
+
break
|
|
183
|
+
|
|
184
|
+
# ------------------ split fees --------------------------------------
|
|
185
|
+
burn_amt = blk.total_fees // 2
|
|
186
|
+
reward_amt = blk.total_fees - burn_amt
|
|
187
|
+
|
|
188
|
+
def _credit(addr: bytes, amt: int):
|
|
189
|
+
acc = blk.accounts.get_account(addr) or Account.create(0, b"", 0)
|
|
190
|
+
blk.accounts.set_account(addr, Account.create(acc.balance() + amt, acc.data(), acc.nonce()))
|
|
191
|
+
|
|
192
|
+
if burn_amt:
|
|
193
|
+
_credit(BURN, burn_amt)
|
|
194
|
+
if reward_amt:
|
|
195
|
+
_credit(blk.validator_pk, reward_amt)
|
|
196
|
+
|
|
197
|
+
# ------------------ update tx limit with natural_rate ---------------
|
|
198
|
+
prev_limit = previous_block.transaction_limit
|
|
199
|
+
prev_tx_count = previous_block.transactions_count
|
|
200
|
+
grow_thr = prev_limit * natural_rate
|
|
201
|
+
shrink_thr = prev_tx_count * natural_rate
|
|
202
|
+
|
|
203
|
+
if prev_tx_count > grow_thr:
|
|
204
|
+
blk.transaction_limit = prev_tx_count
|
|
205
|
+
elif prev_tx_count < shrink_thr:
|
|
206
|
+
blk.transaction_limit = max(1, int(prev_limit * natural_rate))
|
|
207
|
+
else:
|
|
208
|
+
blk.transaction_limit = prev_limit
|
|
209
|
+
|
|
210
|
+
# ------------------ wait for VDF ------------------------------------
|
|
211
|
+
while "y" not in vdf_result:
|
|
212
|
+
pass
|
|
213
|
+
blk.delay_output = vdf_result["y"]
|
|
214
|
+
blk.delay_proof = vdf_result["p"]
|
|
215
|
+
|
|
216
|
+
# ------------------ timing & roots ----------------------------------
|
|
217
|
+
blk.block_time = blk.timestamp - previous_block.timestamp
|
|
218
|
+
blk.accounts_hash = blk.accounts.root_hash
|
|
219
|
+
blk.transactions_root_hash = MerkleTree.from_leaves(blk.tx_hashes).root_hash
|
|
220
|
+
blk.transactions_total_fees = blk.total_fees
|
|
221
|
+
|
|
222
|
+
# ------------------ build full body root ----------------------------
|
|
223
|
+
body_fields = {
|
|
224
|
+
"accounts_hash": blk.accounts_hash,
|
|
225
|
+
"block_time": blk.block_time,
|
|
226
|
+
"delay_difficulty": blk.delay_difficulty,
|
|
227
|
+
"delay_output": blk.delay_output,
|
|
228
|
+
"delay_proof": blk.delay_proof,
|
|
229
|
+
"number": blk.number,
|
|
230
|
+
"prev_block_hash": blk.prev_block_hash,
|
|
231
|
+
"timestamp": blk.timestamp,
|
|
232
|
+
"transaction_limit": blk.transaction_limit,
|
|
233
|
+
"transactions_count": blk.transactions_count,
|
|
234
|
+
"transactions_root_hash": blk.transactions_root_hash,
|
|
235
|
+
"transactions_total_fees": blk.transactions_total_fees,
|
|
236
|
+
"validator_pk": blk.validator_pk,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
leaves: List[bytes] = []
|
|
240
|
+
for k in sorted(body_fields):
|
|
241
|
+
v = body_fields[k]
|
|
242
|
+
if isinstance(v, bytes):
|
|
243
|
+
leaves.append(v)
|
|
244
|
+
else:
|
|
245
|
+
leaves.append(int(v).to_bytes((v.bit_length() + 7) // 8 or 1, "big"))
|
|
246
|
+
|
|
247
|
+
body_root = MerkleTree.from_leaves(leaves).root_hash
|
|
248
|
+
blk.body_tree = MerkleTree.from_leaves([body_root])
|
|
249
|
+
blk.signature = validator_sk.sign(body_root)
|
|
250
|
+
blk.hash = MerkleTree.from_leaves([body_root, blk.signature]).root_hash
|
|
251
|
+
|
|
252
|
+
return blk
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def apply_tx(self, tx: Transaction) -> None:
|
|
256
|
+
# --- lazy state ----------------------------------------------------
|
|
257
|
+
if not hasattr(self, "accounts") or self.accounts is None:
|
|
258
|
+
self.accounts = Accounts(root_hash=self.accounts_hash)
|
|
259
|
+
if not hasattr(self, "total_fees"):
|
|
260
|
+
self.total_fees = 0
|
|
261
|
+
self.tx_hashes = []
|
|
262
|
+
self.transactions_count = 0
|
|
263
|
+
|
|
264
|
+
TREASURY = b"\x11" * 32
|
|
265
|
+
BURN = b"\x00" * 32
|
|
266
|
+
|
|
267
|
+
# --- cap check -----------------------------------------------------
|
|
268
|
+
if self.transactions_count >= self.transaction_limit:
|
|
269
|
+
raise ValueError("block transaction limit reached")
|
|
270
|
+
|
|
271
|
+
# --- unpack tx -----------------------------------------------------
|
|
272
|
+
sender_pk = tx.get_sender_pk()
|
|
273
|
+
recip_pk = tx.get_recipient_pk()
|
|
274
|
+
amount = tx.get_amount()
|
|
275
|
+
fee = tx.get_fee()
|
|
276
|
+
nonce = tx.get_nonce()
|
|
277
|
+
|
|
278
|
+
sender_acct = self.accounts.get_account(sender_pk)
|
|
279
|
+
if (sender_acct is None
|
|
280
|
+
or sender_acct.nonce() != nonce
|
|
281
|
+
or sender_acct.balance() < amount + fee):
|
|
282
|
+
raise ValueError("invalid or unaffordable transaction")
|
|
283
|
+
|
|
284
|
+
# --- debit sender --------------------------------------------------
|
|
285
|
+
self.accounts.set_account(
|
|
286
|
+
sender_pk,
|
|
287
|
+
Account.create(
|
|
288
|
+
balance=sender_acct.balance() - amount - fee,
|
|
289
|
+
data=sender_acct.data(),
|
|
290
|
+
nonce=sender_acct.nonce() + 1,
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# --- destination handling -----------------------------------------
|
|
295
|
+
if recip_pk == TREASURY:
|
|
296
|
+
treasury = self.accounts.get_account(TREASURY)
|
|
297
|
+
|
|
298
|
+
trie = PatriciaTrie(node_get=None, root_hash=treasury.data())
|
|
299
|
+
stake_bytes = trie.get(sender_pk) or b""
|
|
300
|
+
current_stake = int.from_bytes(stake_bytes, "big") if stake_bytes else 0
|
|
301
|
+
|
|
302
|
+
if amount > 0:
|
|
303
|
+
# stake **deposit**
|
|
304
|
+
trie.put(sender_pk, (current_stake + amount).to_bytes(32, "big"))
|
|
305
|
+
new_treas_bal = treasury.balance() + amount
|
|
306
|
+
else:
|
|
307
|
+
# stake **withdrawal**
|
|
308
|
+
if current_stake == 0:
|
|
309
|
+
raise ValueError("no stake to withdraw")
|
|
310
|
+
# move stake back to sender balance
|
|
311
|
+
sender_after = self.accounts.get_account(sender_pk)
|
|
312
|
+
self.accounts.set_account(
|
|
313
|
+
sender_pk,
|
|
314
|
+
Account.create(
|
|
315
|
+
balance=sender_after.balance() + current_stake,
|
|
316
|
+
data=sender_after.data(),
|
|
317
|
+
nonce=sender_after.nonce(),
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
trie.delete(sender_pk)
|
|
321
|
+
new_treas_bal = treasury.balance() # treasury balance unchanged
|
|
322
|
+
|
|
323
|
+
# write back treasury with new trie root
|
|
324
|
+
self.accounts.set_account(
|
|
325
|
+
TREASURY,
|
|
326
|
+
Account.create(
|
|
327
|
+
balance=new_treas_bal,
|
|
328
|
+
data=trie.root_hash,
|
|
329
|
+
nonce=treasury.nonce(),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
else:
|
|
334
|
+
recip_acct = self.accounts.get_account(recip_pk) or Account.create(0, b"", 0)
|
|
335
|
+
self.accounts.set_account(
|
|
336
|
+
recip_pk,
|
|
337
|
+
Account.create(
|
|
338
|
+
balance=recip_acct.balance() + amount,
|
|
339
|
+
data=recip_acct.data(),
|
|
340
|
+
nonce=recip_acct.nonce(),
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# --- accumulate fee & record --------------------------------------
|
|
345
|
+
self.total_fees += fee
|
|
346
|
+
self.tx_hashes.append(tx.hash)
|
|
347
|
+
self.transactions_count += 1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astreum
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.25
|
|
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
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import List, Dict, Any, Optional, Union
|
|
4
|
-
|
|
5
|
-
from astreum.models.account import Account
|
|
6
|
-
from astreum.models.accounts import Accounts
|
|
7
|
-
from astreum.models.patricia import PatriciaTrie
|
|
8
|
-
from ..crypto import ed25519
|
|
9
|
-
from .merkle import MerkleTree
|
|
10
|
-
|
|
11
|
-
# Constants for integer field names
|
|
12
|
-
_INT_FIELDS = {
|
|
13
|
-
"delay_difficulty",
|
|
14
|
-
"number",
|
|
15
|
-
"timestamp",
|
|
16
|
-
"transaction_limit",
|
|
17
|
-
"transactions_total_fees",
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
class Block:
|
|
21
|
-
def __init__(
|
|
22
|
-
self,
|
|
23
|
-
block_hash: bytes,
|
|
24
|
-
body_tree: Optional[MerkleTree] = None,
|
|
25
|
-
signature: Optional[bytes] = None,
|
|
26
|
-
) -> None:
|
|
27
|
-
self._block_hash = block_hash
|
|
28
|
-
self._body_tree = body_tree
|
|
29
|
-
self._signature = signature
|
|
30
|
-
# store field names in alphabetical order for consistent indexing
|
|
31
|
-
self._field_names = [
|
|
32
|
-
"accounts_hash",
|
|
33
|
-
"delay_difficulty",
|
|
34
|
-
"delay_output",
|
|
35
|
-
"delay_proof",
|
|
36
|
-
"number",
|
|
37
|
-
"prev_block_hash",
|
|
38
|
-
"timestamp",
|
|
39
|
-
"transaction_limit",
|
|
40
|
-
"transactions_root_hash",
|
|
41
|
-
"transactions_total_fees",
|
|
42
|
-
"validator_pk",
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
@property
|
|
46
|
-
def hash(self) -> bytes:
|
|
47
|
-
"""Return the block hash (Merkle root of body_root || signature)."""
|
|
48
|
-
return self._block_hash
|
|
49
|
-
|
|
50
|
-
@classmethod
|
|
51
|
-
def create(
|
|
52
|
-
cls,
|
|
53
|
-
number: int,
|
|
54
|
-
prev_block_hash: bytes,
|
|
55
|
-
timestamp: int,
|
|
56
|
-
accounts_hash: bytes,
|
|
57
|
-
transactions_total_fees: int,
|
|
58
|
-
transaction_limit: int,
|
|
59
|
-
transactions_root_hash: bytes,
|
|
60
|
-
delay_difficulty: int,
|
|
61
|
-
delay_output: bytes,
|
|
62
|
-
delay_proof: bytes,
|
|
63
|
-
validator_pk: bytes,
|
|
64
|
-
signature: bytes,
|
|
65
|
-
) -> Block:
|
|
66
|
-
"""Build a new block by hashing the provided fields into Merkle trees."""
|
|
67
|
-
# map fields by name
|
|
68
|
-
field_map: Dict[str, Any] = {
|
|
69
|
-
"accounts_hash": accounts_hash,
|
|
70
|
-
"delay_difficulty": delay_difficulty,
|
|
71
|
-
"delay_output": delay_output,
|
|
72
|
-
"delay_proof": delay_proof,
|
|
73
|
-
"number": number,
|
|
74
|
-
"prev_block_hash": prev_block_hash,
|
|
75
|
-
"timestamp": timestamp,
|
|
76
|
-
"transaction_limit": transaction_limit,
|
|
77
|
-
"transactions_root_hash": transactions_root_hash,
|
|
78
|
-
"transactions_total_fees": transactions_total_fees,
|
|
79
|
-
"validator_pk": validator_pk,
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
leaves: List[bytes] = []
|
|
83
|
-
for name in sorted(field_map):
|
|
84
|
-
v = field_map[name]
|
|
85
|
-
if isinstance(v, bytes):
|
|
86
|
-
leaf_bytes = v
|
|
87
|
-
elif isinstance(v, int):
|
|
88
|
-
length = (v.bit_length() + 7) // 8 or 1
|
|
89
|
-
leaf_bytes = v.to_bytes(length, "big")
|
|
90
|
-
else:
|
|
91
|
-
raise TypeError(f"Unsupported field type for '{name}': {type(v)}")
|
|
92
|
-
leaves.append(leaf_bytes)
|
|
93
|
-
|
|
94
|
-
body_tree = MerkleTree.from_leaves(leaves)
|
|
95
|
-
body_root = body_tree.root_hash
|
|
96
|
-
top_tree = MerkleTree.from_leaves([body_root, signature])
|
|
97
|
-
block_hash = top_tree.root_hash
|
|
98
|
-
|
|
99
|
-
return cls(block_hash, body_tree, signature)
|
|
100
|
-
|
|
101
|
-
def get_body_hash(self) -> bytes:
|
|
102
|
-
"""Return the Merkle root of the body fields."""
|
|
103
|
-
if not self._body_tree:
|
|
104
|
-
raise ValueError("Body tree not available for this block instance.")
|
|
105
|
-
return self._body_tree.root_hash
|
|
106
|
-
|
|
107
|
-
def get_signature(self) -> bytes:
|
|
108
|
-
"""Return the block's signature leaf."""
|
|
109
|
-
if self._signature is None:
|
|
110
|
-
raise ValueError("Signature not available for this block instance.")
|
|
111
|
-
return self._signature
|
|
112
|
-
|
|
113
|
-
def get_field(self, name: str) -> Union[int, bytes]:
|
|
114
|
-
"""Query a single body field by name, returning an int or bytes."""
|
|
115
|
-
if name not in self._field_names:
|
|
116
|
-
raise KeyError(f"Unknown field: {name}")
|
|
117
|
-
if not self._body_tree:
|
|
118
|
-
raise ValueError("Body tree not available for field queries.")
|
|
119
|
-
idx = self._field_names.index(name)
|
|
120
|
-
leaf_bytes = self._body_tree.leaves[idx]
|
|
121
|
-
if name in _INT_FIELDS:
|
|
122
|
-
return int.from_bytes(leaf_bytes, "big")
|
|
123
|
-
return leaf_bytes
|
|
124
|
-
|
|
125
|
-
def verify_block_signature(self) -> bool:
|
|
126
|
-
"""Verify the block's Ed25519 signature against its body root."""
|
|
127
|
-
pub = ed25519.Ed25519PublicKey.from_public_bytes(
|
|
128
|
-
self.get_field("validator_pk")
|
|
129
|
-
)
|
|
130
|
-
try:
|
|
131
|
-
pub.verify(self.get_signature(), self.get_body_hash())
|
|
132
|
-
return True
|
|
133
|
-
except Exception:
|
|
134
|
-
return False
|
|
135
|
-
|
|
136
|
-
@classmethod
|
|
137
|
-
def genesis(cls, validator_addr: bytes) -> "Block":
|
|
138
|
-
# 1 . validator-stakes sub-trie
|
|
139
|
-
stake_trie = PatriciaTrie()
|
|
140
|
-
stake_trie.put(validator_addr, (1).to_bytes(32, "big"))
|
|
141
|
-
stake_root = stake_trie.root_hash
|
|
142
|
-
|
|
143
|
-
# 2 . build the two Account bodies
|
|
144
|
-
validator_acct = Account.create(balance=0, data=b"", nonce=0)
|
|
145
|
-
treasury_acct = Account.create(balance=1, data=stake_root, nonce=0)
|
|
146
|
-
|
|
147
|
-
# 3 . global Accounts structure
|
|
148
|
-
accts = Accounts()
|
|
149
|
-
accts.set_account(validator_addr, validator_acct)
|
|
150
|
-
accts.set_account(b"\x11" * 32, treasury_acct)
|
|
151
|
-
accounts_hash = accts.root_hash
|
|
152
|
-
|
|
153
|
-
# 4 . constant body fields for genesis
|
|
154
|
-
body_kwargs = dict(
|
|
155
|
-
number = 0,
|
|
156
|
-
prev_block_hash = b"\x00" * 32,
|
|
157
|
-
timestamp = 0,
|
|
158
|
-
accounts_hash = accounts_hash,
|
|
159
|
-
transactions_total_fees = 0,
|
|
160
|
-
transaction_limit = 0,
|
|
161
|
-
transactions_root_hash = b"\x00" * 32,
|
|
162
|
-
delay_difficulty = 0,
|
|
163
|
-
delay_output = b"",
|
|
164
|
-
delay_proof = b"",
|
|
165
|
-
validator_pk = validator_addr,
|
|
166
|
-
signature = b"",
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
# 5 . build and return the block
|
|
170
|
-
return cls.create(**body_kwargs)
|
|
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
|
|
File without changes
|
|
File without changes
|