astreum 0.2.61__py3-none-any.whl → 0.3.9__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.
Files changed (86) hide show
  1. astreum/__init__.py +16 -7
  2. astreum/{_communication → communication}/__init__.py +3 -3
  3. astreum/communication/handlers/handshake.py +89 -0
  4. astreum/communication/handlers/object_request.py +176 -0
  5. astreum/communication/handlers/object_response.py +115 -0
  6. astreum/communication/handlers/ping.py +34 -0
  7. astreum/communication/handlers/route_request.py +76 -0
  8. astreum/communication/handlers/route_response.py +53 -0
  9. astreum/communication/models/__init__.py +0 -0
  10. astreum/communication/models/message.py +124 -0
  11. astreum/communication/models/peer.py +51 -0
  12. astreum/{_communication → communication/models}/route.py +7 -12
  13. astreum/communication/processors/__init__.py +0 -0
  14. astreum/communication/processors/incoming.py +98 -0
  15. astreum/communication/processors/outgoing.py +20 -0
  16. astreum/communication/setup.py +166 -0
  17. astreum/communication/start.py +37 -0
  18. astreum/{_communication → communication}/util.py +7 -0
  19. astreum/consensus/__init__.py +20 -0
  20. astreum/consensus/genesis.py +66 -0
  21. astreum/consensus/models/__init__.py +0 -0
  22. astreum/consensus/models/account.py +84 -0
  23. astreum/consensus/models/accounts.py +72 -0
  24. astreum/consensus/models/block.py +364 -0
  25. astreum/{_consensus → consensus/models}/chain.py +7 -7
  26. astreum/{_consensus → consensus/models}/fork.py +8 -8
  27. astreum/consensus/models/receipt.py +98 -0
  28. astreum/{_consensus → consensus/models}/transaction.py +76 -78
  29. astreum/{_consensus → consensus}/setup.py +18 -50
  30. astreum/consensus/start.py +67 -0
  31. astreum/consensus/validator.py +95 -0
  32. astreum/{_consensus → consensus}/workers/discovery.py +19 -1
  33. astreum/consensus/workers/validation.py +307 -0
  34. astreum/{_consensus → consensus}/workers/verify.py +29 -2
  35. astreum/crypto/chacha20poly1305.py +74 -0
  36. astreum/machine/__init__.py +20 -0
  37. astreum/machine/evaluations/__init__.py +0 -0
  38. astreum/{_lispeum → machine/evaluations}/high_evaluation.py +237 -236
  39. astreum/machine/evaluations/low_evaluation.py +281 -0
  40. astreum/machine/evaluations/script_evaluation.py +27 -0
  41. astreum/machine/models/__init__.py +0 -0
  42. astreum/machine/models/environment.py +31 -0
  43. astreum/{_lispeum → machine/models}/expression.py +36 -8
  44. astreum/machine/tokenizer.py +90 -0
  45. astreum/node.py +78 -767
  46. astreum/storage/__init__.py +7 -0
  47. astreum/storage/actions/get.py +183 -0
  48. astreum/storage/actions/set.py +178 -0
  49. astreum/{_storage → storage/models}/atom.py +55 -57
  50. astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
  51. astreum/storage/requests.py +28 -0
  52. astreum/storage/setup.py +22 -15
  53. astreum/utils/config.py +48 -0
  54. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/METADATA +27 -26
  55. astreum-0.3.9.dist-info/RECORD +71 -0
  56. astreum/_communication/message.py +0 -101
  57. astreum/_communication/peer.py +0 -23
  58. astreum/_communication/setup.py +0 -322
  59. astreum/_consensus/__init__.py +0 -20
  60. astreum/_consensus/account.py +0 -95
  61. astreum/_consensus/accounts.py +0 -38
  62. astreum/_consensus/block.py +0 -311
  63. astreum/_consensus/genesis.py +0 -72
  64. astreum/_consensus/receipt.py +0 -136
  65. astreum/_consensus/workers/validation.py +0 -125
  66. astreum/_lispeum/__init__.py +0 -16
  67. astreum/_lispeum/environment.py +0 -13
  68. astreum/_lispeum/low_evaluation.py +0 -123
  69. astreum/_lispeum/tokenizer.py +0 -22
  70. astreum/_node.py +0 -198
  71. astreum/_storage/__init__.py +0 -7
  72. astreum/_storage/setup.py +0 -35
  73. astreum/format.py +0 -75
  74. astreum/models/block.py +0 -441
  75. astreum/models/merkle.py +0 -205
  76. astreum/models/patricia.py +0 -393
  77. astreum/storage/object.py +0 -68
  78. astreum-0.2.61.dist-info/RECORD +0 -57
  79. /astreum/{models → communication/handlers}/__init__.py +0 -0
  80. /astreum/{_communication → communication/models}/ping.py +0 -0
  81. /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
  82. /astreum/{_lispeum → machine/models}/meter.py +0 -0
  83. /astreum/{_lispeum → machine}/parser.py +0 -0
  84. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
  85. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
  86. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/top_level.txt +0 -0
astreum/models/block.py DELETED
@@ -1,441 +0,0 @@
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, vdf_verify
7
- from astreum._consensus.account import Account
8
- from astreum._consensus.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_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_hash = transactions_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
- # Backwards/forwards alias for clarity with external specs
78
- @property
79
- def validator_public_key(self) -> Optional[bytes]:
80
- return self.validator_pk
81
-
82
- @validator_public_key.setter
83
- def validator_public_key(self, value: Optional[bytes]) -> None:
84
- self.validator_pk = value
85
-
86
- def get_field(self, name: str) -> Union[int, bytes]:
87
- """Query a single body field by name, returning an int or bytes."""
88
- if name not in self._field_names:
89
- raise KeyError(f"Unknown field: {name}")
90
- if not self._body_tree:
91
- raise ValueError("Body tree not available for field queries.")
92
- idx = self._field_names.index(name)
93
- leaf_bytes = self._body_tree.leaves[idx]
94
- if name in _INT_FIELDS:
95
- return int.from_bytes(leaf_bytes, "big")
96
- return leaf_bytes
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. three Account bodies
106
- validator_acct = Account.create(balance=0, data=b"", counter=0)
107
- treasury_acct = Account.create(balance=1, data=stake_root, counter=0)
108
- burn_acct = Account.create(balance=0, data=b"", counter=0)
109
-
110
- # 3. global Accounts structure
111
- accts = Accounts()
112
- accts.set_account(validator_addr, validator_acct)
113
- accts.set_account(b"\x11" * 32, treasury_acct)
114
- accts.set_account(b"\x00" * 32, burn_acct)
115
- accounts_hash = accts.root_hash
116
-
117
- # 4. constant body fields for genesis
118
- body_kwargs = dict(
119
- block_hash = b"",
120
- number = 0,
121
- prev_block_hash = b"\x00" * 32,
122
- timestamp = 0,
123
- block_time = 0,
124
- accounts_hash = accounts_hash,
125
- accounts = accts,
126
- transactions_total_fees = 0,
127
- transaction_limit = 1,
128
- transactions_hash = b"\x00" * 32,
129
- transactions_count = 0,
130
- delay_difficulty = 1,
131
- delay_output = b"",
132
- delay_proof = b"",
133
- validator_pk = validator_addr,
134
- signature = b"",
135
- )
136
-
137
- # 5. build and return the block
138
- return cls.create(**body_kwargs)
139
-
140
- @classmethod
141
- def build(
142
- cls,
143
- previous_block: "Block",
144
- transactions: List[Transaction],
145
- *,
146
- validator_sk,
147
- natural_rate: float = 0.618,
148
- ) -> "Block":
149
- BURN = b"\x00" * 32
150
-
151
- blk = cls(
152
- block_hash=b"",
153
- number=previous_block.number + 1,
154
- prev_block_hash=previous_block.hash,
155
- timestamp=previous_block.timestamp + 1,
156
- accounts_hash=previous_block.accounts_hash,
157
- transaction_limit=previous_block.transaction_limit,
158
- transactions_count=0,
159
- validator_pk=validator_sk.public_key().public_bytes(),
160
- )
161
-
162
- # ------------------ difficulty via natural_rate -----------------------
163
- prev_bt = previous_block.block_time or 0
164
- prev_diff = previous_block.delay_difficulty or 1
165
- if prev_bt <= 1:
166
- blk.delay_difficulty = max(1, int(prev_diff / natural_rate)) # increase
167
- else:
168
- blk.delay_difficulty = max(1, int(prev_diff * natural_rate)) # decrease
169
-
170
- # ------------------ launch VDF in background --------------------------
171
- vdf_result: dict[str, bytes] = {}
172
-
173
- def _vdf_worker():
174
- y, p = vdf_generate(previous_block.delay_output, blk.delay_difficulty, -4)
175
- vdf_result["y"] = y
176
- vdf_result["p"] = p
177
-
178
- Thread(target=_vdf_worker, daemon=True).start()
179
-
180
- # ------------------ process transactions -----------------------------
181
- for tx in transactions:
182
- try:
183
- blk.apply_tx(tx)
184
- except ValueError:
185
- break
186
-
187
- # ------------------ split fees --------------------------------------
188
- burn_amt = blk.total_fees // 2
189
- reward_amt = blk.total_fees - burn_amt
190
-
191
- def _credit(addr: bytes, amt: int):
192
- acc = blk.accounts.get_account(addr) or Account.create(0, b"", 0)
193
- blk.accounts.set_account(addr, Account.create(acc.balance + amt, acc.data, acc.counter))
194
-
195
- if burn_amt:
196
- _credit(BURN, burn_amt)
197
- if reward_amt:
198
- _credit(blk.validator_pk, reward_amt)
199
-
200
- # ------------------ update tx limit with natural_rate ---------------
201
- prev_limit = previous_block.transaction_limit
202
- prev_tx_count = previous_block.transactions_count
203
- grow_thr = prev_limit * natural_rate
204
- shrink_thr = prev_tx_count * natural_rate
205
-
206
- if prev_tx_count > grow_thr:
207
- blk.transaction_limit = prev_tx_count
208
- elif prev_tx_count < shrink_thr:
209
- blk.transaction_limit = max(1, int(prev_limit * natural_rate))
210
- else:
211
- blk.transaction_limit = prev_limit
212
-
213
- # ------------------ wait for VDF ------------------------------------
214
- while "y" not in vdf_result:
215
- pass
216
- blk.delay_output = vdf_result["y"]
217
- blk.delay_proof = vdf_result["p"]
218
-
219
- # ------------------ timing & roots ----------------------------------
220
- blk.block_time = blk.timestamp - previous_block.timestamp
221
- blk.accounts_hash = blk.accounts.root_hash
222
- blk.transactions_hash = MerkleTree.from_leaves(blk.tx_hashes).root_hash
223
- blk.transactions_total_fees = blk.total_fees
224
-
225
- # ------------------ build full body root ----------------------------
226
- body_fields = {
227
- "accounts_hash": blk.accounts_hash,
228
- "block_time": blk.block_time,
229
- "delay_difficulty": blk.delay_difficulty,
230
- "delay_output": blk.delay_output,
231
- "delay_proof": blk.delay_proof,
232
- "number": blk.number,
233
- "prev_block_hash": blk.prev_block_hash,
234
- "timestamp": blk.timestamp,
235
- "transaction_limit": blk.transaction_limit,
236
- "transactions_count": blk.transactions_count,
237
- "transactions_hash": blk.transactions_hash,
238
- "transactions_total_fees": blk.transactions_total_fees,
239
- "validator_pk": blk.validator_pk,
240
- }
241
-
242
- leaves: List[bytes] = []
243
- for k in sorted(body_fields):
244
- v = body_fields[k]
245
- if isinstance(v, bytes):
246
- leaves.append(v)
247
- else:
248
- leaves.append(int(v).to_bytes((v.bit_length() + 7) // 8 or 1, "big"))
249
-
250
- body_root = MerkleTree.from_leaves(leaves).root_hash
251
- blk.body_tree = MerkleTree.from_leaves([body_root])
252
- blk.signature = validator_sk.sign(body_root)
253
- blk.hash = MerkleTree.from_leaves([body_root, blk.signature]).root_hash
254
-
255
- return blk
256
-
257
-
258
- def apply_tx(self, tx: Transaction) -> None:
259
- # --- lazy state ----------------------------------------------------
260
- if not hasattr(self, "accounts") or self.accounts is None:
261
- self.accounts = Accounts(root_hash=self.accounts_hash)
262
- if not hasattr(self, "total_fees"):
263
- self.total_fees = 0
264
- self.tx_hashes = []
265
- self.transactions_count = 0
266
-
267
- TREASURY = b"\x11" * 32
268
- BURN = b"\x00" * 32
269
-
270
- # --- cap check -----------------------------------------------------
271
- if self.transactions_count >= self.transaction_limit:
272
- raise ValueError("block transaction limit reached")
273
-
274
- # --- unpack tx -----------------------------------------------------
275
- sender_pk = tx.get_sender_pk()
276
- recip_pk = tx.get_recipient_pk()
277
- amount = tx.get_amount()
278
- fee = tx.get_fee()
279
- nonce = tx.get_nonce()
280
-
281
- sender_acct = self.accounts.get_account(sender_pk)
282
- if (sender_acct is None
283
- or sender_acct.counter != nonce
284
- or sender_acct.balance < amount + fee):
285
- raise ValueError("invalid or unaffordable transaction")
286
-
287
- # --- debit sender --------------------------------------------------
288
- self.accounts.set_account(
289
- sender_pk,
290
- Account.create(
291
- balance=sender_acct.balance - amount - fee,
292
- data=sender_acct.data,
293
- counter=sender_acct.counter + 1,
294
- )
295
- )
296
-
297
- # --- destination handling -----------------------------------------
298
- if recip_pk == TREASURY:
299
- treasury = self.accounts.get_account(TREASURY)
300
-
301
- trie = PatriciaTrie(node_get=None, root_hash=treasury.data)
302
- stake_bytes = trie.get(sender_pk) or b""
303
- current_stake = int.from_bytes(stake_bytes, "big") if stake_bytes else 0
304
-
305
- if amount > 0:
306
- # stake **deposit**
307
- trie.put(sender_pk, (current_stake + amount).to_bytes(32, "big"))
308
- new_treas_bal = treasury.balance + amount
309
- else:
310
- # stake **withdrawal**
311
- if current_stake == 0:
312
- raise ValueError("no stake to withdraw")
313
- # move stake back to sender balance
314
- sender_after = self.accounts.get_account(sender_pk)
315
- self.accounts.set_account(
316
- sender_pk,
317
- Account.create(
318
- balance=sender_after.balance + current_stake,
319
- data=sender_after.data,
320
- counter=sender_after.counter,
321
- )
322
- )
323
- trie.delete(sender_pk)
324
- new_treas_bal = treasury.balance # treasury balance unchanged
325
-
326
- # write back treasury with new trie root
327
- self.accounts.set_account(
328
- TREASURY,
329
- Account.create(
330
- balance=new_treas_bal,
331
- data=trie.root_hash,
332
- counter=treasury.counter,
333
- )
334
- )
335
-
336
- else:
337
- recip_acct = self.accounts.get_account(recip_pk) or Account.create(0, b"", 0)
338
- self.accounts.set_account(
339
- recip_pk,
340
- Account.create(
341
- balance=recip_acct.balance + amount,
342
- data=recip_acct.data,
343
- counter=recip_acct.counter,
344
- )
345
- )
346
-
347
- # --- accumulate fee & record --------------------------------------
348
- self.total_fees += fee
349
- self.tx_hashes.append(tx.hash)
350
- self.transactions_count += 1
351
-
352
- def validate_block(self, remote_get_fn) -> bool:
353
- NAT = 0.618
354
- _i2b = lambda i: i.to_bytes((i.bit_length() + 7) // 8 or 1, "big")
355
-
356
- # ---------- 1. block-hash & signature -----------------------------
357
- blk_mt = MerkleTree(node_get=remote_get_fn, root_hash=self.hash)
358
- body_root = blk_mt.get(0); sig = blk_mt.get(1)
359
- ed25519.verify_signature(public_key=self.validator_pk, message=body_root, signature=sig)
360
-
361
- # ---------- 2. rebuild body_root from fields ----------------------
362
- f_names = (
363
- "accounts_hash","block_time","delay_difficulty","delay_output","delay_proof",
364
- "number","prev_block_hash","timestamp","transaction_limit",
365
- "transactions_count","transactions_hash","transactions_total_fees",
366
- "validator_pk",
367
- )
368
- leaves = [
369
- v if isinstance(v := self.get_field(n), bytes) else _i2b(v)
370
- for n in sorted(f_names)
371
- ]
372
- if MerkleTree.from_leaves(leaves).root_hash != body_root:
373
- raise ValueError("body root mismatch")
374
-
375
- # ---------- 3. previous block header & VDF ------------------------
376
- prev_mt = MerkleTree(node_get=remote_get_fn, root_hash=self.prev_block_hash)
377
- prev_body_root, prev_sig = prev_mt.get(0), prev_mt.get(1)
378
- prev_body_mt = MerkleTree(node_get=remote_get_fn, root_hash=prev_body_root)
379
- prev_blk = Block(block_hash=self.prev_block_hash,
380
- body_tree=prev_body_mt, signature=prev_sig)
381
- prev_out = prev_blk.get_field("delay_output")
382
- prev_diff = prev_blk.get_field("delay_difficulty")
383
- prev_bt = prev_blk.get_field("block_time")
384
- prev_limit = prev_blk.get_field("transaction_limit")
385
- prev_cnt = prev_blk.get_field("transactions_count")
386
-
387
- if not vdf_verify(prev_out, self.delay_output, self.delay_proof,
388
- T=self.delay_difficulty, D=-4):
389
- raise ValueError("bad VDF proof")
390
-
391
- # ---------- 4. replay all txs -------------------------------------
392
- accs = Accounts(root_hash=prev_blk.get_field("accounts_hash"),
393
- node_get=remote_get_fn)
394
- tx_mt = MerkleTree(node_get=remote_get_fn,
395
- root_hash=self.transactions_hash)
396
- if tx_mt.leaf_count() != self.transactions_count:
397
- raise ValueError("transactions_count mismatch")
398
-
399
- dummy = Block(block_hash=b"", accounts=accs,
400
- accounts_hash=accs.root_hash,
401
- transaction_limit=prev_limit)
402
- for i in range(self.transactions_count):
403
- h = tx_mt.get(i)
404
- tm = MerkleTree(node_get=remote_get_fn, root_hash=h)
405
- tx = Transaction(h, tree=tm, node_get=remote_get_fn)
406
- dummy.apply_tx(tx)
407
-
408
- # fee split identical to build()
409
- burn = dummy.total_fees // 2
410
- rew = dummy.total_fees - burn
411
- if burn:
412
- dummy.accounts.set_account(
413
- b"\x00"*32,
414
- Account.create(burn, b"", 0)
415
- )
416
- if rew:
417
- v_acct = dummy.accounts.get_account(self.validator_pk) or Account.create(0,b"",0)
418
- dummy.accounts.set_account(
419
- self.validator_pk,
420
- Account.create(v_acct.balance+rew, v_acct.data, v_acct.counter)
421
- )
422
-
423
- if dummy.accounts.root_hash != self.accounts_hash:
424
- raise ValueError("accounts_hash mismatch")
425
-
426
- # ---------- 5. natural-rate rules --------------------------------
427
- grow_thr = prev_limit * NAT
428
- shrink_thr = prev_cnt * NAT
429
- expect_lim = prev_cnt if prev_cnt > grow_thr \
430
- else max(1, int(prev_limit * NAT)) if prev_cnt < shrink_thr \
431
- else prev_limit
432
- if self.transaction_limit != expect_lim:
433
- raise ValueError("tx-limit rule")
434
-
435
- expect_diff = max(1, int(prev_diff / NAT)) if prev_bt <= 1 \
436
- else max(1, int(prev_diff * NAT))
437
- if self.delay_difficulty != expect_diff:
438
- raise ValueError("difficulty rule")
439
-
440
- return True
441
-
astreum/models/merkle.py DELETED
@@ -1,205 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Callable, Dict, List, Optional, Tuple
4
-
5
- import blake3
6
- from ..format import encode, decode
7
-
8
- class MerkleNode:
9
- def __init__(
10
- self,
11
- left: Optional[bytes],
12
- right: Optional[bytes],
13
- value: Optional[bytes],
14
- ) -> None:
15
- self.left = left
16
- self.right = right
17
- self.value = value
18
- self._hash: Optional[bytes] = None
19
-
20
- def to_bytes(self) -> bytes:
21
- return encode([self.left, self.right, self.value])
22
-
23
- @classmethod
24
- def from_bytes(cls, blob: bytes) -> "MerkleNode":
25
- left, right, value = decode(blob)
26
- return cls(left, right, value)
27
-
28
- def _compute_hash(self) -> bytes:
29
- if self.value is not None:
30
- return blake3.blake3(self.value).digest()
31
- left = self.left or b""
32
- right = self.right or b""
33
- return blake3.blake3(left + right).digest()
34
-
35
- def hash(self) -> bytes:
36
- if self._hash is None:
37
- self._hash = self._compute_hash()
38
- return self._hash
39
-
40
-
41
- class MerkleTree:
42
- def __init__(
43
- self,
44
- global_get_fn: Callable[[bytes], Optional[bytes]],
45
- root_hash: Optional[bytes] = None,
46
- height: Optional[int] = None,
47
- ) -> None:
48
- self._global_get_fn = global_get_fn
49
- self.nodes: Dict[bytes, MerkleNode] = {}
50
- self.root_hash = root_hash
51
- self._height: Optional[int] = height
52
-
53
- @classmethod
54
- def from_leaves(
55
- cls,
56
- leaves: List[bytes],
57
- global_get_fn: Callable[[bytes], Optional[bytes]] | None = None,
58
- ) -> "MerkleTree":
59
- if not leaves:
60
- raise ValueError("must supply at least one leaf")
61
-
62
- global_get_fn = global_get_fn or (lambda _h: None)
63
- tree = cls(global_get_fn=global_get_fn)
64
-
65
- # Step 1 – create leaf nodes list[bytes]
66
- level_hashes: List[bytes] = []
67
- for val in leaves:
68
- leaf = MerkleNode(None, None, val)
69
- h = leaf.hash()
70
- tree.nodes[h] = leaf
71
- level_hashes.append(h)
72
-
73
- height = 1 # current level (leaves)
74
-
75
- # Step 2 – build upper levels until single root remains
76
- while len(level_hashes) > 1:
77
- next_level: List[bytes] = []
78
- it = iter(level_hashes)
79
- for left_hash in it:
80
- try:
81
- right_hash = next(it)
82
- except StopIteration:
83
- right_hash = None
84
- parent = MerkleNode(left_hash, right_hash, None)
85
- ph = parent.hash()
86
- tree.nodes[ph] = parent
87
- next_level.append(ph)
88
- level_hashes = next_level
89
- height += 1
90
-
91
- tree.root_hash = level_hashes[0]
92
- tree._height = height
93
- return tree
94
-
95
- def _fetch(self, h: bytes | None) -> Optional[MerkleNode]:
96
- if h is None:
97
- return None
98
- node = self.nodes.get(h)
99
- if node is None:
100
- raw = self._global_get_fn(h)
101
- if raw is None:
102
- return None
103
- node = MerkleNode.from_bytes(raw)
104
- self.nodes[h] = node
105
- return node
106
-
107
- def _invalidate(self, node: MerkleNode) -> None:
108
- node._hash = None
109
-
110
- def _ensure_height(self) -> None:
111
- if self._height is None:
112
- h = 0
113
- nh = self.root_hash
114
- while nh is not None:
115
- node = self._fetch(nh)
116
- nh = node.left if node and node.value is None else None
117
- h += 1
118
- self._height = h or 1
119
-
120
- def _capacity(self) -> int:
121
- self._ensure_height()
122
- assert self._height is not None
123
- return 1 << (self._height - 1)
124
-
125
- def _path_bits(self, index: int) -> List[int]:
126
- self._ensure_height()
127
- assert self._height is not None
128
- bits = []
129
- for shift in range(self._height - 2, -1, -1):
130
- bits.append((index >> shift) & 1)
131
- return bits
132
-
133
- # ------------------------------------------------------------------
134
- # get / put
135
- # ------------------------------------------------------------------
136
- def get(self, index: int) -> Optional[bytes]:
137
- if index < 0 or self.root_hash is None or index >= self._capacity():
138
- return None
139
-
140
- node_hash = self.root_hash
141
- for bit in self._path_bits(index):
142
- node = self._fetch(node_hash)
143
- if node is None:
144
- return None
145
- node_hash = node.right if bit else node.left
146
- if node_hash is None:
147
- return None
148
- leaf = self._fetch(node_hash)
149
- return leaf.value if leaf else None
150
-
151
- def put(self, index: int, value: bytes) -> None:
152
- # 1 . input validation
153
- if index < 0:
154
- raise IndexError("negative index")
155
- if self.root_hash is None:
156
- raise IndexError("tree is empty – build it first with from_leaves()")
157
- if index >= self._capacity():
158
- raise IndexError("index beyond tree capacity")
159
-
160
- # 2 . walk down to the target leaf
161
- node_hash = self.root_hash
162
- stack: List[Tuple[MerkleNode, bytes, bool]] = []
163
- for bit in self._path_bits(index):
164
- node = self._fetch(node_hash)
165
- if node is None:
166
- raise IndexError("missing node along path")
167
- went_right = bool(bit)
168
- child_hash = node.right if went_right else node.left
169
- if child_hash is None:
170
- raise IndexError("path leads into non-existent branch")
171
- stack.append((node, node.hash(), went_right))
172
- node_hash = child_hash
173
-
174
- # 3 . update the leaf
175
- leaf = self._fetch(node_hash)
176
- if leaf is None or leaf.value is None:
177
- raise IndexError("target leaf missing")
178
-
179
- old_hash = leaf.hash()
180
- leaf.value = value
181
- self._invalidate(leaf)
182
- new_hash = leaf.hash()
183
-
184
- if new_hash != old_hash:
185
- self.nodes.pop(old_hash, None)
186
- self.nodes[new_hash] = leaf
187
-
188
- # 4 . bubble the change up
189
- for parent, old_hash, went_right in reversed(stack):
190
- if went_right:
191
- parent.right = new_hash
192
- else:
193
- parent.left = new_hash
194
-
195
- self._invalidate(parent)
196
- new_hash = parent.hash()
197
-
198
- if new_hash != old_hash:
199
- self.nodes.pop(old_hash, None)
200
- self.nodes[new_hash] = parent
201
-
202
- # 5 . finalise the new root
203
- self.root_hash = new_hash
204
-
205
-