astreum 0.2.39__py3-none-any.whl → 0.2.40__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of astreum might be problematic. Click here for more details.
- astreum/_communication/__init__.py +2 -0
- astreum/{models → _communication}/message.py +100 -64
- astreum/_communication/ping.py +33 -0
- astreum/_communication/route.py +53 -20
- astreum/_communication/setup.py +240 -99
- astreum/_communication/util.py +42 -0
- astreum/_consensus/__init__.py +2 -0
- astreum/_consensus/block.py +76 -51
- astreum/_consensus/chain.py +5 -2
- astreum/_consensus/fork.py +3 -1
- astreum/_consensus/receipt.py +167 -0
- astreum/_consensus/setup.py +11 -15
- astreum/_consensus/transaction.py +18 -22
- astreum/_storage/patricia.py +443 -0
- astreum/models/block.py +8 -8
- {astreum-0.2.39.dist-info → astreum-0.2.40.dist-info}/METADATA +1 -1
- {astreum-0.2.39.dist-info → astreum-0.2.40.dist-info}/RECORD +20 -16
- {astreum-0.2.39.dist-info → astreum-0.2.40.dist-info}/WHEEL +0 -0
- {astreum-0.2.39.dist-info → astreum-0.2.40.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.39.dist-info → astreum-0.2.40.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
|
astreum/_consensus/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from .block import Block
|
|
2
2
|
from .chain import Chain
|
|
3
3
|
from .fork import Fork
|
|
4
|
+
from .receipt import Receipt
|
|
4
5
|
from .transaction import Transaction
|
|
5
6
|
from .setup import consensus_setup
|
|
6
7
|
|
|
@@ -9,6 +10,7 @@ __all__ = [
|
|
|
9
10
|
"Block",
|
|
10
11
|
"Chain",
|
|
11
12
|
"Fork",
|
|
13
|
+
"Receipt",
|
|
12
14
|
"Transaction",
|
|
13
15
|
"consensus_setup",
|
|
14
16
|
]
|
astreum/_consensus/block.py
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
from typing import Callable, List, Optional, Tuple
|
|
1
|
+
|
|
2
|
+
from typing import 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:
|
|
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:
|
|
74
|
-
6:
|
|
75
|
-
7:
|
|
76
|
-
8:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
126
|
-
details_ids.append(
|
|
127
|
-
atoms_acc.
|
|
128
|
-
|
|
129
|
-
# 0:
|
|
130
|
-
|
|
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:
|
|
140
|
-
_emit(self.
|
|
141
|
-
# 6:
|
|
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
|
-
#
|
|
158
|
+
# 8: delay_output
|
|
144
159
|
_emit(self.delay_output or b"")
|
|
145
|
-
#
|
|
160
|
+
# 9: validator_public_key
|
|
146
161
|
_emit(self.validator_public_key or b"")
|
|
147
162
|
|
|
148
163
|
# Build body list
|
|
@@ -206,23 +221,20 @@ class Block:
|
|
|
206
221
|
raise ValueError("malformed body (value)")
|
|
207
222
|
cur_elem_id = body_val.next
|
|
208
223
|
|
|
209
|
-
def
|
|
224
|
+
def _read_detail_bytes(elem_id: bytes) -> bytes:
|
|
210
225
|
elem = storage_get(elem_id)
|
|
211
226
|
if elem is None:
|
|
212
227
|
return b""
|
|
213
228
|
child_id = elem.data
|
|
214
|
-
|
|
215
|
-
if
|
|
216
|
-
return b""
|
|
217
|
-
val = storage_get(typ.next)
|
|
218
|
-
return val.data if val is not None else b""
|
|
229
|
+
detail = storage_get(child_id)
|
|
230
|
+
return detail.data if detail is not None else b""
|
|
219
231
|
|
|
220
232
|
details: List[bytes] = []
|
|
221
|
-
# We read up to
|
|
222
|
-
for _ in range(
|
|
233
|
+
# We read up to 10 fields if present
|
|
234
|
+
for _ in range(10):
|
|
223
235
|
if not cur_elem_id:
|
|
224
236
|
break
|
|
225
|
-
b =
|
|
237
|
+
b = _read_detail_bytes(cur_elem_id)
|
|
226
238
|
details.append(b)
|
|
227
239
|
nxt = storage_get(cur_elem_id)
|
|
228
240
|
cur_elem_id = nxt.next if nxt is not None else b""
|
|
@@ -233,21 +245,23 @@ class Block:
|
|
|
233
245
|
|
|
234
246
|
# Map details back per the defined order
|
|
235
247
|
get = lambda i: details[i] if i < len(details) else b""
|
|
236
|
-
b.
|
|
248
|
+
b.previous_block_hash = get(0) or ZERO32
|
|
249
|
+
b.previous_block = None
|
|
237
250
|
b.number = _be_bytes_to_int(get(1))
|
|
238
251
|
b.timestamp = _be_bytes_to_int(get(2))
|
|
239
252
|
b.accounts_hash = get(3) or None
|
|
240
253
|
b.transactions_total_fees = _be_bytes_to_int(get(4))
|
|
241
|
-
b.
|
|
242
|
-
b.
|
|
243
|
-
b.
|
|
244
|
-
b.
|
|
254
|
+
b.transactions_hash = get(5) or None
|
|
255
|
+
b.receipts_hash = get(6) or None
|
|
256
|
+
b.delay_difficulty = _be_bytes_to_int(get(7))
|
|
257
|
+
b.delay_output = get(8) or None
|
|
258
|
+
b.validator_public_key = get(9) or None
|
|
245
259
|
|
|
246
|
-
# 4) Parse signature if present (supports raw or typed '
|
|
260
|
+
# 4) Parse signature if present (supports raw or typed 'bytes' atom)
|
|
247
261
|
if sig_atom_id is not None:
|
|
248
262
|
sa = storage_get(sig_atom_id)
|
|
249
263
|
if sa is not None:
|
|
250
|
-
if sa.data == b"
|
|
264
|
+
if sa.data == b"bytes":
|
|
251
265
|
sval = storage_get(sa.next)
|
|
252
266
|
b.signature = sval.data if sval is not None else b""
|
|
253
267
|
else:
|
|
@@ -282,13 +296,24 @@ class Block:
|
|
|
282
296
|
raise ValueError("invalid signature") from e
|
|
283
297
|
|
|
284
298
|
# 2) Timestamp monotonicity against previous block
|
|
285
|
-
|
|
299
|
+
prev_ts: Optional[int] = None
|
|
300
|
+
prev_hash = self.previous_block_hash or ZERO32
|
|
301
|
+
|
|
302
|
+
if self.previous_block is not None:
|
|
303
|
+
prev_ts = int(self.previous_block.timestamp or 0)
|
|
304
|
+
prev_hash = self.previous_block.hash or prev_hash or ZERO32
|
|
305
|
+
|
|
306
|
+
if prev_hash and prev_hash != ZERO32 and prev_ts is None:
|
|
286
307
|
# If previous block cannot be loaded, treat as unverifiable, not malicious
|
|
287
308
|
try:
|
|
288
|
-
prev = Block.from_atom(storage_get,
|
|
309
|
+
prev = Block.from_atom(storage_get, prev_hash)
|
|
289
310
|
except Exception:
|
|
290
311
|
return False
|
|
291
312
|
prev_ts = int(prev.timestamp or 0)
|
|
313
|
+
|
|
314
|
+
if prev_hash and prev_hash != ZERO32:
|
|
315
|
+
if prev_ts is None:
|
|
316
|
+
return False
|
|
292
317
|
cur_ts = int(self.timestamp or 0)
|
|
293
318
|
if cur_ts < prev_ts + 1:
|
|
294
319
|
raise ValueError("timestamp must be at least prev+1")
|
astreum/_consensus/chain.py
CHANGED
|
@@ -54,10 +54,13 @@ class Chain:
|
|
|
54
54
|
self.malicious_block_hash = getattr(blk, "hash", None)
|
|
55
55
|
raise
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
|
|
58
|
+
if prev_hash == ZERO32:
|
|
58
59
|
break
|
|
59
60
|
# Move to previous block using cache-aware loader
|
|
60
|
-
|
|
61
|
+
prev_blk = load_block(prev_hash)
|
|
62
|
+
blk.previous_block = prev_blk # cache the object for any downstream use
|
|
63
|
+
blk = prev_blk
|
|
61
64
|
|
|
62
65
|
self.validated_upto_block = blk
|
|
63
66
|
return blk
|
astreum/_consensus/fork.py
CHANGED
|
@@ -92,7 +92,9 @@ class Fork:
|
|
|
92
92
|
self.validated_upto = blk.hash
|
|
93
93
|
return True
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
|
|
96
|
+
nxt = load_block(prev_hash)
|
|
96
97
|
if nxt is None:
|
|
97
98
|
return False
|
|
99
|
+
blk.previous_block = nxt # cache for future use
|
|
98
100
|
blk = nxt
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Callable, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from .._storage.atom import Atom, ZERO32
|
|
7
|
+
|
|
8
|
+
STATUS_SUCCESS = 0
|
|
9
|
+
STATUS_FAILED = 1
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _int_to_be_bytes(value: Optional[int]) -> bytes:
|
|
13
|
+
if value is None:
|
|
14
|
+
return b""
|
|
15
|
+
value = int(value)
|
|
16
|
+
if value == 0:
|
|
17
|
+
return b"\x00"
|
|
18
|
+
size = (value.bit_length() + 7) // 8
|
|
19
|
+
return value.to_bytes(size, "big")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _be_bytes_to_int(data: Optional[bytes]) -> int:
|
|
23
|
+
if not data:
|
|
24
|
+
return 0
|
|
25
|
+
return int.from_bytes(data, "big")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
|
|
29
|
+
atoms: List[Atom] = []
|
|
30
|
+
next_hash = ZERO32
|
|
31
|
+
elements: List[Atom] = []
|
|
32
|
+
for child_id in reversed(child_ids):
|
|
33
|
+
elem = Atom.from_data(data=child_id, next_hash=next_hash)
|
|
34
|
+
next_hash = elem.object_id()
|
|
35
|
+
elements.append(elem)
|
|
36
|
+
elements.reverse()
|
|
37
|
+
list_value = Atom.from_data(data=len(child_ids).to_bytes(8, "little"), next_hash=next_hash)
|
|
38
|
+
list_type = Atom.from_data(data=b"list", next_hash=list_value.object_id())
|
|
39
|
+
atoms.extend(elements)
|
|
40
|
+
atoms.append(list_value)
|
|
41
|
+
atoms.append(list_type)
|
|
42
|
+
return list_type.object_id(), atoms
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _read_list_entries(
|
|
46
|
+
storage_get: Callable[[bytes], Optional[Atom]], start: bytes
|
|
47
|
+
) -> List[bytes]:
|
|
48
|
+
entries: List[bytes] = []
|
|
49
|
+
current = start if start and start != ZERO32 else b""
|
|
50
|
+
while current:
|
|
51
|
+
elem = storage_get(current)
|
|
52
|
+
if elem is None:
|
|
53
|
+
break
|
|
54
|
+
entries.append(elem.data)
|
|
55
|
+
nxt = elem.next
|
|
56
|
+
current = nxt if nxt and nxt != ZERO32 else b""
|
|
57
|
+
return entries
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _read_payload_bytes(
|
|
61
|
+
storage_get: Callable[[bytes], Optional[Atom]], object_id: bytes
|
|
62
|
+
) -> bytes:
|
|
63
|
+
if not object_id or object_id == ZERO32:
|
|
64
|
+
return b""
|
|
65
|
+
atom = storage_get(object_id)
|
|
66
|
+
if atom is None:
|
|
67
|
+
return b""
|
|
68
|
+
if atom.data == b"bytes":
|
|
69
|
+
value_atom = storage_get(atom.next)
|
|
70
|
+
return value_atom.data if value_atom is not None else b""
|
|
71
|
+
return atom.data
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class Receipt:
|
|
76
|
+
transaction_hash: bytes = ZERO32
|
|
77
|
+
cost: int = 0
|
|
78
|
+
logs: bytes = b""
|
|
79
|
+
status: int = 0
|
|
80
|
+
|
|
81
|
+
def to_atom(self) -> Tuple[bytes, List[Atom]]:
|
|
82
|
+
"""Serialise the receipt into Atom storage."""
|
|
83
|
+
if self.status not in (STATUS_SUCCESS, STATUS_FAILED):
|
|
84
|
+
raise ValueError("unsupported receipt status")
|
|
85
|
+
|
|
86
|
+
atoms: List[Atom] = []
|
|
87
|
+
|
|
88
|
+
tx_atom = Atom.from_data(data=bytes(self.transaction_hash))
|
|
89
|
+
status_atom = Atom.from_data(data=_int_to_be_bytes(self.status))
|
|
90
|
+
cost_atom = Atom.from_data(data=_int_to_be_bytes(self.cost))
|
|
91
|
+
logs_atom = Atom.from_data(data=bytes(self.logs))
|
|
92
|
+
|
|
93
|
+
atoms.extend([tx_atom, status_atom, cost_atom, logs_atom])
|
|
94
|
+
|
|
95
|
+
body_child_ids = [
|
|
96
|
+
tx_atom.object_id(),
|
|
97
|
+
status_atom.object_id(),
|
|
98
|
+
cost_atom.object_id(),
|
|
99
|
+
logs_atom.object_id(),
|
|
100
|
+
]
|
|
101
|
+
body_id, body_atoms = _make_list(body_child_ids)
|
|
102
|
+
atoms.extend(body_atoms)
|
|
103
|
+
|
|
104
|
+
type_atom = Atom.from_data(data=b"receipt", next_hash=body_id)
|
|
105
|
+
atoms.append(type_atom)
|
|
106
|
+
|
|
107
|
+
top_list_id, top_atoms = _make_list([type_atom.object_id(), body_id])
|
|
108
|
+
atoms.extend(top_atoms)
|
|
109
|
+
|
|
110
|
+
return top_list_id, atoms
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_atom(
|
|
114
|
+
cls,
|
|
115
|
+
storage_get: Callable[[bytes], Optional[Atom]],
|
|
116
|
+
receipt_id: bytes,
|
|
117
|
+
) -> Receipt:
|
|
118
|
+
"""Materialise a Receipt from Atom storage."""
|
|
119
|
+
top_type_atom = storage_get(receipt_id)
|
|
120
|
+
if top_type_atom is None or top_type_atom.data != b"list":
|
|
121
|
+
raise ValueError("not a receipt (outer list missing)")
|
|
122
|
+
|
|
123
|
+
top_value_atom = storage_get(top_type_atom.next)
|
|
124
|
+
if top_value_atom is None:
|
|
125
|
+
raise ValueError("malformed receipt (outer value missing)")
|
|
126
|
+
|
|
127
|
+
head = top_value_atom.next
|
|
128
|
+
first_elem = storage_get(head) if head else None
|
|
129
|
+
if first_elem is None:
|
|
130
|
+
raise ValueError("malformed receipt (type element missing)")
|
|
131
|
+
|
|
132
|
+
type_atom_id = first_elem.data
|
|
133
|
+
type_atom = storage_get(type_atom_id)
|
|
134
|
+
if type_atom is None or type_atom.data != b"receipt":
|
|
135
|
+
raise ValueError("not a receipt (type mismatch)")
|
|
136
|
+
|
|
137
|
+
remainder_entries = _read_list_entries(storage_get, first_elem.next)
|
|
138
|
+
if not remainder_entries:
|
|
139
|
+
raise ValueError("malformed receipt (body missing)")
|
|
140
|
+
body_id = remainder_entries[0]
|
|
141
|
+
|
|
142
|
+
body_type_atom = storage_get(body_id)
|
|
143
|
+
if body_type_atom is None or body_type_atom.data != b"list":
|
|
144
|
+
raise ValueError("malformed receipt body (type)")
|
|
145
|
+
|
|
146
|
+
body_value_atom = storage_get(body_type_atom.next)
|
|
147
|
+
if body_value_atom is None:
|
|
148
|
+
raise ValueError("malformed receipt body (value)")
|
|
149
|
+
|
|
150
|
+
body_entries = _read_list_entries(storage_get, body_value_atom.next)
|
|
151
|
+
if len(body_entries) < 4:
|
|
152
|
+
body_entries.extend([ZERO32] * (4 - len(body_entries)))
|
|
153
|
+
|
|
154
|
+
transaction_hash_bytes = _read_payload_bytes(storage_get, body_entries[0])
|
|
155
|
+
status_bytes = _read_payload_bytes(storage_get, body_entries[1])
|
|
156
|
+
cost_bytes = _read_payload_bytes(storage_get, body_entries[2])
|
|
157
|
+
logs_bytes = _read_payload_bytes(storage_get, body_entries[3])
|
|
158
|
+
status_value = _be_bytes_to_int(status_bytes)
|
|
159
|
+
if status_value not in (STATUS_SUCCESS, STATUS_FAILED):
|
|
160
|
+
raise ValueError("unsupported receipt status")
|
|
161
|
+
|
|
162
|
+
return cls(
|
|
163
|
+
transaction_hash=transaction_hash_bytes or ZERO32,
|
|
164
|
+
cost=_be_bytes_to_int(cost_bytes),
|
|
165
|
+
logs=logs_bytes,
|
|
166
|
+
status=status_value,
|
|
167
|
+
)
|
astreum/_consensus/setup.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import Any, Dict, Optional, Tuple
|
|
|
8
8
|
from .block import Block
|
|
9
9
|
from .chain import Chain
|
|
10
10
|
from .fork import Fork
|
|
11
|
-
from .transaction import Transaction
|
|
11
|
+
from .transaction import Transaction, apply_transaction
|
|
12
12
|
from .._storage.atom import ZERO32, Atom
|
|
13
13
|
|
|
14
14
|
|
|
@@ -17,11 +17,6 @@ def current_validator(node: Any) -> bytes:
|
|
|
17
17
|
raise NotImplementedError("current_validator must be implemented by the host node")
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def apply_transaction(node: Any, block: object, transaction_hash: bytes) -> None:
|
|
21
|
-
"""Apply transaction to the candidate block. Override downstream."""
|
|
22
|
-
pass
|
|
23
|
-
|
|
24
|
-
|
|
25
20
|
def consensus_setup(node: Any) -> None:
|
|
26
21
|
# Shared state
|
|
27
22
|
node.validation_lock = getattr(node, "validation_lock", threading.RLock())
|
|
@@ -194,16 +189,17 @@ def consensus_setup(node: Any) -> None:
|
|
|
194
189
|
break
|
|
195
190
|
|
|
196
191
|
# Start workers as daemons
|
|
197
|
-
node.
|
|
198
|
-
target=_discovery_worker, daemon=True, name="
|
|
192
|
+
node.consensus_discovery_thread = threading.Thread(
|
|
193
|
+
target=_discovery_worker, daemon=True, name="consensus-discovery"
|
|
199
194
|
)
|
|
200
|
-
node.
|
|
201
|
-
target=_verify_worker, daemon=True, name="
|
|
195
|
+
node.consensus_verify_thread = threading.Thread(
|
|
196
|
+
target=_verify_worker, daemon=True, name="consensus-verify"
|
|
202
197
|
)
|
|
203
|
-
node.
|
|
204
|
-
target=_validation_worker, daemon=True, name="validation
|
|
198
|
+
node.consensus_validation_thread = threading.Thread(
|
|
199
|
+
target=_validation_worker, daemon=True, name="consensus-validation"
|
|
205
200
|
)
|
|
206
|
-
node.
|
|
207
|
-
node.
|
|
201
|
+
node.consensus_discovery_thread.start()
|
|
202
|
+
node.consensus_verify_thread.start()
|
|
208
203
|
if getattr(node, "validation_secret_key", None):
|
|
209
|
-
node.
|
|
204
|
+
node.consensus_validation_thread.start()
|
|
205
|
+
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Callable, List, Optional, Tuple
|
|
4
|
+
from typing import Any, Callable, List, Optional, Tuple
|
|
5
5
|
|
|
6
6
|
from .._storage.atom import Atom, ZERO32
|
|
7
7
|
|
|
@@ -22,12 +22,6 @@ def _be_bytes_to_int(data: Optional[bytes]) -> int:
|
|
|
22
22
|
return int.from_bytes(data, "big")
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def _make_typed_bytes(payload: bytes) -> Tuple[bytes, List[Atom]]:
|
|
26
|
-
value_atom = Atom.from_data(data=payload)
|
|
27
|
-
type_atom = Atom.from_data(data=b"byte", next_hash=value_atom.object_id())
|
|
28
|
-
return type_atom.object_id(), [value_atom, type_atom]
|
|
29
|
-
|
|
30
|
-
|
|
31
25
|
def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
|
|
32
26
|
atoms: List[Atom] = []
|
|
33
27
|
next_hash = ZERO32
|
|
@@ -61,9 +55,9 @@ class Transaction:
|
|
|
61
55
|
acc: List[Atom] = []
|
|
62
56
|
|
|
63
57
|
def emit(payload: bytes) -> None:
|
|
64
|
-
|
|
65
|
-
body_child_ids.append(
|
|
66
|
-
acc.
|
|
58
|
+
atom = Atom.from_data(data=payload)
|
|
59
|
+
body_child_ids.append(atom.object_id())
|
|
60
|
+
acc.append(atom)
|
|
67
61
|
|
|
68
62
|
emit(_int_to_be_bytes(self.amount))
|
|
69
63
|
emit(_int_to_be_bytes(self.counter))
|
|
@@ -141,23 +135,20 @@ class Transaction:
|
|
|
141
135
|
if len(body_entries) < 5:
|
|
142
136
|
body_entries.extend([ZERO32] * (5 - len(body_entries)))
|
|
143
137
|
|
|
144
|
-
def
|
|
145
|
-
if
|
|
138
|
+
def read_detail_bytes(entry_id: bytes) -> bytes:
|
|
139
|
+
if entry_id == ZERO32:
|
|
146
140
|
return b""
|
|
147
141
|
elem = storage_get(entry_id)
|
|
148
142
|
if elem is None:
|
|
149
143
|
return b""
|
|
150
|
-
|
|
151
|
-
if
|
|
152
|
-
return b""
|
|
153
|
-
value_atom = storage_get(type_atom.next)
|
|
154
|
-
return value_atom.data if value_atom is not None else b""
|
|
144
|
+
detail_atom = storage_get(elem.data)
|
|
145
|
+
return detail_atom.data if detail_atom is not None else b""
|
|
155
146
|
|
|
156
|
-
amount_bytes =
|
|
157
|
-
counter_bytes =
|
|
158
|
-
data_bytes =
|
|
159
|
-
recipient_bytes =
|
|
160
|
-
sender_bytes =
|
|
147
|
+
amount_bytes = read_detail_bytes(body_entries[0])
|
|
148
|
+
counter_bytes = read_detail_bytes(body_entries[1])
|
|
149
|
+
data_bytes = read_detail_bytes(body_entries[2])
|
|
150
|
+
recipient_bytes = read_detail_bytes(body_entries[3])
|
|
151
|
+
sender_bytes = read_detail_bytes(body_entries[4])
|
|
161
152
|
|
|
162
153
|
signature_atom = storage_get(signature_atom_id)
|
|
163
154
|
signature_bytes = signature_atom.data if signature_atom is not None else b""
|
|
@@ -170,3 +161,8 @@ class Transaction:
|
|
|
170
161
|
sender=sender_bytes,
|
|
171
162
|
signature=signature_bytes,
|
|
172
163
|
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def apply_transaction(node: Any, block: object, transaction_hash: bytes) -> None:
|
|
167
|
+
"""Apply transaction to the candidate block. Override downstream."""
|
|
168
|
+
pass
|