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.
- 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 +6 -0
- astreum/_consensus/account.py +170 -0
- astreum/_consensus/accounts.py +67 -0
- astreum/_consensus/block.py +84 -52
- astreum/_consensus/chain.py +65 -62
- astreum/_consensus/fork.py +99 -97
- astreum/_consensus/genesis.py +141 -0
- astreum/_consensus/receipt.py +177 -0
- astreum/_consensus/setup.py +21 -162
- astreum/_consensus/transaction.py +43 -23
- astreum/_consensus/workers/__init__.py +9 -0
- astreum/_consensus/workers/discovery.py +48 -0
- astreum/_consensus/workers/validation.py +122 -0
- astreum/_consensus/workers/verify.py +63 -0
- astreum/_storage/atom.py +24 -7
- astreum/_storage/patricia.py +443 -0
- astreum/models/block.py +10 -10
- astreum/node.py +755 -753
- {astreum-0.2.39.dist-info → astreum-0.2.41.dist-info}/METADATA +1 -1
- astreum-0.2.41.dist-info/RECORD +53 -0
- astreum/lispeum/__init__.py +0 -0
- astreum/lispeum/environment.py +0 -40
- astreum/lispeum/expression.py +0 -86
- astreum/lispeum/parser.py +0 -41
- astreum/lispeum/tokenizer.py +0 -52
- astreum/models/account.py +0 -91
- astreum/models/accounts.py +0 -34
- astreum/models/transaction.py +0 -106
- astreum/relay/__init__.py +0 -0
- astreum/relay/peer.py +0 -9
- astreum/relay/route.py +0 -25
- astreum/relay/setup.py +0 -58
- astreum-0.2.39.dist-info/RECORD +0 -55
- {astreum-0.2.39.dist-info → astreum-0.2.41.dist-info}/WHEEL +0 -0
- {astreum-0.2.39.dist-info → astreum-0.2.41.dist-info}/licenses/LICENSE +0 -0
- {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
|
astreum/_consensus/__init__.py
CHANGED
|
@@ -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()}
|
astreum/_consensus/block.py
CHANGED
|
@@ -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:
|
|
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
|
|
@@ -166,7 +181,14 @@ class Block:
|
|
|
166
181
|
return self.hash, atoms_acc
|
|
167
182
|
|
|
168
183
|
@classmethod
|
|
169
|
-
def from_atom(cls,
|
|
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
|
|
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
|
-
|
|
215
|
-
if
|
|
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
|
|
222
|
-
for _ in range(
|
|
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 =
|
|
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.
|
|
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.
|
|
242
|
-
b.
|
|
243
|
-
b.
|
|
244
|
-
b.
|
|
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 '
|
|
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"
|
|
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
|
-
|
|
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,
|
|
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")
|
astreum/_consensus/chain.py
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|