astreum 0.2.39__py3-none-any.whl → 0.2.41__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 (42) hide show
  1. astreum/_communication/__init__.py +2 -0
  2. astreum/{models → _communication}/message.py +100 -64
  3. astreum/_communication/ping.py +33 -0
  4. astreum/_communication/route.py +53 -20
  5. astreum/_communication/setup.py +240 -99
  6. astreum/_communication/util.py +42 -0
  7. astreum/_consensus/__init__.py +6 -0
  8. astreum/_consensus/account.py +170 -0
  9. astreum/_consensus/accounts.py +67 -0
  10. astreum/_consensus/block.py +84 -52
  11. astreum/_consensus/chain.py +65 -62
  12. astreum/_consensus/fork.py +99 -97
  13. astreum/_consensus/genesis.py +141 -0
  14. astreum/_consensus/receipt.py +177 -0
  15. astreum/_consensus/setup.py +21 -162
  16. astreum/_consensus/transaction.py +43 -23
  17. astreum/_consensus/workers/__init__.py +9 -0
  18. astreum/_consensus/workers/discovery.py +48 -0
  19. astreum/_consensus/workers/validation.py +122 -0
  20. astreum/_consensus/workers/verify.py +63 -0
  21. astreum/_storage/atom.py +24 -7
  22. astreum/_storage/patricia.py +443 -0
  23. astreum/models/block.py +10 -10
  24. astreum/node.py +755 -753
  25. {astreum-0.2.39.dist-info → astreum-0.2.41.dist-info}/METADATA +1 -1
  26. astreum-0.2.41.dist-info/RECORD +53 -0
  27. astreum/lispeum/__init__.py +0 -0
  28. astreum/lispeum/environment.py +0 -40
  29. astreum/lispeum/expression.py +0 -86
  30. astreum/lispeum/parser.py +0 -41
  31. astreum/lispeum/tokenizer.py +0 -52
  32. astreum/models/account.py +0 -91
  33. astreum/models/accounts.py +0 -34
  34. astreum/models/transaction.py +0 -106
  35. astreum/relay/__init__.py +0 -0
  36. astreum/relay/peer.py +0 -9
  37. astreum/relay/route.py +0 -25
  38. astreum/relay/setup.py +0 -58
  39. astreum-0.2.39.dist-info/RECORD +0 -55
  40. {astreum-0.2.39.dist-info → astreum-0.2.41.dist-info}/WHEEL +0 -0
  41. {astreum-0.2.39.dist-info → astreum-0.2.41.dist-info}/licenses/LICENSE +0 -0
  42. {astreum-0.2.39.dist-info → astreum-0.2.41.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,42 @@
1
+ from typing import Tuple
2
+
3
+
4
+ def address_str_to_host_and_port(address: str) -> Tuple[str, int]:
5
+ """Parse `host:port` (or `[ipv6]:port`) into a tuple."""
6
+ addr = address.strip()
7
+ if not addr:
8
+ raise ValueError("address cannot be empty")
9
+
10
+ host: str
11
+ port_str: str
12
+
13
+ if addr.startswith('['):
14
+ end = addr.find(']')
15
+ if end == -1:
16
+ raise ValueError("missing closing ']' in IPv6 address")
17
+ host = addr[1:end]
18
+ remainder = addr[end + 1 :]
19
+ if not remainder.startswith(':'):
20
+ raise ValueError("missing port separator after IPv6 address")
21
+ port_str = remainder[1:]
22
+ else:
23
+ if ':' not in addr:
24
+ raise ValueError("address must contain ':' separating host and port")
25
+ host, port_str = addr.rsplit(':', 1)
26
+
27
+ host = host.strip()
28
+ if not host:
29
+ raise ValueError("host cannot be empty")
30
+ port_str = port_str.strip()
31
+ if not port_str:
32
+ raise ValueError("port cannot be empty")
33
+
34
+ try:
35
+ port = int(port_str, 10)
36
+ except ValueError as exc:
37
+ raise ValueError(f"invalid port number: {port_str}") from exc
38
+
39
+ if not (0 < port < 65536):
40
+ raise ValueError(f"port out of range: {port}")
41
+
42
+ return host, port
@@ -1,6 +1,9 @@
1
+ from .account import Account
2
+ from .accounts import Accounts
1
3
  from .block import Block
2
4
  from .chain import Chain
3
5
  from .fork import Fork
6
+ from .receipt import Receipt
4
7
  from .transaction import Transaction
5
8
  from .setup import consensus_setup
6
9
 
@@ -9,6 +12,9 @@ __all__ = [
9
12
  "Block",
10
13
  "Chain",
11
14
  "Fork",
15
+ "Receipt",
12
16
  "Transaction",
17
+ "Account",
18
+ "Accounts",
13
19
  "consensus_setup",
14
20
  ]
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Callable, List, Optional, Tuple
5
+
6
+ from .._storage.atom import Atom, ZERO32
7
+
8
+
9
+ def _int_to_be_bytes(value: int) -> bytes:
10
+ value = int(value)
11
+ if value < 0:
12
+ raise ValueError("account integers must be non-negative")
13
+ if value == 0:
14
+ return b"\x00"
15
+ size = (value.bit_length() + 7) // 8
16
+ return value.to_bytes(size, "big")
17
+
18
+
19
+ def _be_bytes_to_int(data: Optional[bytes]) -> int:
20
+ if not data:
21
+ return 0
22
+ return int.from_bytes(data, "big")
23
+
24
+
25
+ def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
26
+ next_hash = ZERO32
27
+ elements: List[Atom] = []
28
+ for child_id in reversed(child_ids):
29
+ elem = Atom.from_data(data=child_id, next_hash=next_hash)
30
+ next_hash = elem.object_id()
31
+ elements.append(elem)
32
+ elements.reverse()
33
+ value_atom = Atom.from_data(
34
+ data=len(child_ids).to_bytes(8, "little"),
35
+ next_hash=next_hash,
36
+ )
37
+ type_atom = Atom.from_data(data=b"list", next_hash=value_atom.object_id())
38
+ atoms = elements + [value_atom, type_atom]
39
+ return type_atom.object_id(), atoms
40
+
41
+
42
+ def _resolve_storage_get(source: Any) -> Callable[[bytes], Optional[Atom]]:
43
+ if callable(source):
44
+ return source
45
+ getter = getattr(source, "_local_get", None)
46
+ if callable(getter):
47
+ return getter
48
+ raise TypeError("Account.from_atom needs a callable storage getter or node with '_local_get'")
49
+
50
+
51
+ def _read_list_entries(
52
+ storage_get: Callable[[bytes], Optional[Atom]],
53
+ start: bytes,
54
+ ) -> List[bytes]:
55
+ entries: List[bytes] = []
56
+ current = start if start and start != ZERO32 else b""
57
+ while current:
58
+ elem = storage_get(current)
59
+ if elem is None:
60
+ break
61
+ entries.append(elem.data)
62
+ nxt = elem.next
63
+ current = nxt if nxt and nxt != ZERO32 else b""
64
+ return entries
65
+
66
+
67
+ @dataclass
68
+ class Account:
69
+ _balance: int
70
+ _data: bytes
71
+ _nonce: int
72
+ hash: bytes = ZERO32
73
+ atoms: List[Atom] = field(default_factory=list)
74
+
75
+ @staticmethod
76
+ def _encode(balance: int, data: bytes, nonce: int) -> Tuple[bytes, List[Atom]]:
77
+ balance_atom = Atom.from_data(data=_int_to_be_bytes(balance))
78
+ data_atom = Atom.from_data(data=bytes(data))
79
+ nonce_atom = Atom.from_data(data=_int_to_be_bytes(nonce))
80
+
81
+ field_atoms = [balance_atom, data_atom, nonce_atom]
82
+ field_ids = [a.object_id() for a in field_atoms]
83
+
84
+ body_id, body_atoms = _make_list(field_ids)
85
+ type_atom = Atom.from_data(data=b"account", next_hash=body_id)
86
+ top_id, top_atoms = _make_list([type_atom.object_id(), body_id])
87
+
88
+ atoms = field_atoms + body_atoms + [type_atom] + top_atoms
89
+ return top_id, atoms
90
+
91
+ @classmethod
92
+ def create(cls, balance: int, data: bytes, nonce: int) -> "Account":
93
+ account_hash, atoms = cls._encode(balance, data, nonce)
94
+ return cls(
95
+ _balance=int(balance),
96
+ _data=bytes(data),
97
+ _nonce=int(nonce),
98
+ hash=account_hash,
99
+ atoms=atoms,
100
+ )
101
+
102
+ @classmethod
103
+ def from_atom(cls, source: Any, account_id: bytes) -> "Account":
104
+ storage_get = _resolve_storage_get(source)
105
+
106
+ outer_list = storage_get(account_id)
107
+ if outer_list is None or outer_list.data != b"list":
108
+ raise ValueError("not an account (outer list missing)")
109
+
110
+ outer_value = storage_get(outer_list.next)
111
+ if outer_value is None:
112
+ raise ValueError("malformed account (outer value missing)")
113
+
114
+ entries = _read_list_entries(storage_get, outer_value.next)
115
+ if len(entries) < 2:
116
+ raise ValueError("malformed account (type/body missing)")
117
+
118
+ type_atom_id, body_id = entries[0], entries[1]
119
+ type_atom = storage_get(type_atom_id)
120
+ if type_atom is None or type_atom.data != b"account":
121
+ raise ValueError("not an account (type mismatch)")
122
+
123
+ body_list = storage_get(body_id)
124
+ if body_list is None or body_list.data != b"list":
125
+ raise ValueError("malformed account body (type)")
126
+
127
+ body_value = storage_get(body_list.next)
128
+ if body_value is None:
129
+ raise ValueError("malformed account body (value)")
130
+
131
+ field_ids = _read_list_entries(storage_get, body_value.next)
132
+ if len(field_ids) < 3:
133
+ field_ids.extend([ZERO32] * (3 - len(field_ids)))
134
+
135
+ def _read_field(field_id: bytes) -> bytes:
136
+ if not field_id or field_id == ZERO32:
137
+ return b""
138
+ atom = storage_get(field_id)
139
+ return atom.data if atom is not None else b""
140
+
141
+ balance_bytes = _read_field(field_ids[0])
142
+ data_bytes = _read_field(field_ids[1])
143
+ nonce_bytes = _read_field(field_ids[2])
144
+
145
+ account = cls.create(
146
+ balance=_be_bytes_to_int(balance_bytes),
147
+ data=data_bytes,
148
+ nonce=_be_bytes_to_int(nonce_bytes),
149
+ )
150
+ if account.hash != account_id:
151
+ raise ValueError("account hash mismatch while decoding")
152
+ return account
153
+
154
+ def balance(self) -> int:
155
+ return self._balance
156
+
157
+ def data(self) -> bytes:
158
+ return self._data
159
+
160
+ def nonce(self) -> int:
161
+ return self._nonce
162
+
163
+ def body_hash(self) -> bytes:
164
+ return self.hash
165
+
166
+ def to_atom(self) -> Tuple[bytes, List[Atom]]:
167
+ account_hash, atoms = self._encode(self._balance, self._data, self._nonce)
168
+ self.hash = account_hash
169
+ self.atoms = atoms
170
+ return account_hash, list(atoms)
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Dict, Iterable, Optional, Tuple
5
+
6
+ from .._storage.atom import Atom
7
+ from .._storage.patricia import PatriciaTrie
8
+ from .account import Account
9
+
10
+
11
+ class Accounts:
12
+ def __init__(
13
+ self,
14
+ root_hash: Optional[bytes] = None,
15
+ ) -> None:
16
+ self._trie = PatriciaTrie(root_hash=root_hash)
17
+ self._cache: Dict[bytes, Account] = {}
18
+ self._staged: Dict[bytes, Account] = {}
19
+ self._staged_hashes: Dict[bytes, bytes] = {}
20
+ self._staged_atoms: Dict[bytes, Iterable[Atom]] = {}
21
+ self._node: Optional[Any] = None
22
+
23
+ @property
24
+ def root_hash(self) -> Optional[bytes]:
25
+ return self._trie.root_hash
26
+
27
+ def _resolve_node(self, node: Optional[Any]) -> Any:
28
+ if node is not None:
29
+ if self._node is None:
30
+ self._node = node
31
+ return node
32
+ if self._node is None:
33
+ raise ValueError("Accounts requires a node reference for trie access")
34
+ return self._node
35
+
36
+ def get_account(self, address: bytes, *, node: Optional[Any] = None) -> Optional[Account]:
37
+ if address in self._staged:
38
+ return self._staged[address]
39
+
40
+ cached = self._cache.get(address)
41
+ if cached is not None:
42
+ return cached
43
+
44
+ storage_node = self._resolve_node(node)
45
+ account_id: Optional[bytes] = self._trie.get(storage_node, address)
46
+ if account_id is None:
47
+ return None
48
+
49
+ account = Account.from_atom(storage_node, account_id)
50
+ self._cache[address] = account
51
+ return account
52
+
53
+ def set_account(self, address: bytes, account: Account) -> None:
54
+ account_hash, atoms = account.to_atom()
55
+ self._staged[address] = account
56
+ self._staged_hashes[address] = account_hash
57
+ self._staged_atoms[address] = tuple(atoms)
58
+ self._cache[address] = account
59
+
60
+ def staged_items(self) -> Iterable[Tuple[bytes, Account]]:
61
+ return self._staged.items()
62
+
63
+ def staged_hashes(self) -> Dict[bytes, bytes]:
64
+ return dict(self._staged_hashes)
65
+
66
+ def staged_atoms(self) -> Dict[bytes, Iterable[Atom]]:
67
+ return {addr: tuple(atoms) for addr, atoms in self._staged_atoms.items()}
@@ -1,7 +1,12 @@
1
-
2
- from typing import Callable, List, Optional, Tuple
1
+
2
+ from typing import Any, Callable, List, Optional, Tuple, TYPE_CHECKING
3
3
 
4
4
  from .._storage.atom import Atom, ZERO32
5
+
6
+ if TYPE_CHECKING:
7
+ from .._storage.patricia import PatriciaTrie
8
+ from .transaction import Transaction
9
+ from .receipt import Receipt
5
10
  from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
6
11
  from cryptography.exceptions import InvalidSignature
7
12
 
@@ -22,16 +27,6 @@ def _be_bytes_to_int(b: Optional[bytes]) -> int:
22
27
  return int.from_bytes(b, "big")
23
28
 
24
29
 
25
- def _make_typed_bytes(data: bytes) -> Tuple[bytes, List[Atom]]:
26
- """Create a typed 'byte' atom for the given payload.
27
-
28
- Returns (object_id, atoms_in_dependency_order).
29
- """
30
- val = Atom.from_data(data=data)
31
- typ = Atom.from_data(data=b"byte", next_hash=val.object_id())
32
- return typ.object_id(), [val, typ]
33
-
34
-
35
30
  def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
36
31
  """Create a typed 'list' atom for child object ids.
37
32
 
@@ -65,15 +60,16 @@ class Block:
65
60
  signature_atom = Atom(data=<signature-bytes>)
66
61
 
67
62
  Details order in body_list:
68
- 0: previous_block (bytes)
63
+ 0: previous_block_hash (bytes)
69
64
  1: number (int → big-endian bytes)
70
65
  2: timestamp (int → big-endian bytes)
71
66
  3: accounts_hash (bytes)
72
67
  4: transactions_total_fees (int → big-endian bytes)
73
- 5: transactions_root_hash (bytes)
74
- 6: delay_difficulty (int → big-endian bytes)
75
- 7: delay_output (bytes)
76
- 8: validator_public_key (bytes)
68
+ 5: transactions_hash (bytes)
69
+ 6: receipts_hash (bytes)
70
+ 7: delay_difficulty (int → big-endian bytes)
71
+ 8: delay_output (bytes)
72
+ 9: validator_public_key (bytes)
77
73
 
78
74
  Notes:
79
75
  - "body tree" is represented here by the body_list id (self.body_hash), not
@@ -85,14 +81,16 @@ class Block:
85
81
 
86
82
  # essential identifiers
87
83
  hash: bytes
88
- previous_block: bytes
84
+ previous_block_hash: bytes
85
+ previous_block: Optional["Block"]
89
86
 
90
87
  # block details
91
88
  number: Optional[int]
92
89
  timestamp: Optional[int]
93
90
  accounts_hash: Optional[bytes]
94
91
  transactions_total_fees: Optional[int]
95
- transactions_root_hash: Optional[bytes]
92
+ transactions_hash: Optional[bytes]
93
+ receipts_hash: Optional[bytes]
96
94
  delay_difficulty: Optional[int]
97
95
  delay_output: Optional[bytes]
98
96
  validator_public_key: Optional[bytes]
@@ -101,33 +99,48 @@ class Block:
101
99
  body_hash: Optional[bytes]
102
100
  signature: Optional[bytes]
103
101
 
102
+ # structures
103
+ accounts: Optional["PatriciaTrie"]
104
+ transactions: Optional[List["Transaction"]]
105
+ receipts: Optional[List["Receipt"]]
106
+
107
+
108
+
104
109
  def __init__(self) -> None:
105
110
  # defaults for safety
106
111
  self.hash = b""
107
- self.previous_block = ZERO32
112
+ self.previous_block_hash = ZERO32
113
+ self.previous_block = None
108
114
  self.number = None
109
115
  self.timestamp = None
110
116
  self.accounts_hash = None
111
117
  self.transactions_total_fees = None
112
- self.transactions_root_hash = None
118
+ self.transactions_hash = None
119
+ self.receipts_hash = None
113
120
  self.delay_difficulty = None
114
121
  self.delay_output = None
115
122
  self.validator_public_key = None
116
123
  self.body_hash = None
117
124
  self.signature = None
125
+ self.accounts = None
126
+ self.transactions = None
127
+ self.receipts = None
118
128
 
119
129
  def to_atom(self) -> Tuple[bytes, List[Atom]]:
120
- # Build body details as typed bytes, in defined order
130
+ # Build body details as direct byte atoms, in defined order
121
131
  details_ids: List[bytes] = []
122
132
  atoms_acc: List[Atom] = []
123
133
 
124
134
  def _emit(detail_bytes: bytes) -> None:
125
- oid, ats = _make_typed_bytes(detail_bytes)
126
- details_ids.append(oid)
127
- atoms_acc.extend(ats)
128
-
129
- # 0: previous_block
130
- _emit(self.previous_block or ZERO32)
135
+ atom = Atom.from_data(data=detail_bytes)
136
+ details_ids.append(atom.object_id())
137
+ atoms_acc.append(atom)
138
+
139
+ # 0: previous_block_hash
140
+ prev_hash = self.previous_block_hash or (self.previous_block.hash if self.previous_block else b"")
141
+ prev_hash = prev_hash or ZERO32
142
+ self.previous_block_hash = prev_hash
143
+ _emit(prev_hash)
131
144
  # 1: number
132
145
  _emit(_int_to_be_bytes(self.number))
133
146
  # 2: timestamp
@@ -136,13 +149,15 @@ class Block:
136
149
  _emit(self.accounts_hash or b"")
137
150
  # 4: transactions_total_fees
138
151
  _emit(_int_to_be_bytes(self.transactions_total_fees))
139
- # 5: transactions_root_hash
140
- _emit(self.transactions_root_hash or b"")
141
- # 6: delay_difficulty
152
+ # 5: transactions_hash
153
+ _emit(self.transactions_hash or b"")
154
+ # 6: receipts_hash
155
+ _emit(self.receipts_hash or b"")
156
+ # 7: delay_difficulty
142
157
  _emit(_int_to_be_bytes(self.delay_difficulty))
143
- # 7: delay_output
158
+ # 8: delay_output
144
159
  _emit(self.delay_output or b"")
145
- # 8: validator_public_key
160
+ # 9: validator_public_key
146
161
  _emit(self.validator_public_key or b"")
147
162
 
148
163
  # Build body list
@@ -166,7 +181,14 @@ class Block:
166
181
  return self.hash, atoms_acc
167
182
 
168
183
  @classmethod
169
- def from_atom(cls, storage_get: Callable[[bytes], Optional[Atom]], block_id: bytes) -> "Block":
184
+ def from_atom(cls, source: Any, block_id: bytes) -> "Block":
185
+ storage_get: Optional[Callable[[bytes], Optional[Atom]]]
186
+ if callable(source):
187
+ storage_get = source
188
+ else:
189
+ storage_get = getattr(source, "_local_get", None)
190
+ if not callable(storage_get):
191
+ raise TypeError("Block.from_atom requires a node with '_local_get' or a callable storage getter")
170
192
  # 1) Expect main list
171
193
  main_typ = storage_get(block_id)
172
194
  if main_typ is None or main_typ.data != b"list":
@@ -206,23 +228,20 @@ class Block:
206
228
  raise ValueError("malformed body (value)")
207
229
  cur_elem_id = body_val.next
208
230
 
209
- def _read_typed_bytes(elem_id: bytes) -> bytes:
231
+ def _read_detail_bytes(elem_id: bytes) -> bytes:
210
232
  elem = storage_get(elem_id)
211
233
  if elem is None:
212
234
  return b""
213
235
  child_id = elem.data
214
- typ = storage_get(child_id)
215
- if typ is None or typ.data != b"byte":
216
- return b""
217
- val = storage_get(typ.next)
218
- return val.data if val is not None else b""
236
+ detail = storage_get(child_id)
237
+ return detail.data if detail is not None else b""
219
238
 
220
239
  details: List[bytes] = []
221
- # We read up to 9 fields if present
222
- for _ in range(9):
240
+ # We read up to 10 fields if present
241
+ for _ in range(10):
223
242
  if not cur_elem_id:
224
243
  break
225
- b = _read_typed_bytes(cur_elem_id)
244
+ b = _read_detail_bytes(cur_elem_id)
226
245
  details.append(b)
227
246
  nxt = storage_get(cur_elem_id)
228
247
  cur_elem_id = nxt.next if nxt is not None else b""
@@ -233,21 +252,23 @@ class Block:
233
252
 
234
253
  # Map details back per the defined order
235
254
  get = lambda i: details[i] if i < len(details) else b""
236
- b.previous_block = get(0) or ZERO32
255
+ b.previous_block_hash = get(0) or ZERO32
256
+ b.previous_block = None
237
257
  b.number = _be_bytes_to_int(get(1))
238
258
  b.timestamp = _be_bytes_to_int(get(2))
239
259
  b.accounts_hash = get(3) or None
240
260
  b.transactions_total_fees = _be_bytes_to_int(get(4))
241
- b.transactions_root_hash = get(5) or None
242
- b.delay_difficulty = _be_bytes_to_int(get(6))
243
- b.delay_output = get(7) or None
244
- b.validator_public_key = get(8) or None
261
+ b.transactions_hash = get(5) or None
262
+ b.receipts_hash = get(6) or None
263
+ b.delay_difficulty = _be_bytes_to_int(get(7))
264
+ b.delay_output = get(8) or None
265
+ b.validator_public_key = get(9) or None
245
266
 
246
- # 4) Parse signature if present (supports raw or typed 'byte' atom)
267
+ # 4) Parse signature if present (supports raw or typed 'bytes' atom)
247
268
  if sig_atom_id is not None:
248
269
  sa = storage_get(sig_atom_id)
249
270
  if sa is not None:
250
- if sa.data == b"byte":
271
+ if sa.data == b"bytes":
251
272
  sval = storage_get(sa.next)
252
273
  b.signature = sval.data if sval is not None else b""
253
274
  else:
@@ -282,13 +303,24 @@ class Block:
282
303
  raise ValueError("invalid signature") from e
283
304
 
284
305
  # 2) Timestamp monotonicity against previous block
285
- if self.previous_block and self.previous_block != ZERO32:
306
+ prev_ts: Optional[int] = None
307
+ prev_hash = self.previous_block_hash or ZERO32
308
+
309
+ if self.previous_block is not None:
310
+ prev_ts = int(self.previous_block.timestamp or 0)
311
+ prev_hash = self.previous_block.hash or prev_hash or ZERO32
312
+
313
+ if prev_hash and prev_hash != ZERO32 and prev_ts is None:
286
314
  # If previous block cannot be loaded, treat as unverifiable, not malicious
287
315
  try:
288
- prev = Block.from_atom(storage_get, self.previous_block)
316
+ prev = Block.from_atom(storage_get, prev_hash)
289
317
  except Exception:
290
318
  return False
291
319
  prev_ts = int(prev.timestamp or 0)
320
+
321
+ if prev_hash and prev_hash != ZERO32:
322
+ if prev_ts is None:
323
+ return False
292
324
  cur_ts = int(self.timestamp or 0)
293
325
  if cur_ts < prev_ts + 1:
294
326
  raise ValueError("timestamp must be at least prev+1")
@@ -1,63 +1,66 @@
1
1
  # chain.py
2
- from typing import Callable, Dict, Optional
3
- from .block import Block
4
- from .._storage.atom import ZERO32, Atom
5
-
6
- class Chain:
7
- def __init__(self, head_block: Block):
8
- self.head_block = head_block
9
- self.validated_upto_block = None
10
- # Root (genesis) hash for this chain; set by validation setup when known
11
- self.root: Optional[bytes] = None
12
- # Fork position: the head hash of the default/current fork for this chain
13
- self.fork_position: Optional[bytes] = getattr(head_block, "hash", None)
14
- # Mark the first malicious block encountered during validation; None means not found
15
- self.malicious_block_hash: Optional[bytes] = None
16
-
17
- def validate(self, storage_get: Callable[[bytes], Atom]) -> Block:
18
- """Validate the chain from head to genesis and return the root block.
19
-
20
- Incorporates per-block validation (signature on body and timestamp
21
- monotonicity). Uses a simple cache to avoid duplicate Atom fetches and
22
- duplicate block decoding during the backward walk.
23
- """
24
- # Atom and Block caches for this validation pass
25
- atom_cache: Dict[bytes, Optional[Atom]] = {}
26
- block_cache: Dict[bytes, Block] = {}
27
-
28
- def get_cached(k: bytes) -> Optional[Atom]:
29
- if k in atom_cache:
30
- return atom_cache[k]
31
- a = storage_get(k)
32
- atom_cache[k] = a
33
- return a
34
-
35
- def load_block(bid: bytes) -> Block:
36
- if bid in block_cache:
37
- return block_cache[bid]
38
- b = Block.from_atom(get_cached, bid) # type: ignore[arg-type]
39
- block_cache[bid] = b
40
- return b
41
-
42
- blk = self.head_block
43
- # Ensure head is in cache if it has a hash
44
- if getattr(blk, "hash", None):
45
- block_cache[blk.hash] = blk # type: ignore[attr-defined]
46
-
47
- # Walk back, validating each block
48
- while True:
49
- # Validate current block (signature over body, timestamp rule)
50
- try:
51
- blk.validate(get_cached) # may decode previous but uses cached atoms
52
- except Exception:
53
- # record first failure point then propagate
54
- self.malicious_block_hash = getattr(blk, "hash", None)
55
- raise
56
-
57
- if blk.previous_block == ZERO32:
58
- break
59
- # Move to previous block using cache-aware loader
60
- blk = load_block(blk.previous_block)
61
-
62
- self.validated_upto_block = blk
63
- return blk
2
+ from typing import Callable, Dict, Optional
3
+ from .block import Block
4
+ from .._storage.atom import ZERO32, Atom
5
+
6
+ class Chain:
7
+ def __init__(self, head_block: Block):
8
+ self.head_block = head_block
9
+ self.validated_upto_block = None
10
+ # Root (genesis) hash for this chain; set by validation setup when known
11
+ self.root: Optional[bytes] = None
12
+ # Fork position: the head hash of the default/current fork for this chain
13
+ self.fork_position: Optional[bytes] = getattr(head_block, "hash", None)
14
+ # Mark the first malicious block encountered during validation; None means not found
15
+ self.malicious_block_hash: Optional[bytes] = None
16
+
17
+ def validate(self, storage_get: Callable[[bytes], Atom]) -> Block:
18
+ """Validate the chain from head to genesis and return the root block.
19
+
20
+ Incorporates per-block validation (signature on body and timestamp
21
+ monotonicity). Uses a simple cache to avoid duplicate Atom fetches and
22
+ duplicate block decoding during the backward walk.
23
+ """
24
+ # Atom and Block caches for this validation pass
25
+ atom_cache: Dict[bytes, Optional[Atom]] = {}
26
+ block_cache: Dict[bytes, Block] = {}
27
+
28
+ def get_cached(k: bytes) -> Optional[Atom]:
29
+ if k in atom_cache:
30
+ return atom_cache[k]
31
+ a = storage_get(k)
32
+ atom_cache[k] = a
33
+ return a
34
+
35
+ def load_block(bid: bytes) -> Block:
36
+ if bid in block_cache:
37
+ return block_cache[bid]
38
+ b = Block.from_atom(get_cached, bid)
39
+ block_cache[bid] = b
40
+ return b
41
+
42
+ blk = self.head_block
43
+ # Ensure head is in cache if it has a hash
44
+ if getattr(blk, "hash", None):
45
+ block_cache[blk.hash] = blk # type: ignore[attr-defined]
46
+
47
+ # Walk back, validating each block
48
+ while True:
49
+ # Validate current block (signature over body, timestamp rule)
50
+ try:
51
+ blk.validate(get_cached) # may decode previous but uses cached atoms
52
+ except Exception:
53
+ # record first failure point then propagate
54
+ self.malicious_block_hash = getattr(blk, "hash", None)
55
+ raise
56
+
57
+ prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
58
+ if prev_hash == ZERO32:
59
+ break
60
+ # Move to previous block using cache-aware loader
61
+ prev_blk = load_block(prev_hash)
62
+ blk.previous_block = prev_blk # cache the object for any downstream use
63
+ blk = prev_blk
64
+
65
+ self.validated_upto_block = blk
66
+ return blk