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

astreum/models/block.py CHANGED
@@ -1,6 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
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 astreum.models.transaction import Transaction
4
9
  from ..crypto import ed25519
5
10
  from .merkle import MerkleTree
6
11
 
@@ -17,83 +22,44 @@ class Block:
17
22
  def __init__(
18
23
  self,
19
24
  block_hash: bytes,
25
+ *,
26
+ number: Optional[int] = None,
27
+ prev_block_hash: Optional[bytes] = None,
28
+ timestamp: Optional[int] = None,
29
+ accounts_hash: Optional[bytes] = None,
30
+ accounts: Optional[Accounts] = None,
31
+ transaction_limit: Optional[int] = None,
32
+ transactions_total_fees: Optional[int] = None,
33
+ transactions_root_hash: Optional[bytes] = None,
34
+ transactions_count: Optional[int] = None,
35
+ delay_difficulty: Optional[int] = None,
36
+ delay_output: Optional[bytes] = None,
37
+ delay_proof: Optional[bytes] = None,
38
+ validator_pk: Optional[bytes] = None,
20
39
  body_tree: Optional[MerkleTree] = None,
21
40
  signature: Optional[bytes] = None,
22
- ) -> None:
23
- self._block_hash = block_hash
24
- self._body_tree = body_tree
25
- self._signature = signature
26
- # store field names in alphabetical order for consistent indexing
27
- self._field_names = [
28
- "accounts_hash",
29
- "delay_difficulty",
30
- "delay_output",
31
- "delay_proof",
32
- "number",
33
- "prev_block_hash",
34
- "timestamp",
35
- "transaction_limit",
36
- "transactions_root_hash",
37
- "transactions_total_fees",
38
- "validator_pk",
39
- ]
41
+ ):
42
+ self.hash = block_hash
43
+ self.number = number
44
+ self.prev_block_hash = prev_block_hash
45
+ self.timestamp = timestamp
46
+ self.accounts_hash = accounts_hash
47
+ self.accounts = accounts
48
+ self.transaction_limit = transaction_limit
49
+ self.transactions_total_fees = transactions_total_fees
50
+ self.transactions_root_hash = transactions_root_hash
51
+ self.transactions_count = transactions_count
52
+ self.delay_difficulty = delay_difficulty
53
+ self.delay_output = delay_output
54
+ self.delay_proof = delay_proof
55
+ self.validator_pk = validator_pk
56
+ self.body_tree = body_tree
57
+ self.signature = signature
40
58
 
41
59
  @property
42
60
  def hash(self) -> bytes:
43
- """Return the block hash (Merkle root of body_root || signature)."""
44
61
  return self._block_hash
45
62
 
46
- @classmethod
47
- def create(
48
- cls,
49
- number: int,
50
- prev_block_hash: bytes,
51
- timestamp: int,
52
- accounts_hash: bytes,
53
- transactions_total_fees: int,
54
- transaction_limit: int,
55
- transactions_root_hash: bytes,
56
- delay_difficulty: int,
57
- delay_output: bytes,
58
- delay_proof: bytes,
59
- validator_pk: bytes,
60
- signature: bytes,
61
- ) -> Block:
62
- """Build a new block by hashing the provided fields into Merkle trees."""
63
- # map fields by name
64
- field_map: Dict[str, Any] = {
65
- "accounts_hash": accounts_hash,
66
- "delay_difficulty": delay_difficulty,
67
- "delay_output": delay_output,
68
- "delay_proof": delay_proof,
69
- "number": number,
70
- "prev_block_hash": prev_block_hash,
71
- "timestamp": timestamp,
72
- "transaction_limit": transaction_limit,
73
- "transactions_root_hash": transactions_root_hash,
74
- "transactions_total_fees": transactions_total_fees,
75
- "validator_pk": validator_pk,
76
- }
77
-
78
- leaves: List[bytes] = []
79
- for name in sorted(field_map):
80
- v = field_map[name]
81
- if isinstance(v, bytes):
82
- leaf_bytes = v
83
- elif isinstance(v, int):
84
- length = (v.bit_length() + 7) // 8 or 1
85
- leaf_bytes = v.to_bytes(length, "big")
86
- else:
87
- raise TypeError(f"Unsupported field type for '{name}': {type(v)}")
88
- leaves.append(leaf_bytes)
89
-
90
- body_tree = MerkleTree.from_leaves(leaves)
91
- body_root = body_tree.root_hash
92
- top_tree = MerkleTree.from_leaves([body_root, signature])
93
- block_hash = top_tree.root_hash
94
-
95
- return cls(block_hash, body_tree, signature)
96
-
97
63
  def get_body_hash(self) -> bytes:
98
64
  """Return the Merkle root of the body fields."""
99
65
  if not self._body_tree:
