astreum 0.2.41__py3-none-any.whl → 0.2.61__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.
@@ -1,328 +1,311 @@
1
-
2
- from typing import Any, Callable, List, Optional, Tuple, TYPE_CHECKING
3
-
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
10
- from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
11
- from cryptography.exceptions import InvalidSignature
12
-
13
-
14
- def _int_to_be_bytes(n: Optional[int]) -> bytes:
15
- if n is None:
16
- return b""
17
- n = int(n)
18
- if n == 0:
19
- return b"\x00"
20
- size = (n.bit_length() + 7) // 8
21
- return n.to_bytes(size, "big")
22
-
23
-
24
- def _be_bytes_to_int(b: Optional[bytes]) -> int:
25
- if not b:
26
- return 0
27
- return int.from_bytes(b, "big")
28
-
29
-
30
- def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
31
- """Create a typed 'list' atom for child object ids.
32
-
33
- Encodes elements as a linked chain of element-atoms with data=child_id and
34
- next pointing to the next element's object id. The list value atom contains
35
- the element count and points to the head of the element chain. The type atom
36
- identifies the structure as a list.
37
- """
38
- acc: List[Atom] = []
39
- next_hash = ZERO32
40
- elem_atoms: List[Atom] = []
41
- # Build element chain in reverse, then flip to maintain forward order
42
- for h in reversed(child_ids):
43
- a = Atom.from_data(data=h, next_hash=next_hash)
44
- next_hash = a.object_id()
45
- elem_atoms.append(a)
46
- elem_atoms.reverse()
47
- head = next_hash
48
- val = Atom.from_data(data=(len(child_ids)).to_bytes(8, "little"), next_hash=head)
49
- typ = Atom.from_data(data=b"list", next_hash=val.object_id())
50
- return typ.object_id(), acc + elem_atoms + [val, typ]
51
-
52
-
53
- class Block:
54
- """Validation Block representation using Atom storage.
55
-
56
- Top-level encoding:
57
- block_id = list([ type_atom, body_list, signature_atom ])
58
- where: type_atom = Atom(data=b"block", next=body_list_id)
59
- body_list = list([...details...])
60
- signature_atom = Atom(data=<signature-bytes>)
61
-
62
- Details order in body_list:
63
- 0: previous_block_hash (bytes)
64
- 1: number (int → big-endian bytes)
65
- 2: timestamp (int → big-endian bytes)
66
- 3: accounts_hash (bytes)
67
- 4: transactions_total_fees (int → big-endian 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)
73
-
74
- Notes:
75
- - "body tree" is represented here by the body_list id (self.body_hash), not
76
- embedded again as a field to avoid circular references.
77
- - "signature" is a field on the class but is not required for validation
78
- navigation; include it in the instance but it is not encoded in atoms
79
- unless explicitly provided via details extension in the future.
80
- """
81
-
82
- # essential identifiers
83
- hash: bytes
84
- previous_block_hash: bytes
85
- previous_block: Optional["Block"]
86
-
87
- # block details
88
- number: Optional[int]
89
- timestamp: Optional[int]
90
- accounts_hash: Optional[bytes]
91
- transactions_total_fees: Optional[int]
92
- transactions_hash: Optional[bytes]
93
- receipts_hash: Optional[bytes]
94
- delay_difficulty: Optional[int]
95
- delay_output: Optional[bytes]
96
- validator_public_key: Optional[bytes]
97
-
98
- # additional
99
- body_hash: Optional[bytes]
100
- signature: Optional[bytes]
101
-
102
- # structures
103
- accounts: Optional["PatriciaTrie"]
104
- transactions: Optional[List["Transaction"]]
105
- receipts: Optional[List["Receipt"]]
106
-
107
-
108
-
109
- def __init__(self) -> None:
110
- # defaults for safety
111
- self.hash = b""
112
- self.previous_block_hash = ZERO32
113
- self.previous_block = None
114
- self.number = None
115
- self.timestamp = None
116
- self.accounts_hash = None
117
- self.transactions_total_fees = None
118
- self.transactions_hash = None
119
- self.receipts_hash = None
120
- self.delay_difficulty = None
121
- self.delay_output = None
122
- self.validator_public_key = None
123
- self.body_hash = None
124
- self.signature = None
125
- self.accounts = None
126
- self.transactions = None
127
- self.receipts = None
128
-
129
- def to_atom(self) -> Tuple[bytes, List[Atom]]:
130
- # Build body details as direct byte atoms, in defined order
131
- details_ids: List[bytes] = []
132
- atoms_acc: List[Atom] = []
133
-
134
- def _emit(detail_bytes: bytes) -> None:
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)
144
- # 1: number
145
- _emit(_int_to_be_bytes(self.number))
146
- # 2: timestamp
147
- _emit(_int_to_be_bytes(self.timestamp))
148
- # 3: accounts_hash
149
- _emit(self.accounts_hash or b"")
150
- # 4: transactions_total_fees
151
- _emit(_int_to_be_bytes(self.transactions_total_fees))
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
157
- _emit(_int_to_be_bytes(self.delay_difficulty))
158
- # 8: delay_output
159
- _emit(self.delay_output or b"")
160
- # 9: validator_public_key
161
- _emit(self.validator_public_key or b"")
162
-
163
- # Build body list
164
- body_id, body_atoms = _make_list(details_ids)
165
- atoms_acc.extend(body_atoms)
166
- self.body_hash = body_id
167
-
168
- # Type atom points to body list
169
- type_atom = Atom.from_data(data=b"block", next_hash=body_id)
170
-
171
- # Signature atom (raw byte payload)
172
- sig_atom = Atom.from_data(data=self.signature or b"", next_hash=ZERO32)
173
-
174
- # Main block list: [type_atom, body_list, signature]
175
- main_id, main_atoms = _make_list([type_atom.object_id(), body_id, sig_atom.object_id()])
176
- atoms_acc.append(type_atom)
177
- atoms_acc.append(sig_atom)
178
- atoms_acc.extend(main_atoms)
179
-
180
- self.hash = main_id
181
- return self.hash, atoms_acc
182
-
183
- @classmethod
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")
192
- # 1) Expect main list
193
- main_typ = storage_get(block_id)
194
- if main_typ is None or main_typ.data != b"list":
195
- raise ValueError("not a block (main list missing)")
196
- main_val = storage_get(main_typ.next)
197
- if main_val is None:
198
- raise ValueError("malformed block list (missing value)")
199
- # length is little-endian u64 per storage format
200
- if len(main_val.data) < 1:
201
- raise ValueError("malformed block list (length)")
202
- head = main_val.next
203
-
204
- # read first 2 elements: [type_atom_id, body_list_id]
205
- first_elem = storage_get(head)
206
- if first_elem is None:
207
- raise ValueError("malformed block list (head element)")
208
- type_atom_id = first_elem.data
209
- second_elem = storage_get(first_elem.next)
210
- if second_elem is None:
211
- raise ValueError("malformed block list (second element)")
212
- body_list_id = second_elem.data
213
- # optional 3rd element: signature atom id
214
- third_elem = storage_get(second_elem.next) if second_elem.next else None
215
- sig_atom_id: Optional[bytes] = third_elem.data if third_elem is not None else None
216
-
217
- # 2) Validate type atom and linkage to body
218
- type_atom = storage_get(type_atom_id)
219
- if type_atom is None or type_atom.data != b"block" or type_atom.next != body_list_id:
220
- raise ValueError("not a block (type atom)")
221
-
222
- # 3) Parse body list of details
223
- body_typ = storage_get(body_list_id)
224
- if body_typ is None or body_typ.data != b"list":
225
- raise ValueError("malformed body (type)")
226
- body_val = storage_get(body_typ.next)
227
- if body_val is None:
228
- raise ValueError("malformed body (value)")
229
- cur_elem_id = body_val.next
230
-
231
- def _read_detail_bytes(elem_id: bytes) -> bytes:
232
- elem = storage_get(elem_id)
233
- if elem is None:
234
- return b""
235
- child_id = elem.data
236
- detail = storage_get(child_id)
237
- return detail.data if detail is not None else b""
238
-
239
- details: List[bytes] = []
240
- # We read up to 10 fields if present
241
- for _ in range(10):
242
- if not cur_elem_id:
243
- break
244
- b = _read_detail_bytes(cur_elem_id)
245
- details.append(b)
246
- nxt = storage_get(cur_elem_id)
247
- cur_elem_id = nxt.next if nxt is not None else b""
248
-
249
- b = cls()
250
- b.hash = block_id
251
- b.body_hash = body_list_id
252
-
253
- # Map details back per the defined order
254
- get = lambda i: details[i] if i < len(details) else b""
255
- b.previous_block_hash = get(0) or ZERO32
256
- b.previous_block = None
257
- b.number = _be_bytes_to_int(get(1))
258
- b.timestamp = _be_bytes_to_int(get(2))
259
- b.accounts_hash = get(3) or None
260
- b.transactions_total_fees = _be_bytes_to_int(get(4))
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
266
-
267
- # 4) Parse signature if present (supports raw or typed 'bytes' atom)
268
- if sig_atom_id is not None:
269
- sa = storage_get(sig_atom_id)
270
- if sa is not None:
271
- if sa.data == b"bytes":
272
- sval = storage_get(sa.next)
273
- b.signature = sval.data if sval is not None else b""
274
- else:
275
- b.signature = sa.data
276
-
277
- return b
278
-
279
- def validate(self, storage_get: Callable[[bytes], Optional[Atom]]) -> bool:
280
- """Validate this block against storage.
281
-
282
- Checks:
283
- - Signature: signature must verify over the body list id using the
284
- validator's public key.
285
- - Timestamp monotonicity: if previous block exists (not ZERO32), this
286
- block's timestamp must be >= previous.timestamp + 1.
287
- """
288
- # Unverifiable if critical fields are missing
289
- if not self.body_hash:
290
- return False
291
- if not self.signature:
292
- return False
293
- if not self.validator_public_key:
294
- return False
295
- if self.timestamp is None:
296
- return False
297
-
298
- # 1) Signature check over body hash
299
- try:
300
- pub = Ed25519PublicKey.from_public_bytes(bytes(self.validator_public_key))
301
- pub.verify(self.signature, self.body_hash)
302
- except InvalidSignature as e:
303
- raise ValueError("invalid signature") from e
304
-
305
- # 2) Timestamp monotonicity against previous block
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:
314
- # If previous block cannot be loaded, treat as unverifiable, not malicious
315
- try:
316
- prev = Block.from_atom(storage_get, prev_hash)
317
- except Exception:
318
- return False
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
324
- cur_ts = int(self.timestamp or 0)
325
- if cur_ts < prev_ts + 1:
326
- raise ValueError("timestamp must be at least prev+1")
327
-
328
- return True
1
+
2
+ from typing import Any, Callable, List, Optional, Tuple, TYPE_CHECKING
3
+
4
+ from .._storage.atom import Atom, AtomKind, ZERO32
5
+
6
+ if TYPE_CHECKING:
7
+ from .._storage.patricia import PatriciaTrie
8
+ from .transaction import Transaction
9
+ from .receipt import Receipt
10
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
11
+ from cryptography.exceptions import InvalidSignature
12
+
13
+
14
+ def _int_to_be_bytes(n: Optional[int]) -> bytes:
15
+ if n is None:
16
+ return b""
17
+ n = int(n)
18
+ if n == 0:
19
+ return b"\x00"
20
+ size = (n.bit_length() + 7) // 8
21
+ return n.to_bytes(size, "big")
22
+
23
+
24
+ def _be_bytes_to_int(b: Optional[bytes]) -> int:
25
+ if not b:
26
+ return 0
27
+ return int.from_bytes(b, "big")
28
+
29
+
30
+ class Block:
31
+ """Validation Block representation using Atom storage.
32
+
33
+ Top-level encoding:
34
+ block_id = type_atom.object_id()
35
+ chain: type_atom --next--> signature_atom --next--> body_list_atom --next--> ZERO32
36
+ where: type_atom = Atom(kind=AtomKind.SYMBOL, data=b"block")
37
+ signature_atom = Atom(kind=AtomKind.BYTES, data=<signature-bytes>)
38
+ body_list_atom = Atom(kind=AtomKind.LIST, data=<body_head_id>)
39
+
40
+ Details order in body_list:
41
+ 0: previous_block_hash (bytes)
42
+ 1: number (int -> big-endian bytes)
43
+ 2: timestamp (int -> big-endian bytes)
44
+ 3: accounts_hash (bytes)
45
+ 4: transactions_total_fees (int -> big-endian bytes)
46
+ 5: transactions_hash (bytes)
47
+ 6: receipts_hash (bytes)
48
+ 7: delay_difficulty (int -> big-endian bytes)
49
+ 8: delay_output (bytes)
50
+ 9: validator_public_key (bytes)
51
+
52
+ Notes:
53
+ - "body tree" is represented here by the body_list id (self.body_hash), not
54
+ embedded again as a field to avoid circular references.
55
+ - "signature" is a field on the class but is not required for validation
56
+ navigation; include it in the instance but it is not encoded in atoms
57
+ unless explicitly provided via details extension in the future.
58
+ """
59
+
60
+ # essential identifiers
61
+ hash: bytes
62
+ previous_block_hash: bytes
63
+ previous_block: Optional["Block"]
64
+
65
+ # block details
66
+ number: Optional[int]
67
+ timestamp: Optional[int]
68
+ accounts_hash: Optional[bytes]
69
+ transactions_total_fees: Optional[int]
70
+ transactions_hash: Optional[bytes]
71
+ receipts_hash: Optional[bytes]
72
+ delay_difficulty: Optional[int]
73
+ delay_output: Optional[bytes]
74
+ validator_public_key: Optional[bytes]
75
+
76
+ # additional
77
+ body_hash: Optional[bytes]
78
+ signature: Optional[bytes]
79
+
80
+ # structures
81
+ accounts: Optional["PatriciaTrie"]
82
+ transactions: Optional[List["Transaction"]]
83
+ receipts: Optional[List["Receipt"]]
84
+
85
+
86
+
87
+ def __init__(self) -> None:
88
+ # defaults for safety
89
+ self.hash = b""
90
+ self.previous_block_hash = ZERO32
91
+ self.previous_block = None
92
+ self.number = None
93
+ self.timestamp = None
94
+ self.accounts_hash = None
95
+ self.transactions_total_fees = None
96
+ self.transactions_hash = None
97
+ self.receipts_hash = None
98
+ self.delay_difficulty = None
99
+ self.delay_output = None
100
+ self.validator_public_key = None
101
+ self.body_hash = None
102
+ self.signature = None
103
+ self.accounts = None
104
+ self.transactions = None
105
+ self.receipts = None
106
+
107
+ def to_atom(self) -> Tuple[bytes, List[Atom]]:
108
+ # Build body details as direct byte atoms, in defined order
109
+ details_ids: List[bytes] = []
110
+ block_atoms: List[Atom] = []
111
+
112
+ def _emit(detail_bytes: bytes) -> None:
113
+ atom = Atom.from_data(data=detail_bytes, kind=AtomKind.BYTES)
114
+ details_ids.append(atom.object_id())
115
+ block_atoms.append(atom)
116
+
117
+ # 0: previous_block_hash
118
+ _emit(self.previous_block_hash)
119
+ # 1: number
120
+ _emit(_int_to_be_bytes(self.number))
121
+ # 2: timestamp
122
+ _emit(_int_to_be_bytes(self.timestamp))
123
+ # 3: accounts_hash
124
+ _emit(self.accounts_hash or b"")
125
+ # 4: transactions_total_fees
126
+ _emit(_int_to_be_bytes(self.transactions_total_fees))
127
+ # 5: transactions_hash
128
+ _emit(self.transactions_hash or b"")
129
+ # 6: receipts_hash
130
+ _emit(self.receipts_hash or b"")
131
+ # 7: delay_difficulty
132
+ _emit(_int_to_be_bytes(self.delay_difficulty))
133
+ # 8: delay_output
134
+ _emit(self.delay_output or b"")
135
+ # 9: validator_public_key
136
+ _emit(self.validator_public_key or b"")
137
+
138
+ # Build body list chain (head points to the first detail atom id)
139
+ body_atoms: List[Atom] = []
140
+ body_head = ZERO32
141
+ for child_id in reversed(details_ids):
142
+ node = Atom.from_data(data=child_id, next_hash=body_head, kind=AtomKind.BYTES)
143
+ body_head = node.object_id()
144
+ body_atoms.append(node)
145
+ body_atoms.reverse()
146
+
147
+ block_atoms.extend(body_atoms)
148
+
149
+ body_list_atom = Atom.from_data(data=body_head, kind=AtomKind.LIST)
150
+ self.body_hash = body_list_atom.object_id()
151
+
152
+ # Signature atom links to body list atom; type atom links to signature atom
153
+ sig_atom = Atom.from_data(data=self.signature, next_hash=self.body_hash, kind=AtomKind.BYTES)
154
+ type_atom = Atom.from_data(data=b"block", next_hash=sig_atom.object_id(), kind=AtomKind.SYMBOL)
155
+
156
+ block_atoms.append(body_list_atom)
157
+ block_atoms.append(sig_atom)
158
+ block_atoms.append(type_atom)
159
+
160
+ self.hash = type_atom.object_id()
161
+ return self.hash, block_atoms
162
+
163
+ @classmethod
164
+ def from_atom(cls, source: Any, block_id: bytes) -> "Block":
165
+ storage_get: Optional[Callable[[bytes], Optional[Atom]]]
166
+ if callable(source):
167
+ storage_get = source
168
+ else:
169
+ storage_get = getattr(source, "storage_get", None)
170
+ if not callable(storage_get):
171
+ raise TypeError(
172
+ "Block.from_atom requires a node with 'storage_get' or a callable storage getter"
173
+ )
174
+
175
+ def _atom_kind(atom: Optional[Atom]) -> Optional[AtomKind]:
176
+ kind_value = getattr(atom, "kind", None)
177
+ if isinstance(kind_value, AtomKind):
178
+ return kind_value
179
+ if isinstance(kind_value, int):
180
+ try:
181
+ return AtomKind(kind_value)
182
+ except ValueError:
183
+ return None
184
+ return None
185
+
186
+ def _require_atom(atom_id: Optional[bytes], context: str, expected_kind: Optional[AtomKind] = None) -> Atom:
187
+ if not atom_id or atom_id == ZERO32:
188
+ raise ValueError(f"missing {context}")
189
+ atom = storage_get(atom_id)
190
+ if atom is None:
191
+ raise ValueError(f"missing {context}")
192
+ if expected_kind is not None:
193
+ kind = _atom_kind(atom)
194
+ if kind is not expected_kind:
195
+ raise ValueError(f"malformed {context}")
196
+ return atom
197
+
198
+ def _read_list(head_id: Optional[bytes], context: str) -> List[bytes]:
199
+ entries: List[bytes] = []
200
+ current = head_id
201
+ if not current or current == ZERO32:
202
+ return entries
203
+ while current and current != ZERO32:
204
+ node = storage_get(current)
205
+ if node is None:
206
+ raise ValueError(f"missing list node while decoding {context}")
207
+ node_kind = _atom_kind(node)
208
+ if node_kind is not AtomKind.BYTES:
209
+ raise ValueError(f"list element must be bytes while decoding {context}")
210
+ if len(node.data) != len(ZERO32):
211
+ raise ValueError(f"list element payload has unexpected length while decoding {context}")
212
+ entries.append(node.data)
213
+ current = node.next
214
+ return entries
215
+
216
+ type_atom = _require_atom(block_id, "block type atom", AtomKind.SYMBOL)
217
+ if type_atom.data != b"block":
218
+ raise ValueError("not a block (type atom payload)")
219
+
220
+ sig_atom = _require_atom(type_atom.next, "block signature atom", AtomKind.BYTES)
221
+ body_list_id = sig_atom.next
222
+ body_list_atom = _require_atom(body_list_id, "block body list atom", AtomKind.LIST)
223
+ if body_list_atom.next and body_list_atom.next != ZERO32:
224
+ raise ValueError("malformed block (body list tail)")
225
+
226
+ body_child_ids = _read_list(body_list_atom.data, "block body")
227
+
228
+ details: List[bytes] = []
229
+ for idx, child_id in enumerate(body_child_ids):
230
+ if idx >= 10:
231
+ break
232
+ if not child_id or child_id == ZERO32:
233
+ details.append(b"")
234
+ continue
235
+ detail_atom = storage_get(child_id)
236
+ details.append(detail_atom.data if detail_atom is not None else b"")
237
+
238
+ if len(details) < 10:
239
+ details.extend([b""] * (10 - len(details)))
240
+
241
+ b = cls()
242
+ b.hash = block_id
243
+ b.body_hash = body_list_id
244
+
245
+ get = lambda i: details[i] if i < len(details) else b""
246
+ b.previous_block_hash = get(0) or ZERO32
247
+ b.previous_block = None
248
+ b.number = _be_bytes_to_int(get(1))
249
+ b.timestamp = _be_bytes_to_int(get(2))
250
+ b.accounts_hash = get(3) or None
251
+ b.transactions_total_fees = _be_bytes_to_int(get(4))
252
+ b.transactions_hash = get(5) or None
253
+ b.receipts_hash = get(6) or None
254
+ b.delay_difficulty = _be_bytes_to_int(get(7))
255
+ b.delay_output = get(8) or None
256
+ b.validator_public_key = get(9) or None
257
+
258
+ b.signature = sig_atom.data if sig_atom is not None else None
259
+
260
+ return b
261
+
262
+ def validate(self, storage_get: Callable[[bytes], Optional[Atom]]) -> bool:
263
+ """Validate this block against storage.
264
+
265
+ Checks:
266
+ - Signature: signature must verify over the body list id using the
267
+ validator's public key.
268
+ - Timestamp monotonicity: if previous block exists (not ZERO32), this
269
+ block's timestamp must be >= previous.timestamp + 1.
270
+ """
271
+ # Unverifiable if critical fields are missing
272
+ if not self.body_hash:
273
+ return False
274
+ if not self.signature:
275
+ return False
276
+ if not self.validator_public_key:
277
+ return False
278
+ if self.timestamp is None:
279
+ return False
280
+
281
+ # 1) Signature check over body hash
282
+ try:
283
+ pub = Ed25519PublicKey.from_public_bytes(bytes(self.validator_public_key))
284
+ pub.verify(self.signature, self.body_hash)
285
+ except InvalidSignature as e:
286
+ raise ValueError("invalid signature") from e
287
+
288
+ # 2) Timestamp monotonicity against previous block
289
+ prev_ts: Optional[int] = None
290
+ prev_hash = self.previous_block_hash or ZERO32
291
+
292
+ if self.previous_block is not None:
293
+ prev_ts = int(self.previous_block.timestamp or 0)
294
+ prev_hash = self.previous_block.hash or prev_hash or ZERO32
295
+
296
+ if prev_hash and prev_hash != ZERO32 and prev_ts is None:
297
+ # If previous block cannot be loaded, treat as unverifiable, not malicious
298
+ try:
299
+ prev = Block.from_atom(storage_get, prev_hash)
300
+ except Exception:
301
+ return False
302
+ prev_ts = int(prev.timestamp or 0)
303
+
304
+ if prev_hash and prev_hash != ZERO32:
305
+ if prev_ts is None:
306
+ return False
307
+ cur_ts = int(self.timestamp or 0)
308
+ if cur_ts < prev_ts + 1:
309
+ raise ValueError("timestamp must be at least prev+1")
310
+
311
+ return True