astreum 0.2.23__py3-none-any.whl → 0.2.25__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.

Potentially problematic release.


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

@@ -109,7 +109,7 @@ def deserialize(data: bytes, D: int) -> QuadraticForm:
109
109
 
110
110
  # --- Public VDF API -----------------------------------------------------
111
111
 
112
- def generate(
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 verify(
139
+ def vdf_verify(
140
140
  old_output: bytes,
141
141
  new_output: bytes,
142
142
  proof: bytes,
astreum/models/block.py CHANGED
@@ -1,10 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from threading import Thread
3
4
  from typing import List, Dict, Any, Optional, Union
4
5
 
6
+ from astreum.crypto.wesolowski import vdf_generate
5
7
  from astreum.models.account import Account
6
8
  from astreum.models.accounts import Accounts
7
9
  from astreum.models.patricia import PatriciaTrie
10
+ from astreum.models.transaction import Transaction
8
11
  from ..crypto import ed25519
9
12
  from .merkle import MerkleTree
10
13
 
@@ -21,83 +24,44 @@ class Block:
21
24
  def __init__(
22
25
  self,
23
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,
24
41
  body_tree: Optional[MerkleTree] = None,
25
42
  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
- ]
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
44
60
 
45
61
  @property
46
62
  def hash(self) -> bytes:
47
- """Return the block hash (Merkle root of body_root || signature)."""
48
63
  return self._block_hash
49
64
 
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
65
  def get_body_hash(self) -> bytes:
102
66
  """Return the Merkle root of the body fields."""
103
67
  if not self._body_tree:
@@ -168,3 +132,216 @@ class Block:
168
132
 
169
133
  # 5 . build and return the block
170
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.23
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
@@ -4,7 +4,7 @@ astreum/node.py,sha256=dPloCXuDyIn3-KDqxlgl3jxsonJlFMLi_quwJRsoLC8,46259
4
4
  astreum/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  astreum/crypto/ed25519.py,sha256=FRnvlN0kZlxn4j-sJKl-C9tqiz_0z4LZyXLj3KIj1TQ,1760
6
6
  astreum/crypto/quadratic_form.py,sha256=pJgbORey2NTWbQNhdyvrjy_6yjORudQ67jBz2ScHptg,4037
7
- astreum/crypto/wesolowski.py,sha256=FDAX82L5cceR6DGTtUO57ZhcxpBNiskGrnLWnd_3BSw,4084
7
+ astreum/crypto/wesolowski.py,sha256=SUgGXW3Id07dJtWzDcs4dluIhjqbRWQ8YWjn_mK78AQ,4092
8
8
  astreum/crypto/x25519.py,sha256=i29v4BmwKRcbz9E7NKqFDQyxzFtJUqN0St9jd7GS1uA,1137
9
9
  astreum/lispeum/__init__.py,sha256=K-NDzIjtIsXzC9X7lnYvlvIaVxjFcY7WNsgLIE3DH3U,58
10
10
  astreum/lispeum/parser.py,sha256=jQRzZYvBuSg8t_bxsbt1-WcHaR_LPveHNX7Qlxhaw-M,1165
@@ -12,12 +12,12 @@ astreum/lispeum/tokenizer.py,sha256=J-I7MEd0r2ZoVqxvRPlu-Afe2ZdM0tKXXhf1R4SxYTo,
12
12
  astreum/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  astreum/models/account.py,sha256=sHujGSwtV13rvOGJ5LZXuMrJ4F9XUdvyuWKz-zJ9lkE,2986
14
14
  astreum/models/accounts.py,sha256=aFSEWlq6zRf65-KGAdNGqEJyNVY3fpKhx8y1vU6sgSc,1164
15
- astreum/models/block.py,sha256=3OyAAqw853aLnnWogYdkxOg_WXNEsfn2g8jrFEip2aY,6141
15
+ astreum/models/block.py,sha256=4slG6z6pZUyxQUu2on4c8nT24ztzJuQBZRje9KyRTXE,13840
16
16
  astreum/models/merkle.py,sha256=merV3rx2iRfzvglV6gNusrJf7OMbcVV854T-DUWCC64,6733
17
17
  astreum/models/patricia.py,sha256=ohmXrcaz7Ae561tyC4u4iPOkQPkKr8N0IWJek4upFIg,13392
18
18
  astreum/models/transaction.py,sha256=yBarvRK2ybMAHHEQPZpubGO7gms4U9k093xQGNQHQ4Q,3043
19
- astreum-0.2.23.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
20
- astreum-0.2.23.dist-info/METADATA,sha256=itKxfzkJrO8JcpMVCW3nDA_8NdtdndEj9zKJaoT5GGc,5478
21
- astreum-0.2.23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
- astreum-0.2.23.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
23
- astreum-0.2.23.dist-info/RECORD,,
19
+ astreum-0.2.25.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
20
+ astreum-0.2.25.dist-info/METADATA,sha256=BqaRK8hea6p3iMUkin5gqzivjdOz16_alamM9jFkteQ,5478
21
+ astreum-0.2.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
+ astreum-0.2.25.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
23
+ astreum-0.2.25.dist-info/RECORD,,