@@ -128,3 +94,207 @@ class Block:
128
94
  return True
129
95
  except Exception:
130
96
  return False
97
+
98
+ @classmethod
99
+ def genesis(cls, validator_addr: bytes) -> "Block":
100
+ # 1 . validator-stakes sub-trie
101
+ stake_trie = PatriciaTrie()
102
+ stake_trie.put(validator_addr, (1).to_bytes(32, "big"))
103
+ stake_root = stake_trie.root_hash
104
+
105
+ # 2 . build the two Account bodies
106
+ validator_acct = Account.create(balance=0, data=b"", nonce=0)
107
+ treasury_acct = Account.create(balance=1, data=stake_root, nonce=0)
108
+
109
+ # 3 . global Accounts structure
110
+ accts = Accounts()
111
+ accts.set_account(validator_addr, validator_acct)
112
+ accts.set_account(b"\x11" * 32, treasury_acct)
113
+ accounts_hash = accts.root_hash
114
+
115
+ # 4 . constant body fields for genesis
116
+ body_kwargs = dict(
117
+ number = 0,
118
+ prev_block_hash = b"\x00" * 32,
119
+ timestamp = 0,
120
+ accounts_hash = accounts_hash,
121
+ transactions_total_fees = 0,
122
+ transaction_limit = 0,
123
+ transactions_root_hash = b"\x00" * 32,
124
+ delay_difficulty = 0,
125
+ delay_output = b"",
126
+ delay_proof = b"",
127
+ validator_pk = validator_addr,
128
+ signature = b"",
129
+ )
130
+
131
+ # 5 . build and return the block
132
+ return cls.create(**body_kwargs)
133
+
134
+ @classmethod
135
+ def build(
136
+ cls,
137
+ previous_block: "Block",
138
+ transactions: list[Transaction],
139
+ *,
140
+ validator_pk: bytes,
141
+ natural_rate: float = 0.618,
142
+ ) -> "Block":
143
+ BURN = b"\x00" * 32
144
+
145
+ # --- 0. create an empty block-in-progress, seeded with parent fields ----
146
+ blk = cls(
147
+ block_hash=b"", # placeholder; set at the end
148
+ number=previous_block.number + 1,
149
+ prev_block_hash=previous_block.hash,
150
+ timestamp=previous_block.timestamp + 1,
151
+ accounts_hash=previous_block.accounts_hash,
152
+ transaction_limit=previous_block.transaction_limit,
153
+ transactions_count=0,
154
+ )
155
+
156
+ # --- 1. apply up to transaction_limit txs -------------------------------
157
+ for tx in transactions:
158
+ try:
159
+ blk.apply_tx(tx) # ← NEW single-line call
160
+ except ValueError:
161
+ break # stop at first invalid or cap reached
162
+
163
+ # --- 2. split fees after all txs ----------------------------------------
164
+ burn_amt = blk.total_fees // 2
165
+ reward_amt = blk.total_fees - burn_amt
166
+ if burn_amt:
167
+ blk.accounts.set_account(
168
+ BURN,
169
+ Account.create(
170
+ balance=(blk.accounts.get_account(BURN) or Account.create(0, b"", 0)).balance() + burn_amt,
171
+ data=b"",
172
+ nonce=0,
173
+ ),
174
+ )
175
+ if reward_amt:
176
+ blk.accounts.set_account(
177
+ validator_pk,
178
+ Account.create(
179
+ balance=(blk.accounts.get_account(validator_pk) or Account.create(0, b"", 0)).balance() + reward_amt,
180
+ data=b"",
181
+ nonce=0,
182
+ ),
183
+ )
184
+
185
+ # --- 3. recalc tx-limit via prev metrics -------------------------------
186
+ prev_limit = previous_block.transaction_limit
187
+ prev_tx_count = previous_block.transactions_count
188
+ threshold = prev_limit * natural_rate
189
+ if prev_tx_count > threshold:
190
+ blk.transaction_limit = prev_tx_count
191
+ elif prev_tx_count < threshold:
192
+ blk.transaction_limit = max(1, int(prev_limit * natural_rate))
193
+ else:
194
+ blk.transaction_limit = prev_limit
195
+
196
+ # --- 4. finalise block hash & header roots ------------------------------
197
+ blk.accounts_hash = blk.accounts.root_hash
198
+ blk.transactions_root_hash = MerkleTree.from_leaves(blk.tx_hashes).root_hash
199
+ blk.hash = MerkleTree.from_leaves([
200
+ blk.transactions_root_hash,
201
+ blk.accounts_hash,
202
+ blk.total_fees.to_bytes(8, "big"),
203
+ ]).root_hash # or your existing body-root/signing scheme
204
+
205
+ return blk
206
+
207
+
208
+ def apply_tx(self, tx: Transaction) -> None:
209
+ # --- lazy state ----------------------------------------------------
210
+ if not hasattr(self, "accounts") or self.accounts is None:
211
+ self.accounts = Accounts(root_hash=self.accounts_hash)
212
+ if not hasattr(self, "total_fees"):
213
+ self.total_fees = 0
214
+ self.tx_hashes = []
215
+ self.transactions_count = 0
216
+
217
+ TREASURY = b"\x11" * 32
218
+ BURN = b"\x00" * 32
219
+
220
+ # --- cap check -----------------------------------------------------
221
+ if self.transactions_count >= self.transaction_limit:
222
+ raise ValueError("block transaction limit reached")
223
+
224
+ # --- unpack tx -----------------------------------------------------
225
+ sender_pk = tx.get_sender_pk()
226
+ recip_pk = tx.get_recipient_pk()
227
+ amount = tx.get_amount()
228
+ fee = tx.get_fee()
229
+ nonce = tx.get_nonce()
230
+
231
+ sender_acct = self.accounts.get_account(sender_pk)
232
+ if (sender_acct is None
233
+ or sender_acct.nonce() != nonce
234
+ or sender_acct.balance() < amount + fee):
235
+ raise ValueError("invalid or unaffordable transaction")
236
+
237
+ # --- debit sender --------------------------------------------------
238
+ self.accounts.set_account(
239
+ sender_pk,
240
+ Account.create(
241
+ balance=sender_acct.balance() - amount - fee,
242
+ data=sender_acct.data(),
243
+ nonce=sender_acct.nonce() + 1,
244
+ )
245
+ )
246
+
247
+ # --- destination handling -----------------------------------------
248
+ if recip_pk == TREASURY:
249
+ treasury = self.accounts.get_account(TREASURY)
250
+
251
+ trie = PatriciaTrie(node_get=None, root_hash=treasury.data())
252
+ stake_bytes = trie.get(sender_pk) or b""
253
+ current_stake = int.from_bytes(stake_bytes, "big") if stake_bytes else 0
254
+
255
+ if amount > 0:
256
+ # stake **deposit**
257
+ trie.put(sender_pk, (current_stake + amount).to_bytes(32, "big"))
258
+ new_treas_bal = treasury.balance() + amount
259
+ else:
260
+ # stake **withdrawal**
261
+ if current_stake == 0:
262
+ raise ValueError("no stake to withdraw")
263
+ # move stake back to sender balance
264
+ sender_after = self.accounts.get_account(sender_pk)
265
+ self.accounts.set_account(
266
+ sender_pk,
267
+ Account.create(
268
+ balance=sender_after.balance() + current_stake,
269
+ data=sender_after.data(),
270
+ nonce=sender_after.nonce(),
271
+ )
272
+ )
273
+ trie.delete(sender_pk)
274
+ new_treas_bal = treasury.balance() # treasury balance unchanged
275
+
276
+ # write back treasury with new trie root
277
+ self.accounts.set_account(
278
+ TREASURY,
279
+ Account.create(
280
+ balance=new_treas_bal,
281
+ data=trie.root_hash,
282
+ nonce=treasury.nonce(),
283
+ )
284
+ )
285
+
286
+ else:
287
+ recip_acct = self.accounts.get_account(recip_pk) or Account.create(0, b"", 0)
288
+ self.accounts.set_account(
289
+ recip_pk,
290
+ Account.create(
291
+ balance=recip_acct.balance() + amount,
292
+ data=recip_acct.data(),
293
+ nonce=recip_acct.nonce(),
294
+ )
295
+ )
296
+
297
+ # --- accumulate fee & record --------------------------------------
298
+ self.total_fees += fee
299
+ self.tx_hashes.append(tx.hash)
300
+ self.transactions_count += 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.22
3
+ Version: 0.2.24
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
@@ -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=YIY3qLneFM7OooY29khLDEs3NIEA7y8VhlXitflkxTo,4564
15
+ astreum/models/block.py,sha256=mqHpsN4-aDyW9Q9emFBVIEyNvLAqRJZbqCN-4vS4Hw0,11795
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.22.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
20
- astreum-0.2.22.dist-info/METADATA,sha256=sjq4dOe1ozAw17evuGntKyahKDMfqQwcrcpp_b6QhK4,5478
21
- astreum-0.2.22.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
- astreum-0.2.22.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
23
- astreum-0.2.22.dist-info/RECORD,,
19
+ astreum-0.2.24.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
20
+ astreum-0.2.24.dist-info/METADATA,sha256=bcrWSsFCFwM884WECJ9THr12tMUS8MAG3VwLyaCEosg,5478
21
+ astreum-0.2.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
+ astreum-0.2.24.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
23
+ astreum-0.2.24.dist-info/RECORD,,