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.
- astreum/_communication/message.py +1 -0
- astreum/_communication/peer.py +23 -11
- astreum/_communication/route.py +40 -3
- astreum/_communication/setup.py +72 -4
- astreum/_consensus/account.py +62 -137
- astreum/_consensus/accounts.py +7 -36
- astreum/_consensus/block.py +311 -328
- astreum/_consensus/genesis.py +15 -84
- astreum/_consensus/receipt.py +67 -108
- astreum/_consensus/setup.py +50 -3
- astreum/_consensus/transaction.py +141 -118
- astreum/_consensus/workers/validation.py +5 -2
- astreum/_consensus/workers/verify.py +1 -1
- astreum/_lispeum/expression.py +190 -37
- astreum/_lispeum/high_evaluation.py +232 -173
- astreum/_lispeum/low_evaluation.py +21 -21
- astreum/_lispeum/parser.py +26 -31
- astreum/_node.py +154 -14
- astreum/_storage/__init__.py +7 -5
- astreum/_storage/atom.py +88 -96
- astreum/_storage/patricia.py +51 -16
- astreum/_storage/setup.py +35 -0
- astreum/models/block.py +20 -20
- astreum/utils/bytes.py +24 -0
- astreum/utils/integer.py +25 -0
- astreum/utils/logging.py +219 -0
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/METADATA +14 -1
- astreum-0.2.61.dist-info/RECORD +57 -0
- astreum-0.2.41.dist-info/RECORD +0 -53
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/WHEEL +0 -0
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.41.dist-info → astreum-0.2.61.dist-info}/top_level.txt +0 -0
astreum/_consensus/block.py
CHANGED
|
@@ -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
|
-
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
self.
|
|
119
|
-
|
|
120
|
-
self.
|
|
121
|
-
|
|
122
|
-
self.
|
|
123
|
-
|
|
124
|
-
self.
|
|
125
|
-
|
|
126
|
-
self.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
b =
|
|
250
|
-
b.
|
|
251
|
-
b.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
b.
|
|
256
|
-
b.
|
|
257
|
-
|
|
258
|
-
b.
|
|
259
|
-
|
|
260
|
-
b
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
#
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